如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包

MSBuild 的 Task 为我们扩展项目的编译过程提供了强大的扩展性,它使得我们可以用 C# 语言编写扩展;利用这种扩展性,我们可以为我们的项目定制一部分的编译细节。NuGet 为我们提供了一种自动导入 .props 和 .targets 的方法,同时还是一个 .NET 的包平台;我们可以利用 NuGet 发布我们的工具并自动启用这样的工具。

制作这样的一个跨平台 NuGet 工具,我们能够为安装此工具的项目提供自动的但定制化的编译细节——例如自动生成版本号,自动生成某些中间文件等。

本文更偏向于入门,只在帮助你一步一步地制作一个最简单的 NuGet 工具包,以体验和学习这个过程。然后我会在另一篇博客中完善其功能,做一个完整可用的 NuGet 工具。


关于创建跨平台 NuGet 工具包的博客,我写了两篇。一篇介绍写基于 MSBuild Task 的 dll,一篇介绍写任意的命令行工具,可以是用于 .NET Framework 的 exe,也可以是基于 .NET Core 的 dll,甚至可以是使用本机工具链编译的平台相关的各种格式的命令行工具。内容是相似的但关键的坑不同。我分为两篇可以减少完成单个任务的理解难度:

  • 如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包

  • 如何创建一个基于命令行工具的跨平台的 NuGet 工具包

本文内容
  • 第零步:前置条件

  • 第一步:创建一个项目,用来写工具的核心逻辑

  • 第二步:组织 NuGet 目录

  • 第三步:编写 Target

  • 第四部:调试

    • 让我们的 Target 能够正确找到我们新生成的 dll

    • 准备一个用于测试 Task 的测试项目

    • 让我们自定义的 Task 开始工作,并能够进入断点

  • 第五步:发挥你的想象力

    • .targets 向 Task 传参数

    • Task 向 .targets 返回参数

    • 在 Target 里编写调试代码

    • 在 Task 输出错误或警告

    • 加入差量编译支持

    • 本地测试 NuGet 包

  • 总结

    • 参考资料

第零步:前置条件

第一步:创建一个项目,用来写工具的核心逻辑

为了方便制作跨平台的 NuGet 工具,新建项目时我们优先选用 .NET Core Library 项目或 .NET Standard Library 项目。

紧接着,我们需要打开编辑此项目的 .csproj 文件,将目标框架改成多框架的,并填写必要的信息。

<!-- Walterlv.NuGetTool.csproj --><Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><!-- 给一个初始的版本号。 --><Version>1.0.0-alpha</Version><!-- 使用 .NET Framework 4.7 和 .NET Core 2.0。 要点 1: - 加入 net47 的支持是为了能让基于 .NET Framework 的 msbuild 能够使用此工具编译; - 加入 netcoreapp2.0 的支持是为了能让基于 .NET Core 的 dotnet build (Roslyn) 能够使用此工具编译; - 当然 net47 太新了,只适用于 Visual Studio 2017 的较新版本,如果你需要照顾到更多用户,建议使用 net46。 要点 2: 注意,我们使用 NuGet 包来依赖 Task 框架,但此 NuGet 包要求的最低 .NET Framework 版本为 4.6。 如果需要制作 .NET Framework 4.5 及以下版本,就必须改为引用以下程序集: - Microsoft.Build - Microsoft.Build.Framework - Microsoft.Build.Tasks.v4.0 - Microsoft.Build.Utilities.v4.0 --><TargetFrameworks>net47;netcoreapp2.0</TargetFrameworks><!-- 这个就是创建项目时使用的名称。 --><AssemblyName>Walterlv.NuGetTool</AssemblyName><!-- 此值设为 true,才会在编译之后生成 NuGet 包。 --><GeneratePackageOnBuild>true</GeneratePackageOnBuild><!-- 作者的 Id,如果要发布到 nuget.org,那么这里就是 NuGet 用户 Id。 --><Authors>walterlv</Authors></PropertyGroup></Project>

然后,安装如下 NuGet 包:

  • Microsoft.Build.Framework: 提供了编写 ITask 的框架,有了这个才能写 ITask

  • Microsoft.Build.Utilities.Core: 提供了 ITask 框架的基本实现,这样才能用更少的代码写完 Task

要特别注意:由于我们是一个 NuGet 工具,不需要被其他项目直接依赖,所以此项目的依赖包不应该传递到下一个项目中。所以请将所有的 NuGet 包资产都声明成私有的,方法是在 NuGet 包的引用后面加上 PrivateAssets="All"。想了解 PrivateAssets 的含义一起相关属性,可以阅读我的另一篇文章项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦) - 吕毅。

<ItemGroup><PackageReference Include="Microsoft.Build.Framework" Version="15.6.85" /><PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.6.85" /><PackageReference Update="@(PackageReference)" PrivateAssets="All" /></ItemGroup>

接下来就是取名字的时间了!为 Class1 类改一个名字。这个类将成为我们这个 NuGet 工具包的入口类。

比如我们想做一个用 Git 提交信息来生成版本号的类,可以叫做 GitVersion;想做一个生成多语言文件的类,可以叫做 LangGenerator。在这里,为了示范而不是真正的实现功能,我取名为 DemoTool。

取好名字之后,让这个类继承自 Microsoft.Build.Utilities.Task:

// DemoTool.csusing Microsoft.Build.Utilities;namespace Walterlv.NuGetTool{public class DemoTool : Task{        public override bool Execute({            return true;}}}

这时进行编译,我们的 NuGet 包就会出现在项目的输出目录 bin\Debug 下了。

第二步:组织 NuGet 目录

刚刚生成的 NuGet 包还不能真正拿来用。事实上你也可以拿去安装,不过最终的效果只是加了一个毫无作用的引用程序集而已(顺便还带来一堆垃圾的间接引用)。

所以,我们需要进行“一番配置”,使得这个项目编译成一个NuGet 工具,而不是一个依赖包。

现在,介绍一下 NuGet 预设的目录(如果你想看,可以去解压 .nupkg 文件):

// 根目录,用来放 readme.txt 的(已经有人提 issue 要求加入 markdown 支持了)+ /// 用来放引用程序集 .dll,文档注释 .xml 和符号文件 .pdb 的+ lib/// 用来放那些与平台相关的 .dll/.pdb/.pri 的+ runtimes/// 任意种类的文件,在这个文件夹中的文件会在编译时拷贝到输出目录(保持文件夹结构)+ content/// 这里放 .props 和 .targets 文件,会自动被 NuGet 导入,成为项目的一部分(要求文件名与包名相同)+ build/// 这里也是放 .props 和 .targets 文件,会自动被 NuGet 导入,成为项目的一部分(要求文件名与包名相同)+ buildMultiTargeting/// PowerShell 脚本或者程序,在这里的工具可以在“包管理控制台”(Package Manager Console) 中使用+ tools/

▲ 以上结构可以去官网翻阅原文 How to create a NuGet package - Microsoft Docs,不过我这里额外写了一个预设目录 buildMultiTargeting,官方文档却没有说。

注意到我们的 csproj 文件中的 <TargetFrameworks> 节点吗?如果指定为单个框架,则自动导入的是 build 目录下的;如果指定为多个框架,则自动导入的是 buildMultiTargeting 目录下的。

我们的初衷是做一个 NuGet 工具,所以我们需要选择合适的目录来存放我们的输出文件。

我们要放一个 Walterlv.NuGetTool.targets 文件到 build 和 buildMultiTargeting 文件夹中,以便能够让我们定制编译流程。我们要让我们写的 dll(也就是那个 Task)能够工作,但是以上任何预定义的文件夹都不能满足我们的要求,于是我们建一个自定义的文件夹,取名为 tasks,这样 NuGet 便不会对我们的这个 dll 进行特殊处理,而将处理权全部交给我们。

于是我们自己的目录结构为:

+ build/- Walterlv.NuGetTool.targets+ buildMultiTargeting/- Walterlv.NuGetTool.targets+ tasks/+ net47/- Walterlv.NuGetTool.dll+ netcoreapp2.0/- Walterlv.NuGetTool.dll- readme.txt

那么,如何改造我们的项目才能够生成这样的 NuGet 目录结构呢?

我们先在 Visual Studio 里建好文件夹:

随后去编辑项目的 .csproj 文件,在最后的 </Project> 前面添加下面这些项:

<!-- Walterlv.NuGetTool.csproj --><ItemGroup><None Include="Assets\build\**" Pack="True" PackagePath="build\" /><None Include="Assets\buildMultiTargeting\**" Pack="True" PackagePath="buildMultiTargeting\" /><None Include="Assets\readme.txt" Pack="True" PackagePath="" /></ItemGroup>

None 表示这一项要显示到 Visual Studio 解决方案中(其实对于不认识的文件,None 就是默认值);Include 表示相对于项目文件的路径(支持通配符);Pack 表示这一项要打包到 NuGet;PackagePath 表示这一项打包到 NuGet 中的路径。(如果你想了解更多 csproj 中的 NuGet 属性,可以阅读我的另一篇文章:项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦) - 吕毅)

后来的我们

主演:井柏然 / 周冬雨 / 田壮壮

猫眼电影演出 广告
购买

这样的一番设置,我们的 build、buildMultiTargeting 和 readme.txt 准备好了,但是 tasks 文件夹还没有。由于我们是把我们生成的 dll 放到 tasks 里面,第一个想到的当然是修改输出路径——然而这是不靠谱的,因为 NuGet 并不识别输出路径。事实上,我们还可以设置一个属性 <BuildOutputTargetFolder>,将值指定为 tasks,那么我们就能够将我们的输出文件打包到 NuGet 对应的 tasks 文件夹下了。

至此,我们的 .csproj 文件看起来像如下这样(为了减少行数,我已经去掉了注释):

<!-- Walterlv.NuGetTool.csproj --><Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><Version>1.0.0-alpha</Version><AssemblyName>Walterlv.NuGetTool</AssemblyName><GeneratePackageOnBuild>true</GeneratePackageOnBuild><!-- ↓ 新增的属性 --><BuildOutputTargetFolder>tasks</BuildOutputTargetFolder><!-- ↓ 新增的属性 --><NoPackageAnalysis>true</NoPackageAnalysis><!-- ↓ 新增的属性 --><DevelopmentDependency>true</DevelopmentDependency><Authors>walterlv</Authors></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Build.Framework" Version="15.6.85" /><PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.6.85" /><!-- ↓ 在第一步中不要忘了这一行 --><PackageReference Update="@(PackageReference)" PrivateAssets="All" /></ItemGroup><ItemGroup><Folder Include="Assets\tasks\" /></ItemGroup><ItemGroup><!-- ↓ 新增的三项 --><None Include="Assets\build\**" Pack="True" PackagePath="build\" /><None Include="Assets\buildMultiTargeting\**" Pack="True" PackagePath="buildMultiTargeting\" /><None Include="Assets\readme.txt" Pack="True" PackagePath="" /></ItemGroup></Project>

注意到我同时还在文件中新增了另外两个属性配置 NoPackageAnalysis 和 DevelopmentDependency。由于我们没有 lib 文件夹,所以 NuGet 会给出警告,NoPackageAnalysis 将阻止这个警告。DevelopmentDependency 是为了说明这是一个开发依赖,设置为 true 将阻止包作为依赖传递给下一个项目。(事实上这又是官方的一个骗局!因为新版本的 NuGet 竟然去掉了这个功能!,已经被吐槽了,详见:PackageReference should support DevelopmentDependency metadata · Issue #4125 · NuGet/Home)。关于这些属性更详细的解释,依然可以参见:项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦) - 吕毅。

现在再尝试编译一下我们的项目,去输出目录下解压查看 nupkg 文件,你就能看到期望的 NuGet 文件夹结构了;建议一个个点进去看,你可以看到我们准备好的空的 Walterlv.NuGetTool.targets 文件,也能看到我们生成的 Walterlv.NuGetTool.dll。

第三步:编写 Target

.targets 文件是对项目功能进行扩展的关键文件,由于安装 NuGet 包会自动导入包中的此文件,所以它几乎相当于我们功能的入口。

现在,我们需要徒手编写这个文件了。

<!-- Assets\build\Walterlv.NuGetTool.targets --><Project><PropertyGroup><!-- 我们使用 $(MSBuildRuntimeType) 来判断编译器是 .NET Core 的还是 .NET Framework 的。 然后选用对应的文件夹。--><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tasks\netcoreapp2.0\</NuGetWalterlvTaskFolder><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tasks\net47\</NuGetWalterlvTaskFolder></PropertyGroup><UsingTask TaskName="Walterlv.NuGetTool.DemoTool" AssemblyFile="$(NuGetWalterlvTaskFolder)\Walterlv.NuGetTool.dll" /><Target Name="WalterlvDemo" BeforeTargets="CoreCompile"><DemoTool /></Target></Project>

targets 的文件结构与 csproj 是一样的,你可以阅读我的另一篇文章 理解 C# 项目 csproj 文件格式的本质和编译流程 - 吕毅 了解其结构。

上面的文件中,我们指定 Target 的执行时机为 CoreCompile 之前,也就是编译那些 .cs 文件之前。在这个时机,我们可以修改要编译的 .cs 文件。如果想了解更多关于 Target 执行时机或顺序相关的资料,可以阅读:Target Build Order。

别忘了我们还有一个 buildMultiTargeting 文件夹,也要放一个几乎一样功能的 targets 文件;不过我们肯定不会傻到复制一个一样的。我们在 buildMultiTargeting 文件夹里的 targets 文件中写以下内容,这样我们的注意力便可以集中在前面的 targets 文件中了。

<!-- Assets\buildMultiTargeting\Walterlv.NuGetTool.targets --><Project><!-- 直接 Import 我们在 build 中写的那个 targets 文件。 NuGet 留下了为多框架项目提供特殊扩展的方案,其实有时候也是很有用的。--><Import Project="..\build\Walterlv.NuGetTool.targets" /></Project>

第四部:调试

严格来说,写到这里,我们的跨平台 NuGet 工具已经写完了。在以上状态下,你只需要编译一下,就可以获得一个跨平台的基于 MSBuild Task 的 NuGet 工具。只是——你肯定会非常郁闷——心里非常没谱,这工具到底有没有工作起来!有没有按照我预期的进行工作!如果遇到了 Bug 怎么办!

于是现在我们来掌握一些调试技巧,这样才方便我们一步步完善我们的功能嘛!额外插一句:以上第一到第三步几乎都是结构化的步骤,其实非常适合用工具来自动化完成的。

让我们的 Target 能够正确找到我们新生成的 dll

你应该注意到,我们的 targets 文件在 Assets\build 目录下,而我们的 Assets 文件夹下并没有真实的 tasks 文件夹(里面是空的)。于是我们希望在调试状态下,dll 能够指向输出目录下。于是我们修改 targets 文件添加配置:

<!-- Assets\build\Walterlv.NuGetTool.targets --><Project><PropertyGroup Condition=" $(IsInDemoToolDebugMode) == 'True' "><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\netcoreapp2.0\</NuGetWalterlvTaskFolder><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\net47\</NuGetWalterlvTaskFolder></PropertyGroup><PropertyGroup Condition=" $(IsInDemoToolDebugMode) != 'True' "><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\tasks\netcoreapp2.0\</NuGetWalterlvTaskFolder><NuGetWalterlvTaskFolder Condition=" '$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\tasks\net47\</NuGetWalterlvTaskFolder></PropertyGroup><UsingTask TaskName="Walterlv.NuGetTool.DemoTool" AssemblyFile="$(NuGetWalterlvTaskFolder)\Walterlv.NuGetTool.dll" /><Target Name="WalterlvDemo" BeforeTargets="CoreCompile"><DemoTool /></Target></Project>

这样,我们就拥有了一个可以供用户设置的属性 <IsInDemoToolDebugMode> 了。

准备一个用于测试 Task 的测试项目

接着,我们在解决方案中新建一个调试项目 Walterlv.Debug(我选用了 .NET Standard 2.0 框架)。然后在它的 csproj 中 <Import> 我们刚刚的 .targets 文件,并设置 <IsInDemoToolDebugMode> 属性为 True:

<!-- Walterlv.Debug.csproj --><Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><IsInDemoToolDebugMode>True</IsInDemoToolDebugMode></PropertyGroup><Import Project="..\Walterlv.NuGetTool\Assets\build\Walterlv.NuGetTool.targets" /></Project>

当准备好基本的调试环境之后,我们的解决方案看起来是下面这样的样子:

让我们自定义的 Task 开始工作,并能够进入断点

最简单能够让 DemoTool 这个自定义的 Task 进入断点的方式当然是加上 Debugger.Launch(); 了,就像这样:

// DemoTool.csusing System.Diagnostics;using Microsoft.Build.Utilities;namespace Walterlv.NuGetTool{public class DemoTool : Task{        public override bool Execute({            // 新增了启动调试器的代码。Debugger.Launch();            return true;}}}

这样,一旦此函数开始执行,Windows 将显示一个选择调试器的窗口,我们选择当前打开的 Visual Studio 即可。

当然,也有一些比较正统的方法,为了使这篇文章尽可能简单,我只附一张图,如果有需要,可以自己去尝试:

现在,我们去 Walterlv.Debug 目录下输入 msbuild 命令,在输出到如下部分的时候,就会进入我们的断点了:

这下,我们的调试环境就全部搭建好了,你可以发挥你的想象力在 Task 里面随意挥洒你的代码!

当然,只要你记得去掉 Debugger.Launch();,或者加上 #if DEBUG 这样的条件编译,那么随时打包就是一个可以发布的跨平台 NuGet 工具包了。

提示:一旦调试环境搭建好,你可能会遇到编译 Walterlv.NuGetTool 项目时,发现 dll 被占用的情况,这时,打开任务管理器结束掉 msbuild.exe 进行即可。

第五步:发挥你的想象力

想象力是没有限制的,不过如果不知道 Task 能够为我们提供到底什么样的功能,也是无从下手的。这一节我会说一些 Task 在 C# 代码和 .targets 文件中的互相操作。

.targets 向 Task 传参数

.targets 向 Task 传参数只需要写一个属性赋值的句子就可以了:

<!-- Assets\build\Walterlv.NuGetTool.targets --><Target Name="WalterlvDemo" BeforeTargets="CoreCompile"><DemoTool IntermediateOutputPath="$(IntermediateOutputPath)" /></Target>

这里,$(IntermediateOutputPath) 是 msbuild 编译期间会自动设置的全局属性,代表此项目编译过程中临时文件的存放路径(也就是我们常见的 obj 文件夹)。当然,使用 dotnet build 或者 dotnet msbuild 也是有这样的全局属性的。我们为 <DemoTool>节点也加了一个属性,名为 IntermediateOutputPath。

在 DemoTool 的 C# 代码中,只需要写一个字符串属性即可接收这样的传参。

// DemoTool.cspublic class DemoTool : Task{public string IntermediateOutputPath { get; set; }    public override bool Execute({Debugger.Launch();        var intermediateOutputPath = IntermediateOutputPath;        return true;}}


▲ 在断点中我们能够看到传进来的参数的值

你可以尽情发挥你的想象力,传入更多让人意想不到的参数,实现不可思议的功能。更多 MSBuild 全局参数,可以参考我的另一篇文章项目文件中的已知属性(知道了这些,就不会随便在 csproj 中写死常量啦) - 吕毅。

Task 向 .targets 返回参数

如果只是传入参数,那么我们顶多只能干一些不痛不痒的事情,或者就是两者互相约定了一些常量。什么?你说直接去改源代码?那万一你的代码不幸崩溃了,项目岂不被你破坏了!(当然,你去改了源码,还会破坏 MSBuild 的差量编译。)

我们新定义一个属性,但在属性上面标记 [Output] 特性。这样,这个属性就会作为输出参数传到 .targets 里了。

// DemoTool.csusing System.Diagnostics;using System.IO;using Microsoft.Build.Framework;using Microsoft.Build.Utilities;namespace Walterlv.NuGetTool{public class DemoTool : Task{        public string IntermediateOutputPath { get; set; }[Output]        public string AdditionalCompileFile { get; set; }        public override bool Execute({Debugger.Launch();            var intermediateOutputPath = IntermediateOutputPath;            var additional = Path.Combine(intermediateOutputPath, "DoubiClass.cs");AdditionalCompileFile = Path.GetFullPath(additional);File.WriteAllText(AdditionalCompileFile,                @"using System; namespace Walterlv.Debug { public class Doubi { public string Name { get; } private Doubi(string name) => Name = name; public static Doubi Get() => new Doubi(""吕毅""); } }");            return true;}}}

然后,我们在 .targets 里接收这个输出参数,生成一个属性:

<!-- Assets\build\Walterlv.NuGetTool.targets --><Target Name="WalterlvDemo" BeforeTargets="CoreCompile"><DemoTool IntermediateOutputPath="$(IntermediateOutputPath)"><Output TaskParameter="AdditionalCompileFile" PropertyName="WalterlvDemo_AdditionalCompileFile" /></DemoTool><ItemGroup><Compile Include="$(WalterlvDemo_AdditionalCompileFile)" /></ItemGroup></Target>

这样,我们生成的 Walterlv.Debug 调试项目在编译完成之后,还会额外多出一个“逗比”类。而且——我们甚至能够直接在 Walterlv.Debug 项目的中使用这个编译中生成的新类。

使用编译生成的新类既不会报错,也不会产生警告下划线,就像原生写的类一样。

如果你要在编译期间替换一个类而不是新增一个类,例如将 Class1.cs 更换成新类,那么需要将其从编译列表中移除:

<!-- Assets\build\Walterlv.NuGetTool.targets --><ItemGroup><Compile Remove="Class1.cs" /><Compile Include="$(WalterlvDemo_AdditionalCompileFile)" /></ItemGroup>

需要注意:编译期间才生成的项(<ItemGroup>)或者属性(<PropertyGroup>),需要写在 <Target> 节点的里面。如果写在外面,则不是编译期间生效的,而是始终生效的。当写在外面时,要特别留意可能某些属性没有初始化完全,你应该只使用那些肯定能确认存在的属性或文件。

在 Target 里编写调试代码

虽然说以上的每一个步骤我都是一边实操一边写的,但即便如此,本文都写了 500 多行了,如果你依然能够不出错地完成以上每一步,那也是万幸了!Task 里我能还能用断点调试,那么 Target 里面怎么办呢?

我们可以用 <Message> 节点来输出一些信息:

<!-- Assets\build\Walterlv.NuGetTool.targets --><Target Name="WalterlvDemo" BeforeTargets="CoreCompile"><DemoTool IntermediateOutputPath="$(IntermediateOutputPath)"><Output TaskParameter="AdditionalCompileFile" PropertyName="WalterlvDemo_AdditionalCompileFile" /></DemoTool><Message Text="临时文件的路径为:$(WalterlvDemo_AdditionalCompileFile)" /><ItemGroup><Compile Include="$(WalterlvDemo_AdditionalCompileFile)" /></ItemGroup></Target>

在 Task 输出错误或警告

我们继承了 Microsoft.Build.Utilities.Task,此类有一个 Log 属性,可以用来输出信息。使用 LogWarning 方法可以输出警告,使用 LogError 可以输出错误。如果输出了错误,那么就会导致编译不通过。

加入差量编译支持

如果你觉得你自己写的 Task 执行非常耗时,那么建议加入差量编译的支持。关于加入差量编译,可以参考我的另一篇文章每次都要重新编译?太慢!让跨平台的 MSBuild/dotnet build 的 Target 支持差量编译。

本地测试 NuGet 包

在发布 NuGet 包之前,我们可以先在本地安装测试。由于我们在 C:\Users\lvyi\Desktop\Walterlv.NuGetTool\Walterlv.NuGetTool\bin\Debug输出路径下已经有了打包好的 nupkg 文件,所以可以加一个本地 NuGet 源。

我们找一个其他的项目,然后在 Visual Studio 中设置 NuGet 源为我们那个 NuGet 工具项目的输出路径。

这时安装,编译完之后,我们就会发现我们的项目生成的 dll 中多出了一个“逗比(Doubi)”类,并且可以在那个项目中编写使用 Doubi 的代码了。

总结

不得不说,制作一个跨平台的基于 MSBuild Task 的 NuGet 工具包还是比较麻烦的,我们总结一下:

  1. 准备项目的基本配置(设置各种必要的项目属性,安装必要的 NuGet 依赖)

  2. 建立好 NuGet 的文件夹结构

  3. 编写 Task 和 Target

  4. 新增功能、调试和测试

如果你在实践的过程中遇到了各种问题,欢迎在下面留言,一般我会在一天之内给予回复。

如果在阅读这篇文章时存在一些概念理解上的问题,或者不知道如何扩展本文的功能,可能需要阅读下我的另一些文章:

  • 理解 C# 项目 csproj 文件格式的本质和编译流程 - 吕毅

  • 项目文件中的已知属性(知道了这些,就不会随便在 csproj 中写死常量啦) - 吕毅

  • 项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦) - 吕毅

当然,还有一些正在编写,过一段时间可以阅读到。


参考资料

  • NuGet pack and restore as MSBuild targets - Microsoft Docs

  • Bundling .NET build tools in NuGet

  • Shipping a cross-platform MSBuild task in a NuGet package

  • MSBuild Reserved and Well-Known Properties

  • build process - How does MSBuild check whether a target is up to date or not? - Stack Overflow

  • How to: Build Incrementally

  • How To: Implementing Custom Tasks – Part I – MSBuild Team Blog

  • Overwrite properties with MSBuild - Stack Overflow

  • How to Access MSBuild properties inside custom task

  • visual studio - How to get property value of a project file using msbuild - Stack Overflow

  • davidfowl/NuGetPowerTools: A bunch of powershell modules that make it even easier to work with nuget

  • [MSBuild and Skipping target “ 

    " because it has no outputs - Stack Overflow](https://stackoverflow.com/questions/27377095/msbuild-and-skipping-target-targetname-because-it-has-no-outputs)
  • WriteCodeFragment Task

  • Don’t include dependencies from packages.config file when creating NuGet package - Stack Overflow

  • NuGet 2.7 Release Notes - Microsoft Docs

  • PackageReference should support DevelopmentDependency metadata · Issue #4125 · NuGet/Home

  • debugging - How to debug MSBuild Customtask - Stack Overflow

本文会经常更新,请阅读原文: https://walterlv.github.io/post/create-a-cross-platform-msbuild-task-based-nuget-tool.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

原文地址: https://walterlv.github.io/post/create-a-cross-platform-msbuild-task-based-nuget-tool.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

Reward

长按二维码向我转账

pic_reward_qrcode.2x3534de.png

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321293.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

codeforces F.F. Teodor is not a liar! 最长不降子序列

题意 给出一堆线段。 询问者每次可以询问一个整数点&#xff0c;回答者告诉询问者这个点被多少根线段包括。 问询问者最多问多少次&#xff0c;还不能确定任意一个整数点都不可能被所有的线段包含。 题解 首先用O(n)的方法计算出来每个点被多少条线段包含。 突破点&#x…

2019.01.23【NOIP普及组】模拟赛C组总结

总结 这次的分数是1001000100300 第一题第一题一开始把题看错了&#xff0c;乱打一通&#xff0c;结果才发现自己打错了&#xff0c;把题看清后&#xff0c;就知道用栈&#xff0c;快速解决。第二题和之前做过的一道题十分相似&#xff0c;就是用前缀和来将求一个范围内的巧克…

P2149-[SDOI2009]Elaxia的路线【最短路】

正题 题目链接:https://www.luogu.com.cn/problem/P2149 题目大意 nnn个点mmm条边的一张无向图&#xff0c;给定两个起点和对应的终点。求两个最短路的最长公共距离 解题思路 首先要求是最短路&#xff0c;我们可以先跑一次第一个起点的SPFASPFASPFA&#xff0c;然后从终点开…

Platform.Uno介绍

编者语&#xff1a;Xamarin国内很多人说缺乏可用的实例&#xff0c;我在写书过程中在完善一些常用场景的例子&#xff0c;希望帮到大家。Build 2018结束一周了&#xff0c;善友问我要不要谈谈Xamarin的一些变化&#xff0c;但碍于时间有限一直没有付诸行动。想想总得写点什么给…

codeforces F.Fibonacci String Subsequences

题意 定义F(x)为F(x-1)与F(x-2)的连接&#xff08;其中F(0) ‘0’,F(1) ‘1’&#xff09;。 给出一个长度不超过100的字符串s&#xff0c;询问s在F(x)的所有子序列中出现了多少次。 题解 数量很大的计数问题&#xff0c;我们首先想到的解决方案就是dp。 我们考虑F(x) F…

【dfs】GCD与LCM(jzoj 1608)

GCD与LCM 题目大意&#xff1a; 给出a,b的最大公因数和最小公倍数&#xff0c;求出符合条件的a,b的最小差值 样例输入 6 36 样例输出 6 数据范围限制 提示 数据说明&#xff1a; 对于50%的数据&#xff0c;1<a<b<10^3。 对于100%的数据&#xff0c;1<a&…

P3889-[GDOI2014]吃【线段树】

正题 题目链接:https://www.luogu.com.cn/problem/P3889 题目大意 nnn个数的序列&#xff0c;mmm次询问&#xff0c;每次给出一个区间[l,r][l,r][l,r]&#xff0c;求在区间内和区间外各选一个数使得他们的gcdgcdgcd最大 解题思路 首先没有修改且没有要求强制在线&#xff0c;…

ASP.NET Core amp; Docker 实战经验分享

一.前言最近一直在研究和实践ASP.NET Core、Docker、持续集成。在ASP.NET Core 和 Dcoker结合下遇到了一些坑&#xff0c;在此记录和分享&#xff0c;希望对大家有一些帮助。二.中间镜像我前面写过一个 《ASP.NET Core & Docker 零基础持续集成 》的教程。里面我们通过持续…

codeforces E. Picking Strings 构造

题目链接 Picking String 题意 给出字符串S和T&#xff0c;1e5个询问&#xff0c;每次询问S的一段区间是否能转变成T的一段区间。 转变方式&#xff1a; A>BCA>BCB>ACB>ACC>ABC>ABAAAAAA可以消除 题解 我们从以上四个条件出发推导出更加精华的条件 B>…

P4169-[Violet]天使玩偶/SJY摆棋子【CDQ分治】

正题 题目链接:https://www.luogu.com.cn/problem/P4169 题目大意 nnn个点&#xff0c;然后每次操作 加一个新的点询问一个点更近的点 解题思路 定义tit_iti​表示第几个操作&#xff0c;开始就有的点tit_iti​为000 假设最近的点在左上角&#xff0c;那么有要求ti<tj,xi…

【Floyed】【匈牙利算法】导弹(jzoj 1610)

题目大意&#xff1a; 有n个城市&#xff0c;有一部分是A国的&#xff0c;有一部分是B国的&#xff08;小于A国的&#xff09;&#xff0c;A国每个城市都有一枚导弹&#xff08;只有一枚&#xff09;&#xff0c;炸毁别的城市的时间是到这个城市的距离&#xff0c;请问A国最快…

codeforces G - Almost Increasing Array 动态规划、动态开点线段树

题意 给出一个序列&#xff0c;允许删除一个元素&#xff0c;并将任意元素的值修改为任意整数&#xff0c;问最少修改多少个元素使得序列变成严格单调递增的序列&#xff1f; 题解 这道题目很具有启发性&#xff1a; 不考虑删除元素&#xff0c;原数列各个数值减去他们下标得…

Oracle .NET Core Beta驱动已出,自己动手写EF Core Oracle

使用.net core也有一段时间了&#xff0c;一直都没有Oracle官方的正式版驱动程序&#xff0c;更别说EF版本了。之前基于Oracle官方的.net core预览版本写了个Dapper的数据库操作实现&#xff0c;但是总感觉不太完美&#xff0c;有消息称Oracle官方的EF版本可能要到第三季度出了…

nssl1452-排行榜【数论】

正题 题目大意 给出nnn&#xff0c;求一个长度为2n2n2n的由1∼n1\sim n1∼n各两个组成的一个序列使得有一个数的前缀数量不小于任何数字。 解题思路 首先这个数字肯定是第一个数字&#xff0c;这里假设为111&#xff0c;那么要求任意位置111的前缀数量都不小于别的数。 也就…

【离散化】【差分】幻灯片(jzoj 1609)

幻灯片 题目大意&#xff1a; 有n个幻灯片映在一起&#xff0c;每个幻灯片的的左上角是a1,a2,右上角是a3,a4,颜色是a5当多个幻灯片在同一个位置时&#xff0c;颜色就是他们的和&#xff0c;求有所少种颜色 样例输入 3 2 2 3 3 2 2 0 4 4 1 1 1 3 5 3 样例输出 4 数据…

.net core在网关中统一配置Swagger

最近在做微服务的时候&#xff0c;由于我们是采用前后端分离来开发的&#xff0c;提供给前端的直接是Swagger&#xff0c;如果Swagger分布在各个API中&#xff0c;前端查看Swagger的时候非常不便&#xff0c;因此&#xff0c;我们试着将Swagger集中放到网关中。这里我用两个API…

P4074-[WC2013]糖果公园【树上带修莫队】

正题 题目链接:https://www.luogu.com.cn/problem/P4074 题目大意 nnn个点的一颗数&#xff0c;第iii个点有一颗cic_ici​种类的糖。 第iii次获得jjj种类的糖可以产生价值wi∗vjw_i*v_jwi​∗vj​。 每次操作 修改一个点的糖果种类询问一个路径的价值和 解题思路 用欧拉序…

闲来无事刷水题、简单博弈论专题、sg函数、洛谷

记 今天闲来无事&#xff0c;不想刷codeforces了&#xff0c;到洛谷提高组训练营找几道水题刷着玩玩&#xff08;虽然自己早已过了打OI的年纪&#xff09;&#xff5e; 简单博弈论专题 P1199 三国游戏 这么考虑&#xff0c;由于电脑总是不能让我搭配出当前能搭配出的最大的…

医院(jzoj 1611)

医院 题目大意&#xff1a; 有n个城市&#xff0c;有m条线路使他们相连&#xff08;有向&#xff09;&#xff0c; 在一些城市中建立医院&#xff0c;相连的城市只能有一间医院&#xff0c;每个医院可以派医生去别的城市&#xff0c;当医生走的线路>3时&#xff0c;他们会…

2019.01.24【NOIP普及组】模拟赛C组

总结 这一次的分数&#xff1a;1002562.562.5250 第一题老实打完&#xff0c;100分&#xff0c;第二题蒙点25分&#xff0c;第三题只打了Floyed&#xff0c;还有一部分&#xff08;匈牙利算法&#xff09;没打&#xff0c;第四题蒙点特判&#xff0c;62.5,&#xff08;说实在我…