Demonstration for creating a go project that builds on NixOS that has correct embedded VCS information in the built output
  • Nix 66.5%
  • Go 33.5%
Find a file
2026-05-19 17:11:01 +00:00
.gitignore initial: go-vcs with flake 2026-05-19 17:04:38 +00:00
flake.lock add flake.lock 2026-05-19 17:06:20 +00:00
flake.nix fix version format 2026-05-19 17:06:03 +00:00
go.mod initial: go-vcs with flake 2026-05-19 17:04:38 +00:00
main.go initial: go-vcs with flake 2026-05-19 17:04:38 +00:00
README.md add README 2026-05-19 17:11:01 +00:00

go-vcs

A minimal Go project demonstrating how to embed VCS metadata (commit hash, build date, version) into a Go binary using Nix flakes and -ldflags.

Quick start

nix build --print-build-logs

The build log will print:

=== Embedded VCS Data ===
Version:    2026.05.19-df7f7b9
Commit:     df7f7b94702c4d70a6aac7e82791e1ccb19a90f0
BuildDate:  20260519170620

=== Go Build Info ===
Go Version: go1.26.2

Project structure

File Purpose
main.go Go binary with Version / Commit / BuildDate vars populated via -ldflags
flake.nix Nix flake that builds with buildGoModule, injecting VCS data into ldflags
go.mod Minimal Go module declaration

How it works

1. Go side — linker variables

main.go declares three package-level variables with safe defaults:

var (
    Version   = "dev"
    Commit    = "unknown"
    BuildDate = "unknown"
)

At compile time, the Go linker overwrites these via -ldflags -X:

-X main.Version=v1.2.3
-X main.Commit=abc1234
-X main.BuildDate=2026-05-19

The binary also calls runtime/debug.ReadBuildInfo() to show Go's own embedded VCS settings for comparison.

2. Nix side — extracting git metadata

flake.nix reads three values from self.sourceInfo, which is populated automatically when the flake lives inside a git checkout:

Nix variable Source Example output
gitShortRev self.sourceInfo.shortRev df7f7b9
gitRev self.sourceInfo.rev df7f7b94702c4d70a6aac7e82791e1ccb19a90f0
gitDate self.sourceInfo.lastModifiedDate 20260519170620

The version string is derived by combining the date (parsed from YYYYMMDDHHMMSS into YYYY.MM.DD) with the short revision:

packageVersion =
  let
    y = builtins.substring 0 4 gitDate;
    m = builtins.substring 4 2 gitDate;
    d = builtins.substring 6 2 gitDate;
  in "${y}.${m}.${d}-${gitShortRev}";

These are passed to buildGoModule as ldflags:

packages.default = pkgs.buildGoModule {
  pname   = "go-vcs";
  version = packageVersion;
  src     = ./.;
  ldflags = [
    "-s" "-w"                                    # strip debug info
    "-X main.Version=${packageVersion}"
    "-X main.Commit=${gitRev}"
    "-X main.BuildDate=${gitDate}"
  ];
  # ...
};

3. Verifying at build time

doInstallCheck = true runs the built binary during the build:

doInstallCheck = true;
installCheckPhase = ''
  echo "--- Running go-vcs binary to verify embedded VCS data ---"
  $out/bin/go-vcs
'';

This means nix build --print-build-logs prints the embedded VCS data directly in the build log — no need to run the binary separately.

Important notes

Clean git tree required

self.sourceInfo only returns real values when the flake is evaluated from a clean git tree. If you have uncommitted changes, you'll see:

Version:    yyyy.mm.dd-dirty
Commit:     dirty
BuildDate:  yyyymmddhhmmss

Always commit (and add flake.lock) before building.

Zero dependencies

This example uses zero third-party Go dependencies, so vendorHash = null with proxyVendor = true. For real projects with dependencies, you'll need to provide a real vendor hash (and update it whenever go.mod/go.sum changes).

The values are baked in at compile time

Because the values come from -ldflags, they become static strings in the ELF binary. They survive stripping (-s -w) and don't depend on the git repo being present at runtime. This is different from Go 1.18+'s vcs.revision / vcs.time build settings, which are only available when the binary is built inside a git worktree.

Adapting for your own project

  1. Copy the ldflags pattern from flake.nix.
  2. In your Go code, declare the same package-level vars (Version, Commit, BuildDate).
  3. Replace vendorHash = null with a real hash from your go.sum.
  4. Make sure your repo is clean before running nix build.
  5. The installCheckPhase is optional — it's just a convenient way to see the VCS data during the build.