Loading

.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.FrameworkMicrosoft.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>

设置 IsInGitHookTaskDebugModetrue, 使用 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 禁止大文件提交到仓库中

参考资料

https://www.cnblogs.com/jasongrass/p/18579008

posted @ 2024-11-30 22:04  J.晒太阳的猫  阅读(33)  评论(0编辑  收藏  举报