当前位置: 首页 > news >正文

.NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?

在为 .NET 项目扩展 MSBuild 编译而编写编译目标(Target)时,我们会遇到用于扩展编译目标用的属性 BeforeTargets AfterTargetsDependsOnTargets

这三个应该分别在什么情况下用呢?本文将介绍其用法。


BeforeTargets / AfterTargets

BeforeTargetsAfterTargets 是用来扩展编译用的。

如果你希望在某个编译任务开始执行一定要执行你的编译目标,那么请使用 BeforeTargets。例如我想多添加一个文件加入编译,那么写:

<Target Name="_WalterlvIncludeSourceFiles"
        BeforeTargets="CoreCompile">
  <ItemGroup>
    <Compile Include="$(MSBuildThisFileFullPath)..\src\Foo.cs" />
  </ItemGroup>
</Target>

这样,一个 Foo.cs 就会在编译时加入到被编译的文件列表中,里面的 Foo 类就可以被使用了。这也是 NuGet 源代码包的核心原理部分。关于 NuGet 源代码包的制作方法,可以扩展阅读:

  • 将 .NET Core 项目打一个最简单的 NuGet 源码包,安装此包就像直接把源码放进项目一样
  • 从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)

如果你希望一旦执行完某个编译任务之后执行某个操作,那么请使用 AfterTargets。例如我想在编译完成生成了输出文件之后,将这些输出文件拷贝到另一个调试目录,那么写:

<Target Name="CopyOutputLibToFastDebug" AfterTargets="AfterBuild">
  <ItemGroup>
    <OutputFileToCopy Include="$(OutputPath)$(AssemblyName).dll"></OutputFileToCopy>
    <OutputFileToCopy Include="$(OutputPath)$(AssemblyName).pdb"></OutputFileToCopy>
  </ItemGroup>
  <Copy SourceFiles="@(OutputFileToCopy)" DestinationFolder="$(MainProjectPath)"></Copy>
</Target>

这种写法可以进行快速的组件调试。下面这篇博客就是用到了 AfterTargets 带来的此机制来实现的:

  • Roslyn 让 VisualStudio 急速调试底层库方法

如果 BeforeTargetsAfterTargets 中写了多个 Target 的名称(用分号分隔),那么只要任何一个准备执行或者执行完毕,就会触发此 Target 的执行。

DependsOnTargets

DependsOnTargets 是用来指定依赖的。

DependsOnTargets 并不会直接帮助你扩展一个编译目标,也就是说如果你只为你的 Target 写了一个名字,然后添加了 DependsOnTargets 属性,那么你的 Target 在编译期间根本都不会执行。

但是,使用 DependsOnTargets,你可以更好地控制执行流程和其依赖关系。

例如上面的 CopyOutputLibToFastDebug 这个将输出文件复制到另一个目录的编译目标(Target),依赖于一个 MainProjectPath 属性,因此计算这个属性值的编译目标(Target)应该设成此 Target 的依赖。

当 A 的 DependsOnTargets 设置为 B;C;D 时,那么一旦准备执行 A 时将会发生:

  • 如果 B C D 中任何一个曾经已经执行过,那么就忽略(因为已经执行过了)
  • 如果 B C D 中还有没有执行的,就立刻执行

实践

当我们实际上在扩展编译的时候,我们会用到不止一个编译目标,因此这几个属性都是混合使用的。但是,你应该在合适的地方编写合适的属性设置。

例如我们做一个 NuGet 包,这个 NuGet 包的 .targets 文件中写了下面几个 Target:

  1. _WalterlvEvaluateProperties
    • 用于初始化一些属性和参数,其他所有的 Target 都依赖于这些参数
  2. _WalterlvGenerateStartupObject
    • 生成一个类,包含 Main 入口点函数,然后将入口点设置成这个类
  3. _WalterlvIncludeSourceFiles
    • 为目标项目添加一些源代码,这就包含刚刚新生成的入口点类
  4. _WalterlvPackOutput
    • 将目标项目中生成的文件进行自定义打包

那么我们改如何为每一个 Target 设置正确的属性呢?

第一步:找出哪些编译目标是真正完成编译任务的,这些编译目标需要通过 BeforeTargetsAfterTarget 设置扩展编译。

于是我们可以找到 _WalterlvIncludeSourceFiles_WalterlvPackOutput

  • _WalterlvIncludeSourceFiles 需要添加参与编译的源代码文件,因此我们需要将 BeforeTargets 设置为 CoreCompile
  • _WalterlvPackOutput 需要在编译完成后进行自定义打包,因此我们将 AfterTargets 设置为 AfterBuild。这个时候可以确保文件已经生成完毕可以使用了。

第二步:找到依赖关系,这些依赖关系需要通过 DependsOnTargets 来执行。

于是我们可以找到 _WalterlvEvaluateProperties_WalterlvGenerateStartupObject

  • _WalterlvEvaluateProperties 被其他所有的编译目标使用了,因此,我们需要将后面所有的 DependsOnTargets 属性设置为 _WalterlvEvaluateProperties
  • _WalterlvGenerateStartupObject 生成的入口点函数被 _WalterlvIncludeSourceFiles 加入到编译中,因此 _WalterlvIncludeSourceFilesDependsOnTargets 属性需要添加 _WalterlvGenerateStartupObject(添加方法是使用分号“;”分隔)。

将所有的这些编译任务合在一起写,将是下面这样:

<Target Name="_WalterlvEvaluateProperties">
</Target>
<Target Name="_WalterlvGenerateStartupObject"
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>
<Target Name="_WalterlvIncludeSourceFiles"
        BeforeTargets="CoreCompile"
        DependsOnTargets="_WalterlvEvaluateProperties;_WalterlvGenerateStartupObject">
</Target>
<Target Name="_WalterlvPackOutput"
        AfterTargets="AfterBuild"
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>

具体依赖于抽象

我们平时在编写代码时会考虑面向对象的六个原则,其中有一个是依赖倒置原则,即具体依赖于抽象。

你不这么写代码当然不会带来错误,但会带来维护性困难。在编写扩展编译目标的时候,这一条同样适用。

假如我们要写的编译目标不止上面这些,还有更多:

  • _WalterlvConvertTemplateCompileToRealCompile
    • 包里有一些模板代码,会在编译期间转换为真实代码并加入编译
  • _WalterlvConditionalImportedSourceCode
    • 会根据 NuGet 包用户的设置有条件地引入一些额外的源代码

那么这个时候我们前面写的用于引入源代码的 _WalterlvIncludeSourceFiles 编译目标其依赖的 Target 会更多。似乎看起来应该这么写了:

<Target Name="_WalterlvIncludeSourceFiles"
        BeforeTargets="CoreCompile"
        DependsOnTargets="_WalterlvEvaluateProperties;_WalterlvGenerateStartupObject;_WalterlvConvertTemplateCompileToRealCompile;_WalterlvConditionalImportedSourceCode">
</Target>

但你小心:

  1. 这个列表会越来越长,而且指不定还会增加一些边边角角的引入的新的源代码呢
  2. _WalterlvConditionalImportedSourceCode 是有条件的,而我们 DependsOnTargets 这样的写法会导致这个 Target 的条件失效

这里更抽象的编译目标是 _WalterlvIncludeSourceFiles,我们的依赖关系倒置了!

为了解决这样的问题,我们引入一个新的属性 _WalterlvIncludeSourceFilesDependsOn,如果有编译目标在编译过程中生成了新的源代码,那么就需要将自己加入到此属性中。

现在的源代码看起来是这样的:

<!-- 这里是一个文件 -->

<PropertyGroup>
  <_WalterlvIncludeSourceFilesDependsOn>
    $(_WalterlvIncludeSourceFilesDependsOn);
    _WalterlvGenerateStartupObject
  </_WalterlvIncludeSourceFilesDependsOn>
</PropertyGroup>

<Target Name="_WalterlvEvaluateProperties">
</Target>
<Target Name="_WalterlvGenerateStartupObject"
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>
<Target Name="_WalterlvIncludeSourceFiles"
        BeforeTargets="CoreCompile"
        DependsOnTargets="$(_WalterlvIncludeSourceFilesDependsOn)">
</Target>
<Target Name="_WalterlvPackOutput"
        AfterTargets="AfterBuild"
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>
<!-- 这里是另一个文件 -->

<PropertyGroup>
  <_WalterlvIncludeSourceFilesDependsOn>
    $(_WalterlvIncludeSourceFilesDependsOn);
    _WalterlvConvertTemplateCompileToRealCompile;
    _WalterlvConditionalImportedSourceCode
  </_WalterlvIncludeSourceFilesDependsOn>
</PropertyGroup>

<PropertyGroup Condition=" '$(UseWalterlvDemoCode)' == 'True' ">
  <_WalterlvIncludeSourceFilesDependsOn>
    $(_WalterlvIncludeSourceFilesDependsOn);
    _WalterlvConditionalImportedSourceCode
  </_WalterlvIncludeSourceFilesDependsOn>
</PropertyGroup>

<Target Name="_WalterlvConvertTemplateCompileToRealCompile"
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>
<Target Name="_WalterlvConditionalImportedSourceCode"
        Condition=" '$(UseWalterlvDemoCode)' == 'True' "
        DependsOnTargets="_WalterlvEvaluateProperties">
</Target>

实际上,Microsoft.NET.Sdk 内部有很多的编译任务是通过这种方式提供的扩展,例如:

  • BuildDependsOn
  • CleanDependsOn
  • CompileDependsOn

你可以阅读我的另一篇博客了解更多:

  • 通过重写预定义的 Target 来扩展 MSBuild / Visual Studio 的编译过程

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 在项目文件 / MSBuild / NuGet 包中编写扩展编译的时候,正确使用 props 文件和 targets 文件
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • 如何给 Windows Terminal 增加一个新的终端(以 Bash 为例)
  • 在 Visual Studio 中设置当发生某个特定异常或所有异常时中断
  • .NET/C# 中设置当发生某个特定异常时进入断点(不借助 Visual Studio 的纯代码实现)
  • 如何在 Windows 10 中安装 WSL2 的 Linux 子系统
  • 如何安装和准备 Visual Studio 扩展/插件开发环境
  • 基于 Roslyn 同时为 Visual Studio 插件和 NuGet 包开发 .NET/C# 源代码分析器 Analyzer 和修改器 CodeFixProvider
  • 软件界面中一些易混淆/易用错的界面文案,以及一些约定俗成的文案约定
  • WPF 的 VisualBrush 只刷新显示的视觉效果,不刷新布局范围
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景
  • 使用 Roslyn 分析代码注释,给 TODO 类型的注释添加负责人、截止日期和 issue 链接跟踪
  • 为 NuGet 指定检测的 MSBuild 路径或版本,解决 MSBuild auto-detection: using msbuild version 自动查找路径不合适的问题
  • 解决方案文件 sln 中的项目类型 GUID
  • Angular2开发踩坑系列-生产环境编译
  • Fastjson的基本使用方法大全
  • IDEA 插件开发入门教程
  • uva 10370 Above Average
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 大快搜索数据爬虫技术实例安装教学篇
  • 模型微调
  • 浅谈Golang中select的用法
  • 如何进阶一名有竞争力的程序员?
  • 我建了一个叫Hello World的项目
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • 数据可视化之下发图实践
  • # .NET Framework中使用命名管道进行进程间通信
  • #Linux(make工具和makefile文件以及makefile语法)
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (12)目标检测_SSD基于pytorch搭建代码
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (4) PIVOT 和 UPIVOT 的使用
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (HAL)STM32F103C6T8——软件模拟I2C驱动0.96寸OLED屏幕
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (五)关系数据库标准语言SQL
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转) RFS+AutoItLibrary测试web对话框
  • (总结)Linux下的暴力密码在线破解工具Hydra详解
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .Net Core与存储过程(一)
  • .NET Reactor简单使用教程
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • .NET构架之我见
  • .NET正则基础之——正则委托
  • @property括号内属性讲解
  • [30期] 我的学习方法