利用 MSBuild Task, 可以在编译阶段,完成很多自定义的操作。比如最常见的,就是在编译完成之后,复制一些额外的文件到输出目录中。
对于这些简单任务,可以使用 MSBuild 自带的 Task。
详见:
MSBuild 任务参考 - MSBuild | Microsoft Learn
了解 MSBuild 任务如何执行生成操作 - MSBuild | Microsoft Learn
如果自带的 Task 不能满足需求,可以使用 Exec 任务 ,来执行自定义脚本。
Exec 任务在 Windows 上调用 cmd.exe,在其他操作系统上调用 sh,而不是直接调用进程。
这个的灵活性就会非常大了,自定义脚本里面可以完成很多事情。
如果觉得自定义脚本还是不够灵活,就可以考虑自定义 Task 了,也就是本文的笔记内容。
在考虑自定义 Task 之前,其实想通过 Roslyn 分析器来借道完成一些编译时期望完成的操作。但 Roslyn Analyzer 对 API 使用的限制很严格,代码必须是 Pure
的,不能访问和操作任何外部的东西。
也就是不能使用 IO 相关的 API,想要在这里读写本地文件是不可以的。
使用 MSBuild 代码编写自己的任务 - MSBuild | Microsoft Learn
第一步,先看效果。不考虑使用 nuget 包发布的情况,只考虑当前项目使用。
我打算做一点 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" />
继承自 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 的项目编译失败。 }}
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 工具。
基本流程参照吕毅的博客,所以里面设计到的基本概念就不详细介绍了,我这里做了一点简化。
如何创建一个基于 MSBuild Task 的跨平台的 NuGet 工具包 - walterlv
这个解决方案分为三个项目
Task 实现项目,TargetFramework 为 netstandard2.0,支持输出 nuget 包供外部使用。
Task 的 Debug 项目,使用相对路径直接引用,用于开发时的调试。
Jgrass.GitHookMsbuildTask.Sample
Task 的使用示例项目,通过引用 nuget 包的形式引用 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>
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
项目就会触发调试入口了。
以下是 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>
编译项目,如果看到如下输出,就说明成功啦~
项目源码:https://gitee.com/Jasongrass/demo-msbuild-task
前面说 LargeFileInterceptTask
中,打算实现一个 git hook 的功能,具体怎么实现,以后再说吧。想法的源头来自这里:git 禁止大文件提交到仓库中
参考资料
原文链接: https://cloud.tencent.com/developer/article/2481580
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。