关于 including-generated-files
一项面向 MSBuild 项目的开发技能,介绍如何将构建期间生成的文件加入构建流程,确保其参与输出或编译,并在清理时移除。
主要功能
- 01将生成文件注册到 MSBuild
- 02在编译前加入源文件
- 03为 Clean 目标跟踪文件
- 04按文件类型选择目标时机
- 05使用中间输出路径
适用场景
- 01在构建时编译生成的 C# 文件
- 02复制生成的配置或数据文件
- 03避免构建间残留过期生成文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill including-generated-files安装后在 Claude Code 中输入/including-generated-files确认已加载。
详细介绍
将生成的文件包含到生成过程中
概述
生成过程中生成的文件通常会被生成过程忽略。这会导致一些令人困惑的结果,例如:
- 生成的文件未包含在输出目录中
- 生成的源文件未编译
- Glob 无法捕获生成过程中创建的文件
这是由 MSBuild 的生成阶段工作方式导致的。
简要结论
对于生成过程中生成的代码文件,需要在生成这些文件的目标中,将其添加到 Compile 和 FileWrites 项组:
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
应将生成这些文件的目标挂接到 CoreCompile 和 BeforeCompile 目标之前:BeforeTargets="CoreCompile;BeforeCompile"
为什么生成的文件会被忽略
详细说明请参阅 How MSBuild Builds Projects。
评估阶段
MSBuild 读取项目,导入所有内容,创建属性,展开 Targets 之外 的项的 Glob,并设置生成过程。
执行阶段
MSBuild 使用提供的属性和项运行目标与任务来执行生成。
关键结论: 执行阶段生成的文件在评估阶段尚不存在,因此无法被找到。这对默认使用 Glob 匹配的文件影响尤其明显,例如源文件(.cs)。
解决方案:手动添加生成的文件
在生成过程中生成文件时,需要手动将其添加到生成过程中。具体方法取决于所生成文件的类型。
使用 $(IntermediateOutputPath) 作为生成文件的位置
始终使用 $(IntermediateOutputPath) 作为生成文件的基目录。不要对 obj\ 进行硬编码,也不要手动构造中间路径(例如 obj\$(Configuration)\$(TargetFramework)\)。在某些生成配置中,中间输出路径可能会被重定向到其他位置(例如共享输出目录、CI 环境)。使用 $(IntermediateOutputPath) 可确保目标无论实际路径如何都能正常工作。
始终将生成的文件添加到 FileWrites
每个生成的文件都应添加到 FileWrites 项组中。这样可确保 MSBuild 的 Clean 目标正确删除生成的文件。否则,生成的文件会在多次生成后不断累积,成为过期产物。
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
</ItemGroup>
基本模式(非代码文件)
对于需要复制到输出目录的生成文件(配置文件、数据文件等),请在 BeforeBuild 之前将其添加到 Content 或 None 项中:
<Target Name="IncludeGeneratedFiles" BeforeTargets="BeforeBuild">
<ItemGroup>
<None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
<None Include="$(IntermediateOutputPath)generated\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
<FileWrites Include="$(IntermediateOutputPath)my-generated-file.xyz" />
<FileWrites Include="$(IntermediateOutputPath)generated\*.xyz" />
</ItemGroup>
</Target>
对于生成的源文件(需要编译的代码)
如果生成的是需要编译的 .cs 文件,请使用 BeforeTargets="CoreCompile;BeforeCompile"。这是添加 Compile 项的正确时机——此时文件已生成,但编译器尚未运行。对于某些场景,使用 BeforeBuild 为时过早,而且可能无法与所有 SDK 功能可靠地配合使用。
<Target Name="IncludeGeneratedSourceFiles" BeforeTargets="CoreCompile;BeforeCompile">
<PropertyGroup>
<GeneratedCodeDir>$(IntermediateOutputPath)Generated\</GeneratedCodeDir>
<GeneratedFilePath>$(GeneratedCodeDir)MyGeneratedFile.cs</GeneratedFilePath>
</PropertyGroup>
<MakeDir Directories="$(GeneratedCodeDir)" />
<ItemGroup>
<Compile Include="$(GeneratedFilePath)" />
<FileWrites Include="$(GeneratedFilePath)" />
</ItemGroup>
</Target>
注意:同时指定 CoreCompile 和 BeforeCompile,可确保目标在其中先执行的目标之前运行,从而无论生成过程如何自定义,都能保证可靠的执行顺序。
目标时机
请根据生成文件的类型选择 BeforeTargets 的值:
BeforeTargets="BeforeBuild"—— 对于添加到None或Content的非代码文件。适用于复制到输出目录的场景。BeforeTargets="CoreCompile;BeforeCompile"—— 对于添加到Compile的生成源文件。确保文件在编译器运行之前包含在内。BeforeTargets="AssignTargetPaths"—— 在None和Content项(以及其他项)转换为新项之前的“最后关口”。如果BeforeBuild时机过早,可将此作为备用方案。
Glob 匹配行为
Glob 的行为取决于 执行 Glob 的时间:
| Glob 位置 | 捕获的文件 |
|---|---|
| 目标外部 | 仅在评估阶段(构建开始前)可见的文件 |
| 目标内部 | 目标运行时可见的文件(如果时机合适,也可以捕获生成的文件) |
这就是为什么该解决方案将 <ItemGroup> 放在 <Target> 内部——Glob 会在执行期间运行,此时生成的文件已经存在。