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
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:
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.