Jiacai Liu's personal website

Embed git commit in Zig programs

  tags: zig

Table of Contents

Whether you are writing a fancy database, or simple CLI program, it's always helpful to embed git commit into the binary.

With it, you can know which exact lines of code is to be blamed when users throw you an unhappy backtrace.

Previously I have done similar thing in Go, and it's fairly easy:

go build -ldflags="-X main.Commit=$(git rev-parse HEAD)"

And in you main.go, you have something like

var Commit string

How to do this in Zig? The answer is Step/Options.zig.

In case you don't know how Zig build works, read Zig Build System first.

Options is a builtin build step to generate module at compile time. The main API to define variable is addOption, and after define variables you want to embed, use createModule to create a module for project to use. Take one example:

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    var opt = b.addOptions();
    opt.addOption([]const u8, "git_commit", "HEAD");
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = .{},
        .optimize = .Debug,
    });
    exe.addModule("build_info", opt.createModule());

    b.installArtifact(exe);
}

// src/main.zig
const std = @import("std");
const build_info = @import("build_info");

pub fn main() !void {
    std.debug.print("Commit: {s}.", .{build_info.git_commit});
}

Then build and run, you will get

Commit: HEAD.

Under the hood, Options step will create a Zig source file in zig-cache directory, which contains variables you defined via addOption.

$ rg git_commit
src/main.zig
5:    std.debug.print("Commit: {s}.", .{build_info.git_commit});

build.zig
5:    opt.addOption([]const u8, "git_commit", "HEAD");

zig-cache/c/165762754dd5d4be37683bba59869ce3/options.zig
1:pub const git_commit: []const u8 = "HEAD";

After understand how Options step works, the last task to complete our target is to replace hard-coded HEAD with real commit id, we have two options here:

  1. Shell out to git via exec family API, and parse its stdout
  2. Like how -Dtarget works, add a new option to pass git commit into build.zig

Since solution 2 is both easy and OS-independent, I will only introduce how to do it:

     var opt = b.addOptions();
-    opt.addOption([]const u8, "git_commit", "HEAD");
+    opt.addOption(
+        []const u8,
+        "git_commit",
+        b.option([]const u8, "git_commit", "Current git commit") orelse "Unknown",
+    );

Then build and run again:

$ zig build -Dgit_commit=$(git rev-parse HEAD)

$ ./zig-out/bin/hello
Commit: 6461dee5fd4337ceb79b3f5e5644685d5324533c.

That's it, git commit is embed in our programs!

PS: When build in GitHub Action, we can use builtin env vars directly: zig build -Dgit_commit=${{ github.sha }}.