copy-to-output-directory について
MSBuild の `CopyToOutputDirectory` モードと、発行時の対応設定を選ぶためのガイドスキルです。コピー動作や増分コストに加え、変更された出力ファイルや既存の `Always` 設定への対処を説明します。
主な機能
- 014種類のコピー モードを説明
- 02ファイルの動作に応じたモード選択
- 03`IfDifferent` の制約を解説
- 04既存の `Always` 設定を一括調整
活用シーン
- 01実行時ファイルのコピー モード選択
- 02ビルド間に変更された出力ファイルのリセット
- 03既存プロジェクトでの不要な再コピー削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill copy-to-output-directoryインストール後、Claude Code で/copy-to-output-directoryを入力して読み込まれたか確認します。
詳細
CopyToOutputDirectory モードの選び方
概要
CopyToOutputDirectory メタデータ(およびその発行時の対応項目である CopyToPublishDirectory)は、項目(Content、None、EmbeddedResource、Compile)をビルド出力と同じ場所にコピーするかどうか、また、どのような条件でコピーするかを制御します。誤ったモードを選ぶと、bin/ に古いファイルが残ったり、ビルドのたびに不要なパフォーマンスコストが発生したりします。
MSBuild 17.13 / .NET SDK 9.0.2xx では、次の 4 つの値を使用できます。
| モード | コピーされる条件… | 増分ビルド時のコスト | 一般的な用途 |
|---|---|---|---|
Never(既定) | コピーしない | なし | 実行時に不要なファイル |
PreserveNewest | コピー元がコピー先より新しい場合(またはコピー先がない場合) | 低い(タイムスタンプの確認) | 一般的なケース — 編集するソースファイル |
Always | すべてのビルドで無条件にコピー | 高い — 何も変更がないビルドでも毎回コピー | レガシーな回避策。使用は避けてください(下記参照) |
IfDifferent | コピー元とコピー先がどちらの方向でも異なる場合(新しい場合または古い場合、サイズが異なる場合、またはコピー先がない場合) | 低い(タイムスタンプとサイズの確認) | ビルド間でコピー先が変更される可能性がある場合 |
<ItemGroup>
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="testdata\seed.db" CopyToOutputDirectory="IfDifferent" />
</ItemGroup>
上記の属性形式のほか、子要素形式も使用できます。
<None Include="testdata\seed.db">
<CopyToOutputDirectory>IfDifferent</CopyToOutputDirectory>
</None>
Always が通常は適切でない理由
Always は、通常ならクリーンな増分ビルドや何も変更がないビルドも含め、すべてのビルドでファイルを再コピーします。多数のコンテンツファイルやサイズの大きなコンテンツファイルを含むプロジェクトでは、これは無視できない継続的なコストとなり、「何も変更していないビルドがすぐに終わらないのはなぜか」という報告が発生する一般的な原因になります。
これまでは、特定のシナリオに対処するには Always を使うしかありませんでした。そのシナリオとは、ビルド間にコピー先のファイルが変更される可能性がある場合です。たとえば、SQLite データベース、ストレージ/状態ファイル、テスト実行によって変更される設定ファイルなどです。PreserveNewest の場合、コピー先が変更されて(タイムスタンプがコピー元より新しくなると)、コピー元のファイルは復元されません。コピー元がもはや新しいとは判断されないためです。コピー先を正常な既知の状態に戻すために Always を使い、その副作用としてビルドのたびにコピーコストを支払っていた人もいました。
IfDifferent:異なる場合に、どちらの方向でもコピー
IfDifferent は、このシナリオに的を絞った解決策です。MSBuild がコピー元とコピー先を異なると判断した場合は、コピー元がコピー先より新しいか古いか、サイズが異なるか、コピー先がないかにかかわらず、コピー元をコピー先にコピーします。MSBuild のヒューリスティックに基づいてコピー先に変更がないと判断された場合は、コピーを省略します。
内部では、_CopyDifferingSourceItemsToOutputDirectory ターゲットが SkipUnchangedFiles="true" を指定して Copy タスクを使用します。この「変更なし」の判定はヒューリスティックであり、確認するのは最終更新時刻とファイルサイズのみです。コンテンツハッシュは確認しません。そのため、コピー先がコピー元と同じサイズおよびタイムスタンプになるよう編集されていると、変更なしと判断され、再コピーされません。実際には、これにより、ビルドのたびに無条件でコピーすることなく、次のビルドで変更されたコピー先をコピー元のバージョンに戻せます(これが、以前 Always を使っていた理由です)。
次の場合は IfDifferent を使用してください。
- テスト実行またはアプリ自体がコピーされたファイル(データベース、キャッシュ、状態/ストレージファイル、編集可能な設定ファイル)に書き込みを行い、ビルドのたびにコピー元のバージョンにリセットしたい場合。
- すべてのビルドで必ずコピーする必要があったのではなく、「出力をコピー元と同期した状態に保つ」目的でのみ
Alwaysを使用していた場合。
<ItemGroup>
<None Include="fixtures\catalog.db" CopyToOutputDirectory="IfDifferent" />
</ItemGroup>
$(SkipUnchangedFilesOnCopyAlways) による Always の全体的な動作の緩和
CopyToOutputDirectory="Always" の項目が多数ある既存のコードベースで、各項目を編集せずにパフォーマンスを向上させたい場合は、次のプロパティを設定してください。
<PropertyGroup>
<SkipUnchangedFilesOnCopyAlways>true</SkipUnchangedFilesOnCopyAlways>
</PropertyGroup>
これにより、_CopyOutOfDateSourceItemsToOutputDirectoryAlways ターゲットが Copy タスクに SkipUnchangedFiles="true" を渡すようになり、Always の項目は実際に異なる場合にのみコピーされます。つまり、Always に IfDifferent と同じ変更がない場合はスキップする動作を適用します。
- 既定値は後方互換性のため
falseです(従来のAlwaysはビルドごとにコピーします)。 - リポジトリ全体で一度に有効にするには、
Directory.Build.propsに設定してください。 - 可能であれば個々の項目を
IfDifferentに変更してください。大規模かつ非侵襲的に有効化するほうが実用的な場合は、このプロパティを使用してください。
ビルドで各モードが適用される流れ
GetCopyToOutputDirectoryItems は、各項目を CopyToOutputDirectory の値ごとに分類します。その後、3 つのコピーターゲットが _CopySourceItemsToOutputDirectory の依存関係として処理を実行します(このターゲットは CopyFilesToOutputDirectory から呼び出されます)。
_CopyOutOfDateSourceItemsToOutputDirectory—PreserveNewestの項目(Inputs/Outputsのタイムスタンプ比較による増分処理)。_CopyOutOfDateSourceItemsToOutputDirectoryAlways—Alwaysの項目($(SkipUnchangedFilesOnCopyAlways)がtrueでない限り、無条件にコピーします)。_CopyDifferingSourceItemsToOutputDirectory—IfDifferentの項目(SkipUnchangedFiles="true")。
コピーされたすべてのファイルは FileWrites に登録されるため、dotnet clean で削除されます。
推移的なコピー: Always、PreserveNewest、IfDifferent が指定された項目は、ProjectReference を介して参照元プロジェクトにも渡されます(_CopyToOutputDirectoryTransitiveItems 経由)。Never の項目は渡されません。IfDifferent は、Always/PreserveNewest とともに ClickOnce の発行項目の収集にも使用されます。
バージョン要件
IfDifferent と $(SkipUnchangedFilesOnCopyAlways) には MSBuild 17.13 以降(.NET SDK 9.0.2xx+/Visual Studio 2022 17.13+)が必要です。古いツールセットでは値が認識されず、共通ターゲットの Always/PreserveNewest/IfDifferent の条件に一致しないため、項目は通知なくコピーされません。古い SDK もサポートする必要がある場合は、ツールセットに応じて使用を制御するか、global.json で最小 SDK を指定してください。
簡単な選択ガイド
- 実行時にファイルが不要 →
Never(または省略。既定値です)。 - 通常のソースファイルで、編集する →
PreserveNewest。 - ビルド間にコピー先が変更され、ソースと同じ状態に戻す必要がある →
IfDifferent。 - 本当にビルドのたびに必ず新しいコピーが必要 →
Always(まれなケース)。 - 多数のレガシーな
Alwaysがあり、編集せずにパフォーマンスを向上させたい →Alwaysのまま、$(SkipUnchangedFilesOnCopyAlways)=trueを設定してください。