按文件夹自定义生成

可以添加要由 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 示例

例如,下面的 Directory.Build.props 文件为 Visual Studio 解决方案中的所有项目设置了输出目录。 每个项目的输出都放置在自己的项目名称下。 在此示例中,Directory.Build.props 文件位于一个解决方案文件夹中,其下的子文件夹中有许多项目。 $(MSBuildProjectName) 属性提供了每个项目的名称。 由于 Directory.Build.props 文件将在每个项目生成过程中导入相应的项目,因此会被评估认为是解决方案中每个项目的正确值。

  1. 清理解决方案以移除任何旧的输出文件。

    msbuild /t:Clean SolutionName.sln

  2. 在存储库根目录中创建一个名为 Directory.Build.props 的新文件。

  3. 将以下 XML 添加到此文件。

    <Project>
       <PropertyGroup>
          <OutDir>C:\output\$(MSBuildProjectName)</OutDir>
       </PropertyGroup>
    </Project>
    

    注意

    $(OutDir) 属性是输出的绝对路径,使用该属性时无需为 .NET 项目中通常使用的配置、目标框架或运行时创建子文件夹。 如果希望在自定义输出路径下创建通常的子文件夹,请尝试改用属性 BaseOutputPath

  4. 运行 MSBuild。 项目现有的 Microsoft.Common.props 和 Microsoft.Common.target 导入会查找并导入 Directory.Build.props 文件,并且该文件夹下的所有项目都会使用新的输出文件夹。

搜索范围

搜索 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.prop 很早便已导入 Microsoft.Common.props,因此它无法使用后来定义的属性。 因此,请避免引用尚未定义的属性(否则计算结果将为空)。

Directory.Build.props 中设置的属性可能会在项目文件或导入文件中的其他位置被覆盖,因此,应考虑将 Directory.Build.props 中的设置指定为项目的默认值。

从 NuGet 包导入 .targets 文件后,会从 Microsoft.Common.targets 导入 Directory.Build.targets。 因此,它可以覆盖大多数生成逻辑中定义的属性和目标,或者为所有项目设置属性,而不考虑各个项目的设置。

如果需要为单个项目设置覆盖先前所有设置的属性和目标,请在最终导入后将该逻辑放在项目文件中。 要在 SDK 样式的项目中执行此操作,必须先将 SDK 样式的属性替换为等效的导入项。 查看如何使用 MSBuild 项目 SDK

注意

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。 如果尝试将其与已设置的各种其他属性(如 TargetFramework)一起放在项目文件中的 PropertyGroup 元素中,则会在生成项目时发现该属性不会生效。 如果使用 /pp 选项运行 MSBuild 并在输出中搜索 BaseIntermediateOutputPath,则可以知晓原因。 在这种情况下,在 Microsoft.Common.props 中读取和使用 BaseIntermediateOutput

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