mcpField
D
dotnet-msbuild-authoring

dotnet-msbuild-authoring

github.com/wshaddix開発8679 回インストール35 週間前に更新
リポジトリ

dotnet-msbuild-authoring について

SDK スタイルの .NET プロジェクトで MSBuild 要素を記述するためのスキルです。ターゲット順序、インクリメンタルビルド、インポート、アイテム、条件、`Directory.Build` の応用を解説します。

主な機能

  1. 01カスタムターゲットの実行順序
  2. 02入力と出力による増分ビルド
  3. 03Props と targets のインポート順
  4. 04アイテム、メタデータ、バッチ処理
  5. 05条件とプロパティ関数
  6. 06Directory.Build の応用パターン

活用シーン

  1. 01ビルド処理の前後にターゲットを追加
  2. 02不要な再ビルドの回避
  3. 03共有 props と targets のカスタマイズ
  4. 04アイテムごとのビルド動作の適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-msbuild-authoring

インストール後、Claude Code で/dotnet-msbuild-authoringを入力して読み込まれたか確認します。

詳細

dotnet-msbuild-authoring

MSBuild プロジェクトシステム要素を作成するためのガイダンスです。BeforeTargets/AfterTargets/DependsOnTargets を使ったカスタムターゲット、Inputs/Outputs を使ったインクリメンタルビルド、props と targets のインポート順序、項目と項目メタデータ(Include/Exclude/Update/Remove)、条件、プロパティ関数、既知のメタデータ、Directory.Build.props/Directory.Build.targets の高度なパターンを扱います。

バージョンの前提: .NET 8.0 以降の SDK(MSBuild 17.8 以降)。すべての例で SDK スタイルのプロジェクトを使用します。

対象範囲: このスキルが扱うのは、カスタムターゲット、インポート順序、項目、条件、プロパティ関数、高度な Directory.Build パターンといった MSBuild 作成の基礎です。基本的なソリューション構成と共有設定(Directory.Build.props の構造、CPM、.editorconfig)は [skill:dotnet-project-structure] が扱います。MSBuild エラーの解釈と CI の差異の診断は [skill:dotnet-build-analysis] が扱います。

相互参照:ソリューション構成と基本的な Directory.Build.props の構造については [skill:dotnet-project-structure]、MSBuild エラーの解釈と CI の差異については [skill:dotnet-build-analysis] を参照してください。


カスタムターゲット

ターゲットは MSBuild における実行単位です。各ターゲットは一連のタスクを実行し、ほかのターゲットとの順序関係を宣言できます。

カスタムターゲットの定義
<Target Name="PrintBuildInfo"
        BeforeTargets="Build">
  <Message Importance="high"
           Text="Building $(MSBuildProjectName) v$(Version) for $(TargetFramework)" />
</Target>
順序付け:BeforeTargets、AfterTargets、DependsOnTargets

ターゲットの実行順序は、次の 3 つの仕組みで制御します。

仕組み効果使用する場面
BeforeTargets="X"このターゲットを X より前に実行します既存のパイプラインに処理を挿入する場合(例:Build の前に実行)
AfterTargets="X"このターゲットを X の後に実行します後処理を行う場合(例:Publish の後に出力をコピー)
DependsOnTargets="A;B"このターゲットより前に A と B が実行されるようにします独自のターゲットグラフ内で前提となるターゲットを宣言する場合

<Target Name="CheckLicenseHeaders"
        BeforeTargets="CoreCompile">
  <Exec Command="dotnet tool run license-check -- --verify" />
</Target>


<Target Name="CopyNativeLibs"
        AfterTargets="Publish">
  <Copy SourceFiles="@(NativeLibrary)"
        DestinationFolder="$(PublishDir)runtimes/%(NativeLibrary.RuntimeIdentifier)/native/" />
</Target>


<Target Name="FullValidation"
        DependsOnTargets="CheckLicenseHeaders;RunApiCompat">
  <Message Importance="high" Text="All validations passed." />
</Target>

標準ビルドパイプラインに処理を挿入する場合は、DependsOnTargets よりも BeforeTargets/AfterTargets を優先してください。DependsOnTargets は、独自のカスタムターゲットグラフを制御するのに最適です。

既存の DependsOn リストの拡張

SDK のターゲットは、拡張用に *DependsOn プロパティを公開しています。リストを置き換えるのではなく、ターゲット名を追加してください。

<PropertyGroup>
  <BuildDependsOn>$(BuildDependsOn);GenerateVersionInfo</BuildDependsOn>
</PropertyGroup>

<Target Name="GenerateVersionInfo">
  <WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
                    Lines="[assembly: System.Reflection.AssemblyInformationalVersion(&quot;$(InformationalVersion)&quot;)]"
                    Overwrite="true" />
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)Version.g.cs" />
  </ItemGroup>
</Target>

Inputs/Outputs を使ったインクリメンタルビルド

Inputs と Outputs を持つターゲットは、出力が存在しない場合、または入力より古い場合にのみ実行されます。これはビルドのパフォーマンスにとって非常に重要です。

<Target Name="GenerateEmbeddedResources"
        BeforeTargets="CoreCompile"
        Inputs="@(EmbeddedTemplate)"
        Outputs="@(EmbeddedTemplate->'$(IntermediateOutputPath)%(Filename).g.cs')">
  <Exec Command="dotnet tool run template-gen -- %(EmbeddedTemplate.Identity) -o $(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />
  </ItemGroup>
</Target>

インクリメンタルビルドの仕組み:

  1. MSBuild は Inputs の項目と Outputs の項目のタイムスタンプを比較します。
  2. すべての出力が存在し、すべての入力より新しい場合、ターゲットは完全にスキップされます。
  3. いずれかの入力がいずれかの出力より新しい場合、ターゲット全体が実行されます。

インクリメンタルビルドが失敗するよくある原因:

  • Outputs の指定漏れ: ターゲットは毎回のビルドで実行されます。必ず Inputs と Outputs を組み合わせてください。
  • 変動する出力: 別のターゲットがビルド中に出力パスへ書き込むと、タイムスタンプがリセットされ、不要な再ビルドが発生します。
  • ジェネレーターの副作用: 内容が変わっていなくても無条件に書き込むコードジェネレーターは、インクリメンタルビルドを妨げます。一時ファイルに書き込み、内容に差異がある場合にのみコピーしてください。
  • ファイルコピーのタイムスタンプ: SkipUnchangedFiles="true" を指定した Copy タスクはタイムスタンプを保持します。指定しない場合、コピーのたびにタイムスタンプが更新されます。

Props と Targets:インポート順序

MSBuild は、特定の順序でプロジェクトファイルを評価します。適切にカスタマイズするには、この順序を理解することが不可欠です。

評価順序
1. Directory.Build.props          (imported by SDK early)
2. <Project Sdk="...">            (SDK props imported)
3. Explicit <Import> in project   (your .props imports)
4. Project body <PropertyGroup>,  (project-level properties)
   <ItemGroup>
5. SDK targets imported            (SDK targets)
6. Directory.Build.targets         (imported by SDK late)
7. Explicit .targets imports       (your .targets imports)
ルール
  • .props ファイルは、プロパティの既定値を設定し、項目を定義します。プロジェクト本体より前に実行されるため、プロジェクトレベルのプロパティで上書きできます。
  • .targets ファイルは、ターゲットを定義し、項目リストを確定します。プロジェクト本体より後に実行されるため、プロジェクトレベルの設定をすべて参照できます。

<Project>
  <PropertyGroup>
    <TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
    <Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
  </PropertyGroup>
</Project>

<Project>
  <Target Name="ValidatePackageMetadata"
          BeforeTargets="Pack"
          Condition="'$(IsPackable)' == 'true'">
    <Error Condition="'$(Description)' == ''"
           Text="Description is required for packable projects." />
  </Target>
</Project>

重要なルール: .props ファイルのプロパティには、プロジェクトレベルでの上書きを可能にするため、Condition="'$(Prop)' == ''" を使用してください。.targets ファイルのプロパティは最後に評価されるため、プロジェクトから上書きできません。


項目と項目メタデータ

項目は、ファイルまたは値をまとめた名前付きコレクションです。各項目にはメタデータ(キーと値のペア)を付与できます。

項目の操作
<ItemGroup>
  
  <Content Include="assets/**/*.png" />

  
  <Compile Include="**/*.cs" Exclude="**/*.generated.cs" />

  
  <Content Update="assets/logo.png">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <Pack>true</Pack>
    <PackagePath>contentFiles/any/any/</PackagePath>
  </Content>

  
  <Compile Remove="legacy/**/*.cs" />
</ItemGroup>

SDK スタイルのプロジェクトでは、*.cs ファイルが自動的に含まれます。 <Compile Include="**/*.cs" /> を追加しないでください。重複項目が原因で NETSDK1022 が発生します。条件付きコンパイルのシナリオでは、先に Remove を使い、その後 Include を使ってください。


<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <Compile Remove="Polyfills/**/*.cs" />
</ItemGroup>
既知のメタデータ

すべての項目には、%(ItemName.MetadataName) からアクセスできる組み込みメタデータがあります。

メタデータ値src/Models/Order.cs の例
%(FullPath)絶対パス/repo/src/Models/Order.cs
%(RootDir)ルートディレクトリ/
%(Directory)ルートからの相対ディレクトリrepo/src/Models/
%(Filename)拡張子を除いたファイル名Order
%(Extension)ファイル拡張子.cs
%(RecursiveDir)グロブ内の ** に一致する部分Models/(グロブが src/**/*.cs の場合)
%(Identity)宣言された項目指定src/Models/Order.cs
項目メタデータとバッチ処理

カスタムメタデータを使うと、MSBuild のバッチ処理を通じて項目ごとの動作を設定できます。

<ItemGroup>
  <DbMigration Include="migrations/*.sql">
    <TargetDb>main</TargetDb>
  </DbMigration>
  <DbMigration Include="migrations/audit/*.sql">
    <TargetDb>audit</TargetDb>
  </DbMigration>
</ItemGroup>


<Target Name="RunMigrations">
  <Exec Command="sqlcmd -d %(DbMigration.TargetDb) -i %(DbMigration.Identity)"
        Condition="'@(DbMigration)' != ''" />
</Target>

タスク属性内の %(Metadata) はバッチ処理を開始します。MSBuild はメタデータ値ごとに項目をグループ化し、グループごとにタスクを一度実行します。


条件

条件は、プロパティ、項目、ターゲット、タスクを評価するかどうかを制御します。

プロパティの条件
<PropertyGroup>
  
  <LangVersion Condition="'$(LangVersion)' == ''">latest</LangVersion>

  
  <DefineConstants Condition="$(TargetFramework.StartsWith('net8'))">$(DefineConstants);NET8_OR_GREATER</DefineConstants>

  
  <Optimize Condition="'$(Configuration)' == 'Release'">true</Optimize>

  
  <RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-x64</RuntimeIdentifier>
</PropertyGroup>
項目とターゲットの条件

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>


<Target Name="SignAssembly"
        AfterTargets="Build"
        Condition="'$(Configuration)' == 'Release' AND '$(SignAssembly)' == 'true'">
  <Exec Command="signtool sign /fd SHA256 $(TargetPath)" />
</Target>
条件演算子
演算子例
== / !='$(Config)' == 'Release'
AND / OR'$(A)' == '1' AND '$(B)' != ''
!(否定)!Exists('$(OutDir)')
Exists()Exists('$(SolutionDir)global.json')
HasTrailingSlash()HasTrailingSlash('$(OutputPath)')

比較の両辺は必ずシングルクォートで囲んでください。 '$(Prop)' == 'value' が正しい書き方です。クォートなしの比較は、プロパティが空の場合に失敗します。


プロパティ関数

MSBuild プロパティでは、インラインで .NET の静的メソッドや MSBuild の組み込み関数を呼び出せます。

.NET の静的メソッド呼び出し
<PropertyGroup>
  
  <NormalizedName>$([System.String]::Copy('$(PackageId)').ToLowerInvariant())</NormalizedName>

  
  <ToolPath>$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', 'tools', 'analyzer.dll'))</ToolPath>

  
  <BuildId>$([System.Guid]::NewGuid().ToString('N'))</BuildId>

  
  <CleanVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(Version)', '-.*$', ''))</CleanVersion>

  
  <CiServer>$([System.Environment]::GetEnvironmentVariable('CI'))</CiServer>
</PropertyGroup>
MSBuild の組み込み関数
<PropertyGroup>
  
  <IsWindows>$([MSBuild]::IsOSPlatform('Windows'))</IsWindows>
  <IsLinux>$([MSBuild]::IsOSPlatform('Linux'))</IsLinux>
  <IsMacOS>$([MSBuild]::IsOSPlatform('OSX'))</IsMacOS>

  
  <NextVersion>$([MSBuild]::Add($(PatchVersion), 1))</NextVersion>

  
  <HasModernSdk>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '8.0.100'))</HasModernSdk>

  
  <InputHash>$([MSBuild]::StableStringHash('$(InputFile)'))</InputHash>

  
  <SharedPropsPath>$([MSBuild]::GetPathOfFileAbove('SharedConfig.props', '$(MSBuildProjectDirectory)'))</SharedPropsPath>

  
  <SafePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '..', 'shared'))</SafePath>
</PropertyGroup>
便利な MSBuild プロパティ
プロパティ値
$(MSBuildProjectDirectory)現在の .csproj を含むディレクトリ
$(MSBuildThisFileDirectory)現在の .props/.targets ファイルを含むディレクトリ
$(MSBuildProjectName)拡張子を除いたプロジェクト名
$(IntermediateOutputPath)obj/ 出力パス
$(OutputPath)bin/ 出力パス
$(TargetFramework)現在の TFM(例:net10.0)
$(TargetFrameworks)複数の TFM のリスト(例:net8.0;net10.0)
$(Configuration)Debug または Release
$(SolutionDir)ソリューションディレクトリ(ソリューションからビルドする場合のみ設定)

.props/.targets ファイルでは $(MSBuildProjectDirectory) ではなく $(MSBuildThisFileDirectory) を使ってください。 前者ではなく後者を使うと、ファイル自体の場所を基準に解決されます。NuGet パッケージや別のディレクトリからファイルをインポートする場合は、こちらが正しい値です。


Directory.Build.props/targets の高度なパターン

基本的な Directory.Build の構成については、[skill:dotnet-project-structure] を参照してください。このセクションでは、複数のリポジトリやモノレポのシナリオで使う高度なパターンを説明します。

GetPathOfFileAbove を使ったインポートチェーン

ネストされたディレクトリがあるモノレポでは、各階層で独自の Directory.Build.props を定義し、親へと連鎖させることができます。

repo/
  Directory.Build.props            (repo-wide defaults)
  src/
    Directory.Build.props          (src-specific overrides)
    MyApp/
      MyApp.csproj
  tests/
    Directory.Build.props          (test-specific overrides)
    MyApp.Tests/
      MyApp.Tests.csproj

<Project>
  
  <PropertyGroup>
    <_ParentBuildProps>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..'))</_ParentBuildProps>
  </PropertyGroup>
  <Import Project="$(_ParentBuildProps)" Condition="'$(_ParentBuildProps)' != ''" />

  <PropertyGroup>
    
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>

<Project>
  
  <PropertyGroup>
    <_ParentBuildProps>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..'))</_ParentBuildProps>
  </PropertyGroup>
  <Import Project="$(_ParentBuildProps)" Condition="'$(_ParentBuildProps)' != ''" />

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="xunit.v3" />
    <PackageReference Include="xunit.runner.visualstudio" />
  </ItemGroup>
</Project>
条件ガード

子のインポートによってプロパティ値が上書きされないようにします。


<Project>
  <PropertyGroup>
    
    <TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
    <Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
    <ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
  </PropertyGroup>
</Project>

ルール: .props ファイル内のプロパティには、内側の .props ファイルやプロジェクトレベルのプロパティで上書きできるよう、Condition="'$(Prop)' == ''" ガードを使用してください。無条件に適用するプロパティは、最後に評価される Directory.Build.targets に記述します。

二重インポートの防止

複数の Directory.Build.props ファイルが上位へ連鎖している場合、共有インポートが 2 回評価されることがあります。これを防ぐには、センチネルプロパティを使用します。


<Project>
  
  <PropertyGroup Condition="'$(_CommonPropsImported)' != 'true'">
    <_CommonPropsImported>true</_CommonPropsImported>
    <Authors>My Company</Authors>
    <Company>My Company</Company>
    <Copyright>Copyright (c) My Company. All rights reserved.</Copyright>
  </PropertyGroup>
</Project>

センチネルプロパティとコンテンツプロパティは、!= 'true' 条件を指定した同じ PropertyGroup 内に記述する必要があります。コンテンツを == 'true' 条件を指定した別のブロックに記述しても、再評価は防げません。センチネルはすでに設定されているため、インポートのたびに実行されます。

よりすっきりとした方法として、<Import> 要素に Condition を指定できます。


<Import Project="$(SharedPropsPath)"
        Condition="'$(_CommonPropsImported)' != 'true' AND Exists('$(SharedPropsPath)')" />
Directory.Build.targets での設定の強制

.targets ファイルで設定したプロパティは、プロジェクト本体の後に評価されるため、プロジェクトレベルの PropertyGroup 要素で上書きできません。


<Project>
  
  <PropertyGroup>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
  </PropertyGroup>

  
  <PropertyGroup Condition="'$(IsTestProject)' != 'true' AND '$(IsPackable)' != 'false'">
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>

エージェントが注意すべき点

  1. 条件比較で引用符を付け忘れる。 両辺を必ず引用符で囲みます:'$(Prop)' == 'value'。引用符のない $(Prop) == value は、プロパティが空の場合や空白を含む場合に、エラーを出さずに失敗します。

  2. 共有 .props/.targets ファイルで $(MSBuildProjectDirectory) を使用する。 これはファイル自体のディレクトリではなく、インポート元のプロジェクトのディレクトリに解決されます。.props/.targets ファイル自体を基準にパスを指定するには、$(MSBuildThisFileDirectory) を使用します。

  3. .targets でプロパティを設定し、プロジェクト側で上書きできると想定する。 .targets のプロパティはプロジェクト本体の後に評価されるため、プロジェクトレベルの値を上書きします。上書き可能にするプロパティは、.props で Condition="'$(Prop)' == ''" ガードを付けて設定します。

  4. SDK スタイルのプロジェクトで <Compile Include="**/*.cs" /> を追加する。 SDK スタイルのプロジェクトでは、すべての *.cs ファイルが自動的に含まれます。明示的に含めると、NETSDK1022 の重複項目エラーが発生します。条件付きで処理する場合は、Remove の後に Include を使用します。

  5. Inputs を持つターゲットに Outputs を指定し忘れる。 Inputs はあるが Outputs がないターゲットは、ビルドのたびに実行されます。インクリメンタルに動作させるには、必ず両方を組み合わせて指定します。

  6. .props/.targets ファイルで $(SolutionDir) を使用する。 このプロパティは、ソリューションファイル経由でビルドする場合にのみ設定されます。コマンドラインから dotnet build MyProject.csproj を実行すると、値は空になります。$([MSBuild]::GetPathOfFileAbove('*.sln', '$(MSBuildProjectDirectory)')) を使用するか、$(MSBuildThisFileDirectory) を基準にパスを指定します。

  7. 項目を PropertyGroup に入れたり、プロパティを ItemGroup に入れたりする。 項目(Include= を使用)は <ItemGroup> 内に記述する必要があります。プロパティ(要素の値を使用)は <PropertyGroup> 内に記述する必要があります。混在させると、評価がエラーなく失敗します。

  8. 親インポートチェーンに Condition ガードを付け忘れる。 ファイルが見つからない場合、GetPathOfFileAbove は空の文字列を返します。<Import> に Condition="Exists('$(ResolvedPath)')" を指定しないと、ファイルが見つからないというエラーでビルドが失敗します。


参考資料

コメント