“复制本地”和参考项目的最佳实践是什么?


154

我有一个很大的C#解决方案文件(〜100个项目),并且我正在尝试缩短构建时间。我认为“复制本地”在很多情况下对我们来说都是浪费,但我想知道最佳做法。

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

默认情况下,Visual Studio将这些项目引用标记为“ Copy Local”,这导致每个生成的“ B”都将每个“ C”复制到$(SolutionDir)/ Debug中一次。这似乎很浪费。如果我仅关闭“本地复制”,怎么办?其他拥有大型系统的人会做什么?

跟进:

许多响应建议将构建分解为较小的.sln文件...在上面的示例中,我将首先构建基础类“ C”,然后构建大量模块“ B”,然后构建一些应用程序,“一个”。在这个模型中,我需要从B到C的非项目引用。我遇到的问题是“ Debug”或“ Release”被烘焙到提示路径中,并且我最终构建了“ B”的Release版本。针对“ C”的调试版本。

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


9
您可以通过直接编辑项目文件来使“提示路径”引用Debug或Release目录。使用$(Configuration)代替Debug或Release。例如,<HintPath> .. \ output \ $(Configuration)\ test.dll </ HintPath>当您有很多引用时,这很痛苦(尽管对于某些人来说,写一个加载项并不难)管理)。
09年

4
Visual Studio中的“复制本地”与<Private>True</Private>csproj中的相同吗?
Panic Panic 2014年

但是将a拆分.sln为较小的值会破坏VS对<ProjectReference/>s 的自动相互依赖性计算。我已经从多个较小的.slns移到了一个较大的.sln我,只是因为VS以这种方式导致了更少的问题……所以,也许后续工作是假设对原始问题的不必要的最佳解决方案?;-)
binki 2014年

只是出于好奇。为什么要使事情复杂化并首先拥有100多个项目?这是不好的设计还是什么?
有用蜜蜂

@ColonelPanic是的。至少那是我在GUI中更改切换时磁盘上发生更改的事情。
Zero3,18年

Answers:


85

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

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

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

  3. 框架附带的默认cs目标计算要复制到当前正在构建项目的输出目录中的引用集。由于这需要根据“引用”关系来计算传递闭包,因此这可能变得非常昂贵。我的解决方法是GetCopyToOutputDirectoryItemsCommon.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.targetsCommon.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仍可以确定项目的构建顺序,而不必通过拆分解决方案来手动完成。


您能否描述所做的更改以及原因?经过一整天的编码,我的眼球太累了,无法自己对它进行反向工程:)
Charlie Flowers

如何尝试再次复制并粘贴此代码-如此混乱,就像99%的标签一样。
ZXX

@Charlie Flowers,@ ZXX将文本编辑为描述,无法很好地布局xml。
Bas Bossink

1
从Microsoft.Common.targets:GetCopyToOutputDirectoryItems获取可能需要转移到输出目录的所有项目项。这包括来自可及参考项目的行李。似乎该目标为所有引用的项目计算了内容项的完全可传递关闭;但是事实并非如此。
Brans Ds

2
它仅从直系子代而不是子代子收集内容项。发生这种情况的原因是_SplitProjectReferencesByFileExistence使用的ProjectReferenceWithConfiguration列表仅填充在当前项目中,而在子项中为空。空列表使_MSBuildProjectReferenceExistent为空,并终止递归。因此它似乎没有用。
Brans Ds

32

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

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

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

您还可以阅读本文,以帮助您减少项目数量并缩短编译时间。


1
我希望我可以推荐一位以上的Smacchia做法!减少项目数量是关键,而不是拆分解决方案。
Anthony Mastrean 2011年

23

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


2
我有同样的想法,希望在这里找到其他以同样的方式思考的人。但是,我很好奇为什么这篇文章没有更多的赞誉。意见不同的人:您为什么不同意?
bwerks

除非同一个项目构建两次,否则不会发生这种情况,如果它构建一次并且不复制任何文件,为什么会覆盖,访问,共享冲突呢?
paulm,2015年

这个。如果开发工作流程需要构建sln的一个项目,而该解决方案的另一个项目可运行,则将所有内容放在同一输出目录中将是一团糟。在这种情况下,最好将可执行输出文件夹分开。
马丁·巴

10

你是对的。CopyLocal绝对会缩短您的构建时间。如果您的源代码树很大,则应禁用CopyLocal。不幸的是,它并不像完全禁用它那样容易。我已经回答了有关禁用CopyLocal的确切问题,有关如何从MSBUILD覆盖.NET中引用的CopyLocal(Private)设置。看看这个。以及Visual Studio(2008)中大型解决方案的最佳做法。

在我看来,这是有关CopyLocal的更多信息。

实际上实现了CopyLocal以支持本地调试。在准备打包和部署应用程序时,应将项目构建到相同的输出文件夹,并确保在那里拥有所需的所有引用。

我已经在文章MSBuild:创建可靠构建的最佳实践,第2部分中写了关于如何构建大型源树的文章。


8

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


1
Bruno,请参阅上面的后续问题-如果我们分成较小的.sln文件,您如何管理调试与发布方面,然后将其烘焙到我的引用的提示路径中?
戴夫·摩尔

1
我同意这一点,我正在使用的解决方案有大约100个项目,其中只有少数具有3个以上的类,构建时间令人震惊,结果我的前辈将解决方案分为3个,这完全破坏了“查找”的能力。所有参考资料和重构。整个项目可能只需要几秒钟即可完成的少量项目!
乔恩·M

戴夫,好问题。在我工作的地方,我们具有构建脚本,这些脚本可以执行诸如为给定解决方案构建依赖项之类的工作,并将二进制文件放在问题解决方案可以获取它们的位置。这些脚本已针对调试和发布版本进行了参数设置。缺点是要花很多时间来构建上述脚本,但是可以在各个应用程序之间重用它们。按照我的标准,此解决方案效果很好。
jyoungdev

7

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

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

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


5

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


4

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

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

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



2

设置CopyLocal = false将减少构建时间,但在部署过程中可能导致其他问题。

在很多情况下,需要将“本地复制”保留为True时,例如

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

SO问题
何时应将copy-local设置为true,何时应将其设置为true? ”,
错误消息'无法加载一种或多种请求的类型。获取LoaderExceptions属性以获取更多信息。”中描述了可能的问题
和   aaron-stainback  对此问题的答案

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

解决该问题的时间使设置copyLocal = false的好处更加重要。


设置CopyLocal=False肯定会引起一些问题,但是有解决方案。另外,您应该修复博客的格式,这几乎是不可读的,并且说“在部署期间可能发生的错误,我已经被<random company>的<random consultant>警告”,这并不是一个论点。您需要发展。
苏珊·杜彭(SuzanneDupéron)2013年

@GeorgesDupéron,解决问题的时间过长了设置copyLocal = false的好处。提到顾问不是争论,而是信誉,我的博客解释了问题所在。感谢您的反馈。格式化,我将修复它。
Michael Freidgeim

1

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


1

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

<developmentMode developerInstallation="true" />

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

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


有趣的...这将如何与调试版本和发布版本一起使用?
戴夫·摩尔

我不确定是否存在通过DEVPATH加载调试/发布程序集的合适解决方案,该解决方案仅用于共享程序集,我不建议将其用于常规构建。另请注意,使用此技术时,程序集版本和GAC被覆盖。
Aleksandar

1

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

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

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

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


0

通常,仅当您要使用Bin中的DLL与其他地方(GAC,其他项目等)的项目一起使用时,才需要复制本地。

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

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

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


0

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


1
Bruno-是的,这可以与Project References一起使用,这就是我们最初提出100个项目解决方案的原因之一。它在我浏览到预先构建的Debug版本的引用上不起作用-我最终遇到了针对Debug程序集构建的Release应用程序,这是一个问题
Dave Moore,

4
在文本编辑器中编辑项目文件,然后在HintPath中使用$(Configuration),例如<HintPath> .. \ output \ $(Configuration)\ test.dll </ HintPath>。
ultravelocity


0

如果该引用未包含在GAC中,则必须将Copy Local设置为true,这样应用程序才能运行。如果我们确定该引用将预先安装在GAC中,则可以将其设置为false。


0

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

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

  • 您还可以另外告诉Visual Studio它可以使用哪个临时目录进行构建。

实际上,这还不是全部。但这确实打击了我对性能的理解。
100%的处理器使用率和3分钟之内的所有大型项目,所有依赖项。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.