扩展 Visual Studio 生成过程

Visual Studio 生成过程由导入到项目文件中的一系列 MSBuild .targets 文件定义。 如果您像通常在使用 Visual Studio 项目时那样使用 SDK,那么这些导入是隐式的。 可以扩展其中一个导入的文件 (Microsoft.Common.targets)以允许在生成过程中的多个点运行自定义任务。 本文介绍可用于扩展 Visual Studio 生成过程的三种方法:

  • 创建自定义目标并指定何时应使用 BeforeTargetsAfterTargets 属性运行。

  • 重写在公共目标中定义的DependsOn属性。

  • 在公共目标(Microsoft.Common.targets 或其导入的文件)中覆盖特定的预定义目标。

AfterTargets 和 BeforeTargets

可以在自定义目标上使用 AfterTargetsBeforeTargets 属性来指定何时应运行。

以下示例演示如何使用 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 属性

  1. 在要重写的常见目标中标识预定义 DependsOn 属性。 有关常用重写 DependsOn 属性的列表,请参阅下表。

  2. 在项目文件末尾定义属性或属性的另一个实例。 例如 $(BuildDependsOn),在新属性中包含原始属性。

  3. 在属性定义前后定义自定义目标。

  4. 生成项目文件。

通常重写 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-&gt;'$(_OutputCopyLocation)%(RecursiveDir)%(Filename)%(Extension)')"/>

    <Copy SourceFiles="@(_FilesToCopy)"
          DestinationFiles="@(_FilesToCopy-&gt;'$(_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 在 BeforeBuildCoreBuild 目标之前调用目标,并在 AfterBuild 目标之后 CoreBuild 调用目标。 默认情况下,公共目标中的空目标不执行任何作,但可以通过在项目文件中定义所需的目标来替代其默认行为。 本文前面介绍的方法是首选方法,但可能会遇到使用此方法的较旧代码。

如果项目使用 SDK(例如 Microsoft.Net.Sdk),则需要从隐式导入更改为显式导入,如 显式导入和隐式导入中所述。

覆盖预定义的目标

  1. 如果项目使用 Sdk 特性,请将该属性更改为显式导入语法。 请参阅 显式和隐式导入

  2. 确定要替代的常见目标中的预定义目标。 有关可以安全替代的目标的完整列表,请参阅下表。

  3. 在项目文件的末尾定义目标,紧接在 </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 元素上的属性。

  4. 生成项目文件。

预定义目标表

下表显示了可以替代的常见目标中的所有目标。

目标名称 DESCRIPTION
BeforeCompileAfterCompile 在这些目标之一中插入的任务在核心编译完成之前或之后运行。 大多数自定义项都是在这两个目标之一中完成的。
BeforeBuildAfterBuild 在这些目标之一中插入的任务将在生成中的其他所有内容之前或之后运行。 注意: 大多数项目文件末尾的注释中已经定义了这些 BeforeBuildAfterBuild 目标,使你可以轻松地将预生成事件和后期生成事件添加到项目文件。
BeforeRebuildAfterRebuild 在其中一个目标中插入的任务在调用核心重新生成功能之前或之后运行。 Microsoft.Common.targets 中的目标执行顺序为:BeforeRebuildCleanBuild,然后AfterRebuild
BeforeCleanAfterClean 在其中一个目标中插入的任务在调用核心清理功能之前或之后运行。
BeforePublishAfterPublish 在其中一个目标中插入的任务在调用核心发布功能之前或之后运行。
BeforeResolveReferencesAfterResolveReferences 在其中一个目标中插入的任务在解析程序集引用之前或之后运行。
BeforeResGenAfterResGen 在其中一个目标中插入的任务在生成资源之前或之后运行。

生成系统和 .NET SDK 中还有更多目标,请参阅 MSBuild 目标 - SDK 和默认生成目标

自定义目标的最佳做法

这两个属性 DependsOnTargetsBeforeTargets 都可以指定一个目标必须在另一个目标之前运行,但在不同的情况下需要它们。 它们因在指定依赖需求的目标上有所不同。 你只能控制自己的目标,并且无法安全地修改系统目标或其他导入的目标,以便约束你选择的方法。

创作自定义目标时,请遵循以下一般准则,确保按预期顺序执行目标。

  1. 使用 DependsOnTargets 特性指定在目标执行之前需要完成的目标。 对于您控制的目标链中的每个目标, 每个目标都可以在DependsOnTargets中指定链的前一个成员。

  2. 使用 BeforeTargets 来处理那些在你不控制的情况下必须执行的目标(例如 BeforeTargets="PrepareForBuild",用于在构建过程中需要提前运行的目标)。

  3. 用于 AfterTargets 不控制保证所需输出的任何目标可用。 例如,指定AfterTargets="ResolveReferences"用于修改引用列表。

  4. 可以将这些组合使用。 例如,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 文件中定义的目标。 使用此方法,可以替代 BeforeBuildAfterBuild 如下所述。

后续步骤

使用 MSBuild 可以做更多来自定义构建。 请参阅自定义生成