扩展 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 属性并指定自定义清理操作在目标 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 属性

  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 and CleanDependsOn

下面的示例类似于 BeforeTargetsAfterTargets 示例,但演示了如何实现类似的功能。 它通过使用 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-&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>

元素的顺序非常重要。 导入标准 SDK 目标文件后,必须出现 BuildDependsOnCleanDependsOn 元素。

重写预定义目标

公共 .targets 文件包含一组预定义的空目标,在生成过程中某些主目标的前后会调用这些空目标。 例如,MSBuild 会在主 CoreBuild 目标之前调用 BeforeBuild 目标,在 CoreBuild 目标之后调用 AfterBuild 目标。 默认情况下,公共目标中的空目标不起任何作用,但可以通过在项目文件中定义所需的目标来替代它们的默认行为。 本文前面介绍的方法是首选的,但可能会遇到使用此方法的较旧代码。

如果项目使用 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>
    

    请注意,已删除顶级 Project 元素上的 Sdk 特性。

  4. 生成项目文件。

预定义目标表

下表显示了公共目标中可替代的所有目标。

目标名称 说明
BeforeCompileAfterCompile 插入到这些目标之一中的任务,在完成内核编译之前或之后运行。 大多数自定义均在这两个目标之一中完成。
BeforeBuildAfterBuild 插入到这些目标之一中的任务,在生成中所有其他任务之前或之后运行。 备注:BeforeBuildAfterBuild 目标已在大多数项目文件的注释末尾定义,使你能够轻松向项目文件添加预生成和生成后事件。
BeforeRebuildAfterRebuild 插入到这些目标之一中的任务,在调用内核重新生成功能之前或之后运行。 Microsoft.Common.targets 中的目标执行顺序是:BeforeRebuildCleanBuildAfterRebuild
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 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 文件中定义的目标。 使用此方法,可以替代 BeforeBuildAfterBuild,如下所述。

后续步骤

可以使用 MSBuild 执行更多操作来自定义生成。 请参阅自定义生成