# Basti’s Buggy Blog

Poking at software.

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

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.pngcut topcat.png
• dog--bottom.pngcut bottomdog.png

## The Boring Solution

A simple task for a plain Makefile:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  .PHONY: all all: img-out/calc.jpg img-out/pen.jpg img-out/car.jpg img-out/tea.jpg img-out/%.jpg: img-in/%--ne.jpg convert $< -gravity NorthEast -crop +320+320 +repage$@ img-out/%.jpg: img-in/%--nw.jpg convert $< -gravity NorthWest -crop +320+320 +repage$@ img-out/%.jpg: img-in/%--se.jpg convert $< -gravity SouthEast -crop +320+320 +repage$@ img-out/%.jpg: img-in/%--sw.jpg convert $< -gravity SouthWest -crop +320+320 +repage$@ .PHONY: clean clean: rm -f img-out/* 

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.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33  in-dir := img-in out-dir := img-out # glob all input images in-files := $(wildcard$(in-dir)/*) # img-in/calc--nw.jpg img-in/pen--ne.jpg img-in/car--sw.jpg img-in/tea--se.jpg # replace input directory with output directory out-files-suffix := $(patsubst$(in-dir)%,$(out-dir)%,$(in-files)) # img-out/calc--nw.jpg img-out/pen--ne.jpg img-out/car--sw.jpg img-out/tea--se.jpg # remove the '--*' (urgh!) # ??? not that easy # img-out/calc.jpg img-out/pen.jpg img-out/car.jpg img-out/tea.jpg .PHONY: all all: $(patsubst$(in-dir)%,$(out-dir)%,)$(out-dir)/%.jpg: $(in-dir)/%--ne.jpg convert$< -gravity NorthEast -crop +320+320 +repage $@$(out-dir)/%.jpg: $(in-dir)/%--nw.jpg convert$< -gravity NorthWest -crop +320+320 +repage $@$(out-dir)/%.jpg: $(in-dir)/%--se.jpg convert$< -gravity SouthEast -crop +320+320 +repage $@$(out-dir)/%.jpg: $(in-dir)/%--sw.jpg convert$< -gravity SouthWest -crop +320+320 +repage $@ .PHONY: clean clean: rm -f$(out-dir)/* 

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!

1. Define a “function” that defines a target for each type: <img>--<type>.png<img>.png
2. 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:   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40  in-dir := img-in out-dir := img-out # define variables to set the behavior of each type # NOTE: the '=' assignment was used in order to prevent # expansion of the '<' and '@' variables ne = convert < -gravity NorthEast -crop +320+320 +repage @ nw = convert < -gravity NorthWest -crop +320+320 +repage @ se = convert < -gravity SouthEast -crop +320+320 +repage @ sw = convert < -gravity SouthWest -crop +320+320 +repage @ transformations := ne nw se sw # creates a target applying to all files ending in --(1).jpg # the target will execute the define named (1) define transformation # glob all input files (1)_in :=$$(wildcard$(in-dir)/*--$(1).jpg) # replace the output directory$(1)_out := $$(subst (in-dir)/,(out-dir)/,$$($(1)_in)) # remove the '--' suffix$(1)_out := $$(subst --(1),,$$($(1)_out)) .PHONY:$(1) $(1): $$((1)_out) (out-dir)/%.jpg: (in-dir)/%--(1).jpg$$($(1)) endef # eval all transformations $(foreach t,$(transformations),$(eval$(call transformation,$(t)))) #$(eval $(call transformation,ne)),$(eval $(call transformation,nw)), ... .PHONY: all all:$(transformations) .PHONY: clean clean: rm -f img-out/* 

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.