Crosscompiling Go applications with Make
This article hasn't been updated for over a year.
When a new version of one of my Go apps is ready, I usually want to build and package the binary for multiple platforms. This can easily be done in a shell script, but Make brings a bonus: it can run multiple builds at the same time.
Makefile 101
In case you’ve worked with make before, skip this section! If not: let’s see what a basic makefile looks like.
Make defines targets, these are things that need to be done. Usually a target represents a file that has to be created. Every target contains a list of commands that have to be executed, we call that the recipe. Make will only execute the recipe if a file with the same name as the target does not exist. If your target does not represent a file, it’s a phony target.
mytarget:
echo Hello world.
space: mytarget foo
echo "I'm in space."
foo:
echo I am run only once
touch foo
.PHONY: mytarget space
The above makefile contains 3 targets: mytarget
, foo
and space
. The second target tells us that it needs both other targets, so when you call make space
, make will also execute mytarget
and foo
.
At the bottom of the file we define a special .PHONY target: Here we simply tell make which targets are phony targets. Those do not represent a file on disk. As we usually don’t compile separate Go files, most of our targets will be phony targets.
Let’s see what this does: save the file as Makefile
and run make space
in the same directory:
$ make space
echo Hello world.
Hello world.
echo I am run only once
I am run only once
echo "I'm in space."
I'm in space.
Notice that make always tells us what command it is executing. You can hide a command by prefixing it with @. As we’ll be executing multiple targets concurrently, it may be useful to see which commands are running.
Run make space
again. You’ll see that the foo
target is not run anymore. That’s because we created a file named foo and foo
is not a phony target.
Variables
GNU Make has two types of variables. When we assign to a variable with :=
the contents is evaluated at the time the variable is declared. If we use just =
the contents is evaluated every time we use the variable.
MYVAR := Hello there! I am currently in $(shell pwd)
PWD = $(shell pwd)
The pwd
shell command will be executed every time we use variable $(PWD)
in the makefile, while $(MYVAR)
will always contain the same value.
Cross compiling
Make supports running multiple targets at the same time. This means that we need a separate target for every GOOS and GOARCH in order to take advantage of the concurrency.
A simple way to do this would be:
linux:
GOOS=linux GOARCH=amd64 go build ...
windows:
GOOS=windows GOARCH=amd64 go build ...
release: windows linux
.PHONY release windows linux
This builds both linux and windows binaries when we call make release
. We can improve this Makefile by looping over the platforms and architectures we want to build:
PLATFORMS := linux/amd64 windows/amd64
release: $(PLATFORMS)
$(PLATFORMS):
echo Hello, I am $@
.PHONY release $(PLATFORMS)
What we do here is create a target that contains all possible platforms. Every platform is a separate target, just like it was in the previous Makefile. That also means that we have to add every platform to the phony list. Luckily, we can do that using the same variable. Inside the recipe we can use the special $@
that contains the name of the current target.
The next part is a bit harder, we need to separate OS and ARCH. Remember that variables can be evaluated at runtime? We’ll use that feature to create an $(os)
and $(arch)
var.
temp = $(subst /, ,$@)
os = $(word 1, $(temp))
arch = $(word 2, $(temp))
String manipulation functions in make are limited, so we have to be a bit creative here. We’ll use these variables inside the $(PLATFORM)
target. Remember that $@
contains the name of the current target? When we replace the slash in this string we can use the word
function to get the first and last part.
Combined with our previous Makefile, this becomes:
PLATFORMS := linux/amd64 windows/amd64
temp = $(subst /, ,$@)
os = $(word 1, $(temp))
arch = $(word 2, $(temp))
release: $(PLATFORMS)
$(PLATFORMS):
GOOS=$(os) GOARCH=$(arch) go build -o '$(os)-$(arch)' mypackage
.PHONY release $(PLATFORMS)
Note that variables can only be set outside the recipe. As the contents is only evaluated when we use them, it still contains the correct value.
Concurrency
We now have a Makefile that creates binaries for the platforms we listed in the $(PLATFORMS)
variable. When we call make release
it builds everything we asked, one by one. The main reason to use make is the easy concurrency: simply call make with the -j n
argument, where n is the number of simultaneous jobs.