蓋上戳記吧!所有程式都必須回報版本
最近,在一次正式環境事故的應變過程中,我在不到一小時內就正確猜中了故障的根本原因(很厲害吧!),並提交了一個修正來驗證這個假設,結果卻因為我們無法掌握版本號與部署狀況,只能在黑暗中摸索好幾個小時…… 😞
這次經驗讓我再次思考軟體版本管理的問題,更精確地說,是關於 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 瀏覽器,只是精細程度不同。
這些在不同情境下都是正確且有用的。以下分別舉例說明:
- 「在我這邊用 Chrome 可以正常運作,你有用 Firefox 測試過嗎?」
- 「Chrome 146 含有壞掉的中鍵貼上並導覽功能」
- 「我用的是 Chrome 146.0.7680.80,無法重現你的問題」
- 「在 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
每一個字都是經過仔細斟酌後才放上去的。讓我來逐一剖析:
i3 version 4.24:我本來可以簡寫成i3 4.24或i3 v4.24,但我認為明確一點會比較有幫助,因為i3這個名字實在太短了。使用者可能會喃喃自語:「i-3-4-2-4 是什麼東西?」但如果加上「version」這個字,就暗示了 i3 是某種電腦相關的東西(→ 一個電腦程式),而它的版本是 4.24。(2024-11-06)是發行日期,讓你能立刻判斷「4.24」是不是近期版本。© 2009 Michael Stapelberg(麥可·史塔佩柏格)標示了專案的起始時間以及背後的主要人物。and contributors則是向許多曾協助專案的人致謝。i3 從來不是一個人的專案,它始終是眾人合作的成果。
在進行使用者支援時,有幾個問題在概念上很容易向受影響的使用者提問,卻能為開發者帶來非常有價值的答案:
- 問題:「你用的是哪個版本的 i3?」
- 由於 i3 並非一般在視窗中執行的程式(而是視窗管理器/桌面環境),因此沒有「說明 → 關於」這類選單選項。
- 取而代之,我們開始這樣問:
i3 --version的輸出是什麼?
- 問題:「你回報的是新問題還是舊有的問題?為了確認,你能試著回到之前使用的 i3 版本嗎?」。「回到舊版」的技術用語是 downgrade(降版)、rollback(回滾)或 revert(還原)。
- 取決於 Linux 發行版,這件事可能輕而易舉,也可能是場惡夢。
- 在 NixOS 上,這非常簡單:你只要在開機載入程式中選擇舊的系統「世代(generation)」開機即可。或者,如果你的設定檔有做版本控管,也可以在 Git 中 revert。
- 在使用像 Debian Linux 或 Arch Linux 這類指令式(imperative)Linux 發行版時,如果你沒有做檔案系統層級的快照,升級後就沒有簡單可靠的方法可以回到舊版。如果你夠幸運,或許可以直接用
apt install安裝舊版的 i3。但你可能會遇到相依性衝突(「版本地獄(version hell)」)。 - 我知道透過 snapshot.debian.org 是有可能執行舊版 Debian 的,但至少就我上次嘗試的經驗來說,並不是很實用。
- 你能幫忙確認這個問題在最新的 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
乍看之下這似乎是非常多的細節,但讓我來說明為什麼這份輸出是如此寶貴的除錯工具:
透過 IPC 介面連線到 i3 本身就是一項很有意思的測試。如果使用者能看到
i3 --moreversion的輸出,就表示他們也應該能夠執行像是i3-msg -t get_tree > /tmp/tree.json這類除錯指令,來擷取完整的版面配置狀態。在除錯過程中,執行
i3 --moreversion是個簡便的檢查,可以確認你剛剛建置的版本是否真的已生效(請看Running i3 version這一行)。- 請注意,這正是正式環境事故中也相關的檢查:驗證實際執行中的版本是否與預期應執行的版本一致。
顯示已載入設定檔的完整路徑,可以讓人一眼就看出使用者是否編輯錯了檔案。如果單看路徑還不夠,修改時間(同時以絕對時間與相對時間顯示)也能提醒使用者是否改錯檔案。
順帶一提,我使用的是 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 file或 runtime/debug.ReadBuildInfo(針對目前執行中的二進位檔)或新的 debug/buildinfo 套件。
注意:在 Go 1.18 之前,標準做法是使用 -ldflags -X main.version=$(git describe) 或類似的顯式注入方式。這種設定雖然可行(至今在許多地方仍可見),但需要修改應用程式碼,而 Go 1.18 以上的戳記機制則無需任何額外步驟。
在實務上這代表什麼呢?以下是常見情況的示意圖:從 Git 建置:
這涵蓋了我大多數的業餘專案!
許多工具我都是直接用 go install,如果想輕鬆複製到其他電腦上,就用 CGO_ENABLED=0 go install。不過,我有越來越多的軟體是透過 NixOS 來管理的。
當我發現某個程式還沒被完整納管時,我可以使用 gops 與 go 工具來識別它:
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 相同的缺口:

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 程式碼
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 修訂版本戳記:
- 像
fetchFromGitHub這類 Nix fetchers(擷取器) 是透過從 GitHub 擷取封存檔(.tar.gz)來實作的——完整的.git儲存庫並不會被傳輸,這樣更有效率。 - 即使存在
.git儲存庫,Nix 通常也會為了可重現性而刻意將其移除:.git目錄中包含的 packed objects 會在執行git gc等操作後發生變化,這會破壞可重現建置(相同原始碼卻產生不同的雜湊值)。
因此,這裡的根本張力在於可重現性與 VCS 戳記之間的取捨。
幸好,有一個兩全其美的解決方案:我建立了 stapelberg/nix/go-vcs-stamping Nix overlay(覆蓋層) 模組,你只要匯入它,就能讓你的 buildGoModule Nix 表達式預設擁有可用的 Go VCS 修訂版本戳記!
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 檔案中寫的內容,你可能會落入 Nix 技術堆疊中的 3 個相關部分之一:
- Fetchers。Flakes 使用的就是這個,非 Flakes 的情境也會用到。
- Fixed-output derivations(固定輸出衍生) (FOD)。
pkgs.fetchgit就是這樣實作的,但 FOD 固有的雜湊值頻繁變動(需要不斷更新sha256那一行)很惱人。 - 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 在記憶體中的
.revattrset 中追蹤 VCS 修訂版本。 - Go 則預期透過存取
.git/HEAD檔案以及 git(1) 指令,在.git儲存庫中找到 VCS 修訂版本。
因此,該 overlay 實作了 3 個步驟來讓 Go 蓋上正確的資訊:
- 它合成一個
.git/HEAD檔案,讓 Go 的vcs.FromDir()能偵測到 Git 儲存庫。 - 它在
PATH中注入一個git指令,該指令僅實作 Go 所使用的兩個指令,若遇到其他指令則會大聲報錯(以防 Go 更新其實作)。 - 它在
GOFLAGS環境變數中設定-buildvcs=true。
完整原始碼請見 go-vcs-stamping.nix。
乾淨的修正方式
關於以更乾淨的方式修補這個缺口,請參見 Go issue #77020 與 Go issue #64162:讓套件管理器能夠在呼叫 Go 工具時注入正確的 VCS 資訊。
這將讓 Nix(或 gokrazy)能夠乾淨地傳遞 buildinfo,而無需像我的 workarounds like my go-vcs-stamping 轉接器這類因應措施。
在撰寫本文時,issue #77020 似乎尚未獲得太多關注,仍然處於開啟狀態。
結論:蓋上戳記!串接管線!回報出來!
我的論點很簡單:
蓋上 VCS 修訂版本的戳記在概念上很簡單,但非常重要!
舉例來說,如果我提到的那次事故中的正式環境系統有回報其版本,我們就能省下數小時的緩解時間!
很可惜,許多環境只會識別建置輸出(有用,但正交),卻沒有串接 VCS 修訂版本(更有用!),或至少預設沒有這麼做。
要修正這個問題,你的行動計畫只需要 3 個簡單步驟:
- 蓋上戳記!在你的程式中納入原始碼的 VCS 修訂版本。
- 這不是新點子:i3 的建置自 2012 年起就包含了其 git-describe(1) 修訂版本!
- 串接管線!在建置/打包時,確保 VCS 修訂版本不會遺失。
- 我上面「在 NixOS 上處理 VCS 修訂版本」的案例研究一節,說明了 VCS 修訂版本可能遺失的幾個原因、哪些路徑可行,以及如何修補缺失的管線。
- 回報出來!讓你的軟體在每一個相關的介面上印出其 VCS 修訂版本,例如:
- 可執行程式:在以
--version執行時回報 VCS 修訂版本- 對於 Go 程式,你永遠可以使用
go version -m
- 對於 Go 程式,你永遠可以使用
- 服務與批次工作:在啟動日誌中納入 VCS 修訂版本。
- 發出的 HTTP 請求:在
User-Agent中納入 VCS 修訂版本 - HTTP 回應:在標頭中(內部)納入 VCS 修訂版本
- 遠端程序呼叫 (RPCs):在 RPC 詮釋資料中納入修訂版本
- 使用者介面:在某個顯眼處暴露修訂版本以供除錯。
- 可執行程式:在以
在整個系統中實作「version observability(版本可觀測性)」是一個只需一天就能完成、投資報酬率極高(high-ROI)的專案。
透過我的 Nix 範例,你已經看到 VCS 修訂版本如何在整個技術堆疊中都可取得,卻可能在中途遺失。希望我的資源也能幫助你快速修復你的技術堆疊:
- 用於 Nix / NixOS 的我的
stapelberg/nix/go-vcs-stampingoverlay - 我的
stampit儲存庫是一個收集範例(以 Markdown 內容形式)的社群資源,其中包含一個 Go 模組與幾個輔助工具,能讓版本回報變得輕而易舉。
現在就去為你的程式與資料傳輸蓋上戳記吧!🚀
隨機一篇部落格