MSBuild 如何生成项目

MSBuild 的实际工作原理是什么? 在本文中,你将了解 MSBuild 如何处理项目文件,无论是从 Visual Studio 调用,还是从命令行或脚本调用。 了解 MSBuild 的工作方式可以帮助你更好地诊断问题并更好地自定义生成过程。 本文介绍了生成过程,该过程在很大程度上适用于所有项目类型。

完整的生成过程包括生成项目的目标和任务的初始启动评估执行。 除了这些输入以外,外部导入还会定义生成过程的详细信息,包括标准导入,如解决方案或项目级别的 Microsoft.Common.targets 和 user-configurable imports

启动

MSBuild 可从 Visual Studio 通过 Microsoft.Build.dll 中的 MSBuild 对象模型调用,也可在命令行或脚本(如 CI 系统)中直接调用可执行文件(MSBuild.exedotnet build)。 在任一情况下,影响生成过程的输入都包括项目文件(或 Visual Studio 内部的项目对象),可能是解决方案文件、环境变量、命令行开关或其对象模型等效项。 在启动阶段,使用命令行选项或对象模型等效项来配置 MSBuild 设置,如配置记录器。 使用 -property-p 开关在命令行上设置的属性设置为全局属性,这些属性将重写项目文件中设置的任何值,即使稍后读取项目文件也是如此。

以下各节介绍了输入文件,如解决方案文件或项目文件。

解决方案和项目

作为解决方案的一部分,MSBuild 实例可能包含一个项目,也可以包含多个项目。 解决方案文件不是 MSBuild XML 文件,但 MSBuild 会将其解释为了解需要针对给定的配置和平台设置生成的所有项目。 当 MSBuild 处理此 XML 输入时,称为解决方案生成。 它有一些可扩展点,使你能够在每个解决方案生成中运行一些内容,但由于此生成是独立于单个项目生成单独运行的,因此,解决方案生成中的属性或目标定义的设置与每个项目生成均不相关。

可在自定义解决方案生成中了解如何扩展解决方案生成。

Visual Studio 生成与MSBuild.exe 生成

在 Visual Studio 中生成项目与通过 MSBuild 可执行文件直接调用 MSBuild 或使用 MSBuild 对象模型启动生成之间,有一些显著的区别。 Visual Studio 管理 Visual Studio 生成的项目生成顺序;它仅在单个项目级别调用 MSBuild,并在执行此操作时,将设置几个布尔属性(BuildingInsideVisualStudioBuildProjectReferences),这对 MSBuild 的功能有很大影响。 在每个项目中,执行进行方式与通过 MSBuild 调用时的方式相同,但在引用项目方面存在差异。 在 MSBuild 中,需要引用项目时,实际上会发生生成;也就是说,它会运行任务和工具,并生成输出。 当 Visual Studio 生成查找引用项目时,MSBuild 仅返回所引用项目的预期输出;它允许 Visual Studio 控制其他项目的生成。 Visual Studio 确定生成顺序并单独调用 MSBuild(根据需要),所有这些操作都完全在 Visual Studio 控制下进行。

如果使用解决方案文件调用 MSBuild,情况又会有所不同,MSBuild 将解析解决方案文件,创建标准 XML 输入文件,对文件进行评估,然后作为项目执行文件。 解决方案生成在任何项目之前执行。 从 Visual Studio 生成时,不会出现这些情况;MSBuild 不会看到解决方案文件。 因此,解决方案生成自定义(使用 before.SolutionName.sln.targetsafter.SolutionName.sln.targets)仅适用于 MSBuild.exe、dotnet build、或对象模型驱动的生成,而不适用于 Visual Studio 生成。

项目 SDK

MSBuild 项目文件的 SDK 功能相对较新。 在进行此更改之前,项目文件显式导入了 .targets 和 .props 文件,这些文件为特定项目类型定义了生成过程。

.NET Core 项目导入适用于它们的 .NET SDK 版本。 请参阅概述 .NET Core 项目 SDK 以及对属性的引用。

评估阶段

本节讨论如何处理和分析这些输入文件,以生成确定将生成内容的内存中对象。

评估阶段的目的是基于输入 XML 文件和本地环境在内存中创建对象结构。 评估阶段包含六个处理输入文件(如项目 XML 文件或/和导入的 XML 文件(通常名 .props 或 .targets 文件))的环节,具体取决于它们主要是设置属性还是定义生成目标 。 每个环节都会生成一部分内存中对象,这些对象稍后会在执行阶段用于生成项目,但在评估阶段不会发生实际的生成操作。 在每个环节中,将按照元素的显示顺序对其进行处理。

评估阶段中的各个环节如下:

  • 评估环境变量
  • 评估导入和属性
  • 评估项定义
  • 评估项
  • 评估 UsingTask 元素
  • 评估目标

按外观顺序在同一传递中对导入和属性进行评估,就像导入已就地展开一样。 因此,可在稍后导入的文件中使用之前导入的文件中的属性设置。

这些环节的顺序有重大影响,在自定义项目文件时无比了解这一点。 请参阅属性和项的评估顺序

评估环境变量

在此阶段中,环境变量用于设置等效属性。 例如,PATH 环境变量可作为属性 $(PATH) 提供。 从命令行或脚本运行时,将正常使用命令环境,从 Visual Studio 运行时,将使用 Visual Studio 启动时生效的环境。

评估导入和属性

在此阶段中,将读入整个输入 XML,包括项目文件和整个导入链。 MSBuild 创建一个内存中 XML 结构,该结构表示项目和所有导入文件的 XML。 此时,将对不在目标中的属性进行评估和设置。

由于 MSBuild 会在其进程中及早读取所有 XML 输入文件,因此在生成过程中对这些输入的任何更改都不会影响当前生成。

任何目标之外的属性的处理方式与目标中属性的处理方式不同。 在此阶段中,只评估在任何目标外部定义的属性。

由于属性在属性环节按顺序进行处理,因此输入中任何位置的属性都可以访问在输入中较早出现的属性值,但不能访问后面显示的属性。

由于在评估项之前处理属性,因此在属性环节的任何部分都不能访问任何项的值。

评估项定义

在此阶段中,将解释项定义,并创建这些定义的内存中表示形式。

评估项

在目标内定义的项与任何目标外定义的项的处理方式不同。 在此阶段中,将处理任何目标之外的项及其关联的元数据。 由项定义设置的元数据将被项的元数据设置替代。 由于项按其显示的顺序进行处理,因此你可以引用之前定义的项,但不能引用后面显示的项。 因为项环节在属性环节之后,因此项都可以访问在任何目标外部定义的任何属性,无论稍后是否显示该属性定义。

评估 UsingTask 元素

在此阶段中,将读取 UsingTask 元素,并声明这些任务以供稍后在执行阶段使用。

评估目标

在此阶段中,将在内存中创建所有目标对象结构,以准备执行。 无实际执行发生。

执行阶段

在执行阶段,将对目标进行排序和运行,并执行所有任务。 但首先,在目标内定义的属性和项将以其显示顺序在单个阶段中进行评估。 处理顺序与不在目标中的属性和项的处理顺序截然不同:首先是所有属性,然后是在各个环节中的所有项。 对目标内的属性和项的更改可以在更改它们的目标之后观察到。

目标生成顺序

在单个项目中,目标按顺序执行。 核心问题在于如何确定生成所有内容的顺序,以便使用依赖项按正确顺序生成目标。

目标生成顺序由每个目标上 BeforeTargetsDependsOnTargetsAfterTargets 属性的使用决定。 如果前面的目标修改了这些属性中引用的属性,则在执行前面的目标期间,后面的目标顺序会受到影响。

确定目标生成顺序中介绍了排序规则。 此过程由包含要生成的目标的堆栈结构决定。 此任务顶部的目标将开始执行,如果它依赖于任何其他内容,则会将这些目标推送到堆栈顶部,并开始执行。 如果目标没有任何依赖项,则会执行到完成,并继续执行其父目标。

项目引用

MSBuild 可以采用两种代码路径,一种是此处描述的普通路径,一种是下一部分中所述的图形选项。

单个项目通过 ProjectReference 项来指定它们对其他项目的依赖性。 当堆栈顶部的项目开始生成时,它将达到 ResolveProjectReferences 目标的执行点,即在公用目标文件中定义的标准目标。

ResolveProjectReferences 使用 ProjectReference 项的输入调用 MSBuild 任务以获取输出。 ProjectReference 项转换为本地项,如 Reference。 当前项目的 MSBuild 执行阶段将暂停,同时执行阶段开始处理引用项目(根据需要先完成评估阶段)。 引用项目仅在你开始生成依赖项目后生成,因此,这会创建一个项目生成树。

Visual Studio 允许在解决方案 (.sln) 文件中创建项目依赖项。 依赖项是在解决方案文件中指定的,并且仅在生成解决方案或在 Visual Studio 内生成时才会考虑使用。 如果生成单个项目,则忽略此类型的依赖项。 解决方案引用由 MSBuild 转换为 ProjectReference 项,然后以相同的方式处理。

图形选项

如果指定图形生成开关(-graphBuild-graph),则 ProjectReference 会成为 MSBuild 使用的一级概念。 MSBuild 将分析所有项目并构造生成顺序图,即项目的实际依赖项关系图,然后遍历该关系图以确定生成顺序。 与单个项目中的目标一样,MSBuild 确保引用的项目是在其所依赖的项目之后生成的。

并行执行

如果使用多处理器支持(-maxCpuCount-m 开关),MSBuild 将创建节点,这些节点是使用可用 CPU 内核的 MSBuild 进程。 每个项目都被提交给一个可用节点。 在节点内,各个项目生成将按顺序执行。

可以通过设置布尔变量 BuildInParallel 为并行执行启用任务,该变量是根据 MSBuild 中 $(BuildInParallel) 属性的值设置的。 对于为并行执行启用的任务,工作计划程序会管理节点并将工作分配给节点。

请参阅使用 MSBuild 并行生成多个项目

标准导入

Microsoft.Common.props 和 Microsoft.Common.targets 都是由 .NET 项目文件导入的(在 SDK 样式项目中显式或隐式导入),并且位于 Visual Studio 安装的 MSBuild\Current\bin 文件夹中。 C++ 项目具有其自己的导入层次结构;请参阅 C++ 项目的 MSBuild 内部层次结构

Microsoft.Common.props 文件设置可重写的默认值。 它在项目文件的开头导入(显式或隐式)。 这样一来,项目设置将显示在默认值之后,这样就可以重写它们。

Microsoft.Common.targets 文件及其导入的目标文件定义 .NET 项目的标准生成过程。 它还提供了可用于自定义生成的扩展点。

在实现中,Microsoft.Common.targets 是一个导入 Microsoft.Common.CurrentVersion.targets 的精简包装器。 此文件包含标准属性设置,并定义可定义生成过程的实际目标。 此处定义 Build 目标,但实际上它是空的。 但 Build 目标包含 DependsOnTargets 属性,该属性指定构成实际生成步骤的各个目标,这些步骤是 BeforeBuildCoreBuildAfterBuildBuild 目标定义如下:

  <PropertyGroup>
    <BuildDependsOn>
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>

  <Target
      Name="Build"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(BuildDependsOn)"
      Returns="@(TargetPathWithTargetPlatformMoniker)" />

BeforeBuildAfterBuild 是扩展点。 它们在 Microsoft.Common.CurrentVersion.targets 文件中是空的,但项目可以提供自己的 BeforeBuildAfterBuild 目标,其中包含需要在主生成过程之前或之后执行的任务。 AfterBuild 在无操作目标 Build 前运行,因为 AfterBuild 出现在 Build 目标的 DependsOnTargets 属性中,但在 CoreBuild 之后发生。

CoreBuild 目标包含对生成工具的调用,如下所示:

  <PropertyGroup>
    <CoreBuildDependsOn>
      BuildOnlySettings;
      PrepareForBuild;
      PreBuildEvent;
      ResolveReferences;
      PrepareResources;
      ResolveKeySource;
      Compile;
      ExportWindowsMDFile;
      UnmanagedUnregistration;
      GenerateSerializationAssemblies;
      CreateSatelliteAssemblies;
      GenerateManifests;
      GetTargetPath;
      PrepareForRun;
      UnmanagedRegistration;
      IncrementalClean;
      PostBuildEvent
    </CoreBuildDependsOn>
  </PropertyGroup>
  <Target
      Name="CoreBuild"
      DependsOnTargets="$(CoreBuildDependsOn)">

    <OnError ExecuteTargets="_TimeStampAfterCompile;PostBuildEvent" Condition="'$(RunPostBuildEvent)'=='Always' or '$(RunPostBuildEvent)'=='OnOutputUpdated'"/>
    <OnError ExecuteTargets="_CleanRecordFileWrites"/>

  </Target>

下表对这些目标进行了说明;某些目标仅适用于某些项目类型。

目标 描述
BuildOnlySettings 仅适用于实际生成的设置,而不适用于 Visual Studio 在项目加载时调用 MSBuild。
PrepareForBuild 准备生成的先决条件
PreBuildEvent 项目的扩展点,用于定义在生成之前要执行的任务
ResolveProjectReferences 分析项目依赖项并生成引用的项目
ResolveAssemblyReferences 找到引用的程序集。
ResolveReferences 包含 ResolveProjectReferencesResolveAssemblyReferences,用于查找所有依赖项
PrepareResources 处理资源文件
ResolveKeySource 解析用于对程序集进行签名的强名称密钥以及用于对 ClickOnce 清单进行签名的证书。
Compile 调用编译器
ExportWindowsMDFile 基于编译器生成的 WinMDModule 文件生成 WinMD 文件。
UnmanagedUnregistration 从上一个生成中删除/清除 COM 互操作注册表项
GenerateSerializationAssemblies 使用 sgen.exe 生成 XML 序列化程序集。
CreateSatelliteAssemblies 为资源中的每个唯一区域性创建一个附属程序集。
生成清单 生成 ClickOnce 应用程序和部署清单或本机清单。
GetTargetPath 使用元数据返回包含此项目的生成产品(可执行文件或程序集)的项。
PrepareForRun 将生成输出复制到最终目录(如果它们已更改)。
UnmanagedRegistration 设置 COM 互操作的注册表项
IncrementalClean 删除在先前生成中生成但未在当前生成中生成的文件。 这是使 Clean 在增量生成中工作的必须条件。
PostBuildEvent 项目的扩展点,用于定义在生成之后运行的任务

上表中的许多目标都可在特定于语言的导入中找到,如 Microsoft.CSharp.targets。 此文件定义特定于 C# .NET 项目的标准生成过程中的步骤。 例如,它包含实际调用 C# 编译器的 Compile 目标。

用户可配置的导入

除了标准导入外,还可以添加几个导入来自定义生成过程。

  • Directory.Build.props
  • Directory.Build.targets

这些文件由其下任何子文件夹中的任何项目的标准导入读入。 这通常是解决方案级别的设置,用于控制解决方案中的所有项目,但在文件系统中也可能更高,直至驱动器的根目录。

Directory.Build.props 文件由 Microsoft.Common.props 导入,因此项目文件中提供了其中定义的属性。 可以在项目文件中重新定义这些属性,以便基于每个项目自定义值。 将在项目文件后读入 Directory.Build.targets 文件。 它通常包含目标,但在这里,你还可以定义不希望个别项目重新定义的属性。

项目文件中的自定义项

当你在“解决方案资源管理器”、“属性”窗口或“项目属性”中进行更改时,Visual Studio 将更新项目文件,但你也可以通过直接编辑项目文件自行进行更改。

许多生成行为可以通过设置 MSBuild 属性进行配置,可以在项目本地设置的项目文件中进行,也可以按照前面部分所述,通过创建 Directory.Build.props 文件为项目和解决方案的整个文件夹全局设置属性。 对于命令行或脚本上的临时生成,还可以使用命令行上的 /p 选项为 MSBuild 的特定调用设置属性。 有关可设置的属性的信息,请参阅常见的 MSBuild 项目属性