mcpField
E
extension-points

extension-points

github.com/dotnet開発5.8k3.2k 回インストール1 日前に更新
リポジトリ

extension-points について

MSBuild SDK、NuGet パッケージ、リポジトリのビルド処理を管理する開発者向けのスキルです。拡張フック、インポート順、パッケージ構成と NuGet 転送ファイルの確認方法を扱います。

主な機能

  1. 01CustomBefore/CustomAfter フックの設定
  2. 02拡張ディレクトリとインポート制御の把握
  3. 03NuGet の build/buildTransitive ファイルの構成
  4. 04ソースツリーとパッケージ構成の差異の確認
  5. 05Directory.Build.props の探索階層の処理

活用シーン

  1. 01SDK やリポジトリへのビルド処理の追加
  2. 02NuGet 拡張ファイルのインポート失敗の調査
  3. 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>
使用可能な制御プロパティ
プロパティ無効にするもの
ImportDirectoryBuildPropsDirectory.Build.props の自動検出
ImportDirectoryBuildTargetsDirectory.Build.targets の自動検出
ImportProjectExtensionPropsobj/ 内の NuGet が生成した *.props
ImportProjectExtensionTargetsobj/ 内の 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 つの仕組みによってレイアウトが変わります。

  1. .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 は、ファイル名を変更します。

  2. .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 つのソースを複数の配置先に展開することもできます。ただし、上記の複数項目を使う形式のほうが誤読されにくくなります。

  3. 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> を指摘する前に、次の点を確認してください。

  1. プロジェクトディレクトリとその直上のディレクトリで *.nuspec を探します(それより上の階層は検索しないでください)。インポート対象のパスと一致する target=… を持つ <file> をすべて確認します。
  2. .csproj を確認し、<None>/<Content> 項目に <PackagePath> メタデータがあるか調べます。
  3. ソースツリーと、投影されたパッケージレイアウトの両方で配置先のパスが見つからない場合にのみ、そのインポートを指摘します。

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 は、リポジトリルートの設定を失わせます。

コメント