Building Golang CLI Tools Update

In my previous post I discussed how to use a Makefile to set version and build information at compile time. Although this approach may work fine for you, it has three drawbacks I want to discuss.

1. Simplicity

Andrew responded on the golang-nuts mailing list with the following comment:

To me it seems like you took something simple and cross platform “go generate” + “go install/build” and turned it into something more complicated and less portable.

Although I’m not sure go generate is relevant in this case, I agree that on some level a Makefile is complicating things unnecessarily. Let’s remove it!

2. Non-reproducable builds

Guilio responded with:

I only have an issue with buildTime: it makes the build not reproducible.

This is a valid point. The idea that if you compile a given version of your application, the resulting binary’s hash (be it MD5 or whatever) should be equal to that of any other binary build from that specific version.

By using BuildTime the binary is never the same.

Build time is also irrelevant. It does not matter when a binary was compiled, but it does matter which precise version was build.

Let’s replace BuildTime with the current git commit hash instead.

3. Why is there a VERSION file?

If you’re going to store version information under version control, you might as well put it right in the code, where it belongs.

Let’s remove VERSION and instead create a nice version.go to handle everything.

Let’s refactor

Okay, first things to go are Makefile and VERSION.

Next, I created a core/version.go which contains the necessary version information. I’ve also taken the liberty of creating a proper struct for the version information, including major, minor and patch numbers, as well as a label and release name.

package core

import "fmt"

type version struct {
    Major, Minor, Patch int
    Label               string
    Name                string
}

var Version = version{1, 2, 3, "dev", "Chuck Norris"}

var Build string

func (v version) String() string {
    if v.Label != "" {
        return fmt.Sprintf("Roll version %d.%d.%d-%s \"%s\"\nGit commit hash: %s", v.Major, v.Minor, v.Patch, v.Label, v.Name, Build)
    } else {
        return fmt.Sprintf("Roll version %d.%d.%d \"%s\"\nGit commit hash: %s", v.Major, v.Minor, v.Patch, v.Name, Build)
    }
}

As you can see, it’s quite easy to set and update the version numbers, label and release name.

Build is still set at compile time and contains the current git commit hash.

Because the go build command is quite long, I’ve put it in a nice build.sh file that makes building easier.

go build -ldflags "-X github.com/ariejan/roll/core.Build=`git rev-parse HEAD`" -o roll main.go

This will result in a build that reports version information like this:

$ ./roll version
Roll version 1.2.3-dev "Chuck Norris"
Git commit hash: b72b076af8b18ef4f6b10296f12840f23258acec

Check that SHA

If you want, you can grab the code and run ./build.sh yourself. The resulting binary has a SHA-1 of 3ad7509279690d99e4144332dc200ede732663fd. Yay for reproducable builds!

Naming variables in ldlags

A short note on Peter Kleiweg’s comment. He pointed out that I could use

"-X main.Build=`git rev-parse HEAD`"

This would be true if the Build variable is in the main package. But because it’s not (it’s in core) I have to specify the full package name.

Thank you!

Thanks to all the awesome gophers responding to my previous post! It’s great to get feedback and get to learn more about Golang. Keep the comments coming, please!