Directories as make(1) targets
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 is 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
Using flag files
To get the behavior we really want, we can create an empty file within 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!