extension-points
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMSBuild Extension Points
MSBuild扩展点
How the MSBuild pipeline provides hooks for SDKs, NuGet packages, repos, and users to inject custom logic.
MSBuild流水线如何为SDK、NuGet包、代码库和用户提供注入自定义逻辑的钩子。
CustomBefore / CustomAfter Hooks
CustomBefore/CustomAfter钩子
Every major file defines import hooks:
.targetsxml
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)' == ''">
$(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)"
Condition="'$(CustomBeforeMicrosoftCommonTargets)' != '' and Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
<!-- ... core targets ... -->
<Import Project="$(CustomAfterMicrosoftCommonTargets)"
Condition="'$(CustomAfterMicrosoftCommonTargets)' != '' and Exists('$(CustomAfterMicrosoftCommonTargets)')"/>每个主要的文件都定义了导入钩子:
.targetsxml
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)' == ''">
$(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)"
Condition="'$(CustomBeforeMicrosoftCommonTargets)' != '' and Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
<!-- ... core targets ... -->
<Import Project="$(CustomAfterMicrosoftCommonTargets)"
Condition="'$(CustomAfterMicrosoftCommonTargets)' != '' and Exists('$(CustomAfterMicrosoftCommonTargets)')"/>Rules
规则
- Default path includes version () for side-by-side installations.
v$(MSBuildToolsVersion) - Always check . The file may not be present on every machine.
Exists() - Append to the property (don't overwrite) to chain multiple hooks:
xml
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets>
$(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>- 默认路径包含版本号(),用于并行安装。
v$(MSBuildToolsVersion) - 始终检查。并非每台机器上都存在该文件。
Exists() - 追加到属性中(不要覆盖)以链式调用多个钩子:
xml
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets>
$(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>Wildcard Import Directories
通配符导入目录
MSBuild imports all files in extension directories, sorted alphabetically:
xml
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*"
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == 'true'
and Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore')" />MSBuild会导入扩展目录中的所有文件,并按字母顺序排序:
xml
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*"
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == 'true'
and Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore')" />Key paths
关键路径
| Property | Resolves to | Scope |
|---|---|---|
| | Per-user |
| MSBuild install directory | Machine-wide |
| | Per-project (NuGet) |
Name files with numeric prefixes for ordering: , .
01-first.props02-second.props| 属性 | 解析路径 | 作用范围 |
|---|---|---|
| | 按用户划分 |
| MSBuild安装目录 | 机器全局 |
| | 按项目划分(NuGet) |
为文件添加数字前缀来控制顺序:、。
01-first.props02-second.propsImport Gating — Control Properties
导入门控——控制属性
Every wildcard import is gated by a boolean property:
xml
<PropertyGroup>
<ImportByWildcardBeforeMicrosoftCommonProps
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
<ImportDirectoryBuildProps
Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
</PropertyGroup>每个通配符导入都由一个布尔属性控制:
xml
<PropertyGroup>
<ImportByWildcardBeforeMicrosoftCommonProps
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
<ImportDirectoryBuildProps
Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
</PropertyGroup>Available control properties
可用控制属性
| Property | What it disables |
|---|---|
| Directory.Build.props auto-discovery |
| Directory.Build.targets auto-discovery |
| NuGet-generated |
| NuGet-generated |
| Machine-level ImportBefore extensions |
| Machine-level ImportAfter extensions |
| 属性 | 禁用内容 |
|---|---|
| Directory.Build.props自动发现 |
| Directory.Build.targets自动发现 |
| obj/目录中NuGet生成的 |
| obj/目录中NuGet生成的 |
| 机器级ImportBefore扩展 |
| 机器级ImportAfter扩展 |
NuGet Package Build Extension Layout
NuGet包构建扩展布局
NuGet packages inject build logic via or folders:
build/buildTransitive/text
MyPackage/
build/
MyPackage.props ← imported via *.props wildcard
MyPackage.targets ← imported via *.targets wildcard
buildTransitive/
MyPackage.props ← imported by transitive consumers
MyPackage.targetsNuGet包通过或文件夹注入构建逻辑:
build/buildTransitive/text
MyPackage/
build/
MyPackage.props ← 通过*.props通配符导入
MyPackage.targets ← 通过*.targets通配符导入
buildTransitive/
MyPackage.props ← 被传递性消费者导入
MyPackage.targetsRules
规则
- File names must match the package ID exactly.
- affects direct consumers only.
build/affects the entire dependency chain.buildTransitive/ - Props are imported early (before the project), targets are imported late (after the project).
- 文件名必须完全匹配包ID。
- 仅影响直接消费者。
build/影响整个依赖链。buildTransitive/ - Props会提前导入(在项目之前),Targets会延后导入(在项目之后)。
Forwarding chain: buildTransitive/
→ build/
→ shared
buildTransitive/build/转发链:buildTransitive/
→ build/
→ 共享
buildTransitive/build/Forward and through their sibling / files (chain ) instead of importing directly. This keeps as the single source of truth with a clear ownership chain, so transitive consumers stay in sync with direct consumers instead of the two layouts drifting apart.
buildTransitive/*.propsbuildTransitive/*.targetsbuild/*.propsbuild/*.targetsbuildTransitive → build → sharedbuildMultiTargeting/build/When is packed per-TFM (, via , a per-TFM , or SDK conventions) while is not, a forwarder must include the TFM segment — dropping it resolves to a non-existent package-root and fails transitive consumers with . Derive the segment from the file's own folder, never (NuGet nearest-match can serve a consumer the folder, so may name a folder that was never restored):
build/build/<tfm>/TfmSpecificPackageFile<PackagePath>buildMultiTargeting/buildTransitive/<tfm>/build/MyPackage.propsMSB4019$(TargetFramework)net10.0net9.0$(TargetFramework)xml
<!-- buildTransitive/<tfm>/MyPackage.props -->
<Import Project="$(MSBuildThisFileDirectory)..\..\build\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('$(MSBuildThisFileDirectory)'))))\MyPackage.props" />通过同级的/文件转发和(形成的链条),而非直接导入。这样可以让作为单一可信源,拥有清晰的归属链,确保传递性消费者与直接消费者保持同步,避免两种布局出现差异。
build/*.propsbuild/*.targetsbuildTransitive/*.propsbuildTransitive/*.targetsbuildTransitive → build → 共享buildMultiTargeting/build/当按TFM打包(,通过、按TFM设置的或SDK约定)而不按TFM打包时,转发器必须包含TFM段——如果省略,会解析到不存在的包根目录,导致传递性消费者出现****错误。TFM段应从文件自身的文件夹中获取,绝不能使用(NuGet就近匹配可能会为消费者提供文件夹,因此可能指向从未还原的文件夹):
build/build/<tfm>/TfmSpecificPackageFile<PackagePath>buildMultiTargeting/buildTransitive/<tfm>/build/MyPackage.propsMSB4019$(TargetFramework)net10.0net9.0$(TargetFramework)xml
<!-- buildTransitive/<tfm>/MyPackage.props -->
<Import Project="$(MSBuildThisFileDirectory)..\..\build\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('$(MSBuildThisFileDirectory)'))))\MyPackage.props" />Source Tree vs Packed Layout
源码树与打包后布局
When reviewing a NuGet build-extension package, the source layout in the repository can legitimately differ from the packed layout inside the produced . This is a common source of false-positive "import points at a missing file" findings.
.nupkgThree packaging mechanisms reshape the layout at pack time:
-
.nuspecmappings — copy a single source file into multiple per-TFM targets:<file src=… target=…>xml<!-- Source tree has ONE shared file: buildTransitive\common\MyAdapter.props Pack rewrites it to per-TFM targets inside the .nupkg: buildTransitive\net462\MyAdapter.props buildTransitive\net8.0\MyAdapter.props buildTransitive\net9.0\MyAdapter.props --> <files> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net462\MyAdapter.props" /> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net8.0\MyAdapter.props" /> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net9.0\MyAdapter.props" /> </files>In theelement, a<file>ending intargetis treated as a folder (filename preserved from\); asrcending in a filename renames the file.target -
.csprojmetadata on<PackagePath>or<None Update=…>items — same effect via SDK pack. Use one item per destination to keep the mapping unambiguous:<Content Include=…>xml<ItemGroup> <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net8.0\MyAdapter.props" /> <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net9.0\MyAdapter.props" /> </ItemGroup>NuGet/SDK pack also accepts a semicolon-separated list () to fan one source out to multiple destinations, but the multi-item form above is harder to misread.PackagePath="buildTransitive\net8.0\;buildTransitive\net9.0\" -
SDK conventions —,
IncludeBuildOutput,BuildOutputTargetFolderautomatically place built outputs underIncludeContentInPackorlib/<tfm>/.build/<tfm>/
审查NuGet构建扩展包时,代码库中的源码布局可能与生成的中的打包后布局合法存在差异。这是导致“导入指向不存在文件”误判的常见原因。
.nupkg三种打包机制会在打包时调整布局:
-
.nuspec映射——将单个源码文件复制到多个按TFM划分的目标位置:<file src=… target=…>xml<!-- 源码树中有一个共享文件: buildTransitive\common\MyAdapter.props 打包后会将其重写到.nupkg中的按TFM划分的目标位置: buildTransitive\net462\MyAdapter.props buildTransitive\net8.0\MyAdapter.props buildTransitive\net9.0\MyAdapter.props --> <files> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net462\MyAdapter.props" /> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net8.0\MyAdapter.props" /> <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net9.0\MyAdapter.props" /> </files>在元素中,以<file>结尾的\会被视为文件夹(文件名保留自target);以文件名结尾的src会重命名文件。target -
中
.csproj或<None Update=…>项的<Content Include=…>元数据——通过SDK打包实现相同效果。为每个目标位置使用一个项,保持映射清晰:<PackagePath>xml<ItemGroup> <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net8.0\MyAdapter.props" /> <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net9.0\MyAdapter.props" /> </ItemGroup>NuGet/SDK打包也接受分号分隔的列表(),将一个源码文件分发到多个目标位置,但上面的多项形式更易读,不易出错。PackagePath="buildTransitive\net8.0\;buildTransitive\net9.0\" -
SDK约定——、
IncludeBuildOutput、BuildOutputTargetFolder会自动将构建输出放置在IncludeContentInPack或lib/<tfm>/下。build/<tfm>/
Implication for reviewers
对审查者的提示
A forwarder like the following inside a packed folder is not a "missing-file" bug, even if the source tree has no directory:
build/net462/buildTransitive/net462/xml
<!-- In packed build/net462/MyAdapter.props -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\..\buildTransitive\net462\MyAdapter.props" />
</Project>Before flagging an unguarded inside a or folder:
<Import>build/<tfm>/buildTransitive/<tfm>/- Look for in the project directory and its immediate parent directory (do not walk further up). Read every
*.nuspecwhose<file target=…>matches the imported path.target - Read the for
.csprojmetadata on<PackagePath>/<None>items.<Content> - Only flag the import if the target path is missing from both the source tree and the projected package layout.
See also AP-13 ("NuGet package forwarders" exception).
msbuild-antipatterns打包后的文件夹中的如下转发器不是“文件缺失”漏洞,即使源码树中没有目录:
build/net462/buildTransitive/net462/xml
<!-- 在打包后的build/net462/MyAdapter.props中 -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\..\buildTransitive\net462\MyAdapter.props" />
</Project>在标记或文件夹中未加防护的之前:
build/<tfm>/buildTransitive/<tfm>/<Import>- 在项目目录及其直接父目录中查找(不要向上遍历更多层级)。读取每个
*.nuspec与导入路径匹配的target。<file target=…> - 读取中
.csproj/<None>项的<Content>元数据。<PackagePath> - 只有当目标路径在源码树和预期包布局中都不存在时,才标记该导入。
另请参阅中的AP-13(“NuGet包转发器”例外)。
msbuild-antipatternsImport Guard Pattern
导入防护模式
The file ensures was imported using a guard property:
.targets.propsxml
<!-- End of Microsoft.Common.props -->
<PropertyGroup>
<MicrosoftCommonPropsHasBeenImported>true</MicrosoftCommonPropsHasBeenImported>
</PropertyGroup>
<!-- Top of Microsoft.Common.CurrentVersion.targets -->
<Import Project="Microsoft.Common.props"
Condition="'$(MicrosoftCommonPropsHasBeenImported)' != 'true'" />This handles projects that only import .
.targets.targets.propsxml
<!-- Microsoft.Common.props的结尾 -->
<PropertyGroup>
<MicrosoftCommonPropsHasBeenImported>true</MicrosoftCommonPropsHasBeenImported>
</PropertyGroup>
<!-- Microsoft.Common.CurrentVersion.targets的开头 -->
<Import Project="Microsoft.Common.props"
Condition="'$(MicrosoftCommonPropsHasBeenImported)' != 'true'" />这可以处理仅导入的项目。
.targetsDirectory.Build Discovery
Directory.Build自动发现
MSBuild walks up the directory tree to find the nearest :
Directory.Build.propsxml
<_DirectoryBuildPropsBasePath>
$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props'))
</_DirectoryBuildPropsBasePath>Only the nearest file is discovered. Nested hierarchies must explicitly import parents:
xml
<!-- src/Directory.Build.props -->
<PropertyGroup>
<_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</_ParentPropsPath>
</PropertyGroup>
<Import Project="$(_ParentPropsPath)" Condition="'$(_ParentPropsPath)' != ''" />MSBuild会向上遍历目录树,查找最近的:
Directory.Build.propsxml
<_DirectoryBuildPropsBasePath>
$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props'))
</_DirectoryBuildPropsBasePath>只会发现最近的文件。嵌套层次结构必须显式导入父级文件:
xml
<!-- src/Directory.Build.props -->
<PropertyGroup>
<_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</_ParentPropsPath>
</PropertyGroup>
<Import Project="$(_ParentPropsPath)" Condition="'$(_ParentPropsPath)' != ''" />Creating Your Own Extension Point
创建自定义扩展点
xml
<!-- MySDK.targets -->
<Project>
<Import Project="MySDK.props" Condition="'$(MySDKPropsImported)' != 'true'" />
<PropertyGroup>
<CustomBeforeMySDK Condition="'$(CustomBeforeMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.Before.targets</CustomBeforeMySDK>
<CustomAfterMySDK Condition="'$(CustomAfterMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.After.targets</CustomAfterMySDK>
</PropertyGroup>
<Import Project="$(CustomBeforeMySDK)" Condition="Exists('$(CustomBeforeMySDK)')" />
<PropertyGroup>
<MySDKBuildDependsOn>BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild</MySDKBuildDependsOn>
</PropertyGroup>
<Target Name="MySDKBuild" DependsOnTargets="$(MySDKBuildDependsOn)" />
<Target Name="BeforeMySDKBuild" />
<Target Name="AfterMySDKBuild" />
<Target Name="CoreMySDKBuild">
<!-- implementation -->
</Target>
<Import Project="$(CustomAfterMySDK)" Condition="Exists('$(CustomAfterMySDK)')" />
</Project>xml
<!-- MySDK.targets -->
<Project>
<Import Project="MySDK.props" Condition="'$(MySDKPropsImported)' != 'true'" />
<PropertyGroup>
<CustomBeforeMySDK Condition="'$(CustomBeforeMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.Before.targets</CustomBeforeMySDK>
<CustomAfterMySDK Condition="'$(CustomAfterMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.After.targets</CustomAfterMySDK>
</PropertyGroup>
<Import Project="$(CustomBeforeMySDK)" Condition="Exists('$(CustomBeforeMySDK)')" />
<PropertyGroup>
<MySDKBuildDependsOn>BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild</MySDKBuildDependsOn>
</PropertyGroup>
<Target Name="MySDKBuild" DependsOnTargets="$(MySDKBuildDependsOn)" />
<Target Name="BeforeMySDKBuild" />
<Target Name="AfterMySDKBuild" />
<Target Name="CoreMySDKBuild">
<!-- implementation -->
</Target>
<Import Project="$(CustomAfterMySDK)" Condition="Exists('$(CustomAfterMySDK)')" />
</Project>Common Pitfalls
常见陷阱
- Missing on optional imports causes build failures when files are absent. Exception: imports inside published
Exists()andbuild/<tfm>/folders of a NuGet package are a package contract — the target is guaranteed by the packed layout (see "Source Tree vs Packed Layout" above). Don't guard them and don't flag them.buildTransitive/<tfm>/ - Overwriting Custom properties* drops prior hooks. Append with separator.
; - NuGet package file names not matching package ID silently skips the import.
- Nested Directory.Build.props without parent import loses repo-root settings.
- 可选导入缺少检查会在文件不存在时导致构建失败。例外:NuGet包已发布的
Exists()和build/<tfm>/文件夹中的导入属于包契约——目标文件由打包后布局保证存在(参见上文“源码树与打包后布局”)。不要为这些导入添加防护,也不要标记它们。buildTransitive/<tfm>/ - 覆盖Custom*属性会丢弃之前的钩子。使用分隔符追加内容。
; - NuGet包文件名与包ID不匹配会导致导入被静默跳过。
- 嵌套的Directory.Build.props未导入父级文件会丢失代码库根目录的设置。