打上印记!所有程序都必须报告自身版本
最近,在一次生产环境故障应急中,我在不到一小时内就准确猜中了故障的根本原因(真不错!),并提交了一个修复来排除这一可能性,结果却因为无法看清版本号和发布情况,在黑暗中摸索了好几个小时…… 😞
这次经历让我再次思考起软件版本管理,更具体地说,是关于构建信息(build info)——也就是构建版本管理、版本打标,随你怎么称呼——以及版本报告的问题。我意识到,对于 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 window manager 之后,我很快认识到,对于用户支持而言,程序能够清晰地标识自身是非常有价值的。让我通过下面的案例研究来说明。
案例研究: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 中回退。
- 在使用 Debian Linux 或 Arch Linux 这类命令式发行版时,如果你没有做文件系统级别的快照,升级后就没有简单可靠的方法回退。如果幸运的话,你可以直接
apt install旧版本的 i3,但可能会遇到依赖冲突(“版本地狱”)。 - 我知道通过 snapshot.debian.org 可以运行旧版本的 Debian,但至少在我上次尝试时,这并不怎么实用。
- 你能检查一下最新的 i3 开发版本中是否仍然存在该问题吗?
- 当然,我也可以先用最新的发布版本尝试复现用户的问题,然后再额外一次在最新的开发版本上复现。
- 但这样一来,验证步骤就转移到了受影响的用户身上,这很好,因为它能筛选出积极性高的缺陷报告者(缺陷报告最终得到修复的几率更高!),并且让用户两次复现缺陷,从而判断问题是否不稳定、是否难以复现、复现步骤是否正确等等。
- 一个自然的后续问题是:“这个代码改动是否让问题消失了?”对于已经拥有开发环境的受影响用户来说,这很容易测试。
基于多次提出这些问题的经验,我注意到这些调试会话中有一些规律。作为回应,我在 i3 v4.3(2012 年 9 月发布)中引入了另一种让 i3 报告版本的方式:--moreversion 标志!现在我可以向用户提出第一个问题的一个小变体:i3 --moreversion 的输出是什么?请注意,这在口头交流中也很好传递,例如在计算机聚会上:
迈克尔·斯塔佩尔贝格: 你用的是哪个版本?
User: 怎么查看?
迈克尔·斯塔佩尔贝格: 运行这个命令:
i3 --versionUser: 显示是 4.24。
迈克尔·斯塔佩尔贝格: 很好,这个版本足够新,已经包含了缺陷修复。现在,我们需要更详细的版本信息!请运行
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
乍一看,这似乎包含了很多细节,但让我来说明为什么这个输出是如此有价值的调试工具:
通过 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"
},
[… jakiego}
}
遗憾的是,据我所知,还没有办法从 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 修订版本印记
如上文所见,最有用的一条版本信息就是 VCS 修订版本。我们可以从 VCS 仓库中获取所有其他细节(版本号、日期、作者……)。
现在,让我们通过看看 Go 是如何做到的,来展示最佳实践!
Go 总会打上印记!🥳
多年来,Go 已经成为我最喜欢的编程语言,很大程度上是因为 Go 开发者的良好品味和风格,当然还有高质量的工具链:
因此,我很高兴地说,Go 在软件版本管理方面实现了黄金标准:它默认就会打上 VCS 构建信息!🥳 这一特性在 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 构建信息,因为它目前存在与 Nix 相同的缺口:

Go 版本报告
对于采用滚动发布模式(没有版本号)的 gokrazy packer,我最终用几行 Go 代码(见下文)来显示 Git 修订版本,无论你是通过 Go 模块还是通过 Git 工作区安装的 packer,都能正常工作。
该代码要么直接显示 vcs.revision(简单情况;从 Git 构建),要么从主模块的 Go 模块版本(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 代码以编程方式读取版本
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 获取器是通过从 GitHub 获取归档文件(.tar.gz)来实现的——并不会传输完整的.git仓库,这样更高效。 - 即使存在
.git仓库,Nix 通常也会为了可复现性而有意将其移除:.git目录包含的打包对象会在不同的git gc运行之间发生变化(例如),这会破坏可复现构建(相同源码却得到不同哈希)。
所以,这里的根本矛盾在于可复现性与 VCS 打标之间的冲突。
幸运的是,有一个能兼顾两者的解决方案:我创建了 stapelberg/nix/go-vcs-stamping Nix 覆盖层模块,你只需导入它,就能让你的 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)。 - 打入的构建信息中不包含任何
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 覆盖层添加到你的 nixosSystem.modules 中:
{
nixpkgs.overlays = [
stapelbergnix.overlays.goVcsStamping
];
}
(如果你像我一样使用 nixpkgs-unstable,则需要在两处都应用该覆盖层。)
重新构建后,你的 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 文件中编写的内容,你可能会进入 Nix 技术栈中的 3 个相关部分之一:
- 获取器(Fetchers)。这是 Flakes 所使用的,也适用于非 Flakes 场景。
- 固定输出派生(Fixed-output derivations(固定输出派生) (FOD))。这是
pkgs.fetchgit的实现方式,但 FOD 固有的哈希频繁变动(需要更新sha256行)很烦人。 - 拷贝器(Copiers)。它们只是将文件拷贝到 Nix store 中,并不感知 Git。
就 VCS 修订版本打标而言,你应该:
- 避免使用拷贝器!如果你使用 Flakes:
- ❌ 不要将
url = "/home/michael/dcs"用作 Flake 输入 - ✅ 改用
url = "git+file:///home/michael/dcs"以获得 Git 感知能力
- ❌ 不要将
- 我也会避免使用固定输出派生(FOD)。
- 在构建时获取 Git 仓库既慢又低效。
- 启用这种方式进行 VCS 修订版本打标所需的
leaveDotGit,效率更低,因为必须以确定性的方式重新构造一个新的 Git 仓库,以保持 FOD 的可复现性。
因此,我们将坚持使用最左一列:获取器。
遗憾的是,默认情况下,使用获取器时,存储在 Nix 属性集(在构建过程中位于内存中)的 VCS 修订版本信息并不会进入 Nix store,因此,当 Nix derivation 被求值且 Go 编译源代码时,Go 看不到任何 VCS 修订版本。
我的 stapelberg/nix/go-vcs-stamping Nix 覆盖层模块修复了这一问题,启用该覆盖层后,你就会进入上图中最左一栏的路径:即 Go 二进制文件已被打标的理想路径!
我的变通方案:Nix Git 构建信息覆盖层
go-vcs-stamping 覆盖层是如何工作的?它在 Nix 和 Go 之间充当适配器:
- Nix 在内存中的
.rev属性集里跟踪 VCS 修订版本。 - Go 期望通过访问
.git/HEAD文件和 git(1) 命令,在.git仓库中找到 VCS 修订版本。
因此,该覆盖层通过 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)能够干净地传递构建信息,而无需像我的 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 - 服务和批处理任务:在启动日志中包含 VCS 修订版本。
- 发出的 HTTP 请求:在
User-Agent中包含 VCS 修订版本 - HTTP 响应:在标头中(内部)包含 VCS 修订版本
- 远程过程调用(RPC):在 RPC 元数据中包含修订版本
- 用户界面:在某个可见位置暴露修订版本以便调试。
- 可执行程序:在以
在整个系统中实现“版本可观测性”是一个一天即可完成、投资回报率很高的项目。
通过我的 Nix 示例,你已经看到 VCS 修订版本如何在整个技术栈中都可用,却可能在中间丢失。希望我的这些资源也能帮助你快速修复自己的技术栈:
- 我的
stapelberg/nix/go-vcs-stamping覆盖层,适用于 Nix / NixOS - 我的
stampit仓库是一个社区资源,用于收集示例(以 Markdown 内容形式),并包含一个 Go 模块,提供了几个让版本报告变得轻而易举的辅助工具。
现在就去为你的程序和数据传输打上印记吧!🚀
随机一篇博客