mcpField
C
copy-to-output-directory

copy-to-output-directory

github.com/dotnet开发5.8k2.9k 次安装1 天前更新
代码仓库

关于 copy-to-output-directory

此指导 Skill 帮助开发者选择 MSBuild 的 `CopyToOutputDirectory` 模式及其发布对应项。它说明复制行为和增量成本,并介绍如何处理被修改的输出文件或旧项目中的 `Always` 设置。

主要功能

  1. 01说明四种复制模式
  2. 02按文件行为选择模式
  3. 03介绍 `IfDifferent` 的限制
  4. 04批量处理旧项目的 `Always` 设置

适用场景

  1. 01为运行时文件选择复制模式
  2. 02重置构建间被修改的输出文件
  3. 03减少旧项目中的重复复制

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill copy-to-output-directory

安装后在 Claude Code 中输入/copy-to-output-directory确认已加载。

详细介绍

选择 CopyToOutputDirectory 模式

概述

CopyToOutputDirectory 元数据(以及对应的发布元数据 CopyToPublishDirectory)用于控制某个项(Content、None、EmbeddedResource 或 Compile)是否复制到构建输出目录旁边,以及在什么条件下复制。选择错误的模式会导致 bin/ 中的文件过时,或每次构建都产生不必要的性能开销。

截至 MSBuild 17.13 / .NET SDK 9.0.2xx,共有四种取值:

模式复制条件增量开销典型用途
Never(默认)从不复制无运行时不需要的文件
PreserveNewest源文件比目标文件新(或目标文件不存在)低(时间戳检查)常见情况——你会编辑的源文件
Always每次构建都无条件复制高——即使构建没有任何变化,每次也会复制旧式权宜方案;应避免使用(见下文)
IfDifferent源文件与目标文件存在差异(无论源文件较新还是较旧,或文件大小不同,或目标文件不存在)低(时间戳 + 大小检查)目标文件可能在构建之间被修改
<ItemGroup>
  <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  <None Include="testdata\seed.db"  CopyToOutputDirectory="IfDifferent" />
</ItemGroup>

你可以使用上面展示的属性形式,也可以使用子元素形式:

<None Include="testdata\seed.db">
  <CopyToOutputDirectory>IfDifferent</CopyToOutputDirectory>
</None>

为什么 Always 通常不是正确选择

Always 会在每次构建时重新复制文件,包括其他情况下本应干净的增量构建/无操作构建。对于包含大量或大型内容文件的项目,这会造成可衡量且反复出现的开销,也是“为什么无操作构建不能瞬间完成?”这类问题的常见原因。

过去,Always 是处理特定场景的唯一方法:目标文件可能在构建之间发生变化,例如 SQLite 数据库、存储/状态文件,或测试运行会修改的配置文件。使用 PreserveNewest 时,如果目标文件被修改(导致其时间戳比源文件新),MSBuild 就不会还原原始源文件,因为源文件不再较新。为强制将文件恢复到已知的良好状态,人们便会使用 Always,并因此在每次构建时都承担复制开销。

IfDifferent:无论哪个文件较新,只要有差异就复制

IfDifferent 正是针对这种场景的解决方案。只要 MSBuild 认为两个文件存在差异,就会将源文件复制到目标文件,无论源文件的时间戳比目标文件新还是旧、文件大小是否不同,或目标文件是否不存在;若根据 MSBuild 的启发式判断目标文件未变,则会跳过复制。

在底层,_CopyDifferingSourceItemsToOutputDirectory 目标会使用设置了 SkipUnchangedFiles="true" 的 Copy 任务。“未更改”检查是一种启发式判断:它只比较最后写入时间戳和文件大小,而不比较内容哈希。因此,如果目标文件经过编辑后,大小和时间戳都与源文件相同,就会被视为未更改,不会重新复制。实际上,这会在下次构建时将已修改的目标文件还原为源文件版本(也就是人们使用 Always 的原因),同时避免每次构建都无条件复制。

以下情况适合使用 IfDifferent:

  • 测试运行或应用本身会写入复制后的文件(数据库、缓存、状态/存储文件、可编辑配置),而你希望每次构建都将其重置为源文件版本。
  • 你使用 Always 只是为了“让输出与源文件保持同步”,而不是真的需要每次构建都复制。
<ItemGroup>
  
  <None Include="fixtures\catalog.db" CopyToOutputDirectory="IfDifferent" />
</ItemGroup>

使用 $(SkipUnchangedFilesOnCopyAlways) 全局优化 Always

如果现有代码库中有许多 CopyToOutputDirectory="Always" 项目,并且希望在不逐项编辑的情况下提升性能,请设置此属性:

<PropertyGroup>
  <SkipUnchangedFilesOnCopyAlways>true</SkipUnchangedFilesOnCopyAlways>
</PropertyGroup>

这会让 _CopyOutOfDateSourceItemsToOutputDirectoryAlways 目标将 SkipUnchangedFiles="true" 传递给其 Copy 任务,因此只有 Always 项目确实存在差异时才会复制,从而让 Always 实际上获得与 IfDifferent 相同的跳过未更改文件行为。

  • 为保持向后兼容,默认值为 false(传统的 Always 会在每次构建时都复制)。
  • 在 Directory.Build.props 中设置此属性,即可一次性为整个仓库启用。
  • 尽可能将单个项目转换为 IfDifferent;当批量、无侵入式启用更实用时,再使用此属性。

各模式在构建中的流转方式

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。

评论

支持 Markdown