#development #golang #mac

Since Apple introduced the move to their own processor, called the M1 which is based on the ARM architecture, I wanted to figure out what this means to Go.

Let's see how easy (or difficult) it is to compile a Go program into an executable which runs native on Intel macs and on the new Apple Silicon macs.

I'm testing this on an Intel mac running macOS Big Sur. Running the build on an Apple Silicon mac is more adventurous as tools like homebrew arent't fully supported yet. There are ways to get it working as described by Sam Soffes in his blog post.

Let's use the most basic Go program we can imagine:

main.go

1package main
2
3import (
4    "fmt"
5)
6
7func main() {
8    fmt.Println("hello world")
9}

The first step is to install a version of Golang which supports compiling for the ARM processor. At this moment, the easiest way is to install the development version of Go using gotip:

 1$ go get golang.org/dl/gotip
 2$ gotip download
 3Updating the go development tree...
 4From https://go.googlesource.com/go
 5 * branch            master     -> FETCH_HEAD
 6HEAD is now at e5da18d os/exec: constrain thread usage in leaked descriptor test on illumos
 7Building Go cmd/dist using /usr/local/Cellar/go/1.15.5/libexec. (go1.15.5 darwin/amd64)
 8Building Go toolchain1 using /usr/local/Cellar/go/1.15.5/libexec.
 9Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
10Building Go toolchain2 using go_bootstrap and Go toolchain1.
11Building Go toolchain3 using go_bootstrap and Go toolchain2.
12Building packages and commands for darwin/amd64.
13---
14Installed Go for darwin/amd64 in /Users/pclaerhout/sdk/gotip
15Installed commands in /Users/admin/sdk/gotip/bin
16Success. You may now run 'gotip'!

Compiling for an Intel mac is really easy, we can just run :

1$ go build -o hello-world-x86 main.go
2$ file hello-world-x86
3hello-world-x86: Mach-O 64-bit executable x86_64

Now, let's compile for an Apple Silicon mac by using cross compilation:

1$ GOOS=darwin GOARCH=arm64 go build -o hello-world-arm main.go
2# command-line-arguments
3/usr/local/Cellar/go/1.15.5/libexec/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
4ld: warning: ignoring file /var/folders/w3/x7jg17fj0099ppnd7qh25g1w0000gq/T/go-link-431194554/go.o, building for macOS-x86_64 but attempting to link with file built for unknown-arm64
5Undefined symbols for architecture x86_64:
6  "_main", referenced from:
7     implicit entry/start for main executable
8ld: symbol(s) not found for architecture x86_64
9clang: error: linker command failed with exit code 1 (use -v to see invocation)

This error was expected. The current stable version of Go, version 1.15.5 doesn't support compiling for Apple Silicon. The development version we installed via gotip does which means we need to compile using gotip:

$ GOOS=darwin GOARCH=arm64 gotip build -o hello-world-arm main.go
$ file hello-world-arm
hello-world-arm: Mach-O 64-bit executable arm64

That gives us an Apple Silicon native executable. One annoyance is that we now how a separate executable for x86 vs arm64. The x86 version runs on Intel macs and on Apple Silicon macs via the Rosetta 2 layer. The arm version only runs on Apple Silicon mac.

Luckily, there is a solution for that called "Universal macOS binaries". Let's go ahead and build one. To avoid surprises, we will build both versions using gotip so that they are compiled with the same compiler version.

1$ GOOS=darwin GOARCH=amd64 gotip build -o hello-world-x86 main.go
2$ GOOS=darwin GOARCH=arm64 go build -o hello-world-arm main.go
3$ lipo -create -output hello-world hello-world-x86 hello-world-arm
4$ file hello-world
5hello-world: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
6hello-world (for architecture x86_64):    Mach-O 64-bit executable x86_64
7hello-world (for architecture arm64):    Mach-O 64-bit executable arm64

This results in a single binary which contains the x86 and the arm64 versions. Depending on which type of mac you run it, it will choose the most appropriate version.

There is one last thing we need to take care of. When you move the binary to a different machine and launch it, you might get this as the result:

1$ ./hello-world
2zsh: killed     ./hello-world

Checking the macOS console log reveals:

1$ log stream | grep -i "hello-world"
22020-11-30 15:26:12.744460+0100 0x17efc1   Default     0x0                  0      0    kernel: (AppleSystemPolicy) ASP: Security policy would not allow process: 26315, /Users/admin/Desktop/hello-world

So, macOS refused to run our binary because of security policy violations. The reason for this is that our binary is not code signed. This is required on Apple Silicon macs and is one of those small differences compared to Intel mac.

To code sign, we need to know our Developer ID. Via terminal, you can use the security command to find this out:

1$ security find-identity -v -p codesigning | grep -i "developer id application"
2  1) 9E1528839ADF390BAB7FC11A45TDZSC308265B78 "Developer ID Application: Pieter Claerhout (J5ZM7SDBSA)"

You can use either the UUID or the full name to do the code signing. I prefer the UUID as it's shorter and guaranteed to be unique.

1$ /usr/bin/codesign --force --sign 9E1528839ADF390BAB7FC11A45TDZSC308265B78 hello-world

When you test the code-signed utility, you'll now see the expected result:

1$ ./hello-world
2hello-world

With the next major release of Go, version 1.16, Apple Silicon will be supported out-of-the-box with support for cgo, internal and external linking.

If you need more developer tips on Apple Silicon, this document is worth reading…