MSBuild 的 Task 为我们扩展项目的编译过程提供了强大的扩展性,它使得我们可以用 C# 语言编写扩展;利用这种扩展性,我们可以为我们的项目定制一部分的编译细节。NuGet 为我们提供了一种自动导入 .props 和 .targets 的方法,同时还是一个 .NET 的包平台;我们可以利用 NuGet 发布我们的工具并自动启用这样的工具。
制作这样的一个跨平台 NuGet 工具,我们能够为安装此工具的项目提供自动的但定制化的编译细节——例如自动生成版本号,自动生成某些中间文件等。
本文更偏向于入门,只在帮助你一步一步地制作一个最简单的 NuGet 工具包,以体验和学习这个过程。然后我会在另一篇博客中完善其功能,做一个完整可用的 NuGet 工具。
关于创建跨平台 NuGet 工具包的博客,我写了两篇。一篇介绍写基于 MSBuild Task 的 dll,一篇介绍写任意的命令行工具,可以是用于 .NET Framework 的 exe,也可以是基于 .NET Core 的 dll,甚至可以是使用本机工具链编译的平台相关的各种格式的命令行工具。内容是相似的但关键的坑不同。我分为两篇可以减少完成单个任务的理解难度:
第零步:前置条件
第一步:创建一个项目,用来写工具的核心逻辑
为了方便制作跨平台的 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.cs
using 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
文件夹中,以便能