“复制本地”和项目引用的最佳做法是什么?

时间:2008-11-11 12:24:49

标签: c# .net visual-studio msbuild

我有一个大型的c#解决方案文件(~100个项目),我正在努力改善构建时间。我认为“复制本地”在很多情况下对我们来说是浪费,但我想知道最佳实践。

在我们的.sln中,我们有应用程序A取决于程序集B,它取决于程序集C.在我们的例子中,有几十个“B”和一些“C”。由于这些都包含在.sln中,我们正在使用项目引用。目前所有程序集都构建为$(SolutionDir)/ Debug(或Release)。

默认情况下,Visual Studio将这些项目引用标记为“复制本地”,这会导致每个“C”被复制到$(SolutionDir)/ Debug中,每个“B”构建一次。这似乎很浪费。如果我只关闭“复制本地”,会出现什么问题?大型系统的其他人做了什么?

随访:

很多响应建议将构建分解为较小的.sln文件......在上面的示例中,我首先构建基础类“C”,然后是大部分模块“B”,然后是几个应用程序,“A”。在这个模型中,我需要对来自B的C进行非项目引用。我遇到的问题是“Debug”或“Release”被添加到提示路径中,我最终构建了我的Release版本的“B”反对“C”的调试版本。

对于那些将构建拆分为多个.sln文件的人,您如何解决此问题?

18 个答案:

答案 0 :(得分:84)

在之前的项目中,我使用了一个带有项目引用的大解决方案,并且还遇到了性能问题。解决方案有三个方面:

  1. 始终将“复制本地”属性设置为false并通过自定义msbuild步骤强制执行此操作

  2. 将每个项目的输出目录设置为同一目录(最好是相对于$(SolutionDir)

  3. 框架附带的默认cs目标计算要复制到当前正在构建的项目的输出目录的引用集。由于这需要在“参考”关系下计算传递闭包,因此这可能会变得非常<非常。我的解决方法是在导入GetCopyToOutputDirectoryItems后在每个项目中导入的公共目标文件(例如Common.targets)中重新定义Microsoft.CSharp.targets目标。导致每个项目文件如下所示:

    <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        ... snip ...
      </ItemGroup>
      <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
      <Import Project="[relative path to Common.targets]" />
      <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
           Other similar extension points exist, see Microsoft.Common.targets.
      <Target Name="BeforeBuild">
      </Target>
      <Target Name="AfterBuild">
      </Target>
      -->
    </Project>
    
  4. 这使我们在给定时间内的构建时间从几个小时(主要是由于内存限制)减少到几分钟。

    可以通过将GetCopyToOutputDirectoryItems中的2,438-2,450和2,474-2,524行复制到C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets来创建重新定义的Common.targets

    为了完整性,结果目标定义变为:

    <!-- This is a modified version of the Microsoft.Common.targets
         version of this target it does not include transitively
         referenced projects. Since this leads to enormous memory
         consumption and is not needed since we use the single
         output directory strategy.
    ============================================================
                        GetCopyToOutputDirectoryItems
    
    Get all project items that may need to be transferred to the
    output directory.
    ============================================================ -->
    <Target
        Name="GetCopyToOutputDirectoryItems"
        Outputs="@(AllItemsFullPathWithTargetPath)"
        DependsOnTargets="AssignTargetPaths;_SplitProjectReferencesByFileExistence">
    
        <!-- Get items from this project last so that they will be copied last. -->
        <CreateItem
            Include="@(ContentWithTargetPath->'%(FullPath)')"
            Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
                >
            <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                    Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                    Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
        </CreateItem>
    
        <CreateItem
            Include="@(_EmbeddedResourceWithTargetPath->'%(FullPath)')"
            Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
                >
            <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                    Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                    Condition="'%(_EmbeddedResourceWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
        </CreateItem>
    
        <CreateItem
            Include="@(Compile->'%(FullPath)')"
            Condition="'%(Compile.CopyToOutputDirectory)'=='Always' or '%(Compile.CopyToOutputDirectory)'=='PreserveNewest'">
            <Output TaskParameter="Include" ItemName="_CompileItemsToCopy"/>
        </CreateItem>
        <AssignTargetPath Files="@(_CompileItemsToCopy)" RootFolder="$(MSBuildProjectDirectory)">
            <Output TaskParameter="AssignedFiles" ItemName="_CompileItemsToCopyWithTargetPath" />
        </AssignTargetPath>
        <CreateItem Include="@(_CompileItemsToCopyWithTargetPath)">
            <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                    Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                    Condition="'%(_CompileItemsToCopyWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
        </CreateItem>
    
        <CreateItem
            Include="@(_NoneWithTargetPath->'%(FullPath)')"
            Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always' or '%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"
                >
            <Output TaskParameter="Include" ItemName="AllItemsFullPathWithTargetPath"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectoryAlways"
                    Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
            <Output TaskParameter="Include" ItemName="_SourceItemsToCopyToOutputDirectory"
                    Condition="'%(_NoneWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
        </CreateItem>
    </Target>
    

    通过这种解决方法,我发现可以使用&gt;在一个解决方案中有120个项目,这样做的主要好处是,项目的构建顺序仍然可以由VS确定,而不是通过拆分解决方案来手动完成。

答案 1 :(得分:32)

我建议你阅读Patric Smacchia关于该主题的文章:

  

CC.Net VS项目依赖于复制本地引用程序集选项设置为true。 [...]   这不仅大大增加了编译时间(在NUnit的情况下为x3),而且还会扰乱您的工作环境。最后但同样重要的是,这样做会带来版本化潜在问题的风险。顺便说一句,如果NDepend在2个不同目录中找到2个具有相同名称但不是相同内容或版本的程序集,则会发出警告。

     

正确的做法是定义2个目录$ RootDir $ \ bin \ Debug和$ RootDir $ \ bin \ Release,并配置VisualStudio项目以在这些目录中发出程序集。所有项目引用都应引用Debug目录中的程序集。

您还可以阅读this article来帮助您减少项目编号并缩短编制时间。

答案 2 :(得分:23)

我建议除了位于依赖关系树顶部的项目之外,几乎所有项目都复制local = false。并且对于顶部集合中的所有引用,复制local = true。我看到很多人建议共享输出目录;我认为这是一个基于经验的可怕想法。如果你的启动项目持有对dll的引用,任何其他项目都持有对你的引用,那么即使在所有内容上复制local = false,你的构建也会失败,在某些时候会遇到访问\共享冲突。这个问题非常烦人,难以追查。我完全建议远离分片输出目录,而不是让依赖链顶部的项目将所需的程序集写入相应的文件夹。如果你没有“顶部”的项目,那么我会建议一个构建后的副本,以便将所有内容放在正确的位置。此外,我会尝试记住调试的简易性。任何exe项目我仍然留下copy local = true所以F5调试经验将起作用。

答案 3 :(得分:10)

你是对的。 CopyLocal绝对会扼杀你的构建时间。如果您有一个大的源树,那么您应该禁用CopyLocal。不幸的是,它并不像应该是干净利用它一样容易。我已回答了有关在How do I override CopyLocal (Private) setting for references in .NET from MSBUILD禁用CopyLocal的确切问题。看看这个。以及Best practices for large solutions in Visual Studio (2008).

以下是我看到的有关CopyLocal的更多信息。

CopyLocal实际上是为了支持本地调试而实现的。准备应用程序进行打包和部署时,应将项目构建到同一输出文件夹中,并确保获得所需的所有引用。

我在文章MSBuild: Best Practices For Creating Reliable Builds, Part 2中写过关于如何处理构建大型源代码树的文章。

答案 4 :(得分:8)

在我看来,拥有100个项目的解决方案是一个很大的错误。您可以将解决方案拆分为有效的逻辑小单元,从而简化维护和构建。

答案 5 :(得分:7)

我很惊讶没人提到使用硬链接。它不是复制文件,而是创建原始文件的硬链接。这样可以节省磁盘空间并大大加快构建速度。这可以在命令行上启用以下属性:

/ P:CreateHardLinksForAdditionalFilesIfPossible = TRUE; CreateHardLinksForCopyAdditionalFilesIfPossible = TRUE; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = TRUE; CreateHardLinksForCopyLocalIfPossible = TRUE; CreateHardLinksForPublishFilesIfPossible =真

您还可以将其添加到中央导入文件中,以便您的所有项目也可以获得此优势。

答案 6 :(得分:5)

如果您通过项目引用或通过解决方案级依赖关系定义了依赖关系结构,那么转向“复制本地”是安全的我甚至会说这是最佳实践,因为这样可以让您使用MSBuild 3.5来运行您的构建并行(通过/ maxcpucount)在尝试复制引用的程序集时没有不同的进程相互绊倒。

答案 7 :(得分:4)

我们的“最佳实践”是避免许多项目的解决方案。 我们有一个名为“matrix”的目录,其中包含当前版本的程序集,所有引用都来自此目录。如果更改某个项目并且可以说“现在更改已完成”,则可以将程序集复制到“矩阵”目录中。因此,依赖于此程序集的所有项目都将具有当前(=最新)版本。

如果您的解决方案项目很少,那么构建过程会更快。

您可以使用visual studio宏或“menu - &gt; tools - &gt; external tools ...”自动执行“将程序集复制到矩阵目录”步骤。

答案 8 :(得分:3)

您无需更改CopyLocal值。您需要做的就是为解决方案中的所有项目预定义一个公共$(OutputPath),并将$(UseCommonOutputDirectory)预设为true。看到这个: http://blogs.msdn.com/b/kirillosenkov/archive/2015/04/04/using-a-common-intermediate-and-output-directory-for-your-solution.aspx

答案 9 :(得分:2)

设置CopyLocal = false将减少构建时间,但在部署期间可能会导致不同的问题。

有许多情况,当您需要将“复制本地”左侧设为“真”时,例如

  • 顶级项目,
  • 第二级依赖项,
  • 反射调用的DLL

SO问题中描述的可能问题 “When should copy-local be set to true and when should it not?”,
Error message 'Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.'
aaron-stainbackanswer这个问题。

我设置CopyLocal = false的经验不成功。请参阅我的博文"Do NOT Change "Copy Local” project references to false, unless understand subsequences."

解决问题的时间超重设置copyLocal = false的好处。

答案 10 :(得分:1)

我倾向于构建一个公共目录(例如.. \ bin),因此我可以创建小型测试解决方案。

答案 11 :(得分:1)

这可能不是最好的实践,但这就是我的工作方式。

我注意到托管C ++将其所有二进制文件转储到$(SolutionDir)/'DebugOrRelease'中。 所以我也放弃了所有的C#项目。我还关闭了解决方案中所有项目引用的“复制本地”。在我的小型10项目解决方案中,我有明显的构建时间改进。此解决方案是C#,托管C ++,本机C ++,C#Web服务和安装程序项目的混合体。

也许有些东西被破坏了,但由于这是我工作的唯一方式,我没有注意到它。

找出我要破坏的东西会很有趣。

答案 12 :(得分:1)

您可以尝试使用一个文件夹,在该文件夹中复制项目之间共享的所有程序集,然后创建一个DEVPATH环境变量并设置

<developmentMode developerInstallation="true" />
在每个开发人员的工作站上的machine.config文件中。您唯一需要做的就是复制DEVPATH变量指向的文件夹中的任何新版本。

如果可能,还将您的解决方案划分为几个较小的解

答案 13 :(得分:0)

如果你想拥有一个使用copy local引用DLL的中心位置,那么在没有GAC的情况下false将会失败,除非你这样做。

http://nbaked.wordpress.com/2010/03/28/gac-alternative/

答案 14 :(得分:0)

您可以让项目引用指向dll的调试版本。 在您的msbuild脚本上,您可以设置/p:Configuration=Release,因此您将拥有应用程序的发布版本和所有附属程序集。

答案 15 :(得分:0)

通常,如果您希望项目使用Bin中的DLL与其他地方(GAC,其他项目等),您只需要复制本地

我倾向于同意其他人的意见,如果可能的话,你也应该尝试分解这个解决方案。

您还可以使用Configuration Manager在一个仅构建给定项目集的解决方案中为自己创建不同的构建配置。

如果所有100个项目都相互依赖,那就好了,所以你应该能够分解它或者使用Configuration Manager来帮助自己。

答案 16 :(得分:0)

如果引用未包含在GAC中,我们必须将Copy Local设置为true,以便应用程序可以正常工作,如果我们确定该引用将预先安装在GAC中,则可以将其设置为false。 / p>

答案 17 :(得分:0)

好吧,我当然不知道这些问题是如何解决的,但是我接触到了一个构建解决方案,这个解决方案帮助了所有创建的文件,其中包含了 ramdisk 的帮助符号链接

  • c:\ solution folder \ bin - &gt; ramdisk r:\ solution folder \ bin \
  • c:\ solution folder \ _ obj - &gt; ramdisk r:\ solution folder \ _ obj \

  • 您还可以另外告诉visual studio它可以用于构建的临时目录。

实际上并不是它所做的一切。但这真的让我对性能有了一定的了解。
100%处理器使用和3分钟内的所有依赖项下的巨大项目。