.NET 项目自定义 MSBuild Task
🍉 1 MSBuild Task
利用 MSBuild Task, 可以在编译阶段,完成很多自定义的操作。比如最常见的,就是在编译完成之后,复制一些额外的文件到输出目录中。
对于这些简单任务,可以使用 MSBuild 自带的 Task。
详见:
MSBuild 任务参考 - MSBuild | Microsoft Learn
了解 MSBuild 任务如何执行生成操作 - MSBuild | Microsoft Learn
如果自带的 Task 不能满足需求,可以使用 Exec 任务 ,来执行自定义脚本。
Exec 任务在 Windows 上调用 cmd.exe,在其他操作系统上调用 sh,而不是直接调用进程。
这个的灵活性就会非常大了,自定义脚本里面可以完成很多事情。
如果觉得自定义脚本还是不够灵活,就可以考虑自定义 Task 了,也就是本文的笔记内容。
🍉 2 一些弯路
在考虑自定义 Task 之前,其实想通过 Roslyn 分析器来借道完成一些编译时期望完成的操作。但 Roslyn Analyzer 对 API 使用的限制很严格,代码必须是 Pure
的,不能访问和操作任何外部的东西。
也就是不能使用 IO 相关的 API,想要在这里读写本地文件是不可以的。
🍉 3 自定义 MSBuild Task
使用 MSBuild 代码编写自己的任务 - MSBuild | Microsoft Learn
第一步,先看效果。不考虑使用 nuget 包发布的情况,只考虑当前项目使用。
3.1 新建 Task 项目
我打算做一点 git hook 相关的事情,这里就取名为 Jgrass.GitHookMsbuildTask
,新建一个 C# 类库项目,csproj 文件如下。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.12.6" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
</ItemGroup>
</Project>
1 TargetFramework
需要是 netstandard2.0
这里使用 netstandard2.1
都不行,可能跟 Roslyn 也是只支持 netstandard2.0
原因一样?期望后续的 VS 版本能跟上 .NET 高版本的节奏。
关于 netstandard
的一些相关信息:
2 引入 Microsoft.Build.Framework
和 Microsoft.Build.Utilities.Core
<PackageReference Include="Microsoft.Build.Framework" Version="17.12.6" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
3.2 新建一个 Task
继承自 Microsoft.Build.Utilities.Task
即可。
using Microsoft.Build.Framework;
namespace Jgrass.GitHookMsbuildTask;
public class LargeFileInterceptTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Log.LogMessage(MessageImportance.High, "Normal Message");
Log.LogWarning("Warning Message");
Log.LogError("Error Message");
return false; // Task 执行失败,会让引用了此 Task 的项目编译失败。
}
}
3.3 在目标项目中使用
Task 项目和目标项目在同一个大的仓库中,这里可以使用相对路径的方式直接引用。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<UsingTask TaskName="LargeFileInterceptTask" AssemblyFile="$(MSBuildProjectDirectory)\..\Jgrass.GitHookMsbuildTask\bin\$(Configuration)\netstandard2.0\Jgrass.GitHookMsbuildTask.dll" />
<Target Name="RunGitHookTask" AfterTargets="Build">
<LargeFileInterceptTask />
</Target>
</Project>
编译项目就能看到效果,这里会编译失败,是因为 Task 返回 false 引起的。
如果这个 Task 只是在当前项目中使用,这样基本上就能达到目的了。如果要通过 nuget 作为一个通用的 Task 发布,就会复杂亿丢丢。
PS 如果在修改代码之后,在编译 Task 项目时,发现输出目录的 dll 被占用,直接结束掉 msbuild.exe 进程。推荐使用 PowerToys 的 File Locksmith 工具。
🍉 4 将自定义 Task 打包成 nuget 包
基本流程参照吕毅的博客,所以里面设计到的基本概念就不详细介绍了,我这里做了一点简化。
如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包 - walterlv
这个解决方案分为三个项目
- Jgrass.GitHookMsbuildTask
Task 实现项目,TargetFramework 为 netstandard2.0,支持输出 nuget 包供外部使用。
- Jgrass.GitHookMsbuildTask.Debugger
Task 的 Debug 项目,使用相对路径直接引用,用于开发时的调试。
Jgrass.GitHookMsbuildTask.Sample
Task 的使用示例项目,通过引用 nuget 包的形式引用 Task.
4.1 Task 实现项目配置
Jgrass.GitHookMsbuildTask.csproj
// Jgrass.GitHookMsbuildTask.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
<DevelopmentDependency>true</DevelopmentDependency>
<Version>0.0.7-alpha</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
<NoPackageAnalysis>true</NoPackageAnalysis>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="17.12.6" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageReference Update="@(PackageReference)" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\tasks\" />
</ItemGroup>
<ItemGroup>
<None Include="Assets\build\**" Pack="True" PackagePath="build\" />
<None Include="Assets\readme.md" Pack="True" PackagePath="" />
</ItemGroup>
</Project>
解决方案中的 Assets 文件结构如下
这里有个注意点,Jgrass.GitHookMsbuildTask.targets
文件的名称,必须与 Jgrass.GitHookMsbuildTask
项目的名称是一致的。
也可能 .targets 文件名是必须与 dll 的输出文件名一致?或者必须与 AssemblyName 的设置一致?或者是必须与 nuget 包的名称一致?
这里没有做进一步的探索,总之,要注意这个名称问题,不能随便取。不然其它项目在使用 nuget 包引用时,不会自动加载这个 .targets 文件。
Jgrass.GitHookMsbuildTask.targets
的内容
<Project>
<PropertyGroup Condition=" $(IsInGitHookTaskDebugMode) == 'true' ">
<NuGetTaskFolder>$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\netstandard2.0\</NuGetTaskFolder>
</PropertyGroup>
<PropertyGroup Condition=" $(IsInGitHookTaskDebugMode) != 'true' ">
<NuGetTaskFolder >$(MSBuildThisFileDirectory)..\tasks\netstandard2.0\</NuGetTaskFolder>
</PropertyGroup>
<UsingTask TaskName="LargeFileInterceptTask" AssemblyFile="$(NuGetTaskFolder)Jgrass.GitHookMsbuildTask.dll" />
<Target Name="GitHookTask" AfterTargets="Build">
<LargeFileInterceptTask />
</Target>
</Project>
4.1 Debugger 项目
Jgrass.GitHookMsbuildTask.Debugger
项目的配置
// Jgrass.GitHookMsbuildTask.Debugger.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsInGitHookTaskDebugMode>true</IsInGitHookTaskDebugMode>
</PropertyGroup>
<Import Project="..\Jgrass.GitHookMsbuildTask\Assets\build\Jgrass.GitHookMsbuildTask.targets" />
</Project>
设置 IsInGitHookTaskDebugMode
为 true
, 使用 Import
直接导入相对路径下的 .targets
文件。
要调试的时候,记得开启 Debugger.Launch()
public class LargeFileInterceptTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
#if DEBUG
Debugger.Launch();
#endif
Log.LogMessage(MessageImportance.High, "Normal Message");
Log.LogWarning("Warning Message");
Log.LogError("Error Message");
return false; // Task 执行失败,会让引用了此 Task 的项目编译失败。
}
}
编译 Jgrass.GitHookMsbuildTask.Debugger
项目就会触发调试入口了。
4.2 Task 使用演示
以下是 Jgrass.GitHookMsbuildTask.Sample
的配置,很简单,就是通过 PackageReference 引入普通的 nuget 包。
本地测试时,需要将 nuget 包所在路径,添加为 nuget 包源。
// Jgrass.GitHookMsbuildTask.Sample.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jgrass.GitHookMsbuildTask" Version="0.0.7-alpha" />
</ItemGroup>
</Project>
编译项目,如果看到如下输出,就说明成功啦~
🍉 5 其它
项目源码:https://gitee.com/Jasongrass/demo-msbuild-task
前面说 LargeFileInterceptTask
中,打算实现一个 git hook 的功能,具体怎么实现,以后再说吧。想法的源头来自这里:git 禁止大文件提交到仓库中
参考资料