"Copy Local"和项目引用的最佳实践是什么?

164

我有一个庞大的C#解决方案文件(约100个项目),希望提高构建时间。我认为在很多情况下,“复制本地”是浪费的,但我想知道最佳实践。

在我们的 .sln 中,应用程序A依赖于程序集B,而程序集B依赖于程序集C。在我们的情况下,有数十个“B”和少量的“C”。由于这些都包含在 .sln 中,我们使用项目引用。所有程序集当前都构建到 $(SolutionDir)/Debug(或 Release)中。

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

后续:

许多回复建议将构建分成较小的 .sln 文件......在上面的示例中,我首先构建基础类“C”,然后是大部分模块“B”,最后是一些应用程序“ A”。在这种模式下,我需要从 B 到 C 进行非项目引用。我遇到的问题是“Debug”或“Release”被烘焙进了提示路径中,导致我构建 B 的 Release 版本时对 C 的调试版本进行了构建。

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


9
通过直接编辑项目文件,您可以使提示路径参考 Debug 或 Release 目录。 使用 $(Configuration) 代替 Debug 或 Release。例如,<HintPath>..\output$(Configuration)\test.dll</HintPath>如果有很多引用,这将很麻烦(尽管应该不难编写插件来管理此过程)。 - ultravelocity
5
在Visual Studio中,“Copy Local”与csproj中的“<Private>True</Private>”是否相同? - Colonel Panic
但是将一个“.sln”拆分成较小的项目会破坏VS对“<ProjectReference/>”的自动依赖项计算。我自己从多个较小的“.sln”转移到单个大的“.sln”,只是因为这样做VS会更少出问题...所以,也许后续问题假设了不一定是最佳解决方案的原始问题?;-) - binki
1
@ColonelPanic 是的。至少在我更改GUI中的切换时,磁盘上的内容会发生变化。 - Zero3
18个回答

87

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

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

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

  3. 默认的cs targets与框架一起提供,可计算要复制到当前正在构建的项目的输出目录的引用集。由于这需要在'References'关系下计算传递闭包,因此可能会变得非常昂贵。我的解决方法是在通用目标文件(例如'Common.targets')中重新定义GetCopyToOutputDirectoryItems目标,在每个项目导入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>

通过这种方法,我们将构建时间从几个小时(主要是由于内存限制)缩短到了几分钟。

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

为了完整起见,结果目标定义如下:

<!-- 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>

通过这种解决方法,我发现在一个解决方案中拥有超过120个项目是可行的,这样做的主要好处是项目的构建顺序仍然可以由VS确定,而不需要手动拆分您的解决方案。


为了项目引用,还是所有引用,copy local 应该设为 false? - Michel
禁止无条件建议禁用CopyLocal。将CopyLocal=false设置可能会在部署期间引起不同的问题。请参阅我的博客文章“除非理解后果,否则不要将项目引用的“复制本地”更改为false。”(http://geekswithblogs.net/mnf/archive/2012/12/09/do-not-change-copy-local-project-references-to-false-unless.aspx) - Michael Freidgeim
1
从Microsoft.Common.targets:GetCopyToOutputDirectoryItems获取所有可能需要传输到输出目录的项目项。 这包括来自传递引用项目的行李物品。 似乎该目标计算所有参考项目的内容项的完整传递闭包; 然而,情况并非如此。 - Brans Ds
2
它只从其直接子项收集内容项,而不是子项的子项。发生这种情况的原因是由_SplitProjectReferencesByFileExistence使用的ProjectReferenceWithConfiguration列表仅在当前项目中填充,在子级中为空。空列表导致_MSBuildProjectReferenceExistent为空并终止递归。因此,它似乎没有用处。 - Brans Ds
似乎在输出路径设置中无法使用$(SolutionDir)变量。相反,我必须使用..\bin。无论如何,这对我来说是解决问题最简单的方法,所以谢谢你。 - Dan Bechard
显示剩余10条评论

33
我建议您阅读Patric Smacchia关于这个主题的文章:

CC.Net VS项目依赖于将“复制本地引用程序集”选项设置为true。[...] 不仅会显著增加编译时间(在NUnit的情况下增加了3倍),而且还会搞乱您的工作环境。最重要的是,这样做会引入版本问题的风险。顺便说一下,如果发现两个不同目录中名称相同但内容或版本不同的程序集,NDepend将发出警告。

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

您还可以阅读此文章来帮助您减少项目数量并改善编译时间。


1
我希望我能用不止一个赞来推荐Smacchia的实践!减少项目数量是关键,而不是分解解决方案。 - Anthony Mastrean

23

我建议在除了依赖树最顶端的项目外,几乎所有项目都设置 copy local = false。而对于依赖树最顶端的所有引用,需要将 copy local = true。我看到很多人建议共享输出目录;基于经验,我认为这是一个可怕的想法。如果你的启动项目引用了其他项目引用的 DLL 文件,即使所有东西都设置成 copy local = false,你也会在某个时刻遇到访问/共享冲突,并且构建将失败。这个问题非常烦人,很难追踪。我完全建议避免使用共享的输出目录,而是将位于依赖链顶部的项目所需的程序集写入相应的文件夹中。如果你没有一个位于“顶部”的项目,则建议在生成后复制所有内容以放置在正确的位置。此外,我还建议考虑调试的便利性。任何执行文件项目我仍然设置 copy local = true,以便 F5 调试体验能够正常工作。


2
我也有同样的想法,希望能在这里找到其他有相同想法的人;然而,我很好奇为什么这篇帖子没有更多的赞。持不同意见的人:你们为什么不同意? - bwerks
不可能发生这种情况,除非同一个项目被构建了两次,如果只构建一次且不复制任何文件,为什么会被覆盖、访问或共享违规呢? - paulm
如果开发工作流程要求在运行解决方案的另一个项目可执行文件时构建 sln 的一个项目,则将所有内容放在同一个输出目录中会很混乱。在这种情况下,最好分离可执行文件输出文件夹。 - Martin Ba

10

你说得对。CopyLocal绝对会拖慢你的构建时间。如果你有大型源代码树,那么你应该禁用CopyLocal。不幸的是,干净地禁用它并不像应该的那样容易。我已经回答过关于在.NET中从MSBUILD覆盖CopyLocal(Private)设置的确切问题,访问如何在.NET中从MSBUILD覆盖CopyLocal(Private)设置。看一看。以及Visual Studio(2008)中大型解决方案的最佳实践。

以下是我所了解的CopyLocal的更多信息。

CopyLocal实际上是为了支持本地调试而实现的。当你为打包和部署准备你的应用程序时,你应该将你的项目构建到同一个输出文件夹,并确保你需要的所有引用都在那里。

我在文章MSBuild:创建可靠构建的最佳实践,第2部分中介绍了如何处理构建大型源代码树的问题。


9

我的观点是,拥有一个包含100个项目的解决方案是一个巨大的错误。你可以将你的解决方案分成有效的逻辑小单元,从而简化维护和构建过程。


1
Bruno,请查看我上面的后续问题 - 如果我们将.sln文件分成较小的部分,那么如何管理在引用的提示路径中随后嵌入的Debug vs. Release方面? - Dave Moore
1
我同意这个观点,我正在处理的解决方案有大约100个项目,其中只有少数几个项目有超过3个类,构建时间非常长,因此我的前任将解决方案分成了3个部分,这完全破坏了“查找所有引用”和重构。整个解决方案可以放在少数几个项目中,这些项目可以在几秒钟内构建完成! - Jon M
Dave, 好问题。在我工作的地方,我们有构建脚本来构建给定解决方案的依赖项,并将二进制文件放在解决方案可以获取到的位置。这些脚本针对调试和发布构建进行了参数化。缺点是需要额外的时间来构建这些脚本,但它们可以在应用程序之间重复使用。在我的标准下,这种解决方案效果很好。 - jyoungdev

7
我很惊讶没有人提到使用硬链接。它不会复制文件,而是创建一个指向原始文件的硬链接。这样可以节省磁盘空间,并大大加快构建速度。您可以在命令行上使用以下属性启用此功能:
/p:CreateHardLinksForAdditionalFilesIfPossible=true;CreateHardLinksForCopyAdditionalFilesIfPossible=true;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true;CreateHardLinksForCopyLocalIfPossible=true;CreateHardLinksForPublishFilesIfPossible=true 您还可以将其添加到中央导入文件中,以便所有项目都可以获得此好处。

5

如果您通过项目引用或解决方案级别的依赖关系定义了依赖结构,则可以安全地关闭“复制本地”功能。我甚至会说,这是一种最佳实践,因为这样可以让您使用MSBuild 3.5并行运行构建(通过 /maxcpucount),而不会在尝试复制引用程序集时导致不同进程相互干扰。


4
我们的“最佳实践”是避免使用包含多个项目的解决方案。我们有一个名为“matrix”的目录,其中包含当前程序集的版本,所有引用都来自这个目录。如果你改变了某个项目,然后可以说“现在更改完成了”,你可以将该程序集复制到“matrix”目录中。因此,所有依赖于该程序集的项目都将拥有当前(=最新)版本。
如果解决方案中只有少数几个项目,则构建过程会更快。
你可以使用 Visual Studio 宏或“菜单 -> 工具 -> 外部工具...”自动化“将程序集复制到 matrix 目录”的步骤。

3

我不确定这个功能在2009年是否可用,但它似乎在2015年运行良好。谢谢! - Dave Moore
1
实际链接是404。缓存:https://web.archive.org/web/20150407004936/http://blogs.msdn.com/b/kirillosenkov/archive/2015/04/04/using-a-common-intermediate-and-output-directory-for-your-solution.aspx - Ryan Rodemoyer

2
将CopyLocal设置为false可以缩短构建时间,但可能会在部署过程中引起不同的问题。
有许多情况下需要保持Copy Local为True,例如:
- 顶层项目, - 第二级依赖项, - 被反射调用的DLL。
在SO问题描述中可能出现的问题包括: "何时应将CopyLocal设置为True,何时不应该?", "错误消息“无法加载所请求的一个或多个类型。检索LoaderExceptions属性以获取更多信息。”", 以及此问题的aaron-stainback答案
我的设置CopyLocal=false的经验并不成功。请参见我的博客文章"除非了解后续步骤,否则不要将项目引用的“复制本地”更改为false。" 解决问题所需的时间超过了将copyLocal设置为false所带来的好处。

CopyLocal=False 设置为 False 会导致一些问题,但这些问题是可以解决的。此外,您应该修复博客的格式,因为它几乎无法阅读,并且在那里说“我被<随机顾问>从<随机公司>警告可能在部署期间出现错误”不是一个论点。您需要开发自己的观点。 - Suzanne Soy
@GeorgesDupéron,解决问题所需的时间超过了将copyLocal=false设置为好处。参考顾问并不是一个论点,而是信用,我的博客解释了问题所在。感谢您对格式的反馈,我会进行修复。 - Michael Freidgeim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接