扩展 Visual Studio 生成过程
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
属性并指定自定义清理操作在目标 CoreClean
之前运行,用 CustomClean
目标清理由自定义生成操作创建的文件。
<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
目标。
通过替代在全部公共目标的 DependsOn
特性中使用的 DependsOnTargets
属性,可防止目标被意外替代。 例如,Build
目标包含 "$(BuildDependsOn)"
的 DependsOnTargets
属性值。 请注意以下几点:
<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 and CleanDependsOn
下面的示例类似于 BeforeTargets
和 AfterTargets
示例,但演示了如何实现类似的功能。 它通过使用 BuildDependsOn
来添加你自己的任务 CustomAfterBuild
以在生成后复制输出文件,并使用 CleanDependsOn
添加相应的 CustomClean
任务,从而扩展生成。
在此示例中,这是一个 SDK 样式项目。 如本文前面有关 SDK 样式项目的注释中所述,必须使用手动导入方法,而不是 Visual Studio 生成项目文件时使用的 Sdk
属性。
<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>
元素的顺序非常重要。 导入标准 SDK 目标文件后,必须出现 BuildDependsOn
和 CleanDependsOn
元素。
重写预定义目标
公共 .targets
文件包含一组预定义的空目标,在生成过程中某些主目标的前后会调用这些空目标。 例如,MSBuild 会在主 CoreBuild
目标之前调用 BeforeBuild
目标,在 CoreBuild
目标之后调用 AfterBuild
目标。 默认情况下,公共目标中的空目标不起任何作用,但可以通过在项目文件中定义所需的目标来替代它们的默认行为。 本文前面介绍的方法是首选的,但可能会遇到使用此方法的较旧代码。
如果项目使用 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>
请注意,已删除顶级
Project
元素上的Sdk
特性。生成项目文件。
预定义目标表
下表显示了公共目标中可替代的所有目标。
目标名称 | 说明 |
---|---|
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 Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
以及以下导入语句作为项目文件的最后一行:
<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 导入之后添加自己的代码。 这意味着可以通过在导入的 .props
文件中生效的第一次导入之前设置属性来更改行为,也可以在最终导入之后替代在其中一个 SDK .targets
文件中定义的目标。 使用此方法,可以替代 BeforeBuild
或 AfterBuild
,如下所述。
后续步骤
可以使用 MSBuild 执行更多操作来自定义生成。 请参阅自定义生成。