按文件夹自定义生成

可以添加 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.targets 导入找到并导入 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.props 会在 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)测试 项目 (2-test)设置通用属性。

若要使 MSBuild 正确地将“内部”文件(2-src2 测试)与“外部”文件(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的输出,则可以查看原因。 在这种情况下,在 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>
  ...