我有一个大型 c# 解决方案文件(约 100 个项目),并且我正在尝试缩短构建时间。我认为“复制本地”在很多情况下对我们来说是浪费,但我想知道最佳实践。
在我们的 .sln 中,我们有依赖于程序集 B 的应用程序 A,它依赖于程序集 C。在我们的例子中,有几十个“B”和少数“C”。由于这些都包含在 .sln 中,因此我们使用的是项目引用。当前所有程序集都构建到 $(SolutionDir)/Debug(或 Release)中。
默认情况下,Visual Studio 将这些项目引用标记为“复制本地”,这会导致每个“C”被复制到 $(SolutionDir)/Debug 中,对于每个构建的“B”。这似乎很浪费。如果我只关闭“复制本地”会出现什么问题?其他拥有大型系统的人会做什么?
跟进:
许多响应建议将构建分解为较小的 .sln 文件...在上面的示例中,我将首先构建基础类“C”,然后是大部分模块“B”,然后是一些应用程序,“一个”。在这个模型中,我需要从 B 获得对 C 的非项目引用。我遇到的问题是“调试”或“发布”被纳入提示路径,我最终构建了“B”的发布版本针对“C”的调试版本。
对于那些将构建拆分为多个 .sln 文件的人,您如何处理这个问题?
<Private>True</Private>
相同?
.sln
拆分为更小的 <ProjectReference/>
会破坏 VS 对 <ProjectReference/>
的自动相互依赖性计算。我自己已经从多个较小的 .sln
转移到一个大的 .sln
只是因为 VS 以这种方式导致的问题更少……所以,也许后续假设是对原始问题的非必要最佳解决方案? ;-)
在之前的一个项目中,我使用了一个带有项目引用的大型解决方案,并且也遇到了性能问题。解决方案是三个方面:
始终将 Copy Local 属性设置为 false 并通过自定义 msbuild 步骤强制执行此操作 将每个项目的输出目录设置为同一目录(最好相对于 $(SolutionDir) 框架附带的默认 cs 目标计算引用集被复制到当前正在构建的项目的输出目录。由于这需要在“参考”关系下计算传递闭包,这可能会变得非常昂贵。我的解决方法是在公共目标文件中重新定义 GetCopyToOutputDirectoryItems 目标(例如.common.targets ) 在导入 Microsoft.CSharp.targets 后在每个项目中导入。导致每个项目文件如下所示:
这将我们在给定时间的构建时间从几个小时(主要是由于内存限制)减少到了几分钟。
可以通过将行 2,438–2,450 和 2,474–2,524 从 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets
复制到 Common.targets
来创建重新定义的 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 确定,而不是通过拆分解决方案来手动完成.
我建议您阅读 Patric Smacchia 关于该主题的文章:
通过 .NET 程序集和 Visual Studio 项目对代码库进行分区 --> 每个 Visual Studio 项目真的应该在自己的程序集中吗? 'Copy Local=True' 的真正含义是什么?
从 NUnit 代码库中吸取的教训 --> VisualStudio 项目参考 + 复制本地 true 选项是邪恶的!)
分析 CruiseControl.NET 的代码库 --> 错误使用 Copy Local Reference Assembly 选项设置为 True)
CC.Net VS 项目依赖于设置为 true 的复制本地引用程序集选项。 [...] 这不仅显着增加了编译时间(在 NUnit 的情况下为 x3),而且还扰乱了您的工作环境。最后但同样重要的是,这样做会带来版本控制潜在问题的风险。顺便说一句,如果 NDepend 在 2 个具有相同名称但内容或版本不同的不同目录中找到 2 个程序集,它将发出警告。正确的做法是定义 2 个目录 $RootDir$\bin\Debug 和 $RootDir$\bin\Release,并将您的 VisualStudio 项目配置为在这些目录中发出程序集。所有项目引用都应引用 Debug 目录中的程序集。
您还可以阅读 this article 以帮助您减少项目数量并缩短编译时间。
我建议对几乎所有项目都使用 copy local = false ,除了依赖关系树顶部的项目。对于顶部集合中的所有引用,请复制 local = true。我看到很多人建议共享一个输出目录;我认为这是一个基于经验的可怕想法。如果您的启动项目包含对任何其他项目包含对您的引用的 dll 的引用,即使在所有内容上都复制本地 = false 并且您的构建将失败,有时也会遇到访问\共享冲突。这个问题很烦人,很难追查。我完全建议远离分片输出目录,而不是让项目位于依赖链的顶部,将所需的程序集写入相应的文件夹。如果您在“顶部”没有项目,那么我建议您使用构建后的副本将所有内容放在正确的位置。另外,我会尽量记住调试的便利性。我仍然保留副本 local=true 的任何 exe 项目,因此 F5 调试体验将起作用。
你是对的。 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 中写过如何处理构建大型源代码树。
在我看来,拥有 100 个项目的解决方案是一个大错误。您可以将解决方案拆分为有效的逻辑小单元,从而简化维护和构建。
我很惊讶没有人提到使用硬链接。它不是复制文件,而是创建指向原始文件的硬链接。这可以节省磁盘空间并大大加快构建速度。这可以使用以下属性在命令行上启用:
/p:CreateHardLinksForAdditionalFilesIfPossible=true;CreateHardLinksForCopyAdditionalFilesIfPossible=true;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true;CreateHardLinksForCopyLocalIfPossible=true;CreateHardLinksForPublishFilesIfPossible=true
您还可以将其添加到中央导入文件中,以便您的所有项目也可以获得此好处。
如果您通过项目引用或解决方案级别的依赖项定义了依赖项结构,则可以安全地打开“复制本地”,我什至会说这是一个最佳实践,因为这将让您使用 MSBuild 3.5 并行运行您的构建(通过 /maxcpucount) 尝试复制引用的程序集时,不同的进程不会相互绊倒。
我们的“最佳做法”是避免许多项目的解决方案。我们有一个名为“matrix”的目录,其中包含当前版本的程序集,所有引用都来自该目录。如果您更改了某个项目并且可以说“现在更改已完成”,您可以将程序集复制到“矩阵”目录中。因此,依赖于该程序集的所有项目都将具有当前(=最新)版本。
如果解决方案中的项目很少,则构建过程会快得多。
您可以使用 Visual Studio 宏或使用“菜单 -> 工具 -> 外部工具...”自动执行“将程序集复制到矩阵目录”步骤。
您不需要更改 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
设置 CopyLocal=false 将减少构建时间,但在部署期间可能会导致不同的问题。
有很多情况下,当您需要将 Copy Local' 保留为 True 时,例如
顶级项目,
二级依赖,
反射调用的 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-stainback 的answer 中描述的可能问题。
我设置 CopyLocal=false 的经验并不成功。请参阅我的博文 "Do NOT Change "Copy Local” project references to false, unless understand subsequences."
解决问题的时间超过了设置 copyLocal=false 的好处。
CopyLocal=False
肯定会导致一些问题,但有解决这些问题的方法。此外,您应该修复博客的格式,它几乎不可读,并且在那里说“我被来自<随机公司>的<随机顾问>警告关于部署期间可能出现的错误”不是一个论据。你需要发展。
我倾向于构建到一个公共目录(例如 ..\bin),所以我可以创建小型测试解决方案。
您可以尝试使用将复制项目之间共享的所有程序集的文件夹,然后创建一个 DEVPATH 环境变量并在每个开发人员的 machine.config 文件中设置
<developmentMode developerInstallation="true" />
工作站。您唯一需要做的就是复制 DEVPATH 变量指向的文件夹中的任何新版本。
如果可能,还将您的解决方案分成几个较小的解决方案。
这可能不是最佳实践,但这就是我的工作方式。
我注意到托管 C++ 将其所有二进制文件转储到 $(SolutionDir)/'DebugOrRelease' 中。所以我也把我所有的 C# 项目都放在那里了。我还关闭了解决方案中对项目的所有引用的“复制本地”。在我的小型 10 项目解决方案中,我的构建时间得到了显着改善。此解决方案混合了 C#、托管 C++、本机 C++、C# Web 服务和安装程序项目。
也许有些东西坏了,但由于这是我工作的唯一方式,我没有注意到它。
找出我正在破坏的东西会很有趣。
通常,如果您希望项目使用 Bin 中的 DLL 而不是其他地方(GAC、其他项目等)的 DLL,则只需要复制本地
我倾向于同意其他人的观点,即如果可能的话,您也应该尝试打破该解决方案。
您还可以使用配置管理器在一个仅构建给定项目集的解决方案中为自己设置不同的构建配置。
如果所有 100 个项目都相互依赖,这似乎很奇怪,因此您应该能够将其拆分或使用 Configuration Manager 来帮助自己。
您可以让您的项目引用指向 dll 的调试版本。与您的 msbuild 脚本相比,您可以设置 /p:Configuration=Release
,因此您将拥有应用程序的发布版本和所有附属程序集。
如果您想在没有 GAC 的情况下使用 copy local false 来引用 DLL 的中心位置,除非您这样做。
http://nbaked.wordpress.com/2010/03/28/gac-alternative/
如果引用不包含在 GAC 中,我们必须将 Copy Local 设置为 true 以便应用程序能够正常工作,如果我们确定引用将预安装在 GAC 中,则可以将其设置为 false。
好吧,我当然不知道问题是如何解决的,但是我接触了一个构建解决方案,它可以帮助自己,这样所有创建的文件都可以在符号链接的帮助下放在 ramdisk 上。
c:\solution 文件夹\bin -> ramdisk r:\solution 文件夹\bin\
c:\solution 文件夹\obj -> ramdisk r:\solution 文件夹\obj\
您还可以另外告诉视觉工作室它可以用于构建的临时目录。
事实上,它所做的还不止这些。但这确实触动了我对性能的理解。 3 分钟内 100% 的处理器使用和一个包含所有依赖项的大型项目。