其他
需要给 MSBuild 构建注入或审查扩展点时使用:安全配置…
在 MSBuild 项目中实现或诊断可扩展构建,用钩子和 NuGet 包布局安全插入自定义 .props/.targets,并避免导入被遗漏、覆盖或误报。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 38/100 ×0.15
适用场景
需要给 MSBuild 构建注入或审查扩特定
解决思路
- 在 `.targets` 文件中定义 CustomBefore/CustomAfter 钩子:若 `$(CustomBeforeMicrosoftCommonTargets)` 为空,则默认设为 `$(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets`;导入条件是属性非空且 `Exists()` 通过;对 `CustomAfterMicrosoftCommonTargets` 同样处理。
- 默认路径包含 `v$(MSBuildToolsVersion)`,以支持 MSBuild 并行安装(side-by-side)。
- 所有可选导入都检查 `Exists()`,因为文件可能不在每台机器上。
- 链式挂多个钩子时不要覆盖 `Custom*` 属性,而是追加:`$(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets`。
- 用通配符导入扩展目录下所有文件:导入 `$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*`,条件是 `$(ImportByWildcardBeforeMicrosoftCommonProps)` 为 `true` 且该目录 `Exists()`;MSBuild 按字母顺序导入。
- 使用扩展路径:`$(MSBuildUserExtensionsPath)` 解析到 `%APPDATA%\Microsoft\MSBuild`(按用户);`$(MSBuildExtensionsPath)` 是 MSBuild 安装目录(机器级);`$(MSBuildProjectExtensionsPath)` 是 `obj/` 目录(按项目,NuGet 使用)。
- 对扩展目录中的文件使用数字前缀控制导入顺序:`01-first.props`、`02-second.props`。
- 对每个通配符导入加布尔控制属性;若属性为空则默认 `true`:例如 `ImportByWildcardBeforeMicrosoftCommonProps`、`ImportDirectoryBuildProps`。
- 可用控制属性禁用对应导入:`ImportDirectoryBuildProps` 禁用 Directory.Build.props 自动发现;`ImportDirectoryBuildTargets` 禁用 Directory.Build.targets 自动发现;`ImportProjectExtensionProps` 禁用 obj/ 中 NuGet 生成的 `*.props`;`ImportProjectExtensionTargets` 禁用 obj/ 中 NuGet 生成的 `*.targets`;`ImportByWildcardBefore*` 禁用机器级 ImportBefore 扩展;`ImportByWildcardAfter*` 禁用机器级 ImportAfter 扩展。
- NuGet 包通过 `build/` 或 `buildTransitive/` 文件夹注入构建逻辑:放 `MyPackage.props`(通过 `*.props` 通配符导入)和 `MyPackage.targets`(通过 `*.targets` 通配符导入);`buildTransitive/` 供传递依赖消费者导入。
- NuGet 包扩展文件名必须与包 ID 完全匹配,否则导入会被静默跳过。
- `build/` 只影响直接消费者;`buildTransitive/` 影响整条依赖链。
- `.props` 在项目之前早期导入;`.targets` 在项目之后晚期导入。
- 转发链做法:让 `buildTransitive/*.props` 和 `buildTransitive/*.targets` 通过同级的 `build/*.props` / `build/*.targets` 文件转发(链 `buildTransitive → build → shared`),不要直接导入 `buildMultiTargeting/`,以保持 `build/` 为单一事实来源。
- 当 `build/` 按 TFM 打包为 `build/<tfm>/`(通过 `TfmSpecificPackageFile`、按 TFM 的 `<PackagePath>` 或 SDK 约定),而 `buildMultiTargeting/` 不是按 TFM 时,`buildTransitive/<tfm>/` 转发器必须包含 TFM 段;漏掉会解析到不存在的包根 `build/MyPackage.props` 并使传递消费者报 `MSB4019`。
- 转发器的 TFM 段应从文件自身所在文件夹推导,绝不要用 `$(TargetFramework)`(NuGet 最近匹配可能把 `net10.0` 消费者匹配到 `net9.0` 文件夹)。可用导入表达式:`$(MSBuildThisFileDirectory)..\..\build\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('$(MSBuildThisFileDirectory)'))))\MyPackage.props`。
- 审查 NuGet 构建扩展包时,要意识到源码树布局可以合法地不同于 `.nupkg` 内的打包布局,避免误报“导入指向缺失文件”。
- 打包时三种机制会重塑布局之一:`.nuspec` 的 `<file src=… target=…>` 映射可把一个源文件复制到多个按 TFM 的目标;`target` 以 `\` 结尾表示文件夹(保留 `src` 文件名),以文件名结尾表示重命名。
- 打包时三种机制会重塑布局之二:`.csproj` 中 `<None Update=…>` 或 `<Content Include=…>` 项上的 `<PackagePath>` 元数据可通过 SDK pack 达到同样效果;为每个目标使用一个 item 以保持映射清晰;也可用分号列表 `PackagePath='buildTransitive\net8.0\;buildTransitive\net9.0\'`,但多 item 形式更不易误读。
- 打包时三种机制会重塑布局之三:SDK 约定 `IncludeBuildOutput`、`BuildOutputTargetFolder`、`IncludeContentInPack` 会自动把构建输出放到 `lib/<tfm>/` 或 `build/<tfm>/`。
- 审查 `build/<tfm>/` 或 `buildTransitive/<tfm>/` 中未加守卫的 `<Import>` 前:先在项目目录及其直接父目录查找 `*.nuspec`(不要继续向上),读取所有 `target` 匹配导入路径的 `<file target=…>`。
- 再读取 `.csproj` 中 `<None>`/`<Content>` 项上的 `<PackagePath>` 元数据。
- 只有目标路径在源码树和投影后的包布局中都缺失时,才标记该导入有问题。
- 可参考 `msbuild-antipatterns` AP-13(NuGet package forwarders 例外)。
- 导入守卫模式:在 `Microsoft.Common.props` 末尾设置 `<MicrosoftCommonPropsHasBeenImported>true</MicrosoftCommonPropsHasBeenImported>`。
- 在 `Microsoft.Common.CurrentVersion.targets` 顶部仅当 `$(MicrosoftCommonPropsHasBeenImported)` 不是 `true` 时导入 `Microsoft.Common.props`,以处理只导入 `.targets` 的项目。
- Directory.Build 发现:MSBuild 用 `$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props'))` 向上查找最近的 `Directory.Build.props`。
- 只有最近的文件会被发现;嵌套层级必须显式导入父级:用 `$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))` 得到 `_ParentPropsPath`,再在非空时导入 `$(_ParentPropsPath)`。
- 创建自己的扩展点:在 `MySDK.targets` 中仅当 `$(MySDKPropsImported)` 不是 `true` 时导入 `MySDK.props`。
- 定义 `CustomBeforeMySDK`(默认 `$(MSBuildProjectDirectory)\MySDK.Before.targets`)和 `CustomAfterMySDK`(默认 `$(MSBuildProjectDirectory)\MySDK.After.targets`),仅当为空时赋默认值。
- 在核心逻辑前仅当 `Exists($(CustomBeforeMySDK))` 时导入 `$(CustomBeforeMySDK)`;核心逻辑后用同样条件导入 `$(CustomAfterMySDK)`。
- 定义 `MySDKBuildDependsOn` 为 `BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild`;定义 `MySDKBuild` 目标 `DependsOnTargets=$(MySDKBuildDependsOn)`,以及空的 `BeforeMySDKBuild`、`AfterMySDKBuild` 和实现 `CoreMySDKBuild`。
- 可选导入缺少 `Exists()` 会在文件不存在时导致构建失败;例外:已发布 NuGet 包的 `build/<tfm>/` 和 `buildTransitive/<tfm>/` 文件夹内的导入是包契约,目标由打包布局保证,不要加守卫也不要将其标为问题。
- 不要覆盖 `Custom*` 属性,否则会丢掉先前的钩子;用 `;` 分隔追加。
- NuGet 包文件名与包 ID 不匹配会静默跳过导入。
- 嵌套 `Directory.Build.props` 若不导入父级,会丢失仓库根设置。
适用边界 · 注意事项
- 不适用于非 MSBuild 构建系统。
- 不用于 target authoring patterns(应使用 target-authoring)。
- 不用于 props vs targets placement(应使用 directory-build-organization)或通用 MSBuild 反模式(应使用 msbuild-antipatterns)。