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
The Task
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 imagemagick
.
- Each image file ends in
--<type>.png
- Each type needs to be cropped differently
- The output files should not have the
--<type>
suffix
Examples
cat--top.png
→ cut top →cat.png
dog--bottom.png
→ cut bottom →dog.png
The Boring Solution
A simple task for a plain Makefile:
|
|
The .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 in-dir
and 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>--<type>.png
→ <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.
Perfect!
- Define a “function” that defines a target for each type:
<img>--<type>.png
→<img>.png
- 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 eval
and call
.
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.
Using make’s call
function will practically replace all arguments ($(1)
, $(2)
, …) with the provided strings ($(call myfun,arg1,arg2)
).
Step 3: evil eval.
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 eval
with info
.
This will print the text to stdout instead of eval-ing it.
Our myfun
example yields:
arg1_foo := arg2
arg1_bar := $(arg1_foo)_suffix
build-arg1: arg1-deps
echo baz
Original Problem
Applying our newfound knowledge to the initial problem:
|
|
Results:
- 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 ✔
TL;DR
Carefully evaluate the benefits you get out of abstractions.