msbuild-antipatterns について
プロジェクトファイルの一般的な MSBuild アンチパターンを見つけるためのカタログです。ビルドレビューや監査向けに、問題の兆候、影響、修正案を示し、確定した不具合と条件付きリスクを区別します。
主な機能
- 01番号付きアンチパターン集
- 02ビルドと保守への影響説明
- 03具体的な変換案
- 04クロスプラットフォームのパス処理
活用シーン
- 01MSBuildプロジェクトファイルの監査
- 02ビルドの保守性レビュー
- 03プロジェクト設定リスクの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill msbuild-antipatternsインストール後、Claude Code で/msbuild-antipatternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/additional-antipatterns.mdincremental-build-inputs-outputs.mdprivate-assets.md詳細
MSBuild アンチパターンカタログ
よくある MSBuild のアンチパターンを番号付きでまとめています。各項目は次の形式です。
- 問題点:確認する箇所
- 問題となる理由:ビルド、保守性、または正確性への影響
- 修正方法:具体的な変換
プロジェクトファイルを確認して改善点を探す際に、このカタログを使用してください。
レビュー出力の規約
レビュー、監査、保守性、または正確性のリスクに関する依頼では、次のとおりにしてください。
- ユーザーが修正を明示的に依頼しない限り、ファイルを編集しないでください。
- 提供されたファイルに基づく優先度順の指摘事項を、影響と具体的な推奨事項とともに返してください。
- 確認済みの欠陥、条件付きのリスク、すでに適切なパターンを分けてください。
- 最終回答はレビューとしてまとめてください。変更内容の簡潔な要約に置き換えないでください。
AP-01:組み込みタスクがある操作に対する <Exec>
問題点:<Exec Command="mkdir ..." />、<Exec Command="copy ..." />、<Exec Command="del ..." />
問題となる理由:組み込みタスクはクロスプラットフォームで、インクリメンタルビルドをサポートし、構造化ログを出力して、一貫した方法でエラーを処理します。<Exec> は MSBuild から見ると不透明です。
<Target Name="PrepareOutput">
<Exec Command="mkdir $(OutputPath)logs" />
<Exec Command="copy config.json $(OutputPath)" />
<Exec Command="del $(IntermediateOutputPath)*.tmp" />
</Target>
<Target Name="PrepareOutput">
<MakeDir Directories="$(OutputPath)logs" />
<Copy SourceFiles="config.json" DestinationFolder="$(OutputPath)" />
<Delete Files="@(TempFiles)" />
</Target>
組み込みタスクの代替手段:
| シェルコマンド | MSBuild タスク |
|---|---|
mkdir | <MakeDir> |
copy / cp | <Copy> |
del / rm | <Delete> |
move / mv | <Move> |
echo text > file | <WriteLinesToFile> |
touch | <Touch> |
xcopy /s | アイテムのグロブを使った <Copy> |
AP-02:引用符で囲まれていない条件式
問題点:Condition="$(Foo) == Bar" — 比較対象のどちらかが引用符で囲まれていません。
問題となる理由:プロパティが空、またはスペースや特殊文字を含む場合、条件式が正しく評価されなかったり、解析エラーが発生したりします。確実に比較するには、MSBuild では文字列を単一引用符で囲む必要があります。
<PropertyGroup Condition="$(Configuration) == Release">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
</PropertyGroup>
ルール:== と != の比較では、左右の両方を単一引用符で囲んでください。
AP-03:ハードコードされた絶対パス
問題点:プロジェクトファイル内の C:\tools\、D:\packages\、/usr/local/bin/ のようなパス。
問題となる理由:別のマシン、CI 環境、または別のオペレーティングシステムでは動作しません。移動にも対応できません。
<PropertyGroup>
<ToolPath>C:\tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="C:\repos\shared\common.props" />
<PropertyGroup>
<ToolPath>$(MSBuildThisFileDirectory)tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\common.props" />
推奨されるパスプロパティ:
| プロパティ | 意味 |
|---|---|
$(MSBuildThisFileDirectory) | 現在の .props/.targets ファイルのディレクトリ |
$(MSBuildProjectDirectory) | .csproj のディレクトリ |
$([MSBuild]::GetDirectoryNameOfFileAbove(...)) | 上位階層をたどってマーカーファイルを検索 |
$([MSBuild]::NormalizePath(...)) | パスの各要素を結合して正規化 |
AP-04:SDK の既定値の再指定
問題点:.NET SDK がすでに既定で設定している値をプロパティに設定しています。
問題となる理由:不要な記述が増え、意図的な上書きが分かりにくくなり、実際にカスタマイズされている箇所の特定が難しくなります。新しい SDK で既定値が変更されると、冗長なプロパティによって古い動作が意図せず固定される場合があります。
<PropertyGroup>
<OutputType>Library</OutputType>
<EnableDefaultItems>true</EnableDefaultItems>
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
<RootNamespace>MyLib</RootNamespace>
<AssemblyName>MyLib</AssemblyName>
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
AP-05:SDK 形式のプロジェクトでのファイルの手動列挙
問題点:SDK 形式のプロジェクトでの <Compile Include="File1.cs" />、<Compile Include="File2.cs" />。
問題となる理由:SDK 形式のプロジェクトでは、**/*.cs(およびほかのファイル形式)が自動的にグロブされます。明示的な列挙は冗長で、マージ時の競合を招き、リストに追加し忘れると新しいファイルが漏れる可能性があります。
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Services\MyService.cs" />
<Compile Include="Models\User.cs" />
</ItemGroup>
<ItemGroup>
<Compile Remove="LegacyCode\**" />
</ItemGroup>
例外:SDK 形式ではない(レガシー)プロジェクトでは、ファイルを明示的に含める必要があります。移行する場合は、msbuild-modernization スキルを参照してください。
例外(F# / .fsproj):F# のコンパイルは順序に依存します。コンパイラは <Compile Include> アイテムを順番に処理し、ファイルから参照できるのは、そのファイルより前に列挙されたファイルで宣言されている型やモジュールだけです。そのため、.fsproj ファイルではすべてのソースファイルを依存関係の順に明示的に列挙する必要があります(ユーティリティ/リーフモジュールを先頭に、Program.fs のようなエントリーポイントを末尾に配置します)。.fsi シグネチャファイルを使用する場合は、対応する .fs 実装ファイルの直前に配置する必要があります。
AP-06: NuGet パッケージでの HintPath を使用した <Reference>
問題点: <Reference Include="..." HintPath="..\packages\SomePackage\lib\..." />
問題となる理由: これは従来の packages.config パターンです。推移的な依存関係、バージョン競合の解決、自動復元に対応していません。packages/ フォルダーをコミットするか、別途復元する必要があります。
<ItemGroup>
<Reference Include="Newtonsoft.Json">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\netstandard2.0\Newtonsoft.Json.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
注: HintPath を指定しない <Reference> は、WindowsBase、PresentationCore などの .NET Framework GAC アセンブリでは引き続き有効です。
AP-07: Analyzer/Tool パッケージで PrivateAssets="all" がない
問題点: PrivateAssets="all" を指定せずに <PackageReference Include="StyleCop.Analyzers" Version="..." /> を使用しています。
問題となる理由: PrivateAssets="all" を指定しないと、アナライザーやビルドツールのパッケージが、ライブラリを利用する側に推移的な依存関係として渡されます。利用側には、要求していないアナライザーやビルド時ツールが取り込まれます。
BAD/GOOD の例と、指定が必要なパッケージの全リストについては、references/private-assets.md を参照してください。
AP-08: 複数の .csproj ファイル間でプロパティをコピーしている
問題点: 同じ <PropertyGroup> ブロックが3つ以上のプロジェクトファイルに記述されています。
問題となる理由: メンテナンスの負担が増え、変更のたびにすべてのファイルを修正する必要があります。時間の経過とともに不整合も生じます。
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Directory.Build.props / Directory.Build.targets の構成に関する詳しいガイダンスは、directory-build-organization スキルを参照してください。
AP-09: 一元的なパッケージ管理を行わず、パッケージのバージョンが分散している
問題点: プロジェクト間で同じパッケージのバージョンが異なる状態で、<PackageReference Include="X" Version="1.2.3" /> を使用しています。
問題となる理由: バージョンのずれが発生し、プロジェクトごとに同じパッケージの異なるバージョンが使われるため、実行時の不一致、予期しない動作、またはダイヤモンド依存関係の競合につながります。
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
修正方法: Central Package Management を使用してください。詳しくは、https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management を参照してください。
AP-10: モノリシックなターゲット(1つのターゲットに詰め込みすぎている)
問題点: 1つの <Target> が50行以上あり、無関係な処理を複数実行しています。
問題となる理由: 増分ビルドで個々のステップをスキップできず、デバッグや拡張が難しくなり、ターゲット名も意味をなさなくなります。
<Target Name="PrepareRelease" BeforeTargets="Build">
<WriteLinesToFile File="version.txt" Lines="$(Version)" Overwrite="true" />
<Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" />
<Exec Command="signtool sign /f cert.pfx $(OutputPath)*.dll" />
<MakeDir Directories="$(OutputPath)docs" />
<Copy SourceFiles="@(DocFiles)" DestinationFolder="$(OutputPath)docs" />
</Target>
<Target Name="WriteVersionFile" BeforeTargets="CoreCompile"
Inputs="$(MSBuildProjectFile)" Outputs="$(IntermediateOutputPath)version.txt">
<WriteLinesToFile File="$(IntermediateOutputPath)version.txt" Lines="$(Version)" Overwrite="true" />
</Target>
<Target Name="CopyLicense" AfterTargets="Build">
<Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>
<Target Name="SignAssemblies" AfterTargets="Build" DependsOnTargets="CopyLicense"
Condition="'$(SignAssemblies)' == 'true'">
<Exec Command="signtool sign /f cert.pfx %(AssemblyFiles.Identity)" />
</Target>
AP-11: カスタムターゲットに Inputs と Outputs がない
問題点: Inputs / Outputs 属性のない <Target Name="MyTarget" BeforeTargets="Build"> を使用しています。
問題となる理由: 何も変更されていなくても、ターゲットがビルドのたびに実行されます。これでは増分ビルドの利点が損なわれ、変更のないビルドも遅くなります。
BAD/GOOD の例と、FileWrites の登録を含む完全なパターンについては、references/incremental-build-inputs-outputs.md を参照してください。
Inputs/Outputs、FileWrites、最新状態の確認について詳しくは、incremental-build スキルを参照してください。
AP-12: .targets ではなく .props に既定値を設定する
問題点: .targets ファイル内の <PropertyGroup> に既定値を設定しています。
問題となる理由: .targets ファイルはプロジェクトファイルの後にインポートされます。既定値が設定される時点では、他の .targets ファイルがすでに空または未定義の値を使用している場合があります。.props ファイルは早い段階でインポートされるため、既定値を設定する正しい場所です。
<PropertyGroup>
<MyToolVersion>2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
<Exec Command="mytool --version $(MyToolVersion)" />
</Target>
<PropertyGroup>
<MyToolVersion Condition="'$(MyToolVersion)' == ''">2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
<Exec Command="mytool --version $(MyToolVersion)" />
</Target>
ルール: .props = 既定値と設定(早い段階で評価)。.targets = ビルドロジックとターゲット(遅い段階で評価)。
AP-13: Exists() ガードのないインポート
問題点:Condition="Exists('...')" による確認なしで、<Import Project="some-file.props" /> を記述しています。
問題である理由:ファイルが存在しない場合(まだ作成されていない、パスが誤っている、削除されているなど)、ビルドが分かりにくいエラーで失敗します。オプションのインポートには、必ずガードを設けてください。
<Import Project="$(RepoRoot)eng\custom.props" />
<Import Project="$(RepoRoot)eng\custom.props" Condition="Exists('$(RepoRoot)eng\custom.props')" />
<Project Sdk="Microsoft.NET.Sdk">
例外 — 必須のインポート:ビルドを正しく動作させるために必須のインポートは、速やかに失敗させる必要があるため、ガードを設けないでください。オプションまたは環境固有のインポート(ローカル開発者によるオーバーライドや、CI 固有の設定など)にガードを設けてください。
例外 — NuGet パッケージの転送ファイル:NuGet パッケージの TFM ごとの build/ または buildTransitive/ フォルダーにある .props/.targets ファイルは、Exists() ガードなしで、buildTransitive/<tfm>/… にある同じ階層のファイルを頻繁にインポートします。これはパッケージの契約です。ソースツリーにその相対パスで表示されなくても、対象ファイルは復元されたパッケージ内に必ず存在します。一般的なパッケージレイアウトは、次のいずれかによって生成されます。
- TFM ごとの
<file>エントリを持つカスタム.nuspec。たとえば<file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net8.0\MyAdapter.props" />のように、単一のソースフォルダー(buildTransitive/common/など)からファイルをコピーし、パック時に TFM ごとのサブフォルダーに配置します。 .csproj内の<None Update="...">/<Content Include="...">項目に、TFM ごとの<PackagePath>(例:<PackagePath>buildTransitive/net8.0/</PackagePath>)を指定し、ターゲット TFM ごとに宣言します。- ビルドされた出力を
build/<tfm>/に配置する SDK の規約(IncludeBuildOutput、BuildOutputTargetFolderなど)。
build/ または buildTransitive/ フォルダー内にある、ガードされていない <Import> を指摘する前に、パック後のレイアウトと照合してください。プロジェクトディレクトリとその直上のディレクトリにあるすべての *.nuspec(モノレポでは共有 nuspec が一般的です。それより上へはたどらないでください)を読み、.csproj 内の <None>/<Content> 項目にある <PackagePath> メタデータも確認します。対象パスがソースツリーと想定されるパッケージレイアウトの両方に存在しない場合に限り、指摘してください。完全な相互確認手順については、dotnet-msbuild/extension-points スキルの「ソースツリーとパック後のレイアウト」を参照してください。
buildTransitive/ → build/ の転送:buildMultiTargeting/ に直接転送せず、同じ階層にある build/*.props/build/*.targets ファイルを経由してください。build/ が TFM ごと(build/<tfm>/)の場合は、$(TargetFramework) ではなく、ファイル自身のフォルダーから導出した TFM のセグメントを含めてください。そうしないと、推移的なコンシューマーで MSB4019 が発生します。ルールと導出式については、extension-points スキルの「転送チェーン」を参照してください。
AP-14: パス内のバックスラッシュ — 影響する箇所
問題の兆候:クロスプラットフォームでの実行を意図した .props/.targets ファイルで、パス区切り文字にバックスラッシュを使用しています。
実際にバグとなる箇所(🔴 Error) — MSBuild がパス正規化処理に渡さないパスです。
<Exec Command="...\tools\foo.exe ..." />内の生のシェル文字列 — Unix ではbash/shにそのまま渡され、\はエスケープ文字として扱われます。- CDATA ブロック内のバックスラッシュ区切りのパス、
<WriteLinesToFile>で書き込むソースファイルに埋め込まれたパス、または MSBuild 以外のコンシューマー(カスタムスクリプト、レスポンスファイル、環境変数)向けに構築されたパス。 - MSBuild のパスユーティリティを介さず、OS のファイル API を直接呼び出すカスタムタスクに渡されるパス。
スタイル上の好みにすぎない箇所(🔵 Style) — MSBuild の評価器を通るパス(<Import Project="...">、<Copy>/<MakeDir>/<Delete> などの組み込みタスクで使用されるファイルパスプロパティ、項目の Include=/Exclude= グロブ)です。
MSBuild の評価器は、Unix 系システムでパスを解決する前に \ → / に正規化します。詳しくは、microsoft/msbuild の src/Framework/FileUtilities.cs にある FileUtilities.MaybeAdjustFilePath と ConvertToUnixSlashes を参照してください。そのため、<Import Project="$(MSBuildThisFileDirectory)..\..\build\common.props" /> は現在 Linux/macOS でも正しく解決されます。一貫性のためにスラッシュを使うことが推奨されますが、インポートが失敗することはないため、既存のバックスラッシュ形式のインポートを 🔴 Error として指摘すべきではありません。
<Exec Command="$(MSBuildThisFileDirectory)tools\release\sign.exe $(OutputPath)" />
<Import Project="$(MSBuildThisFileDirectory)..\..\build\common.props" />
<Import Project="$(MSBuildThisFileDirectory)../../build/common.props" />
検証ルール:バックスラッシュを含むパスを 🔴 Error として指摘する前に、「この文字列は MSBuild の評価器を通るのか、それとも MSBuild 以外のコンシューマーにそのまま渡されるのか」と確認してください。正確性の欠陥となるのは後者のみです。
注:$(MSBuildThisFileDirectory) はプラットフォームに適した区切り文字で終わるため、$(MSBuildThisFileDirectory)tools/mytool は両方のプラットフォームで動作します。
AP-15: 複数のスコープでの無条件なプロパティの上書き
問題の兆候:Directory.Build.props と .csproj の両方でプロパティが無条件に設定されており、最後の書き込みが暗黙的に優先されます。
問題となる理由:実際にどの値が使用されているかを追跡しにくくなります。プロジェクトファイルを読む人にとって、ビルドが脆弱で分かりにくくなります。
<PropertyGroup>
<OutputPath>bin\custom\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<OutputPath>bin\other\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<OutputPath Condition="'$(OutputPath)' == ''">bin\custom\</OutputPath>
</PropertyGroup>
その他のアンチパターン(AP-16 から AP-23)とクイックリファレンスのチェックリストについては、additional-antipatterns.md を参照してください。