Makefile is Awesome
Table of Contents
_________________________________________
< $ make help help help please_help touch >
-----------------------------------------
\ . . .
\ . . . ` ,
\ .; . : .' : : : .
\ i..`: i` i.i.,i i .
\ `,--.|i |i|ii|ii|i:
UOOU\.'@@@@@@`.||'
\__/(@@@@@@@@@@)'
(@@@@@@@@)
`YY~~~~YY'
|| ||
Jokes aside, the simplicity of how GNU Make solves complex dependency chains and decides what should be built, is awesome.
Influenced by a work pal in our Go codebase, I started to enjoy with Make:
- Seamless integration with shell.
- Preprocessing for shell scripts.
- Powerful dependency management.
- Tabs instead of spaces… Hum, no, this I dislike.
And after almost a year using it as a ordinary task runner, I started to pay more attention in Makefiles of open source Go projects, and I started to get more interested in it. Then reading more of its Info pages, I got the basics:
- Targets
- Recipes and Canned recipes
- Variables flavors
- Parallelism
- Dependencies and order-only dependencies
All this stuff is pretty much a swissarmy knife for building files.
Example
The Makefile I was using to study and brought this inspiration was to build a React + Vite app (yeah, we use Makefile for all our repos LOL). It's core logic is something like this:
# Adding parallelism by default MAKEFLAGS += -j4 # For tests purposes, logging the sequence of commands Make runs across parallel calls COMMAND_LOG := commands.txt # `&:` means a "grouped target". It means this recipe outputs the all files at once. See Info '(make) Multiple Targets' # It's executed if any of its files needs to be generated, and no matter how much is missing, it runs only once data/a.json data/b.json data/c.json&: @sleep 1 # Adding sleep to test parallelism @echo "$$(date -Ins): generate data to fulfill $@" >>$(COMMAND_LOG) mkdir -p data/ touch data/a.json data/b.json data/c.json # In theory, something that outputs the needed files for `dist` processed-%.json: data/%.json @sleep 1 touch $@ @echo "$$(date -Ins): processed $< data to fulfill $@" >>$(COMMAND_LOG) # This is a trick to let Make know when it's time to update the dependencies. # It generates a blank file inside the dependencies dir, so it can compare the modification time against the lock file. # In this case, for npm, but can be used for any other package manager. node_modules/npm_install: package-lock.json @sleep 2 @echo "$$(date -Ins): npm install" >>$(COMMAND_LOG) mkdir -p node_modules/ # Manually creating because we're not really running `npm install` touch $@ # Important, or else the target will never be satisfied # We can easily depend on any target file of its recipe # Directories are not good as targets as they're mostly called all the time. For my case it's not a problem at all as it previously had PHONY. dist: node_modules/npm_install processed-a.json processed-b.json processed-c.json @sleep 1 mkdir -p $@ @echo "$$(date -Ins): the build here. in my case, it was vite" >>$(COMMAND_LOG) echo 'Ensuring the dependencies are present' \ $(addprefix && test -f ,$^) # Loop all dependencies, adding '&& test -f ' prefix echo 'done' .PHONY: clean clean: rm -r node_modules/ data/ dist/ rm $(COMMAND_LOG) processed-{a,b,c}.json
make clean >/dev/null # Calling clean separately to prevent running it in parallel with the rest \time -f 'not parallel: took %E' make -j1 dist 2>&1 >/dev/null make clean >/dev/null \time -f 'parallel: took %E' make dist 2>&1 >/dev/null
not parallel: took 0:07.10 parallel: took 0:03.05
And let's see the command log:
2025-12-28T08:56:57,548812312-03:00: generate data to fulfill data/a.json 2025-12-28T08:56:58,544584262-03:00: npm install 2025-12-28T08:56:58,562780665-03:00: processed data/b.json data to fulfill processed-b.json 2025-12-28T08:56:58,561773195-03:00: processed data/c.json data to fulfill processed-c.json 2025-12-28T08:56:58,563169488-03:00: processed data/a.json data to fulfill processed-a.json 2025-12-28T08:56:59,576453152-03:00: the build here. in my case, it was vite
Analyzing:
- The grouped target for data generation was called only once as expected.
- The processing of
datawas called the both 3 in parallel. - Despite the dependency installation sleep be larger (2 seconds), it was called in parallel with data generation, and it's delay was imperceptible.
Is't that awesome?
What's Next
make coffee some_work cooked_carrots
This remembers me a FSF promotion video, "User Liberation Video":
make the_future
I'm certain that the_future is PHONY target, but I hope it's not a canned recipe!