這篇筆記躺在草稿夾裡面大約三個月了,現在終於有空整理一下。

上一篇很粗淺地介紹了 Nuke 工具的基礎用法,這次的內容主要是說明如何用它來建立 nuget 套件。

摘要:
  • 建立 .nuspec 檔案
  • 使用 Nuke 建置工具(或「框架」)來編寫與執行打包 Nuget 套件的腳本
  • 使用 GitVersion 來自動指定 Nuget 套件(與 DLL 檔案)的版本號碼

註:本文沒有介紹如何利用 Nuke 來把 nuget 套件自動推送至 nuget 伺服器。

Part 1: 建立 .nuspec 檔案

欲將 .NET 組件打包成 Nuget 套件,我們得先把套件的相關描述與設定寫在一個叫做 .nuspec 的檔案裡。

這個 .nuspec 檔案,可以透過命令列工具 nuget 來產生,或者也可以利用 Visual Studio 2017 的打包(Pack)功能來產生,作法是:在 Solution Explorer 中對專案名稱點右鍵、選 Pack。如下圖:


註:如果你沒看到 Pack 選項,可能是因為你的 .csproj 檔案不是新的 SDK-based 格式。

透過 Visual Studio 的 Pack 功能來打包時,會在專案目錄的 \obj\debug\ 目錄下產生一個 .nuspec 檔案,我是用這個檔案當作範本來進一步修改其內容。

Visual Studio 產生的 .nuspec 檔案,其內容是來自專案屬性的「Package」頁籤中的設定,如下圖:


圖中下方有組件與檔案的版本編號,這個不用管它,因為我打算用 GitVersion 來處理版本編號。

剛才提到,.nuspec 檔案也可以用命令列工具來產生。產生 .nuspec 檔案的命令是 nuget spec

產生檔案之後,可以根據自己的需要來修改內容。底下是我的 NChinese 專案的 .nuspec 檔案內容,檔名是 NChinese.nuspec。

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>NChinese</id>
    <version>$version$</version>
    <authors>Michael Tsai</authors>
    <owners>Michael Tsai</owners>
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
    <licenseUrl>https://github.com/huanlin/nchinese/blob/master/LICENSE</licenseUrl>
    <projectUrl>https://github.com/huanlin/nchinese</projectUrl>
    <description>NChinese is a library for handling Chinese characters and phonetics, including Pinyin (拼音) and Zhuyin (注音, aka BoPoMoFo) .</description>
    <releaseNotes></releaseNotes>
    <tags>chinese pinyin zhuyin bopomofo CJK 中文 漢字 注音 拼音</tags>
    <copyright>Copyright (c) 2018 Michael Tsai (蔡煥麟)</copyright>
    <repository url="https://github.com/huanlin/nchinese" />
    <dependencies>
      <group targetFramework=".NETFramework4.5.2">
        <dependency id="NUnit" version="3.10.1" exclude="Build,Analyzers" />
        <dependency id="System.ValueTuple" version="4.5.0-preview1-26216-02" exclude="Build,Analyzers" />
      </group>
    </dependencies>
    <developmentDependency>false</developmentDependency>
  </metadata>
  <files>
    <file src="src\NChinese\bin\Debug\net452\NChinese.dll" target="lib\net452\NChinese.dll" />
  </files>
</package>

在這個 nuspec 檔案中,我把版本編號的值設定成一個 Nuget replace token(簡單講就是變數):$version$。在使用 Nuke 來執行建置腳本時,這個版本號碼會被 GitVersion 的版本號碼取代。

還有一個問題:這個 .nuspec 檔案應該放哪裡比較好呢?我選擇放在 repo 根目錄下的 nuspec 目錄。整個 NChinese 專案的目錄結構如下圖:


其中的 output 目錄是用來存放打包時輸出的套件檔案。Nuke 會自動建立這個目錄,而我的 .gitignore 檔案會確保每次 git commit 時自動忽略這個 output 目錄。

build 資料夾是用來存放 Nuke 建置專案的原始碼(建置腳本),這個部分在上一篇文章裡面已經提過。

Part 2: 編寫建置腳本

以 Nuke 產生好的建置腳本為基礎,我只加了一個名為 "Pack" 的 target,用途自然是打包 nuget 套件了。

前文提過,Nuke 的建置腳本就是個 Console 應用程式專案,而實際的建置指令都是寫在這個專案的 Build.cs 檔案裡裡。底下便是 Build.cs 的完整程式碼:

using Nuke.Common.Tools.GitVersion;
using Nuke.Common.Tools.NuGet;
using Nuke.Common;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
using static Nuke.Common.Tools.NuGet.NuGetTasks;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;

class Build : NukeBuild
{
    // Console application entry. Also defines the default target.
    public static int Main () => Execute<Build>(x => x.Compile);

    // Auto-injection fields:

    [GitVersion] readonly GitVersion GitVersion;
    // Semantic versioning. Must have 'GitVersion.CommandLine' referenced.

    // [GitRepository] readonly GitRepository GitRepository;
    // Parses origin, branch name and head from git config.

    // [Parameter] readonly string MyGetApiKey;
    // Returns command-line arguments and environment variables.

    Target Clean => _ => _
            .OnlyWhen(() => false) // Disabled for safety.
            .Executes(() =>
            {
                DeleteDirectories(GlobDirectories(SourceDirectory, "**/bin", "**/obj"));
                EnsureCleanDirectory(OutputDirectory);
            });

    Target Restore => _ => _
            .DependsOn(Clean)
            .Executes(() =>
            {
                MSBuild(s => DefaultMSBuildRestore);
            });

    Target Compile => _ => _
            .DependsOn(Restore)
            .Executes(() =>
            {
                MSBuild(s => DefaultMSBuildCompile);
            });

    Target Pack => _ => _
            .DependsOn(Compile)
            .Executes(() =>
            {
                Logger.Info($"Nuget packages will be created in '{OutputDirectory}'");

                var nugetSettings = DefaultNuGetPack.SetBasePath(RootDirectory);
                string nuspecFileName = RootDirectory / $"nuspec/NChinese.nuspec";
                NuGetPack(nuspecFileName, s => nugetSettings);
            });
}

說明:
  • 只有最底下的 Target Pack 是我加上的,其餘幾乎都是 Nuke 工具產生的程式碼。
  • [GitVersion] 那行本來是註解,我把註解取消,就立刻具備了透過 GitVersion 來取得版本編號的能力。
  • Logger.Info() 是 Nuke 框架內建的方法。在除錯建置腳本的時候,常常會需要查看 log 。
  • Nuke 提供了一種銜接檔案路徑的簡便語法:在兩個字串之間使用斜線符號 ("/"`) 來銜接。例如 RootDirectory / "abc.txt",作用等同於 Path.Combine(RootDirectory, "abc.txt")。

注意:在我寫這篇筆記時所使用的 Nuke 版本,必須使用 DefaultNuGetPack.SetBasePath() 來設定 nuget 所要打包的檔案的基礎路徑。若未指定,則執行 Nuke build 時會因為 BasePath 為空字串而發生錯誤:Assertion failed: Directory.Exists(BasePath)。

還有一個要留意的地方是:.nuspec 檔案裡面的 <files> 區段所指定的檔案應使用相對路徑(相對於剛才提到的 BasePath),而不要使用絕對路徑。這樣才能確保整個專案在其他機器上也能順利建置。(註:使用 Visual Studio IDE 產生的 .nuspec 檔案是採用絕對路徑)

Part 3: 利用 GitVersion 來自動處理版本編號

在嘗試編寫 Nuke 建置腳本來自動來打包 Nuget 套件時,有個必須解決的問題:如何指定套件(與 DLL 組件)的版本號碼?

在跟 Nuke 作者請教問題時,他建議我試試 GitVersion,我才知道這個好用的小工具。

我記得前後只花了幾分鐘的時間,匆匆讀了點 GitVersion 官網的一些說明。然後,試著在 Nuke 建置腳本裡面改動一行程式碼,看看結果如何。沒想到,我就這樣糊里糊塗的完成了自動取得和指定版本號碼的工作。

它的用法和運作方式如下:
  • 在 Nuke 建置腳本中啟用 GitVersion 功能(參見剛才的 Build.cs 程式碼)。這個動作只需要做一次。
  • 平日開發時,每一次 commit 至 repo,版本號碼會自動遞增。也可以透過建立 tag 的方式來指定版本號碼。
  • 每當執行建置腳本來打包 Nuget 套件時,Nuke 會透過 GitVersion 取得版本號碼,並以此版號來做為最終打包的套件版號以及輸出組件(.DLL)的版號。

這當中有點 tricky 的地方是這句話:「每一次 commit 至 repo,版本號碼會自動遞增。」這是比較籠統的說法。實際上,新的版本編號究竟會怎麼編呢?這就得要去研究 GitVersion 的文件了。

我懶得去研究和記住這些規則,所以一律採用明確指定版號的方式,也就是:建立 tag。說得更仔細點,每當我的應用程式或套件要 release 新版本時,我會先確保當前的工作目錄已經都 commit,然後用類似底下的指令來建立 tag:

git tag v2.3.15

如此一來,GitVersion 發現有 tag,就會優先採用 tag 所指示的版號。以上例來說,就是 2.3.15。

就這樣,我們只需要管理 git repo 的版本號碼就行了,不再需要去處理 AssemblyInfo.cs 裡面的版本編號,也不用在每次發布新版本時手動修改某個檔案的版本號碼字串。

如果您想要知道 GitVersion 對 master 和 develop 分支各採取何種方式來決定版號,可以閱讀底下這段文字(為免疏漏或內容過時,最好搭配官方文件一起服用):
  • GitVersion 使用語意版本編號(semantic versioning)的格式,範例:1.5.3,2.1.7-alpha。
  • Tag 優先:GitVersion 在決定版本編號時,會優先使用 tag 所定義的版本號碼。實務上,在準備建置與發布新版本時,我們可以在 master 分支建立一個 tag,以 "v#.#.#" 的方式命名。例如 "v1.0.0"。如此一來,GitVersion 就會知道要使用的版本號碼是 "1.0.0"。
  •  預設情況下,GitVersion 對於 master 分支的版本編號會採取「持續交付模式」(continuous delivery),也就是每次 commit 時不遞增版號;當你想要發布新版本時,可以建立 tag 來標示版本編號,例如 "v1.2.0"。
  •  預設情況下,GitVersion 對於 develop 分支的版本編號會採取「持續部署模式」(continous deployment mode),也就是每次 commit  時遞增版號。而且,develop 分支的預設 tag 是 alpha。於是,版本編號會以類似這種格式自動遞增:MyLib.0.2.0-alpha0009.dll。


另外,根據我的摸索和測試,若使用 Nuke 整合 GitVersion.CommandLine 來自動指派版本編號,且專案的 .csproj 是舊格式(而非新的 SDK-based 格式),GitVersion 無法正常運作。(注意:很可能是我寫這篇文章時所用的版本才有這樣的狀況,請自行驗證。)

Part 4: 執行建置腳本

現在,.nuspec 檔案和建置腳本都寫好了,可以執行建置命令來測試看看。執行建置的方法至少有三種:
  • 執行建置專案(.build.csproj)的執行檔。這種方式,我通常是用在 Visual Studio 中執行和除錯時使用。
  • 執行專案根目錄下的 build.ps1。命令: ".\build.ps1"。
  • 執行專案根目錄下的 build.cmd。命令: "build"。

build.cmd 其實會去執行 build.ps1,它只是讓我們在輸入命令時少敲幾個按鍵而已。

使用 build.ps1 和 build.cmd  的好處是在執行真正的建置工作前,會先自動下載必要的相關工具和檔案,例如 nuget.exe。

如果只是要建置專案,我只要在命令視窗中輸入 build,再按 Enter 即可。或者,我也可以在後面加個參數,指定要跑哪個 target。比如說,當我要打包 nuget 套件時,就輸入 "build Pack"。執行過程如下圖:


這樣就差不多了。往後只是反覆微調修改、增加 targets,把建置程序改到(幾乎)完全自動化,以盡量消除重複、瑣碎的工作。

Happy building!

(註記一下,這是今年第 12 次連任  MVP 之後的第一篇文章)