Makefile is an awesome tool to group together a bunch of different rules and automate your build process. Makefile is used by make
which is essentially a file generator tool. It is generally used to compile and build programs from source by following rules
listed in the Makefile. People use Makefile for a lot of different purposes as well, for example converting md
to html
and publish these files to the web server.
Every makefile you see, is composed of rules. A rule
is declaration of a target
and the commands to be executed to generate the target. A target can be a file or an action to be performed (more on that later).
This is how a rule looks like in a Makefile
target: dependencies
recipe
When you run make target
, make
searches for the rule which begins with this target
and executes the dependecies (if required). It then runs a bunch of commands which are listed in the recipe. An important thing to understand here is that make
tracks the dependencies by their last modified time. So if the dependencies haven’t changed, then make will complain with make: 'target' is up to date.
Enough of theory, let’s get our feet wet by writing our first Makefile. One important thing about Makefile
is that you need to use tabs
and not spaces
. It is one of the rare *nix
programs which is whitespace aware and this has been mentioned in The Unix-Haters Handbook as well.
To begin with, let’s write a simple rule which removes any tempory object files using go clean
and previous binary file using good ol rm
:
clean:
go clean
rm -f sample.bin
The target
here is clean
. There is something special going on here though. Imagine we have a file called clean
in our source directory? Let us try to run make clean
now
Our directory structure:.
├── Makefile
├── clean
└── sample.bin
On running make clean
:
make: `clean' is up to date.
Every target in Makefile by default is a file target
. In our case clean
is a file target and make
tries to build this file clean
but since we already have a file with the same name clean
in our directory, make
is complaining there’s nothing to do.
Moreover, in this case our rule is more of an action
rather than building files. So for all such scenarios, make
provides an easy way where we can instruct it to just run the rule and ignore any filename in our directory. This is called a PHONY
target which is a special kind of target. PHONY
is just a way in make
to forcefully run a target and not care about generating files. Our aim with this rule is to run 2 commands and that’s about it. So this is the perfect example for using PHONY
.
We can add .PHONY
target to our Makefile simply by this line:
.PHONY : clean
clean:
go clean
rm -f sample.bin
Now when we run make clean
we get our expected output
go clean
rm -f sample.bin
Let us extend our Makefile to do some common tasks:
.PHONY : build run fresh test clean
test:
go test
build:
go build
run:
./sample.bin
clean:
go clean
rm -f sample.bin
If you have worked on any Golang project, these are very trivial actions on any Golang project. You will soon realise the power of Makefile when you have to do these steps repeatedly. Some people might argue then you can use aliases or simple shell scripts for the same. I vehemently disagree with that. Reason being, make
is a much more powerful tool than just running commands. make
has support for dependency tracking and it will only rebuild whatever is required. If you are working on a huge project where the build times are to the tune of hours, you will soon realise why shell scripts are inferior. Ofcourse, someone can point that they can write a shell script to do even that, by fetching the last modified time but why do the extra work when there’s an already existing tried and tested tool? make
also has support for parallel task execution, so you can just pass the flag -j {num}
to make
and it will run these {num} jobs parallely. All these benefits will be apparent for larger projects, but it is a good habit to write Makefile even for smaller projects.
We will now make our Makefile a bit more sophisticated and introduce variables. If you want to custom name your binary, or inject variables at compile time, you can declare these variables, for example:
BIN := my-awesome-pro.bin
HASH := $(shell git rev-parse --short HEAD)
COMMIT_DATE := $(shell git show -s --format=%ci ${HASH})
BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S')
VERSION := ${HASH} (${COMMIT_DATE})
We can modify our Makefile to use these variables:
build:
go build -o ${BIN} -ldflags="-X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}'"
run:
./${BIN}
test:
go test
clean:
go clean
rm -f ${BIN}
We can auto version our builds and pass variables during the build time with go linker tool, on passing the -X
flag. That’s really neat, now whenever we do a make build
we get new version of the build automagically.
So now we have a working Makefile which helps us with trivial things, but everytime if we need to change something in our program and check, we still need to do these steps manually: make clean
, make build
and make run
. Won’t it be awesome if we could tell Makefile to do all this with just one command? Programmers are lazy creatures after all .
In the beginning we saw a target is composed of recipe and dependencies. So we can just create a new PHONY
target with all these dependencies and any recipe if we want optionally.
fresh: clean build run
We created a new target
which depends on clean
to run first, then build
and finally run
. So everytime if we make some change in our Go program, all we need to run is make fresh
. Awesome, isn’t it?
We will finally add our last target which is a highly opinionated way of generating binaries for different OS and architectures.
prod:
goreleaser --rm-dist --snapshot
cp dist/linux_amd64/${BIN}-linux.bin .
rm -rf dist
This target runs goreleaser
which is a build automation tool. It then copies the required linux binary to the source directory and removes all the other junk.
You can even extend your Makefile to commit files to a repo, and rsync these binaries to the production server or initiate your CI/CD build process. The reason I like Makefile is because it serves as a living documentation for your project on how to build/deploy the project making it easier for new contributors to get started.
🔗Some additional information
-
If you run
make
without passing any target name,make
will run the first target present in the Makefile. To override this, you should set.DEFAULT_GOAL
setting and override the target which you want to make as default. -
.PHONY
is just one way to tellmake
that it is a special kind of target, you can also do the same by creating a target without any recipe. Read this to know more.
To know more about Makefile
, you can read the manual here
I hope you now appreciate Makefile
and try it out in your next project. I’d love feedback on this blog post, do reach me out at twitter or email
Fin!