Automatically Making Build Directories in Make

Published: Thursday, 7 July 2020 by Michael Twomey

The Problem

There are times in a Makefile where you need a common dependency built first. The most obvious one is the output directories. There are a bunch of options for this, including sticking a mkdir -p $(BUILDDIR) in every target or adding it as a dependency. This can result in a lot of noise and duplicate command invocations (and becomes an issue if the command isn’t idempotent or slow). The other problem is you don’t want to keep rebuilding targets because of this dependency (e.g. the build directory timestamp changes).

The Solution

I’ve always wanted an ability to only trigger a common target once during the build if it’s needed. I found out it’s called an “order-only” prerequisite.

https://www.gnu.org/software/make/manual/make.html#Prerequisite-Types

BUILDDIR=$(CURDIR)/build

$(TARGETS): $(SOURCES) | $(BUILDDIR)

$(BUILDDIR):
	mkdir $(BUILDDIR)

The important part is the |. This adds $(BUILDDIR) as a dependency but doesn’t affect re-building of the $(TARGET).

I’ve used this in two different situations in the past: making the output directories and setting up my Python virtualenv (you can also kind of coax multiple docker up invocations into working with this).

Example

Here are three examples of Makefiles doing the same thing, making a build directory and “building” files into it. The last uses order only prequisites.

With mkdir -p in Every Target

This is probably the most direct approach, create your dependencies in the target each time. Note the use of -p, that prevents the second attempted mkdir from failing. This approach is noisy and won’t work well when you need something more complicated (e.g. install dependencies).

BUILDDIR=mkdir-build
TARGETS=$(BUILDDIR)/1.out $(BUILDDIR)/2.out

.phony: all
all: $(TARGETS)

$(BUILDDIR)/1.out: 1.in
	mkdir -p $(BUILDDIR)
	touch $@

$(BUILDDIR)/2.out: 2.in
	mkdir -p $(BUILDDIR)
	touch $@
❯ make
mkdir -p mkdir-build
touch mkdir-build/1.out
mkdir -p mkdir-build
touch mkdir-build/2.out

❯ touch mkdir-build

❯ make
make: Nothing to be done for 'all'.

❯ touch 1.in

❯ make -f mkdir-makefile.mk
mkdir -p mkdir-build
touch mkdir-build/1.out

Here you can see the mkdir -p keeps getting re-run.

With Dependencies

This is pretty close to an optimal approach, now Make is responsible for building it as a target. Now you have the problem where touching the dependency will re-trigger builds on all your targets. Not necessarily a big problem, but probably annoying.

BUILDDIR=dependencies-build
TARGETS=$(BUILDDIR)/1.out $(BUILDDIR)/2.out

.phony: all
all: $(TARGETS)

$(BUILDDIR)/1.out: 1.in $(BUILDDIR)
	touch $@

$(BUILDDIR)/2.out: 2.in $(BUILDDIR)
	touch $@

$(BUILDDIR):
	mkdir $(BUILDDIR)
❯ make
mkdir dependencies-build
touch dependencies-build/1.out
touch dependencies-build/2.out

❯ touch dependencies-build

❯ make
touch dependencies-build/1.out
touch dependencies-build/2.out

❯ touch 1.in

❯ make
touch dependencies-build/1.out

Here you can see touching the build directory forces all the targets to be rebuilt. mkdir is only run once though.

With Order Only Prequisites

This is probably the most optimal solution, Make handles building your dependency but won’t retrigger your target builds if it changes. Effectively you have two sets of targets, and one just asks Make to evalauate the other first. Hence “order only”.

BUILDDIR=order-only-build
TARGETS=$(BUILDDIR)/1.out $(BUILDDIR)/2.out

.phony: all
all: $(TARGETS)

$(BUILDDIR)/1.out: 1.in | $(BUILDDIR)
	touch $@

$(BUILDDIR)/2.out: 2.in | $(BUILDDIR)
	touch $@

$(BUILDDIR):
	mkdir $(BUILDDIR)
❯ make
mkdir order-only-build
touch order-only-build/1.out
touch order-only-build/2.out

❯ touch order-only-build

❯ make
make: Nothing to be done for 'all'.

❯ touch 1.in

❯ make -f order-only-makefile.mk
touch order-only-build/1.out

Here you can see mkdir is only run once and touching the build directory doesn’t force a rebuild of the targets.

Is this a game changer? Probably not, but there are definitely situations where it solves real problems. It’s also a bit neater :)