Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

getting go onto an arm box without dragging a toolchain along

Cross-compiling a small Go service for an ARM single-board computer from a laptop, and the one variable that trips everyone up.

A terminal showing Go build output

I had a small Go service I wanted running on an ARM box in the corner of the room. The obvious approach, building it on the box itself, is fine until you actually try it and remember the thing has 512MB of RAM and the patience of a saint is required while the compiler thrashes swap.

So I built it on my laptop instead. This is the bit Go is genuinely brilliant at, and it took me longer to believe it than to do it.

GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .

That's it. No cross-toolchain to install, no arm-linux-gnueabihf- prefix on anything, no sysroot. scp the binary across, run it, done. After years of fighting C cross-compilation this felt like cheating.

the GOARM trap

The one variable that actually matters here is GOARM, and it's the one that's easy to get wrong. It selects the floating-point ABI. Set it too high for your hardware and you get a binary that builds cleanly, copies across happily, and then dies on the box with illegal instruction. No useful error, just SIGILL and a core dump.

The rule of thumb:

  • GOARM=5 is soft-float, works on basically anything including old ARMv5 kit
  • GOARM=6 for ARMv6, which is what an original Raspberry Pi wants
  • GOARM=7 for ARMv7 with hardware VFP, which covers most of the Pi 2 / Pi 3 / BeagleBone era boards

If you're not sure what's in the box, cat /proc/cpuinfo on it and look at the model name and the CPU architecture line. When in doubt I drop to GOARM=6, lose a fraction of floating-point speed I almost never use, and stop having to think about it.

A single-board computer on a desk

the cgo asterisk

The clean story above holds for as long as you stay in pure Go. The moment you pull in a package that uses cgo, say a SQLite driver that wraps the C library, the magic evaporates. Now you do need a cross C compiler, you need CGO_ENABLED=1, and you need to point CC at the right thing:

CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc \
  GOOS=linux GOARCH=arm GOARM=7 go build -o myservice .

At that point you're back in the world of matching libc versions and hoping the target's shared libraries line up. My advice, learned the tedious way: avoid cgo entirely if you possibly can for these little boxes. A pure-Go SQLite or a pure-Go anything is worth a small performance hit when the payoff is a single static binary you can copy anywhere without a second thought.

For my service the dependency tree was clean, so the one-liner did the whole job. Binary across, systemd unit written, and it's been quietly serving since. The best deployments are the boring ones.