
Make: Dynamic Makefile targets
A common pattern which we want to support when building our services is to build
everything under rpc/{service}/{service}.proto
as well as cmd/*
and other
locations which may be manually written or generated with various tools.
Building multiple targets with Go
Go does actually support building the whole cmd/ folder in one command:
go build -o ./build ./cmd/...
This functionality was added in Go just recently, in the 1.13.4 tag.
It will not create the target folder itself, but will build all your applications
under it, based on the ./cmd/...
argument. Why do we need dynamic Makefile targets?
We can live without them for building multiple Go programs, but we don’t only need
them for building the cmd/ folder, but for other code generation folders as well.
We could resort to //go:generate
tags to invoke our files, but it would again
mean some duplication/replication of responsibility. Defining a single dynamic Makefile
target keeps all the logic in one place. It also allows you to customize the build
steps to target multiple architectures, operating systems, and other use cases.
Let’s learn how dynamic Makefile targets work and try to cover our use cases.
Dynamic Makefile target for building RPC/Protobuf files
The first target that we want to support is running all code generation required
for our RPC services. We define a rpc
Makefile target, and use dynamic execution
to build a list of dynamic targets:
rpc: $(shell ls -d rpc/* | sed -e 's/\//./')
Here we use the make built-in feature to run a shell, and with the output of that shell
command define our dynamic targets. For each folder found under rpc/
, a target like
rpc.{folder}
is created. For example, if we wanted to build the code for a target,
we could do:
rpc.stats:
protoc --proto_path=$GOPATH/src:. -Irpc/stats --go_out=paths=source_relative:. rpc/stats/stats.proto
protoc --proto_path=$GOPATH/src:. -Irpc/stats --twirp_out=paths=source_relative:. rpc/stats/stats.proto
If we used rpc/stats
as the target name, make
wouldn’t do anything as that file/folder
already exists. This is why we are rewriting the target name to include a .
(rpc.stats).
The obvious second part of our requirement is to make this target dynamic. We want to build
any number of services.
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,10 @@
-.PHONY: all
+.PHONY: all rpc
all:
drone exec
+
+rpc: $(shell ls -d rpc/* | sed -e 's/\//./g')
+rpc.%: SERVICE=$*
+rpc.%:
+ @echo '> protoc gen for $(SERVICE)'
+ @protoc --proto_path=$(GOPATH)/src:. -Irpc/$(SERVICE) --go_out=paths=source_relative:. rpc/$(SERVICE)/$(SERVICE).proto
+ @protoc --proto_path=$(GOPATH)/src:. -Irpc/$(SERVICE) --twirp_out=paths=source_relative:. rpc/$(SERVICE)/$(SERVICE).proto
The %
target takes any option (which doesn’t include / in the pattern). We can also declare
variables like SERVICE
for each target. The variable can also be exported into the environment
by prefixing it with export
(example: rpc.%: export SERVICE=$*
). Currently we don’t need this,
but we will need it later on to pass build flags for our cmd/ programs.
The $*
placeholder is the matched target parameter. As the target is rpc.stats
, the variable here
will only contain stats
but not rpc.
since it’s part of the target definition.
With the @
prefix on individual commands in the target, we suppress the output of the command.
All we need to do is update the step in .drone.yml
into make rpc
, and we have support for
a dynamic number of services. Running make verifies this:
# make
[test:0] + make rpc
[test:1] > protoc gen for stats
[test:2] + go mod tidy > /dev/null 2>&1
[test:3] + go mod download > /dev/null 2>&1
[test:4] + go fmt ./... > /dev/null 2>&1
Building our services from cmd/
For each service, we will create a cmd/{service}/*.go
structure, containing at least main.go
.
Let’s start with adding a simple cmd/stats/main.go
with a hello world to greet us. We will come
back and scaffold the actual service later.
package main
void main() {
println("Hello world")
}
The function
println
is a Go built-in function that works without importing any package. It shouldn’t really be used, but as far as providing some test output, it’s the shortest way to do that. We will throw this program away, so don’t pay it much attention.
Using what we learned for dynamic targets with the rpc
target, let’s create a build
target which will build all the applications we will put under cmd/
by running make build
.
build: export GOOS = linux
build: export GOARCH = amd64
build: export CGO_ENABLED = 0
build: $(shell ls -d cmd/* | sed -e 's/cmd\//build./')
echo OK.
build.%: SERVICE=$*
build.%:
go build -o build/$(SERVICE)-$(GOOS)-$(GOARCH) ./cmd/$(SERVICE)/*.go
For the main build
target, we define our build environment variables - we want to build our
services for linux, for amd64 architecture, and we want to disable CGO so we have static binaries.
We list all cmd locations as dynamic targets and remap them to build.%
, similarly to what we
do with the rpc target. All that is left to do is to add make build
at the end of .drone.yml
,
and add our /build
folder into the .gitignore
file for our project.
This article is part of a Advent of Go Microservices book. I’ll be publishing one article each day leading up to christmas. Please consider buying the ebook to support my writing, and reach out to me with feedback which can make the articles more useful for you.
All the articles from the series are listed on the advent2019 tag.
While I have you here...
It would be great if you buy one of my books:
- Go with Databases
- Advent of Go Microservices
- API Foundations in Go
- 12 Factor Apps with Docker and Go
Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.
Want to stay up to date with new posts?
Stay up to date with new posts about Docker, Go, JavaScript and my thoughts on Technology. I post about twice per month, and notify you when I post. You can also follow me on my Twitter if you prefer.