Programming

Improving dev environments: X-compiling Go on Docker

March 23, 2016
--
User avatar
Adrian Perez
@blackxored

A bit of backstory

I've been pretty invested in the Docker ecosystem for quite a while now, evangelizing about the greatest of its advantages, dealing with the quirks in production, and closely following every release and discussion.

One of the main benefits Docker has brought to my workflow is that I seldom install software directly in my computer anymore. Not only that, uninstallation and cleanup have become something that's a breeze, allowing my host OS (OS X if you're interested) by extension to function even better, be less polluted, and use less storage.

Take the last example, while might not the best, it's the most recent for sure: I was watching Ultra on Twitch, and unfortunately the playback performance was horrible on my browser (besides that the chat going at the speed of light couldn't possibly be helping) to the point I couldn't really enjoy it. I knew about this software I used back in my Linux days called LiveStreamer which lets you connect to streams directly from your favorite video player. The software is written in Python, and since I haven't been developing in it for ages I had nothing more that OS X's default Python with no package manager, and I really didn't want to go through installing python, choosing the right version, etc. for just this one time thing. The quick and pain-free solution was, as usual, in the land of Docker; while I had to go through the trouble of creating an image for it, in most cases you would find an existing image already built and maintained. 5 minutes later I was back again watching without no interruptions and no playback issues (and perhaps even as important, no aneurysm-inducing "dance" ASCII-art on a chat).

While Docker has been great for me as a software user, and also for shipping to production, I wasn't quite satisfied with the development workflow I was currently having, to the point I wasn't quite happy with it, as I tweeted a few months back:

I was still using `<INSERTLANGUAGE>vm`_ (as in language version managers, rbenv, gvm, nodenv, you name it), and by extension installing those languages, reverting back to "host-mode" when I encountered one of those hard-to-debug problems. If you're curious, this is the prompt I was referring to at the time:

https://d.pr/i/cXDd

I set myself on a path to improve on this situation, with the idea to go the extremes and then dial back as needed. The result of my findings on how I managed to improve my OS, workflows and tooling will be covered in a future post, but I hope this bit of backstory has set something in motion and you realize one of main the reasons you would want to cross-compile Go inside Docker, without dealing with installation, version managers, $GOPATH et. al; hence welcome to the main topic of this post, which would not only benefit you as a random user in need of a random project (the example I'll cover), but also as a software author in the effort to streamline and automate builds.

The software for this example

If you follow the Node community, you've probably read this post by Azer on un-publishing his NPM modules. If you're not, don't worry, what's relevant to this tutorial is that reading through the comments of that article I stumbled upon a package tool written in Go called Gx that I believed it was promising and worth trying; we're going to use it for this example.

I'm on OS X running Docker through Docker Machine, hence I'd technically be compiling from a Linux OS (inside a VM) to produce a binary that will run in my host, with the help of the Go compiler, the official Go Docker image, and a bit of shell scripting. We're not only going to build for OSX, but I'll demonstrate how to build for other platforms as well.

Setting the project

Let's clone the project to get started:

$ git clone git://github.com/whyrusleeping/gx && cd gx

In order to keep things dynamic, I'll create an slightly more advanced script that will take the input from environment variables, with sane defaults, in order to choose the right target(s) for the build process, fetch dependencies, etc. Let's call it build.sh

# Cleanup
rm -rf build && mkdir build

# go-wrapper is provided by the official Go Docker image
# invokes go get, etc...
go-wrapper download
go-wrapper install

# Cross-compile for every OS and architecture that's passed
# defaulting to OSX
for GOOS in ${GOOS_LIST:-darwin}; do
  for GOARCH in ${GOARCH_LIST:-amd64}; do
    echo "Building for $GOOS-$GOARCH..."
    GOOS=$GOOS GOARCH=$GOARCH go build -v -o build/app-$GOOS-$GOARCH
  done
done

Now that we have our build script inside the directory, we can build for my operating system by simply invoking:

$ docker run --rm -it -v "$PWD":/usr/src/app -w /usr/src/app golang bash ./build.sh

Cross-compiling (aka X-Compiling)

Now we're left with an OS X binary inside the build directory that we can just move to /usr/local/bin or anywhere within our $PATH. Please notice that in projects that embrace Docker, they would provide a more specific version of a script like this as a step in a Dockerfile, in addition to moving the resulting binary to the appropriate location which would have been indicated to volume-mount. Yes, Docker can be only a binary copy channel too.

What if you wanted to compile for both OSX and Linux? It's easy with the environment variable mappings we've created:

$ docker run --rm -it \
  -e GOOS_LIST="darwin linux" \
  -v "$PWD":/usr/src/app \
  -w /usr/src/app \
  golang bash ./build.sh

We can now confirm that we indeed compiled for the right targets manually, or also with another bit of shell wizard-y:

$ for file in build/*; do echo "$file: $(file $file)"; done
build/app-darwin-amd64: build/app-darwin-amd64: Mach-O 64-bit executable x86_64
build/app-linux-amd64: build/app-linux-amd64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

Voila! Now we have our binaries and we can move them to the appropriate location or even machine.

I hope you enjoyed this post, and if you want to remember the extremes I alleged, on my OS shell:

$ go
zsh: command not found: go

Stay tuned for more articles on improving workflow aided by Docker.

~ EOF ~

Craftmanship Journey
λ
Software Engineering Blog

Stay in Touch


© 2020 Adrian Perez