extension-points について
MSBuild SDK、NuGet パッケージ、リポジトリのビルド処理を管理する開発者向けのスキルです。拡張フック、インポート順、パッケージ構成と NuGet 転送ファイルの確認方法を扱います。
主な機能
- 01CustomBefore/CustomAfter フックの設定
- 02拡張ディレクトリとインポート制御の把握
- 03NuGet の build/buildTransitive ファイルの構成
- 04ソースツリーとパッケージ構成の差異の確認
- 05Directory.Build.props の探索階層の処理
活用シーン
- 01SDK やリポジトリへのビルド処理の追加
- 02NuGet 拡張ファイルのインポート失敗の調査
- 03マルチターゲットパッケージの転送パスの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill extension-pointsインストール後、Claude Code で/extension-pointsを入力して読み込まれたか確認します。
詳細
MSBuild の拡張ポイント
MSBuild パイプラインが、SDK、NuGet パッケージ、リポジトリ、ユーザーにカスタムロジックを挿入するフックをどのように提供するかを説明します。
CustomBefore / CustomAfter フック
主要な .targets ファイルはすべて、インポートフックを定義しています。
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)' == ''">
$(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)"
Condition="'$(CustomBeforeMicrosoftCommonTargets)' != '' and Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
<Import Project="$(CustomAfterMicrosoftCommonTargets)"
Condition="'$(CustomAfterMicrosoftCommonTargets)' != '' and Exists('$(CustomAfterMicrosoftCommonTargets)')"/>
ルール
- 既定のパスには、並行インストールに対応するため、バージョン(
v$(MSBuildToolsVersion))が含まれます。 - 常に
Exists()を確認してください。ファイルはすべてのマシンに存在するとは限りません。 - 複数のフックを連鎖させるには、プロパティを上書きせずに追加してください。
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets>
$(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets
</CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>
ワイルドカードインポートディレクトリ
MSBuild は、拡張ディレクトリ内のすべてのファイルをアルファベット順にインポートします。
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*"
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == 'true'
and Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore')" />
主なパス
| プロパティ | 解決先 | スコープ |
|---|---|---|
$(MSBuildUserExtensionsPath) | %APPDATA%\Microsoft\MSBuild | ユーザー単位 |
$(MSBuildExtensionsPath) | MSBuild のインストールディレクトリ | マシン全体 |
$(MSBuildProjectExtensionsPath) | obj/ ディレクトリ | プロジェクト単位(NuGet) |
順序を指定するには、ファイル名に数値の接頭辞を付けます:01-first.props、02-second.props。
インポートの制御 — 制御プロパティ
各ワイルドカードインポートは、ブール値プロパティによって制御されます。
<PropertyGroup>
<ImportByWildcardBeforeMicrosoftCommonProps
Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
<ImportDirectoryBuildProps
Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
</PropertyGroup>
使用可能な制御プロパティ
| プロパティ | 無効にするもの |
|---|---|
ImportDirectoryBuildProps | Directory.Build.props の自動検出 |
ImportDirectoryBuildTargets | Directory.Build.targets の自動検出 |
ImportProjectExtensionProps | obj/ 内の NuGet が生成した *.props |
ImportProjectExtensionTargets | obj/ 内の NuGet が生成した *.targets |
ImportByWildcardBefore* | マシンレベルの ImportBefore 拡張機能 |
ImportByWildcardAfter* | マシンレベルの ImportAfter 拡張機能 |
NuGet パッケージのビルド拡張レイアウト
NuGet パッケージは、build/ または buildTransitive/ フォルダーを通じてビルドロジックを挿入します。
MyPackage/
build/
MyPackage.props ← imported via *.props wildcard
MyPackage.targets ← imported via *.targets wildcard
buildTransitive/
MyPackage.props ← imported by transitive consumers
MyPackage.targets
ルール
- ファイル名は、パッケージ ID と完全に一致する必要があります。
build/は直接の利用者のみに影響します。buildTransitive/は依存関係チェーン全体に影響します。- Props は早い段階(プロジェクトより前)にインポートされ、Targets は遅い段階(プロジェクトの後)にインポートされます。
転送チェーン:buildTransitive/ → build/ → shared
buildTransitive/*.props と buildTransitive/*.targets は、buildMultiTargeting/ を直接インポートするのではなく、同階層の build/*.props / build/*.targets ファイルを通じて転送してください(buildTransitive → build → shared と連鎖させます)。これにより、所有関係が明確なまま build/ を唯一の信頼できる情報源として維持でき、2 つのレイアウトが別々に変化することなく、推移的な利用者を直接の利用者と同期させられます。
build/ が TFM ごと(TfmSpecificPackageFile、TFM ごとの <PackagePath>、または SDK の規約を通じた build/<tfm>/)にパックされ、buildMultiTargeting/ はそうでない場合、buildTransitive/<tfm>/ フォワーダーには TFM セグメントを含める必要があります。これを省略すると、パッケージルートにある存在しない build/MyPackage.props を参照することになり、推移的な利用者で MSB4019 が発生します。セグメントはファイル自身のフォルダーから導出し、$(TargetFramework) は使用しないでください(NuGet の最近傍一致によって、net10.0 の利用者に net9.0 フォルダーが提供されることがあるため、$(TargetFramework) が復元されたことのないフォルダー名を示す場合があります)。
<Import Project="$(MSBuildThisFileDirectory)..\..\build\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('$(MSBuildThisFileDirectory)'))))\MyPackage.props" />
ソースツリーとパッケージ内レイアウト
NuGet ビルド拡張パッケージを確認する際、リポジトリ内のソースレイアウトと、生成された .nupkg 内のパッケージ内レイアウトは、正当に異なる場合があります。これは、「インポート先のファイルが見つからない」という誤検知のよくある原因です。
パッケージ化時には、次の 3 つの仕組みによってレイアウトが変わります。
-
.nuspecの<file src=… target=…>マッピング — 1 つのソースファイルを、複数の TFM 別の配置先にコピーします。<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=…>項目に対する<PackagePath>メタデータ — SDK のパック機能を通じて、同じ効果が得られます。マッピングを明確にするため、配置先ごとに 1 つの項目を使ってください。<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\")を使って、1 つのソースを複数の配置先に展開することもできます。ただし、上記の複数項目を使う形式のほうが誤読されにくくなります。 -
SDK の規約 —
IncludeBuildOutput、BuildOutputTargetFolder、IncludeContentInPackにより、ビルド出力がlib/<tfm>/またはbuild/<tfm>/の下に自動的に配置されます。
レビュー担当者への影響
以下のようなフォワーダーがパッケージ内の build/net462/ フォルダーにあっても、ソースツリーに buildTransitive/net462/ ディレクトリが存在しないことだけを理由に、「ファイルが見つからない」バグとはなりません。
<Project>
<Import Project="$(MSBuildThisFileDirectory)..\..\buildTransitive\net462\MyAdapter.props" />
</Project>
build/<tfm>/ または buildTransitive/<tfm>/ フォルダー内の、ガードされていない <Import> を指摘する前に、次の点を確認してください。
- プロジェクトディレクトリとその直上のディレクトリで
*.nuspecを探します(それより上の階層は検索しないでください)。インポート対象のパスと一致するtarget=…を持つ<file>をすべて確認します。 .csprojを確認し、<None>/<Content>項目に<PackagePath>メタデータがあるか調べます。- ソースツリーと、投影されたパッケージレイアウトの両方で配置先のパスが見つからない場合にのみ、そのインポートを指摘します。
msbuild-antipatterns AP-13(「NuGet パッケージのフォワーダー」の例外)も参照してください。
インポートガードのパターン
.targets ファイルは、ガードプロパティを使って .props がインポート済みであることを確認します。
<PropertyGroup>
<MicrosoftCommonPropsHasBeenImported>true</MicrosoftCommonPropsHasBeenImported>
</PropertyGroup>
<Import Project="Microsoft.Common.props"
Condition="'$(MicrosoftCommonPropsHasBeenImported)' != 'true'" />
これにより、.targets のみをインポートするプロジェクトにも対応できます。
Directory.Build の検出
MSBuild はディレクトリツリーを上にたどり、最も近い Directory.Build.props を探します。
<_DirectoryBuildPropsBasePath>
$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props'))
</_DirectoryBuildPropsBasePath>
検出されるのは最も近いファイルのみです。階層がネストしている場合は、親を明示的にインポートする必要があります。
<PropertyGroup>
<_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</_ParentPropsPath>
</PropertyGroup>
<Import Project="$(_ParentPropsPath)" Condition="'$(_ParentPropsPath)' != ''" />
独自の拡張ポイントを作成する
<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">
</Target>
<Import Project="$(CustomAfterMySDK)" Condition="Exists('$(CustomAfterMySDK)')" />
</Project>
よくある落とし穴
- 省略可能なインポートに
Exists()がないと、ファイルが存在しない場合にビルドが失敗します。例外:NuGet パッケージの公開済みbuild/<tfm>/およびbuildTransitive/<tfm>/フォルダー内のインポートは、パッケージの契約に基づいています。配置先はパッケージ内レイアウトによって保証されます(上記の「ソースツリーとパッケージ内レイアウト」を参照)。ガードを追加せず、問題として指摘しないでください。 - *Custom プロパティを上書きする**と、以前のフックが失われます。
;区切りで追加してください。 - NuGet パッケージのファイル名がパッケージ ID と一致しないと、インポートが黙ってスキップされます。
- 親のインポートがないネストされた Directory.Build.props は、リポジトリルートの設定を失わせます。