依資料夾自訂組建

您可以新增要讓 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.targets 會尋找 Directory.Build.targets

Directory.Build.props 會在匯入檔案序列的早期匯入,如果您需要設定匯入所使用的屬性 (尤其是使用 Sdk 屬性隱含匯入的屬性),這一點可能很重要,例如在大多數 .NET 專案檔中使用 .NET SDK 時。

注意

以 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 會從專案位置 $(MSBuildProjectFullPath) 向上逐步瀏覽目錄結構,並在找到 Directory.Build.props 檔案之後停止。 例如,如果您的 $(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.props 在早期即匯入 Microsoft.Common.props,無法使用較晚才定義的屬性。 因此,請避免參考尚未定義的屬性 (將會評估為空的)。

Directory.Build.props 中設定的屬性可能會在專案檔或匯入的檔案中覆寫,因此您應該將 Directory.Build.props 中的設定視為指定專案的預設值。

從 NuGet 套件匯入 .targets 檔案之後,會從 Microsoft.Common.targets 匯入 Directory.Build.targets。 因此,它可以覆寫大部分建置邏輯中定義的屬性和目標,或為所有專案設定屬性,而不論個別專案設定的內容為何。

當您需要為覆寫任何先前設定的個別專案設定設定屬性或定義目標時,請在最終匯入之後將該邏輯放入專案檔中。 若要在 SDK 樣式專案中執行這項操作,您必須先將 SDK 樣式屬性取代為對等的匯入。 請參閱如何使用 MSBuild 專案 SDK

注意

在開始專案 (包括任何 PreBuildEvent 檔案) 的建置執行前,MSBuild 引擎會在評估期間讀取所有匯入的檔案,因此 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 的輸出,就可以看到原因。 在此案例中,會讀取 BaseIntermediateOutput 及在 Microsoft.Common.props 中使用。

Microsoft.Common.props 中有一個註解,指出必須在此處設定屬性 BaseIntermediateOutput,然後另一個屬性 MSBuildProjectExtensionsPath 才能使用。 您也可以看到,一開始設定 BaseIntermediateOutputPath 時,會檢查預先存在的值,如果尚未定義,則會設定為 obj

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

因此,此設置告訴您:若要設定此屬性,則必須在比這更早的位置加以指定。 在前置處理過的輸出中,您可以在此程式碼前看到 Directory.Build.props 已匯入,因此您可以在這裡設定 BaseIntermediateOutputPath,這樣便可及早設定以達到所需的效果。

下列縮寫的前置處理輸出會顯示在 Directory.Build.props 中放置 BaseIntermediateOutput 設定的結果。 標準匯入上方的註解包括檔案名稱,而且通常有一些有關該檔案匯入原因的實用資訊。

<?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>
  ...