Stamp It! All Programs Must Report Their Version

Michael Stapelberg

蓋上戳記!所有程式都必須回報自己的版本

原文由 Michael Stapelberg 發布,訂閱此部落格

前陣子在處理一次線上事故時,我在不到一小時內就正確猜中了故障的根本原因(不錯吧!),也送出了一個修正來驗證這個假設,結果卻因為看不到版號和部署狀況,在黑暗中摸索了好幾個小時…… 😞

這次經驗讓我再次思考軟體版本管理的問題,更精確地說,是關於建置資訊(build versioning、version stamping,怎麼稱呼都可以)和版本回報。我才意識到,早在十多年前,我在 i3 視窗管理器中就已經把這個問題解決得很好了,所以在工作中發現這個問題竟然完全沒被解決,真的讓我很意外。

在這篇文章中,我會說明只要做到 3 個簡單的步驟(蓋上去!串接起來!回報出來!),就能在事故處理時幫你省下數小時的延誤與壓力。

為什麼我們的版本管理標準這麼低?!

每一台家電都有極其詳細的版本標示!看看這台洗碗機:

一台洗碗機,上面有許多精確的版本資訊

(感謝 Feuermurmel 提供了這個很棒的範例!)

我看過幾次家電維修,給我的感覺是,如果維修人員無法辨識這台機器,他們很可能連碰都不會碰。

相較之下,為什麼我們在電腦領域的標準就這麼低呢?當然,消費性產品通常還是會用某種方式標上版本,而且通常也夠用了(除了像 USB 3.2 Gen 1×2 這種例子之外!)。但最近,我卻遇到太多沒有做好版本標示的開發版建置!

軟體版本管理

不像家電那樣有一塊刻好資訊的金屬銘牌,軟體會不斷更新,而且運行在我們常常根本看不見的地方與架構之中。

讓我們來深入看看,要提升版本管理的水準,我們需要什麼!

通常,軟體會有一個名稱和一個精細程度不一的版號

  • Chrome
  • Chrome 146
  • Chrome 146.0.7680.80
  • Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34}

以上這些都能指稱我電腦上的 Chrome 瀏覽器,只是精細程度不同。

這些說法都正確,也都有用,端看使用情境。以下是各自的範例:

  1. 「在我這邊用 Chrome 是正常的,你有用 Firefox 測過嗎?」
  2. 「Chrome 146 有個壞掉的中鍵貼上並導覽功能」
  3. 「我用的是 Chrome 146.0.7680.80,無法重現你的問題」
  4. 「在 Chrome f08938029c887ea624da7a1717059788ed95034d-refs/branch-heads/7680_65@{#34} 這個版本上套用這個 patch,然後照這些步驟重現:[…]」

在打造了i3 視窗管理器之後,我很快就學到,對使用者支援來說,程式能清楚表明自己的身分是非常有價值的。讓我用下面的案例來說明。

案例研究:i3 的 --version--moreversion

執行 i3 --version 時,你會看到類似這樣的輸出:

% i3 --version
i3 version 4.24 (2024-11-06) © 2009 Michael Stapelberg and contributors

每一個字都是經過仔細斟酌才放上去的。讓我來拆解一下:

  1. i3 version 4.24:我本來可以簡寫成 i3 4.24i3 v4.24,但我覺得明確一點比較好,因為 i3 這個名字實在太短了。使用者可能會喃喃自語「i-3-4-2-4 是什麼?」,但加上「version」這個字,就暗示了 i3 是某種電腦相關的東西(→ 一個電腦程式),而它的版本是 4.24。
  2. (2024-11-06) 是發行日期,讓你能馬上判斷「4.24」是不是夠新。
  3. © 2009 Michael Stapelberg 標示了專案的起始時間以及背後的主要人物。
  4. and contributors 則是向眾多協助過的人致謝。i3 從來就不是一個人的專案,它一直是團隊合作的成果。

在做使用者支援時,有幾個問題在概念上很容易向遇到問題的使用者提問,卻能為開發者帶來非常有價值的答案:

  1. 問題:「你用的是哪個版本的 i3?」
    • 由於 i3 不是那種在視窗中執行的一般程式(而是視窗管理器/桌面環境),所以沒有「說明 → 關於」這種選單選項。
    • 因此,我們改為詢問:i3 --version 的輸出是什麼?
  2. 問題:「你回報的是新問題還是原本就存在的問題?為了確認,你能試著回到之前使用的 i3 版本嗎?」。「回到舊版」的專業術語是 downgrade、rollback 或 revert。
    • 取決於你用的 Linux 發行版,這件事可能非常簡單,也可能是場惡夢。
    • 用 NixOS 的話就很簡單:只要在開機選單中選擇舊的系統「generation」開機就行了。或者,如果你的設定檔有用 git 做版本控管,直接 revert 回去也可以。
    • 對於像 Debian Linux 或 Arch Linux 這種指令式(imperative)的發行版,如果你沒有做檔案系統層級的快照,升級後就沒有簡單又可靠的方法可以回到舊版。運氣好的話,你可以直接用 apt install 安裝舊版的 i3。但你可能會遇到相依性衝突(「版本地獄」)。
    • 我知道用snapshot.debian.org可以跑舊版 Debian 的,但至少就我上次嘗試的經驗來說,並不是很實用。
  3. 你能幫忙確認一下這個問題在最新的 i3 開發版中是否依然存在嗎?
    • 當然,我也可以自己試著用最新的發行版重現使用者回報的問題,然後再多一次用最新的開發版重現。
    • 但把驗證的步驟交給回報問題的使用者來做會更好,因為這樣可以篩選出動機強烈的回報者(這種回報更有機會真的促成修正!),而且會讓使用者把 bug 重現兩次,藉此判斷這是不是偶發性問題、是否難以重現、重現步驟是否正確等等。
    • 接下來很自然的追問就是:「這個程式碼修改有讓問題消失嗎?」對於現在已經有開發環境的當事使用者來說,這很容易測試。

根據我多次提出這些問題的經驗,我注意到這些除錯過程中有一些固定的模式。因此,我在 i3 v4.3(於 2012 年 9 月發布)中為 i3 新增了另一種回報版本的方式:--moreversion 參數!現在我可以問使用者第一個問題的稍微變化版:i3 --moreversion 的輸出是什麼?注意,這個問法連用口語傳達也很順暢,例如在電腦聚會上:

Michael: 你用的是哪個版本?

User: 要怎麼看?

Michael: 執行這個指令:i3 --version

User: 它顯示 4.24。

Michael: 很好,這個版本夠新,已經包含那個修正了。現在,我們需要更詳細的版本資訊!麻煩執行 i3 --moreversion,告訴我你看到了什麼。

當你執行 i3 --moreversion 時,它不只會回報你所呼叫的那個 i3 程式的版本,還會透過IPC(行程間通訊)介面連線到你 X11 工作階段中正在執行的 i3 視窗管理器處理程序,並回報正在執行中的 i3 處理程序的版本,同時顯示其他對使用者有幫助的關鍵資訊,例如載入了哪個設定檔、以及它最後是什麼時候被修改的:

% i3 --moreversion
Binary i3 version:  4.24 (2024-11-06) © 2009 Michael Stapelberg and…
Running i3 version: 4.24 (2024-11-06) (pid 2521)
Loaded i3 config:
  /home/michael/.config/i3/config (main)
  (last modified: 2026-03-15T23:09:27 CET, 1101585 seconds ago)

The i3 binary you just called:
/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3
The i3 binary you are running: i3

乍看之下這份輸出似乎資訊量很大,但讓我來說明為什麼它是如此有價值的除錯工具:

  1. 透過 IPC 介面連線到 i3 本身就是一項很有意義的測試。如果使用者能看到 i3 --moreversion 的輸出,就代表他們應該也能執行像是 i3-msg -t get_tree > /tmp/tree.json 這類除錯指令來擷取完整的版面配置狀態。

  2. 在除錯過程中,執行 i3 --moreversion 可以輕鬆確認你剛剛建置的版本是否真的生效了(請看 Running i3 version 那一行)。

    • 請注意,這正是線上事故處理時也很關鍵的檢查:驗證實際正在執行的版本是否與預期應該執行的版本一致。
  3. 顯示所載入設定檔的完整路徑,能讓人一眼就看出使用者是否改錯了檔案。如果光看路徑還不夠,修改時間(同時以絕對時間與相對時間顯示)也能幫忙揪出改錯檔的情況。

順帶一提,我用的是 NixOS,所以會自動為這一次特定的 i3 建置取得一個穩定的識別碼(0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24)。

% ls -l $(which i3)
lrwxrwxrwx 1 root root 58 1970-01-01 01:00 /run/current-system/sw/bin/i3
-> /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24/bin/i3

要查看產生這個 Nix store 輸出(0zn9r4263…-i3-4.24)的建置配方(在 Nix 術語中稱為「derivation」),我可以執行 nix derivation show

% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
{
  "/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv": {
[…]
點此展開完整的 nix derivation show 輸出(如果你好奇的話)
% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
{
  "/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv": {
    "args": [
      "-e",
      "/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh",
      "/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh"
    ],
    "builder": "/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash",
    "env": {
      "NIX_MAIN_PROGRAM": "i3",
      "name": "i3-4.24",
      "out": "/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24",
      "version": "4.24"
    },
    [… jakiego}
}

可惜的是,據我所知目前沒有辦法從 derivation 反推回 .nix 原始碼,但至少可以檢查某個原始碼是否會產生完全相同的 derivation。

開發版建置

到目前為止我所描述的版本管理方式,對大多數只關心正式發行版、而不會去追蹤軟體中間版本的使用者來說已經足夠了。

但對於開發者,或是任何需要更高精確度的使用者來說呢?

當從 git 建置 i3 時,它會使用git-describe(1) 來回報它是從哪個 git revision 建置而來的:

~/i3/build % git describe
4.25-23-g98f23f54
~/i3/build % ninja
[110/110] Linking target i3
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54 © 2009 Michael Stapelberg and contributors

如果工作目錄有被修改過,會在 revision 後面加上一個 + 來表示:

~/i3/build % echo '// dirty working copy' >> ../src/main.c && ninja
[104/104] Linking target i3bar
~/i3/build % ./i3 --version
i3 version 4.25-23-g98f23f54+ © 2009 Michael Stapelberg and contributors

回報 git revision(或更廣義來說,VCS revision)是最有用的選擇。

這樣一來,我們就能抓到以下常見的錯誤:

  • 大家從錯誤的 revision 建置。
  • 建置了,卻忘了安裝。
  • 安裝了,但目前的工作階段沒有載入到(裝錯位置?)。

最有用的做法:蓋上 VCS Revision 的戳記

如同我們在上面看到的,單一最有用的版本資訊就是 VCS revision。我們可以從 VCS 儲存庫中取得所有其他細節(版號、日期、作者……)。

現在,讓我們透過 Go 的做法來展示最理想的情況!

Go 預設就會蓋戳記! 🥳

Go 這些年來已經成為我最喜愛的程式語言,很大一部分是因為 Go 開發團隊的好品味與風格,當然還有高品質的工具:

為什麼 Go 是我最喜愛的程式語言

因此,我很高興可以說,Go 在軟體版本管理方面做到了黃金標準:它預設就會嵌入 VCS 建置資訊! 🥳 這是在Go 1.18(2022 年 3 月)中引入的:

此外,go 指令會嵌入關於建置的資訊,包含建置與工具標籤(透過 -tags 設定)、編譯器、組譯器與連結器的旗標(例如 -gcflags)、是否啟用了 cgo,以及如果有啟用,cgo 環境變數(如 CGO_CFLAGS)的值。

VCS 與建置資訊都可以透過 go version -m fileruntime/debug.ReadBuildInfo(針對目前正在執行的二進位檔)或新的debug/buildinfo 套件一起讀取。

註:在 Go 1.18 之前,標準做法是使用 -ldflags -X main.version=$(git describe) 或類似的方式明確注入。這種設定是可行的(而且在許多地方仍可看到),但需要修改應用程式的程式碼,而 Go 1.18 以上的自動蓋戳記則不需要任何額外步驟。

在實務上這代表什麼呢?以下是常見情況的示意圖:從 git 建置:

顯示透過執行 go build / go install 從 git 儲存庫產生二進位檔的示意圖

這涵蓋了我大部分的個人興趣專案!

很多工具我都是直接用 go install 安裝,或者用 CGO_ENABLED=0 go install,這樣就能輕鬆複製到其他電腦上。不過,我現在有越來越多軟體是透過 NixOS 來管理的。

當我發現有程式還沒被完整納入管理時,我可以用 gopsgo 工具來辨識它:

root@ax52 ~ % nix run nixpkgs#gops
2573594 1       dcs-package-importer  go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-package-importer
2573576 1       dcs-source-backend    go1.26.1 /nix/store/clby54zb003ibai8j70pwad629lhqfly-dcs-unstable/bin/dcs-source-backend
2573566 1       debiman               go1.25.5 /srv/man/bin/debiman
[…]
root@ax52 ~ % nix run nixpkgs#go -- version -m /srv/man/bin/debiman
/srv/man/bin/debiman: go1.25.5
  path	github.com/Debian/debiman/cmd/debiman
  mod	github.com/Debian/debiman	v0.0.0-20251230101540-ac8f5391b43b+dirty
  build	vcs=git
  build	vcs.revision=ac8f5391b43bc1a9dbdc99f6179e2fb7d7414a04
  build	vcs.time=2025-12-30T10:15:40Z
  build	vcs.modified=true

Go 預設就把事情做對,真的很棒!

由 100% Go 軟體組成的系統(例如我的gokrazy Go 設備平台)就完全都有蓋上戳記!舉例來說,gokrazy 的網頁介面會精確顯示我的scan2drive 設備gokrazy/rsync 這個建置包含了哪些版本與相依套件。

儘管已經完整蓋上了戳記,請注意 gokrazy 目前只會顯示模組版本,而不會顯示 VCS 建置資訊,因為它目前也遇到了和 Nix 一樣的落差:

gokrazy scan2drive rsync 畫面

Go 的版本回報

對於採用滾動式發行模式(沒有版號)的 gokrazy packer,我最後寫了幾行 Go 程式碼(見下方),無論你是從 Go 模組安裝 packer,還是從 git 工作目錄安裝,都能顯示 git revision。

這段程式碼會顯示 vcs.revision(簡單的情況;從 git 建置),或是從主模組的 Go 模組版本中擷取出 revision(BuildInfo.Main.Version):

還有哪些情況呢?以下範例說明了我通常會遇到的情境:

來源(建置來源)建置資訊(蓋到程式上的戳記)
目錄(沒有 git)模組 (devel)
Go 模組模組 v0.3.1-0.20260105212325-5347ac5f5bcb
目錄(git)模組 v0.0.0-20260131174001-ccb1d233f2a4+dirty
vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551
vcs.time=2026-01-31T17:40:01Z
vcs.modified=true
顯示 go 建置資訊蓋戳記兩種情況的示意圖:從 git checkout 建置或從 Go 模組安裝
用 Go 程式碼以程式化方式讀取版本
package version

import (
	"runtime/debug"
	"strings"
)

func readParts() (revision string, modified, ok bool) {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		return "", false, false
	}
	settings := make(map[string]string)
	for _, s := range info.Settings {
		settings[s.Key] = s.Value
	}
	// When built from a local VCS directory, we can use vcs.revision directly.
	if rev, ok := settings["vcs.revision"]; ok {
		return rev, settings["vcs.modified"] == "true", true
	}
	// When built as a Go module (not from a local VCS directory),
	// info.Main.Version is something like v0.0.0-20230107144322-7a5757f46310.
	v := info.Main.Version // for convenience
	if idx := strings.LastIndexByte(v, '-'); idx > -1 {
		return v[idx+1:], false, true
	}
	return "<BUG>", false, false
}

func Read() string {
	revision, modified, ok := readParts()
	if !ok {
		return "<not okay>"
	}
	modifiedSuffix := ""
	if modified {
		modifiedSuffix = " (modified)"
	}

	return "https://github.com/gokrazy/tools/commit/" + revision + modifiedSuffix
}

實際上看起來是這樣:

% go install github.com/gokrazy/tools/cmd/gok@latest
% gok --version
https://github.com/gokrazy/tools/commit/8ed49b4fafc7

但從 git 建置的版本會有完整的 revision 可用(→ 你可以分辨出差異):

% (cd ~gokrazy/../tools && go install ./cmd/...)
% gok --version
https://github.com/gokrazy/tools/commit/ba6a8936f4a88ddcf20a3b8f625e323e65664aa6 (modified)

在 NixOS 上取得 VCS revision

用 Nix 打包 Go 軟體時,很容易就會遺失 Go 的 VCS revision 戳記:

  1. fetchFromGitHub 這類 Nix fetcher 是透過從 GitHub 抓取封存檔(.tar.gz)來實作的——並不會傳輸完整的 .git 儲存庫,這樣比較有效率。
  2. 即使有 .git 儲存庫存在,Nix 通常也會為了可重現性而刻意將其移除:.git 目錄中包含了會在例如執行 git gc 後改變的 packed objects,這會破壞可重現建置(同樣的原始碼卻有不同的 hash)。

所以這裡根本的張力在於可重現性與 VCS 蓋戳記之間的衝突。

幸好,有一個兩全其美的解法:我建立了stapelberg/nix/go-vcs-stamping Nix overlay 模組,只要匯入它,就能讓你的 buildGoModule Nix 表達式預設就擁有可用的 Go VCS revision 戳記!

從 Git 儲存庫到 go 建置的示意圖,比較沒有與有使用我的 go-vcs-stamping overlay  workaround 的差異

Nix Go 建置狀況詳解

提示:如果你不是 Nix 使用者,可以直接跳過這一節。我把這段放在文章中,是為了讓你有一個在最複雜環境中讓 VCS 蓋戳記運作的完整範例。


在 Nix 中打包 Go 軟體其實相當簡單直覺。

舉例來說,Go 的 Protobuf 產生器外掛 protoc-gen-go 在 Nix 中只用了不到 30 行就打包完成:官方 nixpkgs 的 protoc-gen-go package.nix。你只要呼叫buildGoModule,把fetchFromGitHub 的結果當作 src 傳入,再加上幾行 metadata 就好了。

但要讓開發版建置完整蓋上戳記,可就一點也不簡單了!

在打包我自己的軟體時,我想要打包的是個別的 revision(開發版建置),而不只是已發行的版本。我使用同樣的 buildGoModule,如果需要最新版 Go 就用 buildGoLatestModule。我不用 fetchFromGitHub,而是透過 Flakes 來提供原始碼,通常也是來自 GitHub 或其他 Git 儲存庫。例如,我是這樣打包 gokrazy/bull 的:

{
  pkgs,
  pkgs-unstable,
  bullsrc,
  ...
}:

# Use buildGoLatestModule to build with Go 1.26
# even before NixOS 26.05 Yarara is released
# (NixOS 25.11 contains Go 1.25).
pkgs-unstable.buildGoLatestModule {
  pname = "bull";
  version = "unstable";

  src = bullsrc;

  # Needs changing whenever `go mod vendor` changes,
  # i.e. whenever go.mod is updated to use different versions.
  vendorHash = "sha256-sU5j2dji5bX2rp+qwwSFccXNpK2LCpWJq4Omz/jmaXU=";
}

bullsrc 來自我的 flake.nix

點此展開完整的 flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
    nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";
    disko = {
      url = "github:nix-community/disko";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    stapelbergnix.url = "github:stapelberg/nix";
    zkjnastools.url = "github:stapelberg/zkj-nas-tools";
    configfiles = {
      url = "github:stapelberg/configfiles";
      flake = false;
    };
    bullsrc = {
      url = "github:gokrazy/bull";
      flake = false;
    };
    sops-nix = {
      url = "github:Mic92/sops-nix";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs =
    {
      nixpkgs,
      nixpkgs-unstable,
      disko,
      stapelbergnix,
      zkjnastools,
      bullsrc,
      configfiles,
      sops-nix,
      ...
    }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs {
        inherit system;
        config.allowUnfree = false;
      };
      pkgs-unstable = import nixpkgs-unstable {
        inherit system;
        config.allowUnfree = false;
      };
    in
    {
      nixosConfigurations.keep = nixpkgs.lib.nixosSystem {
        inherit system;
        inherit pkgs;
        specialArgs = { inherit configfiles; };
        modules = [
          disko.nixosModules.disko
          sops-nix.nixosModules.sops
          ./configuration.nix
          {
            nixpkgs.overlays = [
              (final: prev: {
                bull = import ./bull-pkg.nix {
                  pkgs = final;
                  pkgs-unstable = pkgs-unstable;
                  inherit bullsrc;
                };
              })
            ];
          }
        ];
      };
    };
}

Go 會為所有建置蓋戳記,但在這裡它沒什麼東西好蓋:

  • 我們是從目錄建置,而不是從 Go 模組,所以模組版本是 (devel)
  • 蓋上去的建置資訊中不包含任何 vcs 資訊。

以下是 gokrazy/bull 的完整範例:

% go version -m \
  /nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull
/nix/store/z3y90ck0fp1wwd4scljffhwxcrxjhb9j-bull-unstable/bin/bull: go1.26.1
        path    github.com/gokrazy/bull/cmd/bull
        mod     github.com/gokrazy/bull (devel) 
        dep     github.com/BurntSushi/toml      v1.4.1-0.20240526193622-a339e1f7089c    
        build   -buildmode=exe
        build   -compiler=gc
        build   -trimpath=true
        build   CGO_ENABLED=0
        build   GOARCH=amd64
        build   GOOS=linux
        build   GOAMD64=v1

要修復 VCS 蓋戳記,請將我的 goVcsStamping overlay 加到你的 nixosSystem.modules 中:

{
  nixpkgs.overlays = [
    stapelbergnix.overlays.goVcsStamping
  ];
}

(如果你跟我一樣使用 nixpkgs-unstable,就需要在兩個地方都套用這個 overlay。)

重新建置後,你的 Go 二進位檔應該就會新蓋上 vcs 建置資訊了:

% go version -m /nix/store/z8mgsf10pkc6dgvi8pfnbb7cs23pqfkn-bull-unstable/bin/bull
[…]
  build   vcs=git
  build   vcs.revision=c0134ef21d37e4ca8346bdcb7ce492954516aed5
  build   vcs.time=2026-03-22T08:32:55Z
  build   vcs.modified=false

太好了!🥳 但……它是怎麼運作的?什麼時候會生效?你要怎麼知道該如何修正你的設定?

我先給你看完整的示意圖,再來解釋該怎麼看:

一張大圖,顯示從 .nix 表達式到已蓋戳記二進位檔或遺失 VCS 資訊二進位檔的所有路徑

取決於你在 .nix 檔案中寫了什麼,你會落在 Nix 堆疊中 3 個相關部分之一:

  1. Fetcher。這是 Flakes 會用到的,但非 Flake 的情境也會用到。
  2. 固定輸出 derivation(Fixed-output derivation,FOD)。pkgs.fetchgit 就是這樣實作的,但 FOD 固有的 hash 不斷變動(需要一直更新 sha256 那一行)很惱人。
  3. Copier。這些只是把檔案複製到 Nix store 中,並沒有 git 感知能力。

為了 VCS revision 蓋戳記的目的,你應該:

  • 避免使用 Copier!如果你使用 Flakes:
    • ❌ 不要把 url = "/home/michael/dcs" 當作 Flake input
    • ✅ 改用 url = "git+file:///home/michael/dcs" 以具備 git 感知能力
  • 我也會避免使用固定輸出 derivation(FOD)。
    • 在建置時才抓取 git 儲存庫既慢又沒效率。
    • 啟用 leaveDotGit(用這種方式做 VCS revision 蓋戳記時需要)會更沒效率,因為為了保持 FOD 的可重現性,必須以確定性的方式重新建構一個 Git 儲存庫。

因此,我們會堅持使用最左邊那一欄:fetcher。

可惜的是,預設情況下,使用 fetcher 時,VCS revision 資訊雖然儲存在 Nix attrset(在建置過程中的記憶體內)中,卻不會進入 Nix store,因此當 Nix derivation 被求值、Go 編譯原始碼時,Go 看不到任何 VCS revision。

我的stapelberg/nix/go-vcs-stamping Nix overlay 模組修正了這個問題,而啟用這個 overlay 就是讓你走上上圖最左側那條路的方式:那條快樂的路徑,你的 Go 二進位檔現在都有蓋上戳記了!

我的 workaround:Nix git 建置資訊 overlay

go-vcs-stamping overlay 是如何運作的呢?它是作為 Nix 與 Go 之間的轉接器:

  • Nix 在 .rev 這個記憶體內的 attrset 中追蹤 VCS revision。
  • Go 則預期透過存取 .git/HEAD 檔案和執行git(1) 指令,在 .git 儲存庫中找到 VCS revision。

因此,這個 overlay 實作了 3 個步驟來讓 Go 蓋上正確的資訊:

  1. 合成一個 .git/HEAD 檔案,讓 Go 的 vcs.FromDir() 能偵測到 git 儲存庫。
  2. PATH 中注入一個 git 指令,它只實作 Go 會用到的兩個指令,其他指令則會大聲地報錯(以防 Go 更新其實作)。
  3. GOFLAGS 環境變數中設定 -buildvcs=true

完整原始碼請見go-vcs-stamping.nix

乾淨的修正方式

關於以更乾淨的方式修補這個落差,請參見Go issue #77020Go issue #64162:讓套件管理器能在呼叫 Go 工具時注入正確的 VCS 資訊。

這樣就能讓 Nix(或 gokrazy)乾淨地傳遞建置資訊,而不需要像我的 go-vcs-stamping 轉接器這種 workaround

截至撰寫本文時,issue #77020 似乎還沒有太多進展,仍然是 open 狀態。

結論:蓋上去!串接起來!回報出來!

我的論點很簡單:

蓋上 VCS revision 的戳記在概念上很簡單,但非常重要!

舉例來說,如果我提到的那次事故中的線上系統有回報自己的版本,我們就能省下好幾個小時的緩解時間!

可惜的是,許多環境只會標示建置產物(有用,但正交),卻沒有把 VCS revision 串接起來(這可有用多了!),或者至少不是預設就有的。

要修正這個問題,你的行動計畫只需要 3 個簡單步驟:

  1. 蓋上去!把原始碼的 VCS revision 包含進你的程式中。
    • 這不是什麼新點子:i3 的建置早在 2012 年就開始包含git-describe(1) 的 revision 了!
  2. 串接起來!在建置/打包時,確保 VCS revision 不會遺失。
    • 上面「在 NixOS 上取得 VCS rev」的案例研究章節,就說明了 VCS revision 可能遺失的幾個原因、哪些路徑可行,以及如何修補缺失的串接。
  3. 回報出來!讓你的軟體在每一個相關的介面上都印出它的 VCS revision,例如:
    • 可執行程式:在用 --version 執行時回報 VCS revision
      • 對於 Go 程式,你永遠都可以用 go version -m
    • 服務與批次工作:在啟動日誌中包含 VCS revision。
    • 送出的 HTTP 請求:User-Agent 中包含 VCS revision
    • HTTP 回應:在 header 中包含 VCS revision(對內)
    • 遠端程序呼叫(RPC):在 RPC metadata 中包含 revision
    • 使用者介面:在某個顯眼可見的地方暴露 revision 以供除錯。

在整個系統中實作「版本可觀測性」是一個只需一天、高投資報酬率的專案。

透過我的 Nix 範例,你已經看到 VCS revision 如何在整個堆疊中都是可取得的,卻可能在中途遺失。希望我的資源也能幫助你快速修復你的技術堆疊:

現在就去為你的程式和資料傳輸蓋上戳記吧!🚀

本文章由 muse-spark-1.2-contributor 進行翻譯

留言