incremental-build について
incremental-buildは、予期しないターゲットの再実行やVisual Studioの最新状態チェックを含む、MSBuildの増分ビルド診断スキルです。バイナリログ、タイムスタンプ、出力追跡、変更なしの連続ビルドに基づく調査を求め、修正依頼がない限りプロジェクトを編集しません。
主な機能
- 01MSBuildのInputsとOutputsの診断
- 02バイナリログと診断ログの調査
- 03タイムスタンプと出力の存在確認
- 04バッチ処理の入力・出力対応確認
- 05Visual Studioの最新状態チェック案内
- 06変更なしビルドでのスキップ検証
活用シーン
- 01MSBuildターゲットの再実行理由の特定
- 02生成出力の追跡とクリーン対象の確認
- 03Visual Studioでの再ビルド差異の調査
MCP 対応
2回目のビルドのバイナリログを調べる優先手段として、binlog MCPサーバーMicrosoft.AITools.BinlogMcpを挙げています。load_binlogやsearch_binlogなどのツールを使う手順を示します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill incremental-buildインストール後、Claude Code で/incremental-buildを入力して読み込まれたか確認します。
詳細
必須の診断出力
診断または説明を求める依頼では、直近の原因が単に
Inputs/Outputs の欠落である場合も含め、最終回答で以下のすべての点を明示してください。
- 対象と、根本原因を示す具体的な証拠。
- 実際の入力と出力のタイムスタンプの比較。
- 欠落、削除、クリーンアップ、または書き換えられた出力。
- 動的または変動する入力を含む、完全な入力一覧。
- 評価された入力/出力パス、および項目リストをバッチ処理する場合の一対一の対応付け。
- 変更のない同一ビルドを2回実行した結果と、2回目のビルドで期待されるスキップ/最新でない状態の証拠。
ユーザーから修正を求められない限り、編集しないでください。チェックを実行できない場合は、省略したり成功したと主張したりせず、未検証のチェックとして明記してください。Inputs または Outputs がない場合は、タイムスタンプ比較または一対一の対応付けについて、未検証の比較ではなく、該当なし — 増分ビルドの仕組みは有効になっていませんと報告してください。
MSBuild の増分ビルドの仕組み
MSBuild の増分ビルド機能では、出力がすでに最新であるターゲットをスキップできるため、2回目以降のビルド時間を大幅に短縮できます。
Inputs属性とOutputs属性があるターゲット:MSBuild は、Inputsに指定されたすべてのファイルと、Outputsに指定されたすべてのファイルのタイムスタンプを比較します。すべての出力ファイルがすべての入力ファイルより新しければ、ターゲット全体がスキップされます。Inputs/Outputsがない場合:ビルドを実行するたびにターゲットが実行されます。これが既定の動作であり、増分ビルドが遅くなる最も一般的な原因です。- タイムスタンプによる比較:MSBuild は、ファイルシステムのタイムスタンプ(最終書き込み時刻)を使って古さを判定します。コンテンツハッシュは使用しません。そのため、ファイルの内容を変えずにタイムスタンプだけを更新すると、再ビルドが発生します。
<Target Name="Transform"
Inputs="@(TransformFiles)"
Outputs="@(TransformFiles->'$(OutputPath)%(Filename).out')">
</Target>
<Target Name="PrintMessage">
<Message Text="This runs every build" />
</Target>
増分ビルドが機能しなくなる理由(主な原因)
-
カスタムターゲットに
Inputs/Outputsがない — 両方の属性がない場合、ターゲットは常に実行されます。これは不要な再ビルドが発生する最も一般的な原因です。 -
Outputsのパスに変動するプロパティが含まれている — 出力パスにビルドごとに変わる値(タイムスタンプ、ビルド番号、ランダムな GUID など)が含まれていると、MSBuild は前回の出力を見つけられず、常に再ビルドします。 -
追跡対象の
Outputs以外へのファイル書き込み — ターゲットがOutputsに記載されていないファイルを書き込んでも、MSBuild はそれを認識しません。宣言された出力が最新であるためターゲットがスキップされても、後続のターゲットが実行される場合があります。 -
FileWritesへの登録漏れ — ビルド中に作成されてもFileWrites項目グループに登録されていないファイルは、dotnet cleanでクリーンアップされません。時間が経つと、古いファイルによって増分チェックが混乱することがあります。 -
Glob の変更 — ソースファイルを追加または削除すると、項目セット(例:
@(Compile))が変わります。これらの項目がInputsに含まれているため、入力セットが変わり、再ビルドが発生します。これは想定された動作ですが、予想外に感じられることがあります。 -
プロパティの変更 —
InputsまたはOutputsのパスに含まれるプロパティ(例:$(Configuration)、$(TargetFramework))を変更すると、再ビルドが発生します。Debug と Release の切り替えで全面的な再ビルドが行われるのは仕様です。 -
NuGet パッケージの更新 — パッケージのバージョンを変更すると、
project.assets.jsonが更新され、解決済みのアセンブリパスも多数変更される可能性があります。これによりResolveAssemblyReferencesとCoreCompileの入力が変わり、再ビルドが発生します。 -
ビルドサーバー VBCSCompiler のキャッシュ無効化 — Roslyn コンパイラーサーバー(
VBCSCompiler)はコンパイル状態をキャッシュします。サーバーが再起動されると(タイムアウト、クラッシュ、手動での停止など)、MSBuild の増分チェックが通っていても、コンパイラーがインメモリーキャッシュを再構築する必要があるため、次回のビルドが遅くなることがあります。
「なぜ再ビルドされたのか」を診断する
バイナリログ(binlog)を使って、ターゲットがスキップされずに実行された理由を正確に把握します。
修正を提案する前に、再実行された各ターゲットについて、次の簡潔なチェックリストを報告してください。
- 実際のタイムスタンプの比較 — 解決後の入力のうち最新のものと、解決後の出力のうち最も古いものを挙げ、それぞれの最終書き込み時刻を示し、どちらの入力が新しいかを明記します。
- 出力の存在 — 宣言されたすべての出力が2回目のビルド前に存在し、別のターゲットによって削除、クリーン、または書き換えられていないことを確認します。
- 入力の完全性 — 出力を無効化するすべてのファイル(変化しやすい依存ファイルや生成された依存ファイルを含む)が
Inputsに含まれていることを確認します。ビルドのたびに入力ファイルに変更を加える生成元がある場合は、その内容が変わっているか確認します。生成元で不要な書き換えを修正します(たとえば、内容が変わった場合にのみ書き込みます)。関連する内容が実際に変わる場合は、依存するターゲットを再実行する必要があります。頻繁に変わるという理由だけでファイルを除外せず、出力に影響しないという根拠がある場合にのみ除外します。 - 解決後のパスの安定性 — 両方のビルドについて、プロパティと相対パスを展開します。出力が同じ場所に解決される必要があり、タイムスタンプ、ビルド番号、GUID を含んではなりません。
- バッチ処理時の1対1の対応関係 — アイテム変換では、各入力の識別子を想定される出力に対応付け、不足、重複、または対応しないペアを報告します。
- 変更なしの検証 — 編集せずに同じビルドを2回実行し、2回目のビルドでのスキップ理由、または最新でないと判断された理由を示します。
診断や説明の依頼では、ユーザーが修正を求めない限り、プロジェクトを編集しないでください。ビルドを実行できない場合は、調査で確認した事実と想定される動作を区別し、修正が適用または検証されたと主張しないでください。
WriteLinesToFile の Overwrite="true" 自体は、インクリメンタルターゲットが再実行される理由にはなりません。このタスクがファイルを書き換えるのは、MSBuild がすでにターゲットの実行をスケジュールした後だけです。正しい Inputs/Outputs が設定されていれば、タスクが実行される前にターゲットをスキップできます。
binlog を使った手順
- 編集せずにビルドを2回実行し、2回目のビルドから構造化された証拠とテキストの証拠の両方を取得します。
1回目のビルドでベースラインを確立します。インクリメンタルに動作することが期待されるのは2回目です。dotnet build /bl:first.binlog dotnet build /bl:second.binlog -fl "-flp:v=diag;logfile=second-full.log;performancesummary"second.binlogを分析します。
優先:binlog MCP
2回目の binlog を分析するには、binlog MCP サーバー(Microsoft.AITools.BinlogMcp。binlog MCP 名前空間で公開されています)を使用します。
load_binlogを使ってsecond.binlogを読み込みます。search_binlogで「Building target completely」、「Building target incrementally」、「Skipping target」、「is newer than output」を検索します。- 再実行されたターゲットに対して
get_target_info_by_nameを使います。 get_project_target_listを使って、ターゲットとそのプロジェクトのコンテキストを確認します。- スキップされなかったターゲットの優先順位付けには、スキップ理由を把握してから
get_expensive_targetsのみを使用します。
フォールバック:診断テキストログ(MCP を利用できない場合)
-
2回目のビルド中に取得した診断ログを読みます。デフォルトのフォールバックでは、別のビルドを実行したり、リプレイ機能に依存したりしないでください。
grep 'Building target\|Target.*was not skipped' second-full.log完全にインクリメンタルなビルドでは、ほとんどのターゲットがスキップされるはずです。
-
診断ログでターゲットの実行メッセージを確認し、スキップされなかったターゲットを調べます。ターゲットが実行された理由を示す「最新でない」というメッセージを確認します。
-
binlog 内の重要なメッセージを探します。
"Building target 'X' completely"— MSBuild が出力を見つけられなかった、またはすべての出力が見つからなかったことを意味します。ターゲットは完全実行されます。"Building target 'X' incrementally"— 一部の出力(すべてではありません)が最新でないことを意味します。"Skipping target 'X' because all output files are up-to-date"— ターゲットは正しくスキップされました。
-
再ビルドのきっかけとなった具体的な入力ファイルを見つけるため、「is newer than output」メッセージを検索します。
grep "is newer than output" second-full.logこれにより、MSBuild がターゲットを最新でないと判断した原因となった入力ファイルのタイムスタンプを正確に把握できます。
その他の診断手法
first.binlogとsecond.binlogを MSBuild Structured Log Viewer で並べて比較し、何が変わったかを確認します。grep 'Target Performance Summary' -A 30 second-full.logを使用して、2 回目のビルドで最も時間がかかったターゲットを確認します。これらが最適化の対象です。- 実行されたにもかかわらず所要時間がゼロのターゲットを確認します。不要な依存関係が原因で実行されている可能性があります。
FileWrites とクリーンビルド
FileWrites アイテムグループは、ビルド中に生成されたファイルを追跡するための MSBuild の仕組みです。dotnet clean を機能させ、正しいインクリメンタルビルドの動作を維持するのに役立ちます。
FileWritesアイテム:カスタムターゲットが作成するファイルを登録すると、dotnet cleanがそれらを削除できるようになります。登録しないと、生成ファイルがビルドのたびに蓄積し、インクリメンタルチェックに影響することがあります。FileWritesShareableアイテム:複数のプロジェクト間で共有されるファイル(共有される生成コードなど)に使用します。これらのファイルは追跡されますが、他のプロジェクトから参照されている場合は削除されません。- 登録しない場合:ファイルが出力ディレクトリと中間ディレクトリに蓄積します。
dotnet cleanでは削除されず、古いデータの問題を引き起こしたり、最新状態のチェックに影響したりすることがあります。
生成ファイルを登録するパターン
ファイルを作成するターゲット内で、生成ファイルを FileWrites に追加します。
<Target Name="MyGenerator" Inputs="..." Outputs="$(IntermediateOutputPath)generated.cs">
<WriteLinesToFile File="$(IntermediateOutputPath)generated.cs" Lines="@(GeneratedLines)" />
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)generated.cs" />
</ItemGroup>
</Target>
Visual Studio の高速最新状態チェック
Visual Studio には、MSBuild の Inputs/Outputs の仕組みとは別に、独自の最新状態チェック(高速最新状態チェック、FUTDC)があります。「コマンドラインでは再ビルドされないのに VS では再ビルドされる」という問題を診断するうえで、両者の違いを理解することが重要です。
- VS FUTDC は高速です。プロセス内で実行され、MSBuild を呼び出さずに既知のアイテムセットをチェックします。既知のアイテムタイプ(Compile、Content、EmbeddedResource など)のタイムスタンプを、プロジェクトの主要な出力と比較します。
- 誤った判定になる場合があります。カスタムビルドアクション、ファイルを生成するカスタムターゲット、または FUTDC が認識しない標準外のアイテムタイプをプロジェクトが使用している場合です。
- FUTDC を無効にすると、Visual Studio は MSBuild の完全なインクリメンタルチェックを使用します。
<PropertyGroup> <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck> </PropertyGroup> - FUTDC の判定を診断するには、VS の出力ウィンドウを表示します。Tools → Options → Projects and Solutions → SDK-Style Projects に移動し、Up-to-date Checks のログレベルを Verbose 以上に設定します。FUTDC は、どのファイルを最新ではないと判断したかを正確に記録します。
- VS FUTDC でよくある問題:
- FUTDC システムに登録されていないカスタムビルドアクション
- 最後のビルドより新しい
CopyToOutputDirectoryアイテム - FUTDC が評価しないターゲットによって動的に追加されたアイテム
- 変更された
CopyToOutputDirectory="PreserveNewest"を持つContentまたはNoneアイテム
カスタムターゲットをインクリメンタルにする
以下は、適切に構成されたインクリメンタルなカスタムターゲットの完全な例です。
<Target Name="GenerateConfig"
Inputs="$(MSBuildProjectFile);@(ConfigInput)"
Outputs="$(IntermediateOutputPath)config.generated.cs"
BeforeTargets="CoreCompile">
<WriteLinesToFile File="$(IntermediateOutputPath)config.generated.cs" Lines="..." />
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)config.generated.cs" />
<Compile Include="$(IntermediateOutputPath)config.generated.cs" />
</ItemGroup>
</Target>
この例の要点:
Inputsに$(MSBuildProjectFile)を含める:プロジェクトファイル自体が変更された場合(生成に影響するプロパティが変更された場合など)に、ターゲットが再実行されます。Inputsに@(ConfigInput)を含める:生成の元となる実際のソースファイルです。Outputsに$(IntermediateOutputPath)を使用する:生成ファイルはobj/ディレクトリに配置されます。このディレクトリは MSBuild によって管理され、自動的にクリーンアップされます。BeforeTargets="CoreCompile":コンパイラーの実行前に、生成ファイルを利用できるようにします。FileWritesの登録:dotnet cleanで生成ファイルが削除されるようにします。Compileへの追加:評価時にファイルが存在している必要なく、生成ファイルをコンパイルに追加します。
避けるべきよくある間違い
<Target Name="BadTarget" BeforeTargets="CoreCompile">
<Exec Command="generate-code.exe" />
</Target>
<Target Name="BadTarget2"
Inputs="@(Compile)"
Outputs="$(OutputPath)gen_$([System.DateTime]::Now.Ticks).cs">
<Exec Command="generate-code.exe" />
</Target>
<Target Name="GoodTarget"
Inputs="@(Compile)"
Outputs="$(IntermediateOutputPath)generated.cs"
BeforeTargets="CoreCompile">
<Exec Command="generate-code.exe -o $(IntermediateOutputPath)generated.cs" />
<ItemGroup>
<FileWrites Include="$(IntermediateOutputPath)generated.cs" />
<Compile Include="$(IntermediateOutputPath)generated.cs" />
</ItemGroup>
</Target>
パフォーマンスの概要と前処理
MSBuild には、何が実行されているか、またその理由を把握するための組み込みツールがあります。
-
/clp:PerformanceSummary— ビルドの最後に、各ターゲットとタスクにかかった時間の概要を追加します。最もコストの高い処理をすばやく特定するには、これを使用します。dotnet build /clp:PerformanceSummaryこれにより、累積時間の順に並んだターゲットの表が表示されるため、インクリメンタルビルドで実行されるべきではないターゲットを簡単に見つけられます。
-
/pp:preprocess.xml— すべてのインポートをインライン化した単一の XML ファイルを生成し、完全に評価されたプロジェクトを表示します。定義されているターゲット、プロパティ、項目と、その定義元を把握するうえで非常に役立ちます。dotnet msbuild /pp:preprocess.xml前処理済みの出力を検索して、任意のターゲットの
InputsとOutputsが定義されている場所を見つけたり、インポートの全体的な連鎖を把握したりできます。 -
両方を併用して、何が実行されているか(
PerformanceSummary)と何がインポートされているか(/pp)を把握し、バイナリログの分析と照合して全体像を確認します。
よくある修正
- カスタムターゲットには必ず
InputsとOutputsを追加します — これは、インクリメンタルビルドのパフォーマンスを向上させるうえで最も効果の大きい変更です。両方の属性がない場合、ターゲットは毎回実行されます。 - 生成ファイルには
$(IntermediateOutputPath)を使用します —obj/内のファイルは MSBuild のクリーン処理で追跡され、構成間で残ることはありません。 - 生成ファイルを
FileWritesに登録します —dotnet cleanで確実に削除されるようになり、古いファイルが蓄積するのを防げます。 - 生成元で不要な変動要因を避けます — 必要でない限り、タイムスタンプ、ランダム値、ビルドカウンターをファイルパスや生成内容に埋め込まないでください。内容が変わらない場合は、生成ファイルのタイムスタンプを保持します(たとえば、
WriteLinesToFileでOverwrite="true" WriteOnlyWhenDifferent="true"を使用します)。実際の依存関係はInputsに残してください。必要な変動する内容が変わった場合は、依存するターゲットの再ビルドを受け入れます。出力に影響しないことを確認したうえでのみ、ファイルを除外してください。 - インクリメンタルビルドの依存関係を作成せずに項目を渡す必要がある場合は、
OutputsではなくReturnsを使用します —Outputsには、インクリメンタルチェックの定義と、ターゲットから返される項目の定義という二つの役割があります。インクリメンタルビルドに影響を与えず、呼び出し元のターゲットに項目を渡すだけであれば、代わりにReturnsを使用します。<Target Name="GetFiles" Outputs="@(DiscoveredFiles)">...</Target> <Target Name="GetFiles" Returns="@(DiscoveredFiles)">...</Target>