3 minutes
Proper Way of Using a Makefile
The Makefile
has been with us for quite a long time. Primarily used with C or
C++ software, but in the recent years it has seen use in projects using almost
any programming language. And why not, it is a powerful tool. Although, I have
seen many people arguing that “it is just another task manager”, which is of
course far from the truth.
I understand why you might think so, because like me, you are probably using
make
and the Makefile
wrong. I knew that it is capable of doing things that
need to be done and not simply do everything all the time, but I never bothered
to actually dive into it to see how I can improve my build times. There are
countless resources in the internet how to get you started with Makefile
, but
most if not all simply just present it as a task runner, even if they do comment
that it is just a basic usage for it.
Basic usage is completely fine, but imagine you are creating a build target to
build multiple services that are built from the source code in a shared
repository. You could rebuild all of them every time you change something, but
why, if you only need to rebuild those that you actually changed? And here comes
Makefile
to the rescue. Lets start with a hypothetical source code structure:
my-awesome-project
|-dist/
|-pkg/
| |-libA/
| `-libA.go
| |-libB/
| `-libB.go
| `-libC/
| `-libC.go
`-internal/
|-serviceA/
| |-main.go
| `-functionality.go
`-serviceB/
|-main.go
`-functionality.go
The general idea is, that we want to rebuild both services A and B if the
libraries in the pkg
directory change and rebuild only service A or B if their
source files change. You might want a Makefile
something like:
all: dist/serviceA dist/serviceB
dist/serviceA: internal/serviceA/*.go pkg/*/*.go
go build -o dist/serviceA ./internal/serviceA/
dist/serviceB: internal/serviceA/*.go pkg/*/*.go
go build -o dist/serviceB ./internal/serviceB/
The first line, all: dist/serviceA dist/serviceB
is just an alias and allows
you to run simply make
and it will execute all of the targets listed on its
line. The further lines are actual build targets, but they are named after the
file that the build will produce followed by files that are used to build it.
Using this format allows make
to track the file change timestamps and when a
run for a specific target is requested make
will check if any of the specified
files have a newer timestamp than the target file and only if they do will it
run run that target.
Of course you can specify aliases for those as well so you do not need to type
make dist/serviceA
should you wish so:
serviceA: dist/serviceA
And now you can execute make serviceA
and it will build service A, but only if
the source has changed.
This can be applied to anything of course, like generating code, tests, etc. Of
course this seems like an overkill for smaller projects and you will spend more
time configuring and writing a Makefile
than you will save by shorter build
times, but if you have a larger project on hand it can be very beneficial to
have certain tasks run only if they are necessary.