Stamp It! All Programs Must Report Their Version

Michael Stapelberg

刻め! すべてのプログラムはバージョンを報告すべきだ

先日、本番環境の障害対応中に、1時間も経たないうちに障害の根本原因を正しく推測し(やった!)、念のため修正まで出したのですが、その後はバージョン番号やロールアウトの状況がまったく見えなかったため、何時間も手探りで時間を費やすことになりました… 😞

この経験をきっかけに、ソフトウェアのバージョニングについて改めて考えさせられました。より正確には、ビルド情報(ビルドバージョニング、バージョンスタンプ、呼び方は何でも構いません)とバージョン報告についてです。i3ウィンドウマネージャーでは10年以上も前にこの問題を解決していたことに気づき、職場ではいまだに解決されていないことがむしろ意外でした。

この記事では、たった3つのシンプルなステップ(刻め! 繋げ! 報告せよ!)で、障害対応時の何時間もの遅延やストレスを防げることを説明します。

なぜバージョン管理の水準はこんなに低いのか?!

どの家電製品にも、驚くほど詳細なバージョン情報が付いています!この食洗機を見てください:

詳細なバージョン情報が多数記載された食洗機

(素敵な例を送ってくれた Feuermurmel さん、ありがとうございます!)

家電の修理に何度か立ち会いましたが、修理業者が機種を特定できないと、そもそも手を付けてもらえないという印象を受けました。

では、コンピューターの世界ではなぜ水準がこんなに低いのでしょうか。確かに、コンシューマー向け製品はたいてい何らかの形でバージョン管理されていて、それで十分なことも多いのですが(USB 3.2 Gen 1×2のような例外はありますが!)。しかし最近、適切にバージョン付けされていない開発ビルドに何度も遭遇しました!

ソフトウェアのバージョニング

金属プレートに刻印された家電製品とは違い、ソフトウェアは常に更新され、目に見えない場所や構造の中で動いています。

バージョニングの水準を上げるために何が必要か、掘り下げてみましょう!

通常、ソフトウェアには名前と、粒度の異なるバージョン番号があります:

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

これらはすべて私のPC上の 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.24i3 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 に戻して試してもらえますか?」。「戻す」の技術的な用語はダウングレード、ロールバック、リバートです。
    • Linux ディストリビューションによっては、これは非常に簡単な場合もあれば、悪夢のように難しい場合もあります。
    • NixOS なら簡単です。ブートローダーで古いシステムの「世代」を選んで起動するだけです。あるいは、設定を git で管理していればリバートするだけです。
    • Debian や Arch Linux のような命令的な Linux ディストリビューションでは、ファイルシステムレベルのスナップショットを取っていなければ、システムをアップグレードした後に簡単かつ確実に戻す方法はありません。運が良ければ apt install で古いバージョンの i3 を入れ直せますが、依存関係の衝突(「バージョン地獄」)に陥ることもあります。
    • snapshot.debian.org を使えば古いバージョンの Debian を動かすことも可能なのは知っていますが、少なくとも私が最後に試したときは、あまり実用的ではありませんでした。
  3. 最新版の i3 開発バージョンでも問題が再現するか確認してもらえますか?
    • もちろん、私自身が最新のリリース版でユーザーの問題を再現し、さらに最新の開発版でもう一度試すこともできます。
    • しかし、この方法では検証のステップを影響を受けているユーザーに移すことになります。これは、モチベーションの高いバグ報告者だけが残る(バグ報告が実際に修正につながる可能性が高まる!)という点で良いことですし、ユーザーがバグを2回再現することで、それが不安定な問題なのか、再現が難しいのか、再現手順は正しいのかなどが明らかになります。
    • 自然な次の質問は「このコード変更で問題は解消しますか?」です。開発環境を手にしたユーザーなら、簡単にテストできます。

これらの質問を何度も重ねるうちに、デバッグの進め方にいくつかのパターンがあることに気づきました。そこで、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 ストアの出力(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 が生成されるかどうかは確認できます。

開発ビルド

ここまで説明してきたバージョニングは、ソフトウェアの中間バージョンではなくリリース版だけを追いたい大多数のユーザーには十分です。

しかし、より高い精度を必要とする開発者やユーザーはどうでしょうか?

i3 を git からビルドすると、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 の 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 からのビルドについて図で示します:

git リポジトリから go build / go install を実行してバイナリを生成する流れを示した図

これで私の趣味のプロジェクトの大半はカバーできます!

多くのツールは単に 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 ワーキングコピーからインストールした場合でも git リビジョンを表示できるよう、数行の Go コード(下記参照)で対応しました。

このコードは、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 の buildinfo スタンプにおける2つのケースを示す図: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 の fetcher は、GitHub からアーカイブ(.tar.gz)ファイルを取得する形で実装されています。完全な .git リポジトリは転送されないため、より効率的です。
  2. たとえ .git リポジトリが存在しても、Nix は再現性のために通常は意図的にそれを削除します。.git ディレクトリには git gc の実行ごとに変化する packed オブジェクトが含まれており(例えば)、再現可能なビルドを壊してしまう(同じソースでもハッシュが異なる)からです。

つまり、ここでの根本的な緊張関係は、再現性と VCS スタンプの間にあります。

幸い、両方を解決する手段があります。私は stapelberg/nix/go-vcs-stamping Nix オーバーレイモジュールを作成しました。これをインポートすれば、buildGoModule の Nix 式でデフォルトで Go の VCS リビジョンスタンプが機能するようになります!

Git リポジトリから go build までの図:私の go-vcs-stamping オーバーレイによる回避策なしの場合とありの場合

Nix での Go ビルド事情の詳細

ヒント: Nix ユーザーでなければ、このセクションは飛ばしても構いません。最も複雑な環境でも VCS スタンプを機能させる完全な例として、この記事に含めました。


Nix で Go ソフトウェアをパッケージングするのは、快適なほどシンプルです。

例えば、Go の Protobuf ジェネレータープラグイン protoc-gen-go は 30行未満で Nix にパッケージされています:公式 nixpkgs の protoc-gen-go package.nixbuildGoModule を呼び出し、src として fetchFromGitHub の結果を渡し、数行のメタデータを追加するだけです。

しかし、開発ビルドを完全にスタンプするのは全く簡単ではありません!

自分のソフトウェアをパッケージングする際、リリース版だけでなく個々のリビジョン(開発ビルド)をパッケージングしたいと考えています。同じ 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 オーバーレイを nixosSystem.modules に追加してください:

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

(私のように nixpkgs-unstable を使っている場合は、両方にオーバーレイを適用する必要があります。)

再ビルド後、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. Fetcher。これが Flakes で使われるもので、Flakes 以外でも使われます。
  2. Fixed-output derivation(FOD)。これは pkgs.fetchgit の実装方法ですが、FOD に固有の絶え間ないハッシュの更新(sha256 行の更新)は煩わしいものです。
  3. Copier。単にファイルを Nix ストアにコピーするだけで、git を認識しません。

VCS リビジョンスタンプの観点では、次のようにすべきです:

  • Copier は避けてください!Flakes を使う場合:
    • ❌ Flake の input として url = "/home/michael/dcs" を使わないでください
    • ✅ 代わりに git を認識させるため url = "git+file:///home/michael/dcs" を使ってください
  • 私は fixed-output derivation(FOD)も避けています。
    • ビルド時に git リポジトリを取得するのは遅くて非効率です。
    • この方法で VCS リビジョンスタンプを行うために必要な leaveDotGit を有効にすると、FOD の再現性を保つために新しい Git リポジトリを決定論的に構築しなければならず、さらに非効率になります。

したがって、最も左の列である fetcher を使い続けます。

残念ながら、デフォルトでは fetcher を使った場合、Nix の attrset(ビルドプロセス中のメモリ上)に保存されている VCS リビジョン情報が Nix ストアに届きません。そのため、Nix derivation が評価され Go がソースコードをコンパイルする際に、Go は VCS リビジョンを認識できないのです。

私の stapelberg/nix/go-vcs-stamping Nix オーバーレイモジュールがこれを修正します。このオーバーレイを有効にすることで、上の図の最も左のレーン、つまり Go バイナリが正しくスタンプされるハッピーパスに進むことができます!

私の回避策:Nix git buildinfo オーバーレイ

go-vcs-stamping オーバーレイはどのように動くのでしょうか?Nix と Go の間のアダプターとして機能します:

  • Nix は VCS リビジョンを .rev のメモリ上 attrset で追跡します。
  • Go は VCS リビジョンを .git リポジトリの中に、.git/HEAD ファイルへのアクセスや git(1) コマンド経由で見つけることを期待します。

そこでオーバーレイは、Go に正しい情報を刻ませるために3つのステップを実装しています:

  1. .git/HEAD ファイルを合成し、Go の vcs.FromDir() が git リポジトリを検出できるようにします。
  2. PATHgit コマンドを注入します。これは Go が使う2つのコマンドだけを実装し、それ以外は(Go の実装が更新された場合に備えて)はっきりと失敗するようにしています。
  3. 環境変数 GOFLAGS-buildvcs=true を設定します。

完全なソースは go-vcs-stamping.nix をご覧ください。

本来の修正

このギャップをよりクリーンに修正する方法については、Go issue #77020Go issue #64162 をご覧ください。パッケージマネージャーが正しい VCS 情報を注入して Go ツールを呼び出せるようにするというものです。

これが実現すれば、Nix(あるいは gokrazy も)が buildinfo をクリーンに渡せるようになり、私の 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 リビジョンを含めます(内部向け)
    • Remote Procedure Calls(RPC): RPC メタデータにリビジョンを含めます
    • ユーザーインターフェース: デバッグのために見える場所にリビジョンを表示します。

システム全体にわたって「バージョンの可観測性」を実装するのは、1日でできる投資対効果の高いプロジェクトです。

私の Nix の例で見たように、VCS リビジョンはスタック全体で利用可能ですが、途中で失われることがあります。私のリソースが、あなたのスタックを素早く修正する助けになれば幸いです:

さあ、あなたのプログラムとデータ転送にバージョンを刻みましょう! 🚀

原文は Michael Stapelberg により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。