Robert D. French

blog(8) System Manager's Manual blog(8)

Some times in our lives, we all have pain, we all have sorrow, and we all need to use a directory as a pre-requisite for a build target.

Consider the (fairly common) use case of storing build products in a build/ directory within the source folder. Clearly, the folder needs to exist before we can write artifacts to it. Here is the obvious approach:

build/a.out: main.c build/
	cc -o build/a.out main.c

build/:
	mkdir build

Seems like it couldn't be simpler, but this has a few subtle problems. First, make(1) selects build targets based on the relative timesamps of their pre-requisites. In UNIX, when you create a file in a directory, the modification time of the directory gets updated. You can observe this by running stat(1):

$ mkdir build
$ stat -f "%m" build # Get the modification time of 'build'
1746818594
$ touch build/hello # Create a file in the 'build' dir
$ stat -f "%m" build
1746818599

So even though it might not seem like we modified the folder, we did. By definition, a folder the set of files that it contains. So by creating a new file, we have in fact modified the enclosing folder. Let's see how this plays out in the Makefile defined above (assuming we have some trivial C program defined in ./main.c):

$ make build/a.out
mkdir build
cc -o build/a.out main.c
$ make build/a.out # Rebuild it to show no work is needed
`build/a.out' is up to date.

That's great, just as we expected! But now let's try to add another target into the mix:

build/a.out: main.c build/
	cc -o build/a.out main.c

build/b.out: main.c build/
	cc -o build/b.out main.c

build/:
	mkdir build

So here we have two targets being built, a.out and b.out. The trouble is, they invalidate one another as soon as they are built:

$ make build/a.out  # Build 'a.out'
mkdir build
cc -o build/a.out main.c
$ make build/a.out  # Rebuild to show no works is needed
`build/a.out' is up to date.
$ make build/b.out  # Build 'b.out', which modified 'build'
cc -o build/b.out main.c
$ make build/a.out  # 'a.out' depends on 'build', so must be rebuilt
cc -o build/a.out main.c
$ make build/b.out  # Now b.out is out of date again
cc -o build/a.out main.c

To get the behavior we really want, we can create an empty file the build directory as a way of saying “this directory has been created”. We can then use that flag file as a pre-requisite for tasks that need the directory to exist. Under that scheme, our Makefile will look like this:

build/a.out: main.c build/.dir
	cc -o build/a.out main.c

build/b.out: main.c build/.dir
	cc -o build/b.out main.c

build/.dir:
	mkdir -p build
	touch build/.dir

This way, when we build a.out and b.out, we are not modifying any of the pre-requisities. The modification time for the build/ directory still changes, but it is no longer a pre-requisite, so it is no longer causing mischief!

2025-05-09 Robert D. French