Stamp It! All Programs Must Report Their Version

Michael Stapelberg

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

原文は 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}

これらはすべて私のコンピュータ上の 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 に戻してみてもらえますか?」。「戻す」ことを表す技術用語は downgrade、rollback、revert です。
    • Linux ディストリビューションによって、これは簡単な場合もあれば悪夢のような場合もあります。
    • NixOS なら簡単です。ブートローダーでそのバージョンを選んで古いシステムの「generation」にブートするだけです。あるいは、設定がバージョン管理されていれば git で revert します。
    • Debian Linux や Arch Linux のような命令的な Linux ディストリビューションでは、ファイルシステムレベルのスナップショットを取っていないと、システムをアップグレードした後に簡単かつ確実に戻す方法はありません。運が良ければ、単に apt install で古いバージョンの i3 をインストールできます。しかし依存関係の衝突(「version hell」)に遭遇するかもしれません。
    • 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"
    },
    [… jakiego}
}

残念ながら、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 がゴールドスタンダードを実装していることを嬉しく思います。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 build info スタンピングの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. Nix fetchers のような fetchFromGitHub は、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. Fetchers。これらは Flakes が使うものですが、Flakes 以外のユースケースでも使われます。
  2. Fixed-output derivations(FOD)。これは pkgs.fetchgit の実装方法ですが、FOD に固有の絶え間ないハッシュの更新(sha256 行の更新)は面倒です。
  3. Copiers。これらは単にファイルを Nix ストアにコピーするだけで、git を認識しません。

VCS リビジョンスタンピングの目的では、次のようにすべきです:

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

したがって、一番左の列である fetchers を使うことにします。

残念ながら、デフォルトでは fetchers を使うと、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 は .rev というメモリ上の attrset で VCS リビジョンを追跡します。
  • Go は .git リポジトリ内で、.git/HEAD ファイルへのアクセスや git(1) コマンドを通じて VCS リビジョンを見つけることを期待します。

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

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

完全なソースについては、go-vcs-stamping.nix を参照してください。

クリーンな修正

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

これが実現すれば、Nix(あるいは gokrazy も)が、私の go-vcs-stamping アダプタのような回避策を必要とせずに、buildinfo をクリーンに渡せるようになります。

執筆時点では、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 メタデータにリビジョンを含める
    • ユーザーインターフェース: デバッグのために見える場所でリビジョンを公開する。

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

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

さあ、あなたのプログラムとデータ転送をスタンプしましょう!🚀

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

コメント