Visual Studio 生成过程由导入到项目文件中的一系列 MSBuild .targets
文件定义。 如果您像通常在使用 Visual Studio 项目时那样使用 SDK,那么这些导入是隐式的。 可以扩展其中一个导入的文件 (Microsoft.Common.targets)以允许在生成过程中的多个点运行自定义任务。 本文介绍可用于扩展 Visual Studio 生成过程的三种方法:
创建自定义目标并指定何时应使用
BeforeTargets
和AfterTargets
属性运行。重写在公共目标中定义的
DependsOn
属性。在公共目标(Microsoft.Common.targets 或其导入的文件)中覆盖特定的预定义目标。
AfterTargets 和 BeforeTargets
可以在自定义目标上使用 AfterTargets
和 BeforeTargets
属性来指定何时应运行。
以下示例演示如何使用 AfterTargets
属性添加一个自定义目标,该目标使用输出文件执行某些作。 在这种情况下,它会将输出文件复制到新的文件夹 CustomOutput。 该示例还演示了如何使用BeforeTargets
属性来清理由CustomClean
目标创建的文件,并指定自定义清理操作在CoreClean
目标之前运行。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<_OutputCopyLocation>$(OutputPath)..\..\CustomOutput\</_OutputCopyLocation>
</PropertyGroup>
<Target Name="CustomAfterBuild" AfterTargets="Build">
<ItemGroup>
<_FilesToCopy Include="$(OutputPath)**\*"/>
</ItemGroup>
<Message Text="_FilesToCopy: @(_FilesToCopy)" Importance="high"/>
<Message Text="DestFiles:
@(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
<Copy SourceFiles="@(_FilesToCopy)"
DestinationFiles=
"@(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
</Target>
<Target Name="CustomClean" BeforeTargets="CoreClean">
<Message Text="Inside Custom Clean" Importance="high"/>
<ItemGroup>
<_CustomFilesToDelete Include="$(_OutputCopyLocation)**\*"/>
</ItemGroup>
<Delete Files='@(_CustomFilesToDelete)'/>
</Target>
</Project>
警告
请务必使用与预定义目标不同的名称(例如,此处的自定义生成目标是CustomAfterBuild
,而不是AfterBuild
),因为这些预定义目标会被 SDK 导入覆盖,而 SDK 导入也会定义这些目标。 有关预定义目标的列表,请参阅本文末尾的 表 。
扩展 DependsOn 属性
扩展生成过程的另一种方法是使用 DependsOn
属性(例如), BuildDependsOn
指定应在标准目标之前运行的目标。
此方法优于重写预定义的目标,下一部分对此进行了讨论。 重写预定义的目标是一种旧方法,仍然受到支持。但是,由于 MSBuild 按顺序评估目标的定义,因此无法阻止其他导入了您项目的项目重写您已经重写过的目标。 因此,例如,导入所有其他项目后在项目文件中定义的最后 AfterBuild
一个目标将是生成期间使用的目标。
可以通过重写在公共目标中使用的 DependsOnTargets
属性所属的 DependsOn
属性,来防止意外覆盖目标。 例如,目标 Build
包含 DependsOnTargets
属性值为 "$(BuildDependsOn)"
. 如下所示:
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)"/>
此 XML 片段指示在 Build
目标可以运行之前,属性中指定的 BuildDependsOn
所有目标必须首先运行。 该 BuildDependsOn
属性定义为:
<PropertyGroup>
<BuildDependsOn>
$(BuildDependsOn);
BeforeBuild;
CoreBuild;
AfterBuild
</BuildDependsOn>
</PropertyGroup>
可以通过声明项目文件末尾命名 BuildDependsOn
的另一个属性来替代此属性值。 在 SDK 样式项目中,这意味着必须使用显式导入。 请参阅 隐式和显式导入,以便可以在上次导入后放置 DependsOn
属性。 通过在新属性中包含上 BuildDependsOn
一个属性,可以将新目标添加到目标列表的开头和末尾。 例如:
<PropertyGroup>
<BuildDependsOn>
MyCustomTarget1;
$(BuildDependsOn);
MyCustomTarget2
</BuildDependsOn>
</PropertyGroup>
<Target Name="MyCustomTarget1">
<Message Text="Running MyCustomTarget1..."/>
</Target>
<Target Name="MyCustomTarget2">
<Message Text="Running MyCustomTarget2..."/>
</Target>
导入您的项目文件的项目可以进一步扩展这些属性,而无需覆盖您所做的自定义设置。
重写 DependsOn 属性
在要重写的常见目标中标识预定义
DependsOn
属性。 有关常用重写DependsOn
属性的列表,请参阅下表。在项目文件末尾定义属性或属性的另一个实例。 例如
$(BuildDependsOn)
,在新属性中包含原始属性。在属性定义前后定义自定义目标。
生成项目文件。
通常重写 DependsOn 属性
属性名称 | 添加的目标在此点之前运行: |
---|---|
BuildDependsOn |
主生成入口点。 如果要在整个生成过程之前或之后插入自定义目标,请重写此属性。 |
RebuildDependsOn |
Rebuild
|
RunDependsOn |
最终生成输出的执行(如果是.EXE) |
CompileDependsOn |
编译(Compile 目标)。 如果要在编译步骤之前或之后插入自定义进程,请重写此属性。 |
CreateSatelliteAssembliesDependsOn |
创建卫星程序集 |
CleanDependsOn |
目标 Clean (删除所有中间和最终的生成输出)。 如果想要清理自定义构建过程的结果,请覆盖此属性。 |
PostBuildEventDependsOn |
目标PostBuildEvent |
PublishBuildDependsOn |
构建发布 |
ResolveAssemblyReferencesDependsOn |
目标 ResolveAssemblyReferences (确定给定依赖项的传递闭包)。 请参阅 ResolveAssemblyReference 。 |
示例:BuildDependsOn 和 CleanDependsOn
下面的示例与 BeforeTargets
示例 AfterTargets
类似,但演示如何实现类似的功能。 该过程通过使用 BuildDependsOn
添加您自己的任务 CustomAfterBuild
来扩展构建,该任务在构建后复制输出文件,同时还通过使用 CleanDependsOn
添加相应的 CustomClean
任务。
在此示例中,这是一个 SDK 样式的项目。 如本文前面的有关 SDK 样式项目的说明中所述,必须使用手动导入方法,而不是 Sdk
Visual Studio 生成项目文件时使用的属性。
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk"/>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk"/>
<PropertyGroup>
<BuildDependsOn>
$(BuildDependsOn);CustomAfterBuild
</BuildDependsOn>
<CleanDependsOn>
$(CleanDependsOn);CustomClean
</CleanDependsOn>
<_OutputCopyLocation>$(OutputPath)..\..\CustomOutput\</_OutputCopyLocation>
</PropertyGroup>
<Target Name="CustomAfterBuild">
<ItemGroup>
<_FilesToCopy Include="$(OutputPath)**\*"/>
</ItemGroup>
<Message Importance="high" Text="_FilesToCopy: @(_FilesToCopy)"/>
<Message Text="DestFiles:
@(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
<Copy SourceFiles="@(_FilesToCopy)"
DestinationFiles="@(_FilesToCopy->'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>
</Target>
<Target Name="CustomClean">
<Message Importance="high" Text="Inside Custom Clean"/>
<ItemGroup>
<_CustomFilesToDelete Include="$(_OutputCopyLocation)**\*"/>
</ItemGroup>
<Delete Files="@(_CustomFilesToDelete)"/>
</Target>
</Project>
元素的顺序非常重要。
BuildDependsOn
导入标准 SDK 目标文件后,必须显示和CleanDependsOn
元素。
重写预定义的目标
常见 .targets
文件包含一组预定义的空目标,这些目标在生成过程中的某些主要目标之前和之后调用。 例如,MSBuild 在 BeforeBuild
主 CoreBuild
目标之前调用目标,并在 AfterBuild
目标之后 CoreBuild
调用目标。 默认情况下,公共目标中的空目标不执行任何作,但可以通过在项目文件中定义所需的目标来替代其默认行为。 本文前面介绍的方法是首选方法,但可能会遇到使用此方法的较旧代码。
如果项目使用 SDK(例如 Microsoft.Net.Sdk
),则需要从隐式导入更改为显式导入,如 显式导入和隐式导入中所述。
覆盖预定义的目标
如果项目使用
Sdk
特性,请将该属性更改为显式导入语法。 请参阅 显式和隐式导入。确定要替代的常见目标中的预定义目标。 有关可以安全替代的目标的完整列表,请参阅下表。
在项目文件的末尾定义目标,紧接在
</Project>
标记之前,并在显式 SDK 导入之后。 例如:<Project> <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" /> ... <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" /> <Target Name="BeforeBuild"> <!-- Insert tasks to run before build here --> </Target> <Target Name="AfterBuild"> <!-- Insert tasks to run after build here --> </Target> </Project>
请注意,
Sdk
已删除顶级Project
元素上的属性。生成项目文件。
预定义目标表
下表显示了可以替代的常见目标中的所有目标。
目标名称 | DESCRIPTION |
---|---|
BeforeCompile 、AfterCompile |
在这些目标之一中插入的任务在核心编译完成之前或之后运行。 大多数自定义项都是在这两个目标之一中完成的。 |
BeforeBuild 、AfterBuild |
在这些目标之一中插入的任务将在生成中的其他所有内容之前或之后运行。
注意: 大多数项目文件末尾的注释中已经定义了这些 BeforeBuild 和 AfterBuild 目标,使你可以轻松地将预生成事件和后期生成事件添加到项目文件。 |
BeforeRebuild 、AfterRebuild |
在其中一个目标中插入的任务在调用核心重新生成功能之前或之后运行。
Microsoft.Common.targets 中的目标执行顺序为:BeforeRebuild 、Clean 、Build ,然后AfterRebuild 。 |
BeforeClean 、AfterClean |
在其中一个目标中插入的任务在调用核心清理功能之前或之后运行。 |
BeforePublish 、AfterPublish |
在其中一个目标中插入的任务在调用核心发布功能之前或之后运行。 |
BeforeResolveReferences 、AfterResolveReferences |
在其中一个目标中插入的任务在解析程序集引用之前或之后运行。 |
BeforeResGen 、AfterResGen |
在其中一个目标中插入的任务在生成资源之前或之后运行。 |
生成系统和 .NET SDK 中还有更多目标,请参阅 MSBuild 目标 - SDK 和默认生成目标。
自定义目标的最佳做法
这两个属性 DependsOnTargets
和 BeforeTargets
都可以指定一个目标必须在另一个目标之前运行,但在不同的情况下需要它们。 它们因在指定依赖需求的目标上有所不同。 你只能控制自己的目标,并且无法安全地修改系统目标或其他导入的目标,以便约束你选择的方法。
创作自定义目标时,请遵循以下一般准则,确保按预期顺序执行目标。
使用
DependsOnTargets
特性指定在目标执行之前需要完成的目标。 对于您控制的目标链中的每个目标, 每个目标都可以在DependsOnTargets
中指定链的前一个成员。使用
BeforeTargets
来处理那些在你不控制的情况下必须执行的目标(例如BeforeTargets="PrepareForBuild"
,用于在构建过程中需要提前运行的目标)。用于
AfterTargets
不控制保证所需输出的任何目标可用。 例如,指定AfterTargets="ResolveReferences"
用于修改引用列表。可以将这些组合使用。 例如,
DependsOnTargets="GenerateAssemblyInfo" BeforeTargets="BeforeCompile"
。
显式和隐式导入
Visual Studio 生成的项目通常使用 Sdk
项目元素上的属性。 这些类型的项目称为 SDK 样式项目。 请参阅 使用 MSBuild 项目 SDK。 下面是一个示例:
<Project Sdk="Microsoft.Net.Sdk">
当你的项目使用 Sdk
属性时,会隐式添加两个导入,一个在项目文件的开头,另一个在末尾。
隐式导入语句等效于在项目文件中 Project
元素之后作为第一行的 import 语句,如下所示:
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
和以下 import 语句作为项目文件中的最后一行:
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
此语法称为 显式 SDK 导入。 使用此显式语法时,应省略 Sdk
项目元素上的属性。
隐式 SDK 导入相当于导入那些在旧项目文件中通常构造为“通用”的特定文件,如 .props
或 .targets
所示:
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
和
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
任何此类旧引用都应替换为本节前面所示的显式 SDK 语法。
使用显式 SDK 语法意味着可以在第一次导入之前或最终的 SDK 导入之后添加自己的代码。 这意味着,可以在第一次导入生效之前设置属性来更改行为,并且可以在最终导入完成后覆盖在其中一个 SDK .targets
文件中定义的目标。 使用此方法,可以替代 BeforeBuild
或 AfterBuild
如下所述。
后续步骤
使用 MSBuild 可以做更多来自定义构建。 请参阅自定义生成。