Stamp It! All Programs Must Report Their Version

Michael Stapelberg

打上印记!所有程序都必须报告自身版本

最近,在一次生产环境故障应急中,我在不到一小时内就准确猜中了故障的根本原因(真不错!),并提交了一个修复来排除这一可能性,结果却因为无法看清版本号和发布情况,在黑暗中摸索了好几个小时…… 😞

这次经历让我再次思考起软件版本管理,更具体地说,是关于构建信息(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 浏览器,只是指代的粒度不同。

它们都是正确且有用的,具体取决于上下文。以下分别是每个粒度的例子:

  1. “在我这儿用 Chrome 是正常的,你在 Firefox 里试过吗?”
  2. “Chrome 146 中存在中键点击粘贴并跳转失效的问题”
  3. “我运行的是 Chrome 146.0.7680.80,无法复现你的问题”
  4. “在这个 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

每个词都是经过仔细斟酌后放置的。让我来逐一解析:

  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 中回退。
    • 在使用 Debian Linux 或 Arch Linux 这类命令式发行版时,如果你没有做文件系统级别的快照,升级后就没有简单可靠的方法回退。如果幸运的话,你可以直接 apt install 旧版本的 i3,但可能会遇到依赖冲突(“版本地狱”)。
    • 我知道通过 snapshot.debian.org 可以运行旧版本的 Debian,但至少在我上次尝试时,这并不怎么实用。
  3. 你能检查一下最新的 i3 开发版本中是否仍然存在该问题吗?
    • 当然,我也可以先用最新的发布版本尝试复现用户的问题,然后再额外一次在最新的开发版本上复现。
    • 但这样一来,验证步骤就转移到了受影响的用户身上,这很好,因为它能筛选出积极性高的缺陷报告者(缺陷报告最终得到修复的几率更高!),并且让用户两次复现缺陷,从而判断问题是否不稳定、是否难以复现、复现步骤是否正确等等。
    • 一个自然的后续问题是:“这个代码改动是否让问题消失了?”对于已经拥有开发环境的受影响用户来说,这很容易测试。

基于多次提出这些问题的经验,我注意到这些调试会话中有一些规律。作为回应,我在 i3 v4.3(2012 年 9 月发布)中引入了另一种让 i3 报告版本的方式:--moreversion 标志!现在我可以向用户提出第一个问题的一个小变体:i3 --moreversion 的输出是什么?请注意,这在口头交流中也很好传递,例如在计算机聚会上:

迈克尔·斯塔佩尔贝格: 你用的是哪个版本?

User: 怎么查看?

迈克尔·斯塔佩尔贝格: 运行这个命令:i3 --version

User: 显示是 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

乍一看,这似乎包含了很多细节,但让我来说明为什么这个输出是如此有价值的调试工具:

  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 修订版本:

~/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 是我最喜欢的编程语言

因此,我很高兴地说,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 代码(见下文)来显示 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 构建信息打标的两种情况:从 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 获取器是通过从 GitHub 获取归档文件(.tar.gz)来实现的——并不会传输完整的 .git 仓库,这样更高效。
  2. 即使存在 .git 仓库,Nix 通常也会为了可复现性而有意将其移除:.git 目录包含的打包对象会在不同的 git gc 运行之间发生变化(例如),这会破坏可复现构建(相同源码却得到不同哈希)。

所以,这里的根本矛盾在于可复现性与 VCS 打标之间的冲突。

幸运的是,有一个能兼顾两者的解决方案:我创建了 stapelberg/nix/go-vcs-stamping Nix 覆盖层模块,你只需导入它,就能让你的 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)
  • 打入的构建信息中不包含任何 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 表达式到已打标二进制文件或 VCS 信息丢失的二进制文件的所有路径

根据你在 .nix 文件中编写的内容,你可能会进入 Nix 技术栈中的 3 个相关部分之一:

  1. 获取器(Fetchers)。这是 Flakes 所使用的,也适用于非 Flakes 场景。
  2. 固定输出派生(Fixed-output derivations(固定输出派生) (FOD))。这是 pkgs.fetchgit 的实现方式,但 FOD 固有的哈希频繁变动(需要更新 sha256 行)很烦人。
  3. 拷贝器(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 打上正确的信息:

  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 适配器这样的变通方案

在撰写本文时,issue #77020 似乎还没有得到太多关注,仍然处于开放状态。

结论:打上印记!贯通链路!报告出来!

我的观点很简单:

打上 VCS 修订版本的印记在概念上很简单,却非常重要!

例如,如果我在开头提到的那次故障中的生产系统能够报告其版本,我们本可以节省数小时的缓解时间!

遗憾的是,许多环境只标识构建产物(有用,但与此正交),却没有将 VCS 修订版本贯通到底(这要有用得多!),或者至少默认没有这样做。

要修复它,你的行动计划只需 3 个简单步骤:

  1. 打上印记!在你的程序中包含源代码的 VCS 修订版本。这不是什么新想法:i3 的构建自 2012 年起就包含了它们的 git-describe(1) 修订版本!
  2. 贯通链路!在构建/打包时,确保 VCS 修订版本不会丢失。上文 “在 NixOS 中获取 VCS 修订版本” 案例研究一节说明了 VCS 修订版本可能丢失的几个原因、哪些路径可行以及如何修复缺失的链路。
  3. 报告出来!让你的软件在每个相关的地方都打印其 VCS 修订版本,例如:
    • 可执行程序:在以 --version 运行时报告 VCS 修订版本。对于 Go 程序,你始终可以使用 go version -m
    • 服务和批处理任务:在启动日志中包含 VCS 修订版本。
    • 发出的 HTTP 请求:User-Agent 中包含 VCS 修订版本
    • HTTP 响应:在标头中(内部)包含 VCS 修订版本
    • 远程过程调用(RPC):在 RPC 元数据中包含修订版本
    • 用户界面:在某个可见位置暴露修订版本以便调试。

在整个系统中实现“版本可观测性”是一个一天即可完成、投资回报率很高的项目。

通过我的 Nix 示例,你已经看到 VCS 修订版本如何在整个技术栈中都可用,却可能在中间丢失。希望我的这些资源也能帮助你快速修复自己的技术栈:

现在就去为你的程序和数据传输打上印记吧!🚀

原文由 Michael Stapelberg 发布

本文章由 muse-spark-1.2-contributor 进行翻译