Stamp It! All Programs Must Report Their Version

Michael Stapelberg

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

最近,在一次正式環境事故的應變過程中,我在不到一小時內就正確猜中了故障的根本原因(很厲害吧!),並提交了一個修正來驗證這個假設,結果卻因為我們無法掌握版本號與部署狀況,只能在黑暗中摸索好幾個小時…… 😞

這次經驗讓我再次思考軟體版本管理的問題,更精確地說,是關於 build info(建置資訊)(build versioning(建置版本控管)、version stamping(版本戳記),隨你怎麼稱呼都行)與 version reporting(版本回報)的問題。我意識到對於 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} 上套用這個修補程式,然後依照這些步驟重現:[…]」

在打造了 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)Linux 發行版時,如果你沒有做檔案系統層級的快照,升級後就沒有簡單可靠的方法可以回到舊版。如果你夠幸運,或許可以直接用 apt install 安裝舊版的 i3。但你可能會遇到相依性衝突(「版本地獄(version hell)」)。
    • 我知道透過 snapshot.debian.org 是有可能執行舊版 Debian 的,但至少就我上次嘗試的經驗來說,並不是很實用。
  3. 你能幫忙確認這個問題在最新的 i3 開發版中是否仍然存在嗎?
    • 當然,我也可以自己嘗試用最新的發行版重現使用者的問題,然後再多一次用最新的開發版重現。
    • 但把驗證步驟交給受影響的使用者是比較好的做法,因為這能篩選出動機強烈的臭蟲回報者(臭蟲回報真正促成修正的機率更高!),而且會讓使用者把臭蟲重現兩次,藉此判斷這是不是個偶發性問題、是否難以重現、重現步驟是否正確等等。
    • 一個很自然的後續問題是:「這個程式碼變更是否讓問題消失了?」對於已經建好開發環境的受影響使用者來說,這很容易測試。

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

麥可: 你用的是哪個版本?

使用者: 要怎麼檢查?

麥可: 請執行這個指令:i3 --version

使用者: 它顯示 4.24。

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

當你執行 i3 --moreversion 時,它不只是回報你所呼叫的那個 i3 程式的版本,還會透過它的 IPC(行程間通訊) (interprocess communication) 介面連線到你 X11 工作階段中正在執行的 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"
    },
    […]}]}
}

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

開發者建置版本

到目前為止我所描述的版本標示方式,對大多數使用者來說已經足夠,因為他們通常不會想追蹤軟體的中間版本,而只關心已發行的版本。

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

當從 Git 建置 i3 時,它會使用 git-describe(1) 來回報其建置來源的 Git 修訂版本:

~/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

若工作目錄有修改過,則會在修訂版本後面加上 + 來表示:

~/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 修訂版本(或更廣義地說,VCS 修訂版本)是最有用的選擇。

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

  • 人們從錯誤的修訂版本進行建置。
  • 人們建置了,卻忘了安裝。
  • 人們安裝了,但他們的工作階段沒有載入到(路徑錯誤?)。

最有用的做法:蓋上 VCS revision(VCS 修訂版本) 的戳記

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

現在,讓我們來看看 Go 是怎麼做的,以此展示最佳情境!

Go 預設就會蓋戳! 🥳

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

Why Go is my favorite programming language(《為什麼 Go 是我最喜愛的程式語言》)

因此,我很樂意說,Go 在軟體版本管理方面實現了黃金標準:它預設就會蓋上 VCS buildinfo(建置資訊) 的戳記! 🥳 這是在 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 buildinfo,因為它目前也存在與 Nix 相同的缺口:

gokrazy scan2drive rsync 介面截圖

Go 的版本回報

對於採用滾動發行模式(沒有版本號碼)的 gokrazy packer,我最後用了幾行 Go 程式碼(見下方)來顯示 Git 修訂版本,無論你是從 Go 模組安裝 packer,還是從 Git 工作複本安裝,都能正常顯示。

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

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

來源(建置來源)buildinfo(蓋入程式中的建置資訊)
目錄(無 Git)module (devel)
Go 模組module v0.3.1-0.20260105212325-5347ac5f5bcb
目錄(Git)module v0.0.0-20260131174001-ccb1d233f2a4+dirty
vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551
vcs.time=2026-01-31T17:40:01Z
vcs.modified=true
示意圖,顯示 Go 建置資訊戳記的兩種情況:從 Git 工作複本建置或從 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 建置的版本則有完整的修訂版本可供使用(→ 你可以分辨出差異):

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

在 NixOS 上處理 VCS 修訂版本

當使用 Nix 打包 Go 軟體時,很容易就會遺失 Go 的 VCS 修訂版本戳記:

  1. fetchFromGitHub 這類 Nix fetchers(擷取器) 是透過從 GitHub 擷取封存檔(.tar.gz)來實作的——完整的 .git 儲存庫並不會被傳輸,這樣更有效率。
  2. 即使存在 .git 儲存庫,Nix 通常也會為了可重現性而刻意將其移除:.git 目錄中包含的 packed objects 會在執行 git gc 等操作後發生變化,這會破壞可重現建置(相同原始碼卻產生不同的雜湊值)。

因此,這裡的根本張力在於可重現性與 VCS 戳記之間的取捨。

幸好,有一個兩全其美的解決方案:我建立了 stapelberg/nix/go-vcs-stamping Nix overlay(覆蓋層) 模組,你只要匯入它,就能讓你的 buildGoModule Nix 表達式預設擁有可用的 Go VCS 修訂版本戳記!

示意圖,顯示從 Git 儲存庫到 Go 建置的流程,比較未使用與使用我的 go-vcs-stamping 覆蓋層因應措施的差異

Nix Go 建置狀況詳解

提示:如果你不是 Nix 使用者,歡迎直接跳過本節。我在本文中納入這一節,是為了提供一個在最複雜環境中讓 VCS 戳記正常運作的完整範例。


在 Nix 中打包 Go 軟體是非常直觀的。

例如,Go Protobuf 產生器外掛 protoc-gen-go 在 Nix 中只需不到 30 行就能完成打包:官方 nixpkgs protoc-gen-go package.nix。你只要呼叫 buildGoModule,將 fetchFromGitHub 的結果作為 src 傳入,再加上幾行詮釋資料即可。

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

在打包我自己的軟體時,我想要打包的是單一修訂版本(開發者建置版本),而不只是已發行的版本。我使用同樣的 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)
  • 蓋入的 buildinfo 中不包含任何 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 buildinfo 的戳記:

% 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. Fetchers。Flakes 使用的就是這個,非 Flakes 的情境也會用到。
  2. Fixed-output derivations(固定輸出衍生) (FOD)。pkgs.fetchgit 就是這樣實作的,但 FOD 固有的雜湊值頻繁變動(需要不斷更新 sha256 那一行)很惱人。
  3. Copiers(複製器)。這些只是將檔案複製到 Nix store 中,並不具備 Git 感知能力。

就 VCS 修訂版本戳記的目的而言,你應該:

  • 避免使用 Copiers!如果你使用 Flakes:
    • ❌ 不要將 url = "/home/michael/dcs" 作為 Flake 輸入
    • ✅ 改用 url = "git+file:///home/michael/dcs" 以具備 Git 感知能力
  • 我也會避免使用 Fixed-output derivation (FOD)。
    • 在建置時才去擷取 Git 儲存庫既緩慢又沒效率。
    • 啟用此方法進行 VCS 修訂版本戳記所需的 leaveDotGit,效率甚至更差,因為必須以確定性的方式重建一個新的 Git 儲存庫,才能保持 FOD 的可重現性。

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

很可惜,預設情況下,使用 fetchers 時,儲存在 Nix attrset(記憶體中,在建置過程中)裡的 VCS 修訂版本資訊並不會進入 Nix store,因此當 Nix derivation 被求值且 Go 編譯原始碼時,Go 看不到任何 VCS 修訂版本。

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

我的因應措施:Nix Git buildinfo overlay

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

  • Nix 在記憶體中的 .rev attrset 中追蹤 VCS 修訂版本。
  • Go 則預期透過存取 .git/HEAD 檔案以及 git(1) 指令,在 .git 儲存庫中找到 VCS 修訂版本。

因此,該 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)能夠乾淨地傳遞 buildinfo,而無需像我的 workarounds like my go-vcs-stamping 轉接器這類因應措施

在撰寫本文時,issue #77020 似乎尚未獲得太多關注,仍然處於開啟狀態。

結論:蓋上戳記!串接管線!回報出來!

我的論點很簡單:

蓋上 VCS 修訂版本的戳記在概念上很簡單,但非常重要!

舉例來說,如果我提到的那次事故中的正式環境系統有回報其版本,我們就能省下數小時的緩解時間!

很可惜,許多環境只會識別建置輸出(有用,但正交),卻沒有串接 VCS 修訂版本(更有用!),或至少預設沒有這麼做。

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

  1. 蓋上戳記!在你的程式中納入原始碼的 VCS 修訂版本。
    • 這不是新點子:i3 的建置自 2012 年起就包含了其 git-describe(1) 修訂版本!
  2. 串接管線!在建置/打包時,確保 VCS 修訂版本不會遺失。
  3. 回報出來!讓你的軟體在每一個相關的介面上印出其 VCS 修訂版本,例如:
    • 可執行程式:在以 --version 執行時回報 VCS 修訂版本
      • 對於 Go 程式,你永遠可以使用 go version -m
    • 服務與批次工作:在啟動日誌中納入 VCS 修訂版本。
    • 發出的 HTTP 請求:User-Agent 中納入 VCS 修訂版本
    • HTTP 回應:在標頭中(內部)納入 VCS 修訂版本
    • 遠端程序呼叫 (RPCs):在 RPC 詮釋資料中納入修訂版本
    • 使用者介面:在某個顯眼處暴露修訂版本以供除錯。

在整個系統中實作「version observability(版本可觀測性)」是一個只需一天就能完成、投資報酬率極高(high-ROI)的專案。

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

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

原文由 Michael Stapelberg 發布

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