159

我有一个大型 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 文件的人,您如何处理这个问题?

4

18 回答 18

85

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

  1. 始终将 Copy Local 属性设置为 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>
    

这将我们在给定时间的构建时间从几个小时(主要是由于内存限制)减少到了几分钟。

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>

有了这个解决方法,我发现在一个解决方案中拥有多达 120 个以上的项目是可行的,这主要的好处是项目的构建顺序仍然可以由 VS 确定,而不是通过拆分解决方案来手动完成.

于 2009-03-16T09:39:39.507 回答
32

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

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

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

您还可以阅读这篇文章来帮助您减少项目数量并缩短编译时间。

于 2009-03-16T09:55:54.400 回答
23

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

于 2011-06-30T02:44:37.353 回答
10

你是对的。CopyLocal 绝对会扼杀您的构建时间。如果你有一个大的源代码树,那么你应该禁用 CopyLocal。不幸的是,干净地禁用它并不容易。我已经在How do I override CopyLocal (Private) setting for references in .NET from MSBUILD 中回答了有关禁用 CopyLocal 的确切问题。看看这个。以及Visual Studio (2008) 中大型解决方案的最佳实践。

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

CopyLocal 的实现是为了支持本地调试。当您准备应用程序以进行打包和部署时,您应该将项目构建到相同的输出文件夹,并确保您拥有所需的所有引用。

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

于 2010-01-07T22:02:31.043 回答
8

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

于 2008-11-11T12:45:15.810 回答
7

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

/p:CreateHardLinksForAdditionalFilesIfPossible=true;CreateHardLinksForCopyAdditionalFilesIfPossible=true;CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true;CreateHardLinksForCopyLocalIfPossible=true;CreateHardLinksForPublishFilesIfPossible=true

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

于 2015-04-17T19:53:16.183 回答
5

如果您通过项目引用或解决方案级别的依赖项定义了依赖项结构,则可以安全地打开“复制本地”,我什至会说这是一个最佳实践,因为这将让您使用 MSBuild 3.5 并行运行您的构建(通过 /maxcpucount) 尝试复制引用的程序集时,不同的进程不会相互绊倒。

于 2009-03-16T07:58:40.437 回答
4

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

如果解决方案中的项目很少,则构建过程会快得多。

您可以使用 Visual Studio 宏或使用“菜单 -> 工具 -> 外部工具...”自动执行“将程序集复制到矩阵目录”步骤。

于 2008-11-11T12:39:34.400 回答
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

于 2015-06-15T22:11:06.760 回答
2

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

有很多情况下,当您需要将 Copy Local' 保留为 True 时,例如

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

SO 问题
何时应将 copy-local 设置为 true,何时不应设置? ”、
错误消息“无法加载一种或多种请求的类型。检索 LoaderExceptions 属性以获取更多信息。”中描述的可能问题。"
和  aaron-stainback对这个问题的回答 。

我设置 CopyLocal=false 的经验并不成功。请参阅我的博客文章“请勿将“复制本地”项目引用更改为 false,除非了解子序列。”

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

于 2012-12-09T02:50:19.213 回答
1

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

于 2008-11-11T12:39:59.203 回答
1

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

<developmentMode developerInstallation="true" />

在每个开发人员工作站上的 machine.config 文件中进行设置。您唯一需要做的就是复制 DEVPATH 变量指向的文件夹中的任何新版本。

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

于 2008-11-11T12:54:06.040 回答
1

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

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

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

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

于 2008-11-11T14:16:33.160 回答
0

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

我倾向于同意其他人的观点,即如果可能的话,您也应该尝试打破该解决方案。

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

如果所有 100 个项目都相互依赖,这似乎很奇怪,因此您应该能够将其拆分或使用 Configuration Manager 来帮助自己。

于 2008-11-11T12:48:08.737 回答
0

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

于 2008-11-12T12:32:37.300 回答
0

如果您想在没有 GAC 的情况下使用 copy local false 来引用 DLL 的中心位置,除非您这样做。

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

于 2010-03-31T13:17:40.173 回答
0

如果引用不包含在 GAC 中,我们必须将 Copy Local 设置为 true 以便应用程序能够工作,如果我们确定引用将预安装在 GAC 中,则可以将其设置为 false。

于 2015-07-16T07:25:36.387 回答
0

好吧,我当然不知道问题是如何解决的,但是我接触了一个构建解决方案,它可以帮助自己,这样所有创建的文件都可以在符号链接的帮助下放在ramdisk上。

  • c:\solution 文件夹\bin -> ramdisk r:\solution 文件夹\bin\
  • c:\solution 文件夹\obj -> ramdisk r:\solution 文件夹\obj\

  • 您还可以另外告诉视觉工作室它可以用于构建的临时目录。

事实上,它所做的还不止这些。但这确实触动了我对性能的理解。
3 分钟内 100% 的处理器使用和一个包含所有依赖项的大型项目。

于 2016-11-24T13:01:13.067 回答