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:

| |-libA/
|   `-libA.go
| |-libB/
|   `-libB.go
| `-libC/
|   `-libC.go
  |  |-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.