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.
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:
space. The second target tells us that it needs both other targets, so when you call
make space, make will also execute
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.
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.
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)
pwd shell command will be executed every time we use variable
$(PWD) in the makefile, while
$(MYVAR) will always contain the same value.
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
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.
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.