Pragmatism in the real world

Building Go binaries for different platforms

One nice thing about Go is that it can cross-compile which allows me to use my Mac to build binaries for different operating systems. This is increibly useful for providing binaries for Rodeo, my command line Flickr uploader.

To do this we set the GOOS and GOARCH environment variables before calling go build. For example to build for x64 on Linux, I do:

version=`git describe --tags HEAD`
env GOOS=linux GOARCH=amd64 go build \
      -ldflags "-X github.com/akrabat/rodeo/commands.Version=$version" \
      -o release/rodeo-linux-amd64

The -o parameter allows setting the filename of the built binary which is particularly useful when creating binaries that don’t work on your own system.

There’s a lot of choice for GOOS and GOARCH, but not all combinations are valid. Fortunately, the list of valid combinations is documented in. the environment variables section of the Go docs.

Scripting the build

As we want to create multiple binaries, we can build a Bash script that does the work for us. We start with a list of platforms and iterate:

version=`git describe --tags HEAD`

platforms=(
"darwin/amd64"
"darwin/arm64"
"linux/amd64"
"linux/arm"
"linux/arm64"
"windows/amd64"
)

for platform in "${platforms[@]}"
do
	# do work
done

We use an array to hold the list of platforms and then loop using a for loop. If you’re new to arrays in Bash, then Robert Aboukhalil’s introductory article is a good place to start.

First thing we do in the loop is work out GOOS, GOARCH, the filename of the binary:

    platform_split=(${platform//\// })
    GOOS=${platform_split[0]}
    GOARCH=${platform_split[1]}

    os=$GOOS
    if [ $os = "darwin" ]; then
        os="macOS"
    fi

    output_name="rodeo-${version}-${os}-${GOARCH}"
    if [ $os = "windows" ]; then
        output_name+='.exe'
    fi

There are a couple of operating system dependent wrinkles here. Firstly, Go uses “darwin” as the GOOS name for macOS. This makes sense as Darwin is the core Unix-like OS at the heart of macOS, iOS, etc. For the rodeo filename, I want “macOS” not “darwin” though as that’s what my users would expect.

Similarly, a binary on Windows has an extension of “.exe” whereas Linux and Mac do not. Hence, for Windows, we append “.exe” so that it matches the expected conventions.

Now we can build the binary:

    echo "Building release/$output_name..."
    env GOOS=$GOOS GOARCH=$GOARCH go build \
      -ldflags "-X github.com/akrabat/rodeo/commands.Version=$version" \
      -o release/$output_name
    if [ $? -ne 0 ]; then
        echo 'An error has occurred! Aborting.'
        exit 1
    fi

We use the ldflags paramter to set the version number of the binary to the correct version based on the git tag as I wrote about before in Setting the version of a Go application when building.

We also check the returned status code from go build which is held in $?. As a good command line citizen, go build returns 0 success and non-zero on failure.

Lastly, we will compress the binary which for Rodeo, roughtly halves the size for download:

    zip_name="rodeo-${version}-${os}-${GOARCH}"
    pushd release > /dev/null
    if [ $os = "windows" ]; then
        zip $zip_name.zip $output_name
        rm $output_name
    else
        chmod a+x $output_name
        gzip $output_name
    fi
    popd > /dev/null

Again, we have a platform-specific wrinkle; the compression file format used on Windows is ZIP, but on Linux and macOS it is gzip.

It’s easier to use zip and gzip from within the directory where the file you’re compressing is in order to avoid issues with paths, so we use pushd and popd to change directory in and out of the releases directory.

and done

We now have a set of files ready to distribute.