Make and Go for Fun and Profit
I’ve been somewhat interested in Go for quite a while now. It’s gotten to the point where it has replaced Ruby for me in those places where I write command line utilities which are too involved for them to make sense to be a shell script. I don’t have too many opinions about the language itself, but I like the static type system and that it’s a compiled language. And to be honest, the build system and how to utilize it have been the most interesting bits for me so far. One thing I especially like is the fact that go provides a bunch of tooling to do different things but how you tie them together is up to you. So this gives rise to some fun use cases for a nice Makefile.
The Basics
Every project I start gets this Makefile with some basic setups and variable definitions that I always want.
export GO15VENDOREXPERIMENT = 1
# variable definitions
NAME := coolthings
DESC := a nice toolkit of helpful things
PREFIX ?= usr/local
VERSION := $(shell git describe --tags --always --dirty)
GOVERSION := $(shell go version)
BUILDTIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
BUILDDATE := $(shell date -u +"%B %d, %Y")
BUILDER := $(shell echo "`git config user.name` <`git config user.email`>")
PKG_RELEASE ?= 1
PROJECT_URL := "https://github.com/mrtazz/$(NAME)"
LDFLAGS := -X 'main.version=$(VERSION)' \
-X 'main.buildTime=$(BUILDTIME)' \
-X 'main.builder=$(BUILDER)' \
-X 'main.goversion=$(GOVERSION)'
For the most part this just defines a whole bunch of meta data that gets compiled into the binaries via linker flags. This is a pattern I have seen in a lot of Go projects and I really like that this is somewhat of a standard thing to do. Especially with the static nature of Go binaries, the more helpful information you can compile into the binary the better it is when you have to figure out where a binary comes from.
I also always have a handful of tasks defined that are helpful for running tests and such, especially to have a uniform and documented way how they are run locally and on CI.
# development tasks
test:
go test $$(go list ./... | grep -v /vendor/ | grep -v /cmd/)
PACKAGES := $(shell find ./* -type d | grep -v vendor)
coverage:
@echo "mode: set" > cover.out
@for package in $(PACKAGES); do \
if ls $${package}/*.go &> /dev/null; then \
go test -coverprofile=$${package}/profile.out $${package}; fi; \
if test -f $${package}/profile.out; then \
cat $${package}/profile.out | grep -v "mode: set" >> cover.out; fi; \
done
@-go tool cover -html=cover.out -o cover.html
benchmark:
@echo "Running tests..."
@go test -bench=. $$(go list ./... | grep -v /vendor/ | grep -v /cmd/)
These make heavy use of go list
to determine existing packages to run tests
for. The rules also exclude the vendor folder as I don’t want to run those
tests and the cmd folder which I will describe more in the next section.
Structure for multiple binaries
Go has this defacto standard of how to structure code if your build produces
multiple executables. Since your main entry point in the app is always the
main package and there can only be one per directory (which is also true for
any other package btw) you need to separate different executables by
directory. The pattern here is basically to have a cmd
folder that contains
subfolders for each executable which in turn just contain a main.go
file.
This is a pretty nice pattern, once you get used to it and is a convention
that lets you easily create make rules for building those executables via the
make wildcarding support.
CMD_SOURCES := $(shell find cmd -name main.go)
TARGETS := $(patsubst cmd/%/main.go,%,$(CMD_SOURCES))
%: cmd/%/main.go
go build -ldflags "$(LDFLAGS)" -o $@ $<
This piece just finds all main.go
files under the cmd folder and creates
targets from them located at the top level of the repo. Then there is a rule
to build those targets via a rule that ties them back to the source file via
wildcarding again and runs go build
with the linker flags from before.
Of course it’s good habit to provide man pages for your tools. So we can rig up a similar set of rules for building man pages for each executable:
MAN_SOURCES := $(shell find man -name "*.md")
MAN_TARGETS := $(patsubst man/man1/%.md,%,$(MAN_SOURCES))
%.1: man/man1/%.1.md
sed "s/REPLACE_DATE/$(BUILDDATE)/" $< | pandoc -s -t man -o $@
all: $(TARGETS) $(MAN_TARGETS)
.DEFAULT_GOAL:=all
This lets us write man pages in markdown under the man/man1/
folder named as
${cmd}.1.md
and again uses wildcards in make to generate them top level via
an implicit rule. I also added an all
target there which is the default and
builds all binaries and man pages.
Over time I’ve come to the conclusion that it’s really a good practice to have
your main.go
files be as slim as possible. Ideally all they should be
concerned with is flag parsing, calling a method from your library packages,
and formatting and printing the output to the terminal. Any actual logic
should live in library modules somewhere else in your repo. This maintains a
good code layout to extend, makes sure code is reusable, and provides good
conventions for testing.
Installation
So now that we have rules to build the binaries, we also want to be able to
install them to the PREFIX
we have defined at the top. Go comes with an
install command already (go install
) which will put binaries in your
$GOPATH/bin
but there is no need to have to rely on that. Plus on a multi
user system you want to provide tools for everyone anyways. Also let’s be
real, go install
is not a replacement for a real package manager. Just
because go builds are fast and produce a static binary doesn’t mean it’s not a
good idea to be able to build packages. Plus you want your man pages to be
installed with your software as well of course. So let’s write some generic
install commands:
INSTALLED_TARGETS = $(addprefix $(PREFIX)/bin/, $(TARGETS))
INSTALLED_MAN_TARGETS = $(addprefix $(PREFIX)/share/man/man1/, $(MAN_TARGETS))
# install tasks
$(PREFIX)/bin/%: %
install -d $$(dirname $@)
install -m 755 $< $@
$(PREFIX)/share/man/man1/%: %
install -d $$(dirname $@)
install -m 644 $< $@
install: $(INSTALLED_TARGETS) $(INSTALLED_MAN_TARGETS)
local-install:
$(MAKE) install PREFIX=usr/local
We’re adding the PREFIX
to all targets and man targets here to generate the
paths to install. Then we write another implicit wildcarding rule that has the
original targets as dependencies and performs install commands to put them
into the prefix. This is a quick and easy way to have a generic make install
target and also lets us easily add a local install target that we can use as a
dependency for building packages later on.
Dependencies, Oh My!
If you’ve spent time with Go and make before, you will maybe have noticed a flaw in the building step of the Makefile so far. To revisit, we are building binaries from the source in the cmd folder with this implicit rule.
%: cmd/%/main.go
go build -ldflags "$(LDFLAGS)" -o $@ $<
However this only tells make about the first level of direct dependencies for
the binary to the cmd source. Chances are you are using library and vendored
code in those. This means while go build
technically knows about all
dependencies, make doesn’t. And it will refuse to rebuild the binaries if
something other than the cmd source changes. This is annoying but fortunately
also fixable. A simple fix would be to just not have dependencies in make for
the executables and mark them as .PHONY
so that they are always regarded out
of date. This pushes all dependency resolution back to the go tool chain which
is nice, but kinda defeats half of the purpose of a Makefile as it will just
run all the commands all the time. To be clear, in practice this is a fine
solution and the downsides are mostly academic with the speed of a usual go
build.
However it’s fun to figure out how to make things work and while we’re here
already, lets utilize make to its full extent and make it aware of all
dependencies. The details for the make side of things I got from this awesome
blogpost which gives a great overview over automatic dependency
management in makefiles. So now all we need is a way to get a list of all
dependencies for a go source file. And of course, go files
to the rescue
again! As it not only lets us print packages for passing to the test runner,
but also can print out all dependencies of a file. And with its -f
parameter
it also supports basic templating for printing out the results. Utilizing that
we only need to do a small amount of post processing to print it in make
dependency format and we are good to go.
# source, dependency and build definitions
DEPDIR = .d
$(shell install -d $(DEPDIR))
MAKEDEPEND = echo "$@: $$(go list -f '{{ join .Deps "\n" }}' $< | awk '/github/ { gsub(/^github.com\/[a-z]*\/[a-z]*\//, ""); printf $$0"/*.go " }')" > $(DEPDIR)/$@.d
$(DEPDIR)/%.d: ;
.PRECIOUS: $(DEPDIR)/%.d
-include $(patsubst %,$(DEPDIR)/%.d,$(TARGETS))
%: cmd/%/main.go $(DEPDIR)/%.d
$(MAKEDEPEND)
go build -ldflags "$(LDFLAGS)" -o $@ $<
The makedepend command here grabs all dependencies that come from github
(which was a good enough approximation for me to filter out the std lib), cuts
off the project prefix and appends /*.go
to each dependency. With the go
rules of having a package per folder, this also is pretty accurate most of the
time and only occasionally serves false positives to result in a rebuild. We
then adapt the implicit build rule to require the dependency file as well but
also rebuild it on each build. And BOOM our Makefile knows almost perfectly a
out all source dependencies.
Packaging and Documentation
I always aim for providing packages and good documentation for my Go projects. But I’ve already written about those things more generally here, so if you’re interested in the details of it, give that blog post a read. The important part is that the Makefile also holds the logic for building docs and packages, so they can be easily triggered from CI.
Cleanup
Since it’s also always good to make it easy to clean up artifacts and generated intermediate and output files, all makefiles also get some clean up tasks.
# clean up tasks
clean-docs:
rm -rf ./docs
clean-deps:
rm -rf $(DEPDIR)
clean: clean-docs clean-deps
rm -rf ./usr
rm $(TARGETS)
rm $(MAN_TARGETS)
.PHONY: all test rpm deb install local-install packages govendor coverage docs jekyll deploy-docs clean-docs clean-deps clean
Equipped with those Make tricks I’ve been having tons of fun building Go code. Some of that is surely more involved than it has to be and especially the dependency resolution stuff is very bonus round. But it’s been super interesting to rig it up and I learned a lot of things about Make. And in the end that’s what it’s all about for me. (Besides having projects with a super nice to use structure :)