研发协作
MSBuild 复制到输出目录模式选择:避免 Always…
在 MSBuild/.NET 项目中为 Content/None/EmbeddedResource/Compile 项选择 CopyToOutputDirectory(及 CopyToPublishDirectory)模式,以消除 Always 的每次构建性能损耗,并在构建间重置被修改的输出文件。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
MSBuild 复制到输出目录模式选择:通用
解决思路
- 根据需求选择四种模式之一:Never(默认,从不复制,适用于运行时不需要的文件)、PreserveNewest(源文件比目标新或目标缺失时复制,时间戳检查开销低,适用于常规编辑的源文件)、Always(每次构建无条件复制,开销高,应避免)、IfDifferent(源与目标任一方向不同即复制,时间戳+大小检查开销低,适用于目标可能在构建间被修改的场景)。
- 使用属性形式或子元素形式配置,例如 <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />,或 <None Include="testdata\seed.db"><CopyToOutputDirectory>IfDifferent</CopyToOutputDirectory></None>。
- 避免使用 Always:它会在每次构建(包括无操作的增量构建)中重新复制文件,在包含大量或大体积内容文件的项目中造成可测量的重复开销,是“无操作构建为何不即时完成”的常见原因。
- 当目标文件可能在构建间被修改(如 SQLite 数据库、存储/状态文件、被测试运行修改的配置文件)时,使用 IfDifferent 而非 Always:PreserveNewest 在目标被修改(时间戳比源新)时不会恢复源文件,而 IfDifferent 会在源与目标不同(源更新或更旧、大小不同、目标缺失)时复制源覆盖目标,从而在下次构建时把被修改的目标重置回源版本。
- 了解 IfDifferent 的底层机制:_CopyDifferingSourceItemsToOutputDirectory 目标使用 Copy 任务并设置 SkipUnchangedFiles="true",其“未更改”判断是启发式的,仅比较最后写入时间戳和文件大小,不做内容哈希,因此被编辑成与源相同大小和时间戳的目标会被视为未更改而不重新复制。
- 若代码库中已有大量 CopyToOutputDirectory="Always" 项且不想逐项修改,可设置 <SkipUnchangedFilesOnCopyAlways>true</SkipUnchangedFilesOnCopyAlways>,使 _CopyOutOfDateSourceItemsToOutputDirectoryAlways 目标向其 Copy 任务传递 SkipUnchangedFiles="true",让 Always 项仅在确实不同时才复制。
- 该属性默认值为 false 以保持向后兼容(经典 Always 即每次构建都复制);可在 Directory.Build.props 中设置以让整个仓库一次性启用。
- 优先将单个项转换为 IfDifferent;当批量、非侵入式启用更实际时,再使用 $(SkipUnchangedFilesOnCopyAlways) 属性。
- 了解模式在构建中的流转:GetCopyToOutputDirectoryItems 按 CopyToOutputDirectory 值对项分桶,三个复制目标作为 _CopySourceItemsToOutputDirectory(由 CopyFilesToOutputDirectory 调用)的依赖执行:_CopyOutOfDateSourceItemsToOutputDirectory 处理 PreserveNewest 项(通过 Inputs/Outputs 时间戳比较实现增量)、_CopyOutOfDateSourceItemsToOutputDirectoryAlways 处理 Always 项(除非 $(SkipUnchangedFilesOnCopyAlways) 为 true,否则无条件复制)、_CopyDifferingSourceItemsToOutputDirectory 处理 IfDifferent 项(SkipUnchangedFiles="true")。
- 注意所有被复制的文件都会登记到 FileWrites,因此 dotnet clean 会删除它们。
- 注意传递复制:标记为 Always、PreserveNewest 或 IfDifferent 的项会通过 ProjectReference(经由 _CopyToOutputDirectoryTransitiveItems)流向引用项目,Never 项则不会;IfDifferent 与 Always/PreserveNewest 一样参与 ClickOnce 发布项收集。
- 注意版本要求:IfDifferent 和 $(SkipUnchangedFilesOnCopyAlways) 需要 MSBuild 17.13 或更高版本(.NET SDK 9.0.2xx+ / Visual Studio 2022 17.13+);在旧工具集上该值不被识别,不会匹配公共目标中的 Always/PreserveNewest/IfDifferent 条件,项会被静默地不复制;若必须支持旧 SDK,需按工具集做条件判断,或通过 global.json 要求最低 SDK 版本。
- 按快速决策指南选择:运行时不需要该文件 → Never(或省略,它是默认值);常规编辑的源文件 → PreserveNewest;目标在构建间被修改且必须重置为源 → IfDifferent;确实需要每次构建都获得全新副本 → Always(罕见);受困于大量遗留 Always 且想在不编辑的情况下获得性能收益 → 保留 Always 但设置 $(SkipUnchangedFilesOnCopyAlways)=true。
适用边界 · 注意事项
- 不要用于通用增量构建诊断(应使用 incremental-build 技能)。
- 不要用于非 MSBuild 构建系统。
- 在低于 MSBuild 17.13(.NET SDK 9.0.2xx / VS 2022 17.13 之前)的工具集上不要使用 IfDifferent 或 $(SkipUnchangedFilesOnCopyAlways),否则项会被静默地不复制。