フォルダー別にビルドをカスタマイズする

MSBuild によってインポートされる特定のファイルを追加して、既定のプロパティ設定をオーバーライドし、カスタム ターゲットを追加できます。 これらのカスタマイズのスコープは、これらのファイルが配置される場所別にフォルダー レベルで制御できます。

この記事では、次のシナリオに適用できるカスタマイズについて説明します。

  • ソリューション内の多くのプロジェクトのビルド設定をカスタマイズする
  • 共通のファイル ディレクトリで多くのソリューションのビルド設定をカスタマイズする
  • 複雑なフォルダー構造のサブフォルダーに対して異なるビルド設定をカスタマイズする
  • 既定の設定、既定のビルド フォルダー、SDK によって設定されたその他の動作 (例: Microsoft.Net.Sdk) をオーバーライドする
  • 任意の数のプロジェクトまたはソリューションに適用されるビルド ターゲットを追加またはカスタマイズする

C++ プロジェクトを使用している場合は、「C++ ビルドのカスタマイズ」で説明しているメソッドを使用することもできます。

Directory.Build.props と Directory.Build.targets

ソースが格納されているルート フォルダー内の Directory.Build.props という単一のファイルで定義することにより、すべてのプロジェクトに新しいプロパティを追加できます。

MSBuild が実行されると、Microsoft.Common.props はディレクトリ構造で Directory.Build.props ファイルを検索します。 見つかった場合は、そのファイルをインポートし、その中で定義されているプロパティを読み取ります。 Directory.Build.props は、ディレクトリの下のプロジェクトをカスタマイズできるようにする、ユーザー定義のファイルです。

同様に、Microsoft.Common.targetsDirectory.Build.targets を検索します。

Directory.Build.props は、インポートされたファイルのシーケンスの早い段階でインポートされるため、インポートで使用されるプロパティ (特に、ほとんどの .NET プロジェクト ファイルで .NET SDK を使用する場合など、Sdk属性を使用して 暗黙的にインポートされるプロパティ) を設定する必要がある場合は重要になることがあります。

Note

Linux ベースのファイル システムは、大文字小文字を区別します。 Directory.Build.props ファイル名の大文字と小文字が正確に一致していることを確認してください。一致していないと、ビルド プロセス中に検出されません。

詳細については、こちらの GitHub の問題のページを参照してください。

Directory.Build.props の例

たとえば、すべてのプロジェクトで新しい Roslyn の /deterministic 機能 (プロパティ $(Deterministic) によって Roslyn CoreCompile ターゲットで公開される) にアクセスできるようにする場合は、次のようにします。

  1. リポジトリのルートに Directory.Build.props という新しいファイルを作成します。

  2. そのファイルに次の XML を追加します。

    <Project>
     <PropertyGroup>
       <Deterministic>true</Deterministic>
     </PropertyGroup>
    </Project>
    
  3. MSBuild を実行します。 プロジェクトの既存の Microsoft.Common.propsMicrosoft.Common.targets のインポートで、ファイルが検索され、インポートされます。

検索範囲

Directory.Build.props ファイルを検索するときに、MSBuild は Directory.Build.props ファイルが見つかるまでプロジェクトの場所 $(MSBuildProjectFullPath) から上方向にディレクトリ構造を調べます。 たとえば、以下のディレクトリ構造のように、$(MSBuildProjectFullPath)c:\users\username\code\test\case1 である場合、MSBuild はそこから検索を開始し、Directory.Build.props ファイルが見つかるまでディレクトリ構造を上方向に検索します。

c:\users\username\code\test\case1
c:\users\username\code\test
c:\users\username\code
c:\users\username
c:\users
c:\

ソリューション ファイルの場所は Directory.Build.props と関連はありません。

インポートの順序

Directory.Build.propsMicrosoft.Common.props の早い段階でインポートされ、後で定義されるプロパティを使用することはできません。 そのため、まだ定義されていない (したがって、評価が空になる) プロパティを参照しないようにしてください。

Directory.Build.props で設定されたプロパティは、プロジェクト ファイルまたはインポートされたファイル内の他の場所でオーバーライドできます。そのため、プロジェクトの既定値を指定する際に、Directory.Build.props の設定を考慮する必要があります。

Directory.Build.targets は、NuGet パッケージから .targets ファイルがインポートされた後に Microsoft.Common.targets からインポートされます。 そのため、ほとんどのビルド ロジックで定義されているプロパティやターゲットをオーバーライドしたり、個々のプロジェクトの設定に関係なく、すべてのプロジェクトのプロパティを設定したりできます。

プロパティを設定するか、前の設定をオーバーライドする個々のプロジェクトのターゲットを定義する必要がある場合は、最後のインポートの後にそのロジックをプロジェクト ファイルに配置します。 SDK スタイルのプロジェクトでこれを行うには、最初に SDK スタイルの属性を同等のインポートに置き換える必要があります。 「MSBuild プロジェクト SDK の使用方法」を参照してください。

Note

MSBuild エンジンは、プロジェクト (PreBuildEvent を含む) のビルド実行を開始する前に、評価中にインポートされたすべてのファイルを読み取ります。そのため、これらのファイルは PreBuildEvent やビルド プロセスの他の部分によって変更されることはないと思われます。 変更は、次に Msbuild.exe が呼び出されるか、次に Visual Studio がビルドされるまで有効になりません。 また、ビルド プロセスに (複数バージョン対応や依存プロジェクトのビルドの場合と同様に) 多くのプロジェクト ビルドが含まれている場合、インポートされたファイル (Directory.build.props を含む) は、個別のプロジェクト ビルドごとに評価が発生したときに読み取られます。

ユース ケース: マルチレベルの結合

この標準のソリューション構造が用意されているとします。

\
  MySolution.sln
  Directory.Build.props     (1)
  \src
    Directory.Build.props   (2-src)
    \Project1
    \Project2
  \test
    Directory.Build.props   (2-test)
    \Project1Tests
    \Project2Tests

すべてのプロジェクト (1) の共通プロパティ、src プロジェクト (2-src) の共通プロパティ、test プロジェクト (2-test) の共通プロパティを用意すると便利な場合があります。

MSBuild で "内" ファイル (2-src2-test) と "外" ファイル (1) を正しく結合するには、MSBuild で Directory.Build.props ファイルが見つかると後続のスキャンが停止することを考慮する必要があります。 スキャンを続行し、外ファイルに結合するには、次のコードを両方の内ファイルに追加します。

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

MSBuild の一般的手法をまとめると次のようになります。

  • MSBuild では、特定のプロジェクトに対して、最初の Directory.Build.props がソリューション構造の上方で見つかると、既定値を使用して結合され、それ以降のスキャンは停止します。
  • 複数レベルで検索して結合する場合は、"内" ファイルから "外" ファイルを <Import...> します (前のコードを参照)。
  • "外" ファイル自体がその上に何かをインポートしない場合、そこでスキャンが停止します。

もっと簡単にまとめると、何もインポートしない最初の Directory.Build.props が MSBuild の停止箇所になります。

インポート プロセスをより明示的に制御するには、プロパティ $(DirectoryBuildPropsPath)$(ImportDirectoryBuildProps)$(DirectoryBuildTargetsPath)$(ImportDirectoryBuildTargets) を使用します。 $(DirectoryBuildPropsPath) は、使用する Directory.Build.props ファイルへのパスを指定するプロパティです。同様に、$(DirectoryBuildTargetsPath)Directory.Build.targets ファイルへのパスを指定します。

ブール型のプロパティである $(ImportDirectoryBuildProps)$(ImportDirectoryBuildTargets) は既定で true に設定されているため、通常はこれらのファイルが MSBuild で検索されますが、false に設定すると、MSBuild でインポートされないようにすることができます。

この例では、前処理された出力を使用して、プロパティを設定する場所を決定する方法を示します。

設定したい特定のプロパティの使用を分析しやすくするために、/preprocess または /pp 引数を指定して MSBuild を実行できます。 出力テキストは、すべてのインポートの結果です。暗黙的にインポートされる Microsoft.Common.props などのシステム インポートや、独自のインポートなども含まれます。 この出力では、プロパティを設定する必要がある場所を、その値が使用される場所に関連して確認できます。

たとえば、.NET Core または .NET 5 以降のシンプルなコンソール アプリ プロジェクトがあり、中間出力フォルダー (通常は obj) をカスタマイズしたいとします。 このパスを指定するプロパティは BaseIntermediateOutput です。 これを、プロジェクト ファイルの PropertyGroup 要素に、そこで既に設定されている他のさまざまなプロパティ (TargetFramework など) と共に配置しようとすると、プロジェクトをビルドするときにプロパティが機能していないことがわかります。 /pp オプションを指定して MSBuild を実行し、BaseIntermediateOutputPath の出力を検索すると、その理由がわかります。 この場合、BaseIntermediateOutputMicrosoft.Common.props で読み取られ、使用されています。

Microsoft.Common.props にコメントがあり、プロパティ BaseIntermediateOutput はここで、別のプロパティ (MSBuildProjectExtensionsPath) によって使用される前に設定する必要があることが示されています。 また、BaseIntermediateOutputPath が最初に設定されるときに、既存の値のチェックがあり、それが未定義の場合は、obj に設定されることも確認できます。

<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>

したがって、この配置は、このプロパティを設定するには、これより前の場所で指定する必要があることを示しています。 前処理された出力のこのコードの直前で、Directory.Build.props がインポートされていることがわかります。そのため、そこで BaseIntermediateOutputPath を設定すると、目的の効果を得るのに十分早い段階で設定されます。

次の省略形の前処理された出力は、BaseIntermediateOutput 設定を Directory.Build.props に配置した結果を示しています。 標準インポートの上部にあるコメントには、ファイル名と、通常はそのファイルがインポートされる理由についての有益な情報が記載されています。

<?xml version="1.0" encoding="IBM437"?>
<!--
============================================================================================================================================
c:\source\repos\ConsoleApp9\ConsoleApp9\ConsoleApp9.csproj
============================================================================================================================================
-->
<Project DefaultTargets="Build">
  <!--
============================================================================================================================================
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk">
  This import was added implicitly because the Project element's Sdk attribute specified "Microsoft.NET.Sdk".

C:\Program Files\dotnet\sdk\7.0.200-preview.22628.1\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Sdk.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!--
      Indicate to other targets that Microsoft.NET.Sdk is being used.

      This must be set here (as early as possible, before Microsoft.Common.props)
      so that everything that follows can depend on it.

      In particular, Directory.Build.props and nuget package props need to be able
      to use this flag and they are imported by Microsoft.Common.props.
    -->
    <UsingMicrosoftNETSdk>true</UsingMicrosoftNETSdk>
    <!--
      Indicate whether the set of SDK defaults that makes SDK style project concise are being used.
      For example: globbing, importing msbuild common targets.

      Similar to the property above, it must be set here.
    -->
    <UsingNETSdkDefaults>true</UsingNETSdkDefaults>
  </PropertyGroup>
  <PropertyGroup Condition="'$(MSBuildProjectFullPath)' == '$(ProjectToOverrideProjectExtensionsPath)'" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <MSBuildProjectExtensionsPath>$(ProjectExtensionsPathForSpecifiedProject)</MSBuildProjectExtensionsPath>
  </PropertyGroup>
  <!--<Import Project="$(AlternateCommonProps)" Condition="'$(AlternateCommonProps)' != ''" />-->
  <!--
============================================================================================================================================
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="'$(AlternateCommonProps)' == ''">

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Microsoft.Common.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (C) Microsoft Corporation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup>
    <ImportByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
    <ImportByWildcardAfterMicrosoftCommonProps Condition="'$(ImportByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportByWildcardAfterMicrosoftCommonProps>
    <ImportUserLocationsByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardBeforeMicrosoftCommonProps>
    <ImportUserLocationsByWildcardAfterMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardAfterMicrosoftCommonProps>
    <ImportDirectoryBuildProps Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
  </PropertyGroup>
  <!--
      Determine the path to the directory build props file if the user did not disable $(ImportDirectoryBuildProps) and
      they did not already specify an absolute path to use via $(DirectoryBuildPropsPath)
  -->
  <PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and '$(DirectoryBuildPropsPath)' == ''">
    <_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props</_DirectoryBuildPropsFile>
    <_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))</_DirectoryBuildPropsBasePath>
    <DirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsBasePath)' != '' and '$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))</DirectoryBuildPropsPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  <Import Project="$(DirectoryBuildPropsPath)" Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')">

c:\source\repos\ConsoleApp9\Directory.Build.props
============================================================================================================================================
-->
  <!-- Directory.build.props
-->
  <PropertyGroup>
    <BaseIntermediateOutputPath>myBaseIntermediateOutputPath</BaseIntermediateOutputPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  </Import>

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
      Prepare to import project extensions which usually come from packages.  Package management systems will create a file at:
        $(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.props

      Each package management system should use a unique moniker to avoid collisions.  It is a wild-card import so the package
      management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
  -->
  <PropertyGroup>
    <!--
        The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
        in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
    -->
    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
    <BaseIntermediateOutputPath Condition="!HasTrailingSlash('$(BaseIntermediateOutputPath)')">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
    <_InitialBaseIntermediateOutputPath>$(BaseIntermediateOutputPath)</_InitialBaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath Condition="'$(MSBuildProjectExtensionsPath)' == '' ">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
    <!--
        Import paths that are relative default to be relative to the importing file.  However, since MSBuildExtensionsPath
        defaults to BaseIntermediateOutputPath we expect it to be relative to the project directory.  So if the path is relative
        it needs to be made absolute based on the project directory.
    -->
    <MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted($(MSBuildProjectExtensionsPath)))' == 'false'">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))</MSBuildProjectExtensionsPath>
    <MSBuildProjectExtensionsPath Condition="!HasTrailingSlash('$(MSBuildProjectExtensionsPath)')">$(MSBuildProjectExtensionsPath)\</MSBuildProjectExtensionsPath>
    <ImportProjectExtensionProps Condition="'$(ImportProjectExtensionProps)' == ''">true</ImportProjectExtensionProps>
    <_InitialMSBuildProjectExtensionsPath Condition=" '$(ImportProjectExtensionProps)' == 'true' ">$(MSBuildProjectExtensionsPath)</_InitialMSBuildProjectExtensionsPath>
  </PropertyGroup>
  ...