什么是“复制本地”和项目参考的最佳做法?
我有一个大的C#解决scheme文件(~100个项目),我正在努力提高生成时间。 我认为“复制本地”在许多情况下对我们来说是浪费的,但我想知道最佳实践。
在我们的.sln中,取决于程序集B的应用程序A取决于程序集C.在我们的例子中,有几十个“B”和一些“C”。 由于这些都包含在.sln中,我们正在使用项目引用。 所有程序集目前都build立到$(SolutionDir)/ Debug(或Release)。
默认情况下,Visual Studio将这些项目引用标记为“Copy Local”,导致每构build一个“B”,每个“C”被复制到$(SolutionDir)/ Debug中一次。 这似乎是浪费。 如果我只是closures“复制本地”,会出现什么问题? 其他大系统的人做什么?
跟进:
很多的反应表明把构build分解成更小的.sln文件…在上面的例子中,我将首先构build基础类“C”,然后是大部分模块“B”,然后是一些应用程序“一个”。 在这个模型中,我需要从B引用非项目引用。我遇到的问题是“debugging”或“释放”被烘焙到提示path中,并且构build了“B”版本的Release版本。针对“C”的debugging版本。
对于那些将构build分解成多个.sln文件的人,你如何解决这个问题?
在之前的一个项目中,我使用了一个带有项目引用的大型解决scheme,并碰到了一个性能问题。 解决scheme有三个方面:
-
始终将“复制本地”属性设置为false,并通过自定义的msbuild步骤执行此操作
-
将每个项目的输出目录设置为相同的目录(最好相对于$(SolutionDir)
-
随框架一起提供的默认cs目标会计算要复制到当前正在构build的项目的输出目录中的引用的集合。 由于这需要根据“参考”关系计算传递闭包,所以这可能会非常昂贵。 我的解决方法是重新定义
GetCopyToOutputDirectoryItems
目标在导入Microsoft.CSharp.targets
后在每个项目中导入的公共目标文件(例如Common.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>
这在几小时内(主要是由于内存限制)在特定时间减less了我们的构build时间,几分钟。
重新定义的GetCopyToOutputDirectoryItems
可以通过将C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets
的行2,438-2,450和2,474-2,524复制到GetCopyToOutputDirectoryItems
中来创build。
为了完整性,最终的目标定义将变成:
<!-- 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>
有了这个解决方法,我发现在一个解决scheme中有多达120个项目是可行的,这有一个主要的好处,即项目的构build顺序仍然可以由VS来确定,而不是通过手工分割您的解决scheme。
我build议你阅读Patric Smacchia关于这个主题的文章:
- 通过.NET程序集和Visual Studio项目对代码库进行分区 – > 每个Visual Studio项目是否都应该在自己的程序集中? “Copy Local = True”是什么意思?
- 从NUnit代码库学到的经验 – > VisualStudio项目参考+复制本地真正的选项是邪恶的! )
- 分析CruiseControl.NET的代码库 – > 将复制本地引用程序集选项设置为True的错误用法)
CC.Net VS项目依赖复制本地引用程序集选项设置为true。 […]不仅这会大大增加编译时间(在NUnit的情况下为x3),而且还会弄乱你的工作环境。 最后但并非最不重要的是,这样做会潜在问题版本化的风险。 顺便说一句,NDepend会发出警告,如果它在2个不同的目录中find2个具有相同名称但不相同的内容或版本的程序集。
正确的做法是定义2个目录$ RootDir $ \ bin \ Debug和$ RootDir $ \ bin \ Release,并configuration您的VisualStudio项目以在这些目录中发出程序集。 所有项目引用都应引用Debug目录中的程序集。
您也可以阅读这篇文章,以帮助您减less项目编号,并缩短编译时间。
我build议复制local = false几乎所有的项目,除了在依赖关系树的顶部。 并在顶部的所有参考设置复制local = true。 我看到很多人build议共享一个输出目录; 我认为这是一个基于经验的可怕的想法。 如果您的启动项目持有对dll的引用,即任何其他项目持有对您的引用,都会遇到访问\共享冲突,即使将所有内容复制到本地= false,您的构build也将失败。 这个问题非常烦人,难以追查。 我完全build议远离分片输出目录,而不是让依赖链顶部的项目将所需的程序集写入相应的文件夹。 如果你没有在“顶部”的项目,那么我会build议一个后期制作的副本,让一切都在正确的地方。 另外,我会尽量记住debugging的方便性。 任何exe项目,我仍然离开copy local = true,所以F5的debugging经验将工作。
你是对的。 CopyLocal绝对会杀死你的构build时间。 如果你有一个大的源代码树,那么你应该禁用CopyLocal。 不幸的是,它不应该干净地禁用它。 我已经回答了有关禁用CopyLocal这个确切的问题, 我如何覆盖MSBUILD中.NET引用的CopyLocal(Private)设置 。 一探究竟。 以及Visual Studio(2008)中大型解决scheme的最佳实践。
这里有一些关于CopyLocal的信息,就像我看到的那样。
CopyLocal实际上是为了支持本地debugging而实现的。 当你准备你的应用程序的打包和部署,你应该build立你的项目到相同的输出文件夹,并确保你有所有你需要的引用。
我已经写了关于如何处理构build大型源代码树的文章MSBuild:创build可靠构build的最佳实践,第2部分 。
在我看来,有100个项目的解决scheme是一个大错误。 您可能可以将您的解决scheme拆分成有效的逻辑小单元,从而简化了维护和构build。
我很惊讶没有人提到使用硬链接。 它不是复制文件,而是创build一个到原始文件的硬链接。 这可以节省磁盘空间,并大大加快构build。 这可以在命令行上启用,具有以下属性:
/ P:CreateHardLinksForAdditionalFilesIfPossible = TRUE; CreateHardLinksForCopyAdditionalFilesIfPossible = TRUE; CreateHardLinksForCopyFilesToOutputDirectoryIfPossible = TRUE; CreateHardLinksForCopyLocalIfPossible = TRUE; CreateHardLinksForPublishFilesIfPossible =真
您也可以将其添加到中央导入文件,以便您的所有项目也可以获得这种好处。
如果通过项目引用或解决scheme级别的依赖关系定义了依赖关系结构,则可以安全地转向“复制本地”我甚至会说这是一个最佳实践,因为这样可以让您使用MSBuild 3.5并行地运行构build通过/ maxcpucount)没有不同的进程在试图复制引用程序集时互相跳闸。
我们的“最佳实践”是避免许多项目的解决scheme。 我们有一个名为“matrix”的目录和当前版本的程序集,所有的引用都来自这个目录。 如果你改变了一些项目,你可以说“现在改变已经完成”,你可以将程序集复制到“matrix”目录中。 所以依赖这个程序集的所有项目都会有当前(最新)的版本。
如果解决scheme中的项目很less,则构build过程要快得多。
您可以使用Visual Studiomacros或“菜单 – >工具 – >外部工具…”自动执行“将程序集复制到matrix目录”步骤。
设置CopyLocal = false将减less构build时间,但在部署过程中会导致不同的问题。
有很多场景,当你需要复制本地“左”为真,例如
- 顶级项目,
- 二级依赖关系,
- 由reflection调用的DLL
SO问题中描述的可能的问题
“ 何时应该复制本地设置为真,什么时候不应该? ”,
“ 错误消息”无法加载一个或多个请求的types,请检索LoaderExceptions属性以获取更多信息。 “
和aaron-stainback对这个问题的答案 。
我设置CopyLocal = false的经验并不成功。 看到我的博客文章“不要改变”复制本地“项目引用为false,除非明白子序列。”
解决问题的时间超过了设置copyLocal = false的好处。
您不需要更改CopyLocal值。 您只需要为解决scheme中的所有项目预定义一个公共$(OutputPath),并将$(UseCommonOutputDirectory)预置为true即可。 看到这个: http : //blogs.msdn.com/b/kirillosenkov/archive/2015/04/04/using-a-common-intermediate-and-output-directory-for-your-solution.aspx
我倾向于build立一个共同的目录(如.. \斌),所以我可以创build小testing解决scheme。
您可以尝试使用一个文件夹,其中项目之间共享的所有程序集都将被复制,然后创build一个DEVPATH环境variables并进行设置
<developmentMode developerInstallation="true" />
在每个开发人员的工作站上的machine.config文件中。 您唯一需要做的是复制DEVPATHvariables指向的文件夹中的任何新版本。
如果可能,也可以将解决scheme分成几个较小的解
这可能不是最好的,但这是我的工作方式。
我注意到托pipeC ++将其所有二进制文件转储到$(SolutionDir)/ DebugOrRelease中。 所以我也抛弃了我所有的C#项目。 我还closures了“解决scheme中项目的所有引用的”复制本地“。 在我的小型项目解决scheme中,我有了改进的构build时间。 此解决scheme是C#,托pipeC ++,本地C ++,C#webservice和安装程序项目的混合。
也许有些东西坏了,但是由于这是我工作的唯一方式,所以我没有注意到它。
弄清楚我打破了什么会很有意思。
通常情况下,如果您希望使用Bin中的DLL与其他地方(GAC,其他项目等)的项目,则只需复制“本地”
我倾向于同意其他人,如果可能的话,你也应该尝试打破这个解决scheme。
您还可以使用configurationpipe理器在一个解决scheme中创build不同的生成configuration,只生成一组给定的项目。
如果所有的100个项目都相互依赖,那么看起来很奇怪,所以你应该能够分解它或者使用configurationpipe理器来帮助你自己。
您可以让您的项目引用指向dll的debugging版本。 在你的msbuild脚本上,你可以设置/p:Configuration=Release
,这样你的应用程序和所有卫星程序集都将有一个发行版本。
如果你想拥有一个中心位置来引用一个使用复制本地错误的DLL,除非你这样做,否则没有GAC就会失败。
如果引用不包含在GAC中,我们必须将Copy Local设置为true,这样应用程序才能工作,如果我们确定引用将被预安装在GAC中,则可以将其设置为false。
那么,我当然不知道这些问题是如何解决的,但是我已经联系到了一个构build解决scheme,这个解决scheme可以帮助所有创build的文件在符号链接的帮助下放置一个虚拟盘 。
- c:\ solution文件夹\ bin – > ramdisk r:\ solution文件夹\ bin \
-
c:\ solution文件夹\ obj – > ramdisk r:\ solution文件夹\ obj \
-
您还可以另外告诉visual studio它可以用于构build的临时目录。
其实这并不是它所做的。 但这真的让我对演出有了一些了解。
100%的处理器使用和一个巨大的项目在3分钟以下的所有依赖项。