MSBuild 的实际工作原理是什么? 在本文中,你将了解 MSBuild 如何处理项目文件,无论是从 Visual Studio 调用还是从命令行或脚本调用。 了解 MSBuild 的工作原理有助于更好地诊断问题并更好地自定义生成过程。 本文介绍生成过程,主要适用于所有项目类型。
完整的生成过程包括 初始启动、评估,以及对构建项目的目标和任务的执行。 除了这些输入之外,外部导入还定义了生成过程的详细信息,包括 标准导入,例如 Microsoft.Common.targets,以及在解决方案或项目级别用户可配置的导入 。
创业公司
可以通过 Microsoft.Build.dll中的 MSBuild 对象模型从 Visual Studio 调用 MSBuild,或者直接在命令行上调用可执行文件(MSBuild.exe
或 dotnet build
),或者在脚本(如 CI 系统中)调用 MSBuild。 在任一情况下,影响生成过程的输入都包括项目文件(或 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,当它执行时,会设置几个布尔属性(BuildingInsideVisualStudio
,BuildProjectReferences
),这严重影响了 MSBuild 的作用。 在每个项目中,执行与通过 MSBuild 调用时相同,但引用的项目会出现差异。 在 MSBuild 中,当需要引用其他项目时,实际上会执行生成过程;也就是说,它会运行任务和工具,并生成输出。 当 Visual Studio 生成找到引用的项目时,MSBuild 仅返回所引用项目的预期输出;它允许 Visual Studio 控制这些其他项目的生成。 Visual Studio 将单独确定生成顺序和对 MSBuild 的调用(根据需要),完全由 Visual Studio 控制。
当使用解决方案文件调用 MSBuild 时,MSBuild 会分析解决方案文件、创建标准 XML 输入文件、评估它并将其作为项目执行时,会出现另一种差异。 解决方案生成会在任何项目之前执行。 从 Visual Studio 生成时,不会发生此情况;MSBuild 永远不会看到解决方案文件。 因此,解决方案生成自定义(使用 before.SolutionName.sln.targets 和 after.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 元素,并在执行阶段声明任务以供以后使用。
评估目标
在此阶段,所有目标对象结构都在内存中创建,为执行做准备。 无实际执行发生。
执行阶段
在执行阶段,将对目标进行排序和运行,并执行所有任务。 但首先,在目标内定义的属性和项将以其显示顺序在单个阶段中进行评估。 处理顺序明显不同于非目标内属性和项的处理方式:首先处理所有属性,然后在单独的过程中处理所有项。 对目标内的属性和项的更改可以在更改它们的目标之后观察到。
目标生成顺序
在单个项目中,目标以串行方式执行。 核心问题是如何确定生成所有内容的顺序,以便依赖项按正确的顺序生成目标。
目标生成顺序取决于在每个目标上使用 BeforeTargets
、DependsOnTargets
和 AfterTargets
属性。 如果早期目标修改了这些属性中引用的属性,则后续目标的顺序可能会影响早期目标的执行。
确定目标生成顺序中介绍了排序规则。 此过程由包含要生成的目标的堆栈结构确定。 此任务顶部的目标开始执行,如果它依赖于任何其他目标,则这些目标将推送到堆栈的顶部,并开始执行。 当某个目标没有任何依赖项时,它会执行直至完成,然后其父目标会继续运行。
项目引用
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)
属性的值设置的。 对于为并行执行启用的任务,工作计划程序管理节点并将工作分配给节点。
标准导入
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
属性,该属性指定构成实际生成步骤(BeforeBuild
、CoreBuild
和 AfterBuild
)的各个目标。 Build
目标定义如下:
<PropertyGroup>
<BuildDependsOn>
BeforeBuild;
CoreBuild;
AfterBuild
</BuildDependsOn>
</PropertyGroup>
<Target
Name="Build"
Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
DependsOnTargets="$(BuildDependsOn)"
Returns="@(TargetPathWithTargetPlatformMoniker)" />
BeforeBuild
和 AfterBuild
是扩展点。 它们在 Microsoft.Common.CurrentVersion.targets 文件中是空的,但项目可以提供自己的 BeforeBuild
和 AfterBuild
目标,其中包含需要在主生成过程之前或之后执行的任务。 AfterBuild
在 no-op 目标 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 | 由 ResolveProjectReferences 和 ResolveAssemblyReferences 组成,用于查找所有依赖项 |
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 项目属性。