Ich bin gerade krank, das bedeutet für mich, ich verbringe Zeit mit mir, meinen Gedanken und einem Buch. Das Buch, das ich gerade lese, heißt 4000 Wochen, ist kurzweilig geschrieben, viel wusste ich aber auch schon bzw. es deckt sich mit meiner Meinung. Darum soll es hier aber nicht gehen. Gegen Ende des Buchs gibt es ein hervorragendes Kapitel unter dem Titel „Die »Dem-Kosmos-ist’s-egal-Therapie«“, das einen Gedanken bei mir entfacht hat.
Recently, during a production incident response, I guessed the root cause of an outage correctly within less than an hour (cool!) and submitted a fix just to rule it out, only to then spend many hours fumbling in the dark because we lacked visibility into version numbers and rollouts… 😞
This experience made me think about software versioning again, or more specifically about build info (build versioning, version stamping, however you want to call it) and version reporting. I realized that for the i3 window manager, I had solved this problem well over a decade ago, so it was really unexpected that the problem was decidedly not solved at work.
In this article, I’ll explain how 3 simple steps (Stamp it! Plumb it! Report it!) are sufficient to save you hours of delays and stress during incident response.
Every household appliance has incredibly detailed versioning! Consider this dishwasher:
(Thank you Feuermurmel for sending me this lovely example!)
I observed a couple household appliance repairs and am under the impression that if a repair person cannot identify the appliance, they would most likely refuse to even touch it.
So why are our standards so low in computers, in comparison? Sure, consumer products are typically versioned somehow and that’s typically good enough (except for, say, USB 3.2 Gen 1×2!). But recently, I have encountered too many developer builds that were not adequately versioned!
Unlike a physical household appliance with a stamped metal plate, software is constantly updated and runs in places and structures we often cannot even see.
Let’s dig into what we need to increase our versioning standard!
Usually, software has a name and some version number of varying granularity:
All of these identify the Chrome browser on my computer, but each at different granularity.
All are correct and useful, depending on the context. Here’s an example for each:
After creating the i3 window manager, I quickly learned that for user support, it is very valuable for programs to clearly identify themselves. Let me illustrate with the following case study.
--version and --moreversionWhen running i3 --version, you will see output like this:
% i3 --version
i3 version 4.24 (2024-11-06) © 2009 Michael Stapelberg and contributors
Each word was carefully deliberated and placed. Let me dissect:
i3 version 4.24: I could have shortened this to i3 4.24 or maybe i3 v4.24, but I figured it would be helpful to be explicit because i3 is such
a short name. Users might mumble aloud “What’s an i-3-4-2-4?”, but when
putting “version” in there, the implication is that i3 is some computer thing
(→ a computer program) that exists in version 4.24.(2024-11-06) is the release date so that you can immediately tell if
“4.24” is recent.© 2009 Michael Stapelberg signals when the project was started and who is
the main person behind it.and contributors gives credit to the many people who helped. i3 was never a
one-person project; it was always a group effort.When doing user support, there are a couple of questions that are conceptually easy to ask the affected user and produce very valuable answers for the developer:
i3 --version?apt install the older version of i3. But you might run into
dependency conflicts (“version hell”).Based on my experiences with asking these questions many times, I noticed a few
patterns in how these debugging sessions went. In response, I introduced another
way for i3 to report its version in i3 v4.3 (released in September 2012): a
--moreversion flag! Now I could ask users a small variation of the first
question: What is the output of i3 --moreversion? Note how this also transfers
well over spoken word, for example at a computer meetup:
Michael: Which version are you using?
User: How can I check?
Michael: Run this command:
i3 --versionUser: It says 4.24.
Michael: Good, that is recent enough to include the bug fix. Now, we need more version info! Run
i3 --moreversionplease and tell me what you see.
When you run i3 --moreversion, it does not just report the version of the i3
program you called, it also connects to the running i3 window manager process in
your X11 session using its IPC (interprocess communication)
interface and reports the running i3 process’s
version, alongside other key details that are helpful to show the user, like
which configuration file is loaded and when it was last changed:
% 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
This might look like a lot of detail on first glance, but let me spell out why this output is such a valuable debugging tool:
Connecting to i3 via the IPC interface is an interesting test in and of
itself. If a user sees i3 --moreversion output, that implies they will also
be able to run debugging commands like (for example) i3-msg -t get_tree > /tmp/tree.json to capture the full layout state.
During a debugging session, running i3 --moreversion is an easy check to
see if the version you just built is actually effective (see the Running i3 version line).
Showing the full path to the loaded config file will make it obvious if the user has been editing the wrong file. If the path alone is not sufficient, the modification time (displayed both absolute and relative) will flag editing the wrong file.
I use NixOS, BTW, so I automatically get a stable identifier
(0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24) for the specific build of i3.
% 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
To see the build recipe (“derivation” in Nix terminology) which produced this
Nix store output (0zn9r4263…-i3-4.24), I can run nix derivation show:
% nix derivation show /nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24
{
"/nix/store/z7ly4kvgixf29rlz01ji4nywbajfifk4-i3-4.24.drv": {
[…]
nix derivation show output if you are curious
% 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",
"__structuredAttrs": "",
"buildInputs": "/nix/store/58q0dn2lbm2p04qmds0aymwdd1fr5j67-libxcb-1.17.0-dev /nix/store/3fcfw014z5i05ay1ag0hfr6p81mb1kzw-libxcb-keysyms-0.4.1-dev /nix/store/2cdrqvd3av1dmxna9xjqv1jccibpvg6m-libxcb-util-0.4.1-dev /nix/store/256alp82fhdgbxx475dp7mk8m29y53rh-libxcb-wm-0.4.2-dev /nix/store/nr44nfhj48abr3s6afqy1fjq4qmr23lz-xcb-util-xrm-1.3 /nix/store/ml4cfhhw6af6qq6g3dn7g5j5alrnii88-libxkbcommon-1.11.0-dev /nix/store/6hnzjg09fd5xkkrdj437wyaj952nlg45-libstartup-notification-0.12 /nix/store/9m0938zahq7kcfzzix4kkpm8d1iz3nmq-libx11-1.8.12-dev /nix/store/vz5gd0rv0m2kjca50gacz0zq9qh7i8xf-pcre2-10.46-dev /nix/store/334cvqpqc9f0plv0aks71g352w6hai0c-libev-4.33 /nix/store/6s3fw10c0441wv53bybjg50fh8ag1561-yajl-2.1.0-unstable-2024-02-01 /nix/store/d6aw2004h90dwlsfcsygzzj4pzm1s31a-libxcb-cursor-0.1.6-dev /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/l6bslkrp59gaknypf1jrs5vbb2xmcwym-pango-1.57.0-dev /nix/store/7s7by82nq8bahsh195qr0mnn9ac8ljmm-perl5.40.0-AnyEvent-I3-0.19 /nix/store/9ml0p4x1cx5k1lla91bxgramc0amsfkf-perl5.40.0-X11-XCB-0.20 /nix/store/67j1sx7qcn6f7qvq1kh3z8i5mpajgq3r-perl5.40.0-IPC-Run-20231003.0 /nix/store/859x84mz38bcq0r7hwksk4b5apcsmf2w-perl5.40.0-ExtUtils-PkgConfig-1.16 /nix/store/q1qydg6frfpq9jkhnymfsjzf71x9jswr-perl5.40.0-Inline-C-0.82",
"builder": "/nix/store/6ph0zypyfc09fw6hlc1ygjvk2hv4j9vd-bash-5.3p3/bin/bash",
"checkPhase": "runHook preCheck\n\ntest_failed=\n# \"| cat\" disables fancy progress reporting which makes the log unreadable.\n./complete-run.pl -p 1 --keep-xserver-output | cat || test_failed=\"complete-run.pl returned $?\"\nif [ -z \"$test_failed\" ]; then\n # Apparently some old versions of `complete-run.pl` did not return a\n # proper exit code, so check the log for signs of errors too.\n grep -q '^not ok' latest/complete-run.log && test_failed=\"test log contains errors\" ||:\nfi\nif [ -n \"$test_failed\" ]; then\n echo \"***** Error: $test_failed\"\n echo \"===== Test log =====\"\n cat latest/complete-run.log\n echo \"===== End of test log =====\"\n false\nfi\n\nrunHook postCheck\n",
"cmakeFlags": "",
"configureFlags": "",
"debug": "/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug",
"depsBuildBuild": "",
"depsBuildBuildPropagated": "",
"depsBuildTarget": "",
"depsBuildTargetPropagated": "",
"depsHostHost": "",
"depsHostHostPropagated": "",
"depsTargetTarget": "",
"depsTargetTargetPropagated": "",
"doCheck": "1",
"doInstallCheck": "",
"mesonFlags": "-Ddocs=true -Dmans=true",
"name": "i3-4.24",
"nativeBuildInputs": "/nix/store/x06h0jfzv99c3dmb8pj8wbmy0v9wj6bd-pkg-config-wrapper-0.29.2 /nix/store/pcdnznc797nmf9svii18k3c5v22sqihs-make-shell-wrapper-hook /nix/store/nzg469dkg5dj7lv4p50pi8zmwzxx73hr-meson-1.9.1 /nix/store/rlcn0x0j22nbhhf8wfp8cwfxgh65l82r-ninja-1.13.1 /nix/store/hs4pgi40k5nbl0fpf0jx8i5f6zrdv63v-install-shell-files /nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0 /nix/store/xiqlw1h0i6a6v59skrg9a7rg3qpanqy7-asciidoc-10.2.1 /nix/store/300facd5m37fwqrypjcikn09vqs488zv-xmlto-0.0.29 /nix/store/yk7avh2szvm6bi5dwgzz4c2iciaipj2p-docbook-xml-4.5 /nix/store/d5qdxn0rjl9s7xfc1rca33gya0fhcvkm-docbook-xsl-nons-1.79.2 /nix/store/2y1r1cpza3lpk7v6y9mf75ak0pswilwi-find-xml-catalogs-hook /nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh /nix/store/xlhipdkyqksxvp73cznnij5q6ilbbqd9-xorg-server-21.1.21-dev /nix/store/i8nxxmw5rzhxlx3n12s3lvplwwap6mpc-xvfb-run-1+g87f6705 /nix/store/a198i9cnhn6y5cajkdxg0hhcrmalazjr-xdotool-3.20211022.1 /nix/store/b4dnjyq2i4kjg8xswkjd7lwfcdps94j8-setxkbmap-1.3.4 /nix/store/cxdbw6iqj1a1r69wb55xl5nwi7abfllb-xrandr-1.5.3 /nix/store/5k4mv2a1qrciv12wywlkgpslc6swyv58-which-2.23",
"out": "/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24",
"outputs": "out debug",
"patches": "",
"pname": "i3",
"postInstall": "wrapProgram \"$out/bin/i3-save-tree\" --prefix PERL5LIB \":\" \"$PERL5LIB\"\nfor program in $out/bin/i3-sensible-*; do\n sed -i 's/which/command -v/' $program\ndone\n\ninstallManPage man/*.1\n",
"postPatch": "patchShebangs .\n\n# This testcase generates a Perl executable file with a shebang, and\n# patchShebangs can't replace a shebang in the middle of a file.\nif [ -f testcases/t/318-i3-dmenu-desktop.t ]; then\n substituteInPlace testcases/t/318-i3-dmenu-desktop.t \\\n --replace-fail \"#!/usr/bin/env perl\" \"#!/nix/store/84mhqfj9amzyvxhp37yh3b0zd8sq0a7p-perl-5.40.0/bin/perl\"\nfi\n",
"propagatedBuildInputs": "",
"propagatedNativeBuildInputs": "",
"separateDebugInfo": "1",
"src": "/nix/store/qx48i7zf9n69yla8gfbif6dskysk0l1w-source",
"stdenv": "/nix/store/43dbh9z6v997g6njz4yqmcrj26zic9ds-stdenv-linux",
"strictDeps": "",
"system": "x86_64-linux",
"version": "4.24"
},
"inputDrvs": {
"/nix/store/0h97zzsaf4ggiiwi0rbdjl3fzjj8vhj0-meson-1.9.1.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/0r073sy0685h3gycpl8kpkgmv5p87rw4-libxcb-1.17.0.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/0rjr80q4lpigwjwaxw089wcrrag7p46m-xmlto-0.0.29.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/14wsbyw3j1h9blcxr16c9663w0piq0p2-bash-5.3p3.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/165y3ip2cqlnqd6qrgh6lzklv21xy11w-make-shell-wrapper-hook.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/1abxvpwsry6q5pijb2j91aryh2ilp929-pango-1.57.0.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/2sjcj6l2959dvd5vlicmkf1sdr0hwqx5-perl5.40.0-ExtUtils-PkgConfig-1.16.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/3jnvpbpi95g6zp8vjq1qafh20lz6kwi3-perl5.40.0-X11-XCB-0.20.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/45szhbhybqh4fkcpmx7sqpcrpwpadvgv-pkg-config-wrapper-0.29.2.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/4r5bd9g98fq40hjbfc7sbnp42jhnzg5h-yajl-2.1.0-unstable-2024-02-01.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/4yw0g3zqw4gn1szw8bqrvgmz5b6qm8s5-stdenv-linux.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/53gin0imc257fibkbyvl0jsi0pm1zvbl-docbook-xml-4.5.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/54q42ddy9jb24v4mbx0f19faqqsw5jga-libxkbcommon-1.11.0.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/56dg95jlnwp6kkifyqh94f548r5cha9b-xrandr-1.5.3.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/6srgz2k17vc6x85s3paccdbgg9rv0bia-asciidoc-10.2.1.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/7xpmbw1xzzwxcd1rnx6qid7zhqnzq3jh-setxkbmap-1.3.4.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/87b385i529h64dzrycf16ksv0jcbzs29-libev-4.33.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/9l94a5gr0wbhaq6zyl30wpqygp1cffrx-pcre2-10.46.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/b8hhyx6rpy47hkbq5wlhrvfrfv3yn7j8-xvfb-run-1+g87f6705.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/bxrnxv90lrpvq06rja47986h057rhwcc-libxcb-cursor-0.1.6.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/cgdz2idkz91w2k7hpb2dymv80938cz9w-libxcb-wm-0.4.2.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/ddvlvaj43mls902nay7ddjrg01d6c2la-perl-5.40.0.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/ddxlvkpjlg6ycayb6az23ldjdr21xlnf-which-2.23.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/ds5ss96inhkj9x2gbd7shinvbiid6v6b-xorg-server-21.1.21.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/f0yqdlwz2vwsx51wlgmi9pjqpdhbprkx-ninja-1.13.1.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/gm613dry4hkv26m7ml49fq60z8p0r0gf-perl5.40.0-IPC-Run-20231003.0.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/h3sjzf7hg9ghbh4hzdg6c4byfky2fjng-libx11-1.8.12.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/j5ji7yjwizrma9h72h2pqgi8ir6ah6q8-libstartup-notification-0.12.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/k2jxg4mck2f4pqlisp6slwhyd3pva8wz-source.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/n19ll9p9ivkni2y9l9i2rypyi5gi8z58-perl5.40.0-Inline-C-0.82.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/nm7v937f2z7srs54idjwc7sl6azc1slj-xdotool-3.20211022.1.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/qzg3b7p4gf4izfjbkc42bjyrvp8vz99k-xcb-util-xrm-1.3.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/rjmh0kp3w170bii9i57z5anlshzm2gll-install-shell-files.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/rrsm8jbqqf58k30cm2lxmgk43fkxsgqp-find-xml-catalogs-hook.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/s4wl1ny41k50rkxw0x0wdjf9l5mjqyv0-libxcb-util-0.4.1.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/vxckbgl5kwf5ikz0ma0fkavsnh683ry0-libxcb-keysyms-0.4.1.drv": {
"dynamicOutputs": {},
"outputs": [
"dev"
]
},
"/nix/store/xxb7x7j73p3sxf03hb1hzaz588avd3yw-docbook-xsl-nons-1.79.2.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/yik59jhh69af5fcvddmxlhfwya69pnzw-perl5.40.0-AnyEvent-I3-0.19.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
}
},
"inputSrcs": [
"/nix/store/l622p70vy8k5sh7y5wizi5f2mic6ynpg-source-stdenv.sh",
"/nix/store/r989dk196nl9frhnfsa1lb7knhbyjxw6-separate-debug-info.sh",
"/nix/store/shkw4qm9qcw5sc5n1k5jznc83ny02r39-default-builder.sh"
],
"name": "i3-4.24",
"outputs": {
"debug": {
"path": "/nix/store/20rgxn6fpywd229vka9dnjiaprypxirh-i3-4.24-debug"
},
"out": {
"path": "/nix/store/0zn9r4263fjpqah6vdzlalfn0ahp8xc2-i3-4.24"
}
},
"system": "x86_64-linux"
}
Unfortunately, I am not aware of a way to go from the derivation to the .nix
source, but at least one can check that a certain source results in an identical
derivation.
The versioning I have described so far is sufficient for most users, who will not be interested in tracking intermediate versions of software, but only the released versions.
But what about developers, or any kind of user who needs more precision?
When building i3 from git, it reports the git revision it was built from, using
git-describe(1)
:
~/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
A modified working copy gets represented by a + after the revision:
~/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
Reporting the git revision (or VCS revision, generally speaking) is the most useful choice.
This way, we catch the following common mistakes:
As we have seen above, the single most useful piece of version information is the VCS revision. We can fetch all other details (version numbers, dates, authors, …) from the VCS repository.
Now, let’s demonstrate the best case scenario by looking at how Go does it!
Go has become my favorite programming language over the years, in big part because of the good taste and style of the Go developers, and of course also because of the high-quality tooling:
I strive to respect everybody’s personal preferences, so I usually steer clear of debates about which is the best programming language, text editor or operating system. However, recently I was asked a couple of times why I like and use a lot of Go, so here is a coherent article to fill in the blanks of my ad-hoc in-person ramblings :-). Read more →
Therefore, I am pleased to say that Go implements the gold standard with regard to software versioning: it stamps VCS buildinfo by default! 🥳 This was introduced in Go 1.18 (March 2022):
Additionally, the go command embeds information about the build, including build and tool tags (set with -tags), compiler, assembler, and linker flags (like -gcflags), whether cgo was enabled, and if it was, the values of the cgo environment variables (like CGO_CFLAGS).
Both VCS and build information may be read together with module information using
go version -m fileor runtime/debug.ReadBuildInfo (for the currently running binary) or the new debug/buildinfo package.
What does this mean in practice? Here is a diagram for the common case: building from git:
This covers most of my hobby projects!
Many tools I just go install, or CGO_ENABLED=0 go install if I want to
easily copy them around to other computers. Although, I am managing more and
more of my software in NixOS.
When I find a program that is not yet fully managed, I can use gops and the
go tool to identify it:
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
[…]
dep pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
dep pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4=
build -buildmode=exe
build -compiler=gc
build DefaultGODEBUG=containermaxprocs=0,decoratemappings=0,tlssha1=1,updatemaxprocs=0,x509sha256skid=0
build CGO_ENABLED=0
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
build vcs=git
build vcs.revision=ac8f5391b43bc1a9dbdc99f6179e2fb7d7414a04
build vcs.time=2025-12-30T10:15:40Z
build vcs.modified=true
root@ax52 ~ %
It’s very cool that Go does the right thing by default!
Systems that consist of 100% Go software (like my gokrazy Go appliance
platform) are fully stamped! For example, the gokrazy web
interface shows me exactly which version and dependencies went into the
gokrazy/rsync build on my scan2drive
appliance.
Despite being fully stamped, note that gokrazy only shows the module versions, and no VCS buildinfo, because it currently suffers from the same gap as Nix:
For the gokrazy packer, which follows a rolling release model (no version numbers), I ended up with a few lines of Go code (see below) to display a git revision, no matter if you installed the packer from a Go module or from a git working copy.
The code either displays vcs.revision (the easy case; built from git) or
extracts the revision from the Go module version of the main module
(BuildInfo.Main.Version):
What are the other cases? These examples illustrate the scenarios I usually deal with:
| source (built from) | buildinfo (stamped into program) |
|---|---|
| directory (no git) | module (devel) |
| Go module | module v0.3.1-0.20260105212325-5347ac5f5bcb |
| directory (git) | module v0.0.0-20260131174001-ccb1d233f2a4+dirty |
vcs.revision=ccb1d233f2a43e9118b9146b3c9a5ded1efb7551 |
|
vcs.time=2026-01-31T17:40:01Z |
|
vcs.modified=true |
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
}
This is what it looks like in practice:
% go install github.com/gokrazy/tools/cmd/gok@latest
% gok --version
https://github.com/gokrazy/tools/commit/8ed49b4fafc7
But a version built from git has the full revision available (→ you can tell them apart):
% (cd ~gokrazy/../tools && go install ./cmd/...)
% gok --version
https://github.com/gokrazy/tools/commit/ba6a8936f4a88ddcf20a3b8f625e323e65664aa6 (modified)
When packaging Go software with Nix, it’s easy to lose Go VCS revision stamping:
fetchFromGitHub are implemented by fetching an archive
(.tar.gz) file from GitHub — the full .git repository is not transferred,
which is more efficient..git repository is present, Nix usually intentionally removes it
for reproducibility: .git directories contain packed objects that change
across git gc runs (for example), which would break reproducible builds
(different hash for the same source).So the fundamental tension here is between reproducibility and VCS stamping.
Luckily, there is a solution that works for both: I created the
stapelberg/nix/go-vcs-stamping Nix overlay
module that you can import to get working Go
VCS revision stamping by default for your buildGoModule Nix expressions!
Tip: If you are not a Nix user, feel free to skip over this section. I included it in this article so that you have a full example of making VCS stamping work in the most complicated environments.
Packaging Go software in Nix is pleasantly straightforward.
For example, the Go Protobuf generator plugin protoc-gen-go is packaged in Nix
with <30 lines: official nixpkgs protoc-gen-go
package.nix. You
call
buildGoModule,
supply as src the result from
fetchFromGitHub
and add a few lines of metadata.
But getting developer builds fully stamped is not straightforward at all!
When packaging my own software, I want to package individual revisions
(developer builds), not just released versions. I use the same buildGoModule,
or buildGoLatestModule if I need the latest Go version. Instead of using
fetchFromGitHub, I provide my sources using Flakes, usually also from GitHub
or from another Git repository. For example, I package gokrazy/bull like so:
{
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=";
}
The bullsrc comes from my 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";
# Use the same version as nixpkgs
inputs.nixpkgs.follows = "nixpkgs";
};
stapelbergnix.url = "github:stapelberg/nix";
zkjnastools.url = "github:stapelberg/zkj-nas-tools";
configfiles = {
url = "github:stapelberg/configfiles";
flake = false; # repo is not a flake
};
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
stapelbergnix.lib.userSettings
stapelbergnix.lib.zshConfig
# Use systemd for network configuration
stapelbergnix.lib.systemdNetwork
# Use systemd-boot as bootloader
stapelbergnix.lib.systemdBoot
# Run prometheus node exporter in tailnet
stapelbergnix.lib.prometheusNode
zkjnastools.nixosModules.zkjbackup
{
nixpkgs.overlays = [
(final: prev: {
bull = import ./bull-pkg.nix {
pkgs = final;
pkgs-unstable = pkgs-unstable;
inherit bullsrc;
};
})
];
}
];
};
formatter.${system} = pkgs.nixfmt-tree;
};
}
Go stamps all builds, but it does not have much to stamp here:
(devel).vcs information.Here’s a full example of 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
dep github.com/fsnotify/fsnotify v1.8.0
dep github.com/google/renameio/v2 v2.0.2
dep github.com/yuin/goldmark v1.7.8
dep go.abhg.dev/goldmark/wikilink v0.5.0
dep golang.org/x/image v0.23.0
dep golang.org/x/sync v0.10.0
dep golang.org/x/sys v0.28.0
build -buildmode=exe
build -compiler=gc
build -trimpath=true
build CGO_ENABLED=0
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1To fix VCS stamping, add my goVcsStamping overlay to your nixosSystem.modules:
{
nixpkgs.overlays = [
stapelbergnix.overlays.goVcsStamping
];
}
(If you are using nixpkgs-unstable, like I am, you need to apply the overlay in both places.)
After rebuilding, your Go binaries should newly be stamped with 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
Nice! 🥳 But… how does it work? When does it apply? How do you know how to fix your config?
I’ll show you the full diagram first, and then explain how to read it:
There are 3 relevant parts of the Nix stack that you can end up in, depending on
what you write into your .nix files:
pkgs.fetchgit is implemented,
but the constant hash churn (updating the sha256 line) inherent to FODs is
annoying.For the purpose of VCS revision stamping, you should:
url = "/home/michael/dcs" as a Flake inputurl = "git+file:///home/michael/dcs" instead for git awarenessleaveDotGit, which is needed for VCS revision stamping with this
approach, is even more inefficient because a new Git repository must be
constructed deterministically to keep the FOD reproducible.Hence, we will stick to the left-most column: fetchers.
Unfortunately, by default, with fetchers, the VCS revision information, which is stored in a Nix attrset (in-memory, during the build process), does not make it into the Nix store, hence, when the Nix derivation is evaluated and Go compiles the source code, Go does not see any VCS revision.
My stapelberg/nix/go-vcs-stamping Nix overlay
module fixes this, and enabling the overlay
is how you end up in the left-most lane of the above diagram: the happy path,
where your Go binaries are now stamped!
How does the go-vcs-stamping overlay work? It functions as an adapter between
Nix and Go:
.rev in-memory attrset..git repository, accessed via
.git/HEAD file access and git(1)
commands.So the overlay implements 3 steps to get Go to stamp the correct info:
.git/HEAD file so that Go’s vcs.FromDir() detects a git
repository.git command into the PATH that implements exactly the two
commands used by Go and fails loudly on anything else (in case Go updates its
implementation).-buildvcs=true in the GOFLAGS environment variable.For the full source, see
go-vcs-stamping.nix.
See Go issue #77020 and Go issue #64162 for a cleaner approach to fixing this gap: allowing package managers to invoke the Go tool with the correct VCS information injected.
This would allow Nix (or also gokrazy) to pass along buildinfo cleanly, without
the need for workarounds like my go-vcs-stamping
adapter.
At the time of writing, issue #77020 does not seem to have much traction and is still open.
My argument is simple:
Stamping the VCS revision is conceptually easy, but very important!
For example, if the production system from the incident I mentioned had reported its version, we would have saved multiple hours of mitigation time!
Unfortunately, many environments only identify the build output (useful, but orthogonal), but do not plumb the VCS revision (much more useful!), or at least not by default.
Your action plan to fix it is just 3 simple steps:
git-describe(1)
revision since 2012!--version
go version -mUser-AgentImplementing “version observability” throughout your system is a one-day high-ROI project.
With my Nix example, you saw how the VCS revision is available throughout the stack, but can get lost in the middle. Hopefully my resources help you quickly fix your stack(s), too:
stapelberg/nix/go-vcs-stamping
overlay for Nix / NixOSstampit repository is a
community resource to collect examples (as markdown content) and includes a Go
module with a few helpers to make version reporting trivial.Now go stamp your programs and data transfers! 🚀
Die Schlagzeile „15-Jähriger klaut Linienbus – und fährt Freundin zur Schule“ im vergangenen Monat ist mir im Kopf geblieben. Ich fand das irgendwie lustig und herzig – vielleicht auch weil alles gut ging, der Bus keine Kratzer hatte und keine Menschen verletzt wurden. So lässt sich dieser Vorfall leicht romantisieren und natürlich bin ich sehr froh, dass nicht eines meiner Kinder diese Idee hatte.
Allerdings dachte ich, als ich das erste Mal von diesem Vorfall las: „Teenager, man muss sie einfach lieben!“ und das meine ich nicht ironisch.
Ich liebe sehr vieles am Teenagergemüt. Die überschäumende Energie, der Experimentierdrang, der Glaube, dass es kein Limit gibt, dass alles möglich ist. Die Scheißegalhaltung manchen Dingen gegenüber und der Wille alles anders zu machen und auf den Kopf zu stellen. Ich liebs einfach.
Und hier passt das Wort Ambiguitätstoleranz auch so schön. Also die Fähigkeit Widersprüche und mehrdeutige Informationen zu ertragen. Denn natürlich finde ich all das auch manchmal gleichzeitig anstrengend und nervig und es macht mir angst. Denn wenn Jugendliche manchmal glauben „Ach, das sind nur 6 Meter, da spring ich einfach runter“, dann kann das natürlich auch richtig schief gehen.
Als Mutter fühle ich mich oft herausgefordert und kann mich schlecht zurückhalten nicht ständig meine Bedenken zu etwas zu äußern. Auf der anderen Seite arbeite ich aber auch wirklich hart daran, nicht alles zu kommentieren, schlecht zu machen oder zu entmutigen, denn nur weil ICH etwas nicht kann, nur weil ICH etwas für unwahrscheinlich halte, heißt das ja noch lange nicht, dass eine andere Person, das nicht kann oder möglich machen kann.
Das Gegenteil vom Teenager-Mindset ist ja dieses Behördenklischee: „Das haben wir noch nie so gemacht! Also machen wir es für alle Zeiten auf diese und keine andere Weise!“ (oder fangen gar nicht erst an) und das geht mir sehr auf den Senkel. Wenn man Möglichkeiten auslotet und sich nur Bedenkenträgern gegenüber sieht, die alle Argumente kennen warum etwas NICHT funktioniert.
Vielleicht finde ich das auch so ermüdend weil in mir vieles schon so geschwächt und schlaff ist. Vielleicht habe ich Angst, dass Bedenkenträger meinen letzten Hauch Lebensfrohsinn zertreten und vielleicht finde ich es deswegen so toll mich mit Teenagern zu unterhalten, für die alles lowkey und easy finden.
Ich wünsche mir auch von Herzen innerlich zu spüren „10 km joggen? Das schaffe ich ohne Vorbereitung!“, „Abschlussprüfung Englisch? Da fange ich zwei Tage vorher an zu lernen.“, „Job? Da gehe ich morgen hin und frage“. Wie gut muss sich das bitte anfühlen?
Eines meiner Kinder hört gerne „Muse“ und ich kanns total verstehen. Ich hab mir „Origin of Symmetry“ angehört und es kaum 5 Lieder ausgehalten. Dieser Soundteppich erschlägt mich regelrecht, all die Ebenen und dann all die Gefühle, die die Stimme und die Texte übertragen. Puh. Aber ich bin ja auch schon alt und alles an mir ist ausgedünnt: meine Haut, mein Nervenkostüm, meine Haarzellen im Innenohr.
Ich bin dann froh und traurig gleichzeitig. Froh, weil ich nicht mehr so viele Gefühle haben muss und nicht alles so intensiv sein muss und traurig weil es eben nicht so ist und alles relativ gleichbleibend in ruhigen Gewässern läuft.
Wahrscheinlich ist es für meine Kinder unglaublich nervig, aber ich höre ihnen gerne zu und sie sind ein Faszinosum. Ich bestaune und bewundere sie und ich bin wirklich voller Liebe für das, was sie geworden sind. Es wird mir schwer fallen irgendwann ein Alltagsleben ohne sie zu finden. Denn irgendwann werden sie ja ausziehen und dann lese ich nur noch von Teenagern in Zeitungen und amüsiere mich über den Ideenreichtum und das Selbstbewusstsein. Denn ich würde mir nicht zutrauen einen Bus von Mainz nach Karlsruhe zu fahren.
Der Beitrag Teenager vs. Fast-Boomer erschien zuerst auf Das Nuf Advanced.
It’s easy to be overwhelmed by the world. I mean, look at … everything. Massive ongoing wars everywhere, Fascism on the rise, exploding inequality. Shit is fucked up and more fucked up on a global scale than it ever was in my life time (I was born in 1979). And with the media landscape and notifications and 24 hour news it’s hard to not feel overwhelmed. Every morning when waking up is basically:

And it is important to be informed. To at least try to see what is going on in order to decide where one can make a difference or maybe at least help? Someone? Anyone?
But this is also no way to live. For a bunch of different reasons. I think given the state of the world it’s fair to let certain crises go into the background (without going full ignorance): You just mentally cannot dive into every crisis all the time. Not just because you don’t have the hours in the day but also because it will destroy your mind.
I have this tendency to believe that if I just dig for more information and understand, that if I can make sense of something, I will feel better and it will create some form of path towards resolution. That it would allow me to send a letter to a politician or support an organization or write or do something that can help turn things around. I believe that knowledge and understanding creates agency. Which isn’t 100% false but in the way I apply it is basically delusional.
And I do that because I am scared. I am scared by the consequences of the chaos. I’ve learned enough about history to understand that when shit hits the fan it’s rarely the powerful and wealthy who suffer the most. That it starts hurting at the bottom and then quickly moves up. And that scares me. Not in the abstract but in my bones. Even more now that I have a son who I just want to be able to live a life full of joy and love.
But being scared is not all I feel (even though it is a big part of it). I am grieving.
I realized that a few days ago when I took some time off of the news and all that. I was exhausted and burned out and took a walk. And understood that I was literally grieving. I was sad for the structure of the world that I see crashing down.
And don’t get me wrong. The structure wasn’t perfect. Or even great. We built a world order based on exploitation of the planet and each other. With some good things bolted to it here or there, some remnants of socialist and human rights thinking. Certain safety nets, certain conventions. It wasn’t much, but it was something. And now that they are being dismantled in record time I am grieving for those tiny things.
Because while that system was in place it did – at least to me, and maybe that was naive – feel as if we could use it as a platform to build something better on. Drive back the inequality and exploitation through collective action. The road to “fully automated luxury space communism” was still very long but it felt like there might be a floor to it all. And that floor was still too low and did not include everyone, probably a minority even. But from my privileged position as someone living in Germany it felt like a foundation to build on. A consensus.
And I miss it. It hurts to see it being killed. To see that in fact there is no consensus that includes any commitment – even a surface level one – to human rights and the will to build something better than “billionaires can get even richer while the world is burning”.
This is not a feeling I am planning to dwell on for too long. But I think it’s important that during the storm of news and notifications and whatever we sometimes take the time to understand how that makes us feel and why?
I am grieving because I had felt like there was sort of a “emergency break” kind of thing that would ensure things would be going too bad. And coming from a family where I inherited my parents’ fear of the threat of downwards social mobility that gave me a lot of emotional support. It was about more about a feeling than it was about facts.
It’s important to understand how the world makes you feel. And share it. Otherwise your emotions are gonna catch up with you at some point.
Now is the time to get back to it. Even if the rules-based order that I grew up in and relied on all my life is crumbling, maybe we can redirect that momentum towards something better. Or at least stop some fascists. “Pessimism of the intellect, optimism of the will” and all that.
This cartoon is by me and Becky Hawkins.
TRANSCRIPT OF CARTOON
This cartoon has nine panels, plus a tiny “kicker” panel at the bottom.
PANEL 1
A mother in the middle seat of an airplane is holding her crying baby, while the annoyed women on either side of her offer their advice.
AISLE SEAT LADY: If you let your baby cry in public you’re a bad mother.
WINDOW SEAT LADY: If you quiet them with screen time you’re a bad mother.
PANEL 2
A smiling woman wearing a mint green gi sits crosslegged next to a potted plant, holding a mug of tea. A large picture window faces a natural scene.
WOMAN: Formula is poison! Quit your job and breastfeed at least every two hours or you don’t love your baby.
PANEL 3
A woman in business wear and red glasses raises her hands in a dismissive gesture.
WOMAN: If you quit working, you’ve personally set feminism back forty years. But you do you!
PANEL 4
A middle-aged man is carrying a tall stack of books and pamphlets, so heavy that he’s bent backwards.
MAN: I brought you some light reading about “wake windows” and optimal nap schedules.
PANEL 5
Most of this center panel is taken up by the title: HELPFUL ADVICE FOR NEW MOMS. Below that, a blonde woman in a green jacket smiles.
WOMAN: Trust your instincts! Which are terrible and wrong.
PANEL 6
A mom has her baby in a stroller in a park, and is just kneeling down to put on some socks. A woman behind her turns red and curves over the mom in an impossible arc to get in her face and yell.
WOMAN: Why isn’t your baby wearing SOCKS?!?
PANEL 7
A couple relaxes on a sofa, her head resting on his shoulder. They talk to us, his expression genial, hers angry.
HIM: Co-sleeping is the natural way to teach your baby to sleep!
HER: Until you roll over and smother them, you murderer!
PANEL 8
An older woman leans close to us and holds up a finger as she gives advice.
WOMAN: Wean too soon and he’ll grow up sickly. Wean too late and he’ll grow up weird!
PANEL 9
A large crowd of people, of various ages and ethnicities and fashion choices, speak in unison. Some are angry, some friendly. One is a mother with a baby in a sling.
EVERYBODY: And remember: Whatever happens, it’s your fault!
“KICKER” PANEL AT THE BOTTOM
Barry is talking to a woman who looks absolutely exhausted.
BARRY: Do you know what “catch 22” means?
TIRED WOMAN: Is it minutes of sleep I caught last night?
CHICKEN FAT WATCH
Chicken fat is ancient cartoonist lingo for fun but unimportant little details in the art.
In panel six, the sockless baby is kicking their feet so much that Becky drew the baby with six adorable little feet.
In panel nine, one woman is wearing a T-Shirt design that’s a mix of an anarchy symbol and a cat’s head. That same design showed up as a poster on the wall in a previous Becky cartoon.
Also in panel nine, one man in the crowd carries a “World’s Best Dad” mug, and the baby’s eyes are hilariously wide and shocked-looking.