1. 程式人生 > 其它 >建立一個基於MSBuilder的nuget包

建立一個基於MSBuilder的nuget包

本文轉自waterlv blog.waterlv.com

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檔案到buildbuildMultiTargeting資料夾中,以便能夠讓我們定製編譯流程。我們要讓我們寫的 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 檔案啦) - 呂毅)

這樣的一番設定,我們的buildbuildMultiTargetingreadme.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>

注意到我同時還在檔案中新增了另外兩個屬性配置NoPackageAnalysisDevelopmentDependency。由於我們沒有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.cs
using 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.cs
public 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.cs
using 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. 新增功能、除錯和測試