Embedding Git Commit Information in Go Binaries

by | May 25, 2022

It is always helpful to know which version you are running of some software, even more so when running development versions. So it’s good if software is able to tell you its version, for example by calling it with --version. For release versions, maintaining this information by hand is feasible, but in order for development versions to show exact revision information, you need some automation, otherwise updating it will be forgotten, leaving you with wrong information.

This blog post shows different techniques that can be used to embed a Git commit hash into programs written in the Go programming language. Finally, it will also show a Git feature that is useful for other languages as well.

Linker Flags

One option is to set variables using linker flags. Just define a string variable somewhere in the code like this:

var Commit string

This variable can then be set to a specific value by specifying extra linker flags:

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

Note that main specifies the package name that contains the variable, so if you want to set a variable in a different package, you have to specify the import path of that package instead.

go generate

Another option is to use go generate, which allow you to run arbitrary commands to update auto-generated files. This can be used to generate Go source code later included in the build, or, starting from Go 1.16, you can also write arbitrary files that can then be embedded into the binary using the embed package. So consider the following Go code:

//go:generate sh -c "printf %s $(git rev-parse HEAD) > commit.txt"
//go:embed commit.txt
var Commit string

The //go:generate directive instructs Go to run a command that writes the current commit hash to a file called commit.txt (the extra printf %s is only there to strip the trailing newline character, this could also be done by stripping it when using the variable) and the //go:embed directive instructs it to set the variable Commit to the contents of this file.

However, these commands are not run automatically, so to build the binary, you now have to invoke two commands:

go generate
go build

But if you happen to use go generate anyways to generate other files, this gives you a nice method of adding Git version information basically for free. Also keep in mind that you have to run this every time, otherwise you might end up using a file from a previous build with outdated information.

Build Information (Go 1.18+)

Go 1.18 added a nice feature: when building a Go project from within a Git checkout, some commit information is added to the binary automatically when running go build without requiring any additional flags (but you can control the behavior in more detail with the -buildvcs flag if you want to). This makes it a very nice solution as it requires no additional build scripts that supply the additional information.

This information can be read from the Go code like this:

import "runtime/debug"

var Commit = func() string {
    if info, ok := debug.ReadBuildInfo(); ok {
        for _, setting := range info.Settings {
            if setting.Key == "vcs.revision" {
                return setting.Value
            }
        }
    }

    return ""
}()

The amount of information available is quite limited though. In case of Git, the following attributes are available besides vcs.revision:

  • vcs.time: Timestamp of the commit.
  • vcs.modified: Set to true (as a string) if the binary was built from a working directory containing uncommitted changes.

git archive

For situations where you’re not building from a Git checkout, there is a convenient Git feature that allows you to substitute placeholders in files when exporting the repository, for example as a .tar.gz file. The placeholders look like this:

const Commit = "$Format:%H$"

This variable will be set to the full commit hash, for a list of available placeholders, you can check the git-log(1) man page. This replacement is not done automatically, but only on files that have the attribute export-subst set in .gitattributes (next to .gitignore) like this:

/commit.go export-subst

When this repository is now exported using git archive, the resulting file looks something like this:

$ git archive HEAD | tar -x --to-stdout commit.go  
package main

const Commit = "31fbe21f80912bb2040027c3239ce6c86f7e109c"

Of course, this substitution only takes place when exporting an archive, which also includes downloads from GitHub. In all other cases, the variable still includes the template string, so any code using such a variable should be aware that this can happen, check if the replacement happened, and treat the commit information as unavailable otherwise.

If you want to see a full working example combining the last two techniques, you can have a look at this pull request for Icinga DB which among other things adds Git commit information to the output of icingadb --version.

 

You May Also Like…

Icinga 2 API and debug console

Icinga 2 API and debug console

Have you ever experienced configuration issues, such as notifications not being sent as expected or apply rules not...

Subscribe to our Newsletter

A monthly digest of the latest Icinga news, releases, articles and community topics.