How To Write Unmaintainable Makefiles - Part 1
Makefiles can be really useful outside the context of building plain old C/C++ applications. The built-in dependency management makes it possible to avoid a lot of boilerplate bash code when writing some build scripts. They also provide quite sophisticated ways to implement abstractions and logic, which often makes the files hard to read.
Table of Contents
I had a large amount of screenshot needing to be cropped in the same way, depending on what kind of screenshot it was.
Screenshot of type A needs to cropped in a different way than screenshot of type B.
To pixel-perfectly crop the images I used
- Each image file ends in
- Each type needs to be cropped differently
- The output files should not have the
cat--top.png→ cut top →
dog--bottom.png→ cut bottom →
The Boring Solution
A simple task for a plain Makefile:
.PHONY statement makes sure that
make runs the target even if there is a file with the target’s name.
This is recommended as good practice and can increase performance (Make Manual).
The pattern target
img-out/%.jpg: img-in/%--ne.jpg does multiple things:
- It defines where to find the source file for the given cropped file
- It writes the cropped images to the output directory
- It removes the suffix by using pattern matching
A simple straight forward Makefile, isn’t it? Who would want that? It’s too maintainable!
A Bit of Abstraction Couldn’t Hurt?
What if we want to change the input and output directory down the road?
A simple search and replace surely would be too complicated!
Let’s define the variables
out-dir to solve that blocker-issue.
Also we don’t want to modify our Makefile every time we add a new image, so let’s sprinkle a
wildcard command in the mix.
As it turns out, the string transformation from
<img>.png is a non-trivial matter in a Makefile.
Surely a problem that abstraction can solve 🙃.
Abstraction == Skill
As we want to show off our Makefile knowledge, abstraction is vital.
Thankfully the make syntax allows for
eval-ing variable contents.
- Define a “function” that defines a target for each type:
- Call the “function” to define a type and the instructions to execute
Makefile Functions (Eval + Call)
Make does not provide function definitions, but we can create our own makeshift functions by using
Step 1: Define a variable that can be used as the function’s body.
define can be used to define multi-line variables.
The thing to keep in mind is that the function’s arguments will be immediately replaced by the provided values.
define myfun # $(1), $(2), ... are function arguments # make variables unique by using arguments in their name # that prevents the variables from being overridden by another function $(1)_foo := $(2) # to access a variable's value the unique name has to be used and # the '$' has to be escaped with another '$' $(1)_bar := $$($(1)_foo)_suffix # target definitions are possible too (use unique name) build-$(1): $(1)-deps echo baz endef
Step 2: Call the define.
call function will practically replace all arguments (
$(2), …) with the provided strings (
Using the output of
call, which should return a valid Makefile target, we can eval it.
Step 2 and 3 combined look like this:
$(eval $(call myfun,arg1,arg2))
Step 4: Debug.
Obviously this step is not necessary, as we are all experts.
For the rare occasion that someone encounters a problem, simply replace the
This will print the text to stdout instead of eval-ing it.
myfun example yields:
arg1_foo := arg2 arg1_bar := $(arg1_foo)_suffix build-arg1: arg1-deps echo baz
Applying our newfound knowledge to the initial problem:
- New image files can be added on the fly, the Makefile does not need to be changed ✔
- The Makefile now is unmaintainable ✔
- A new type can be added by defining a new variable with build instructions ✔
Carefully evaluate the benefits you get out of abstractions.