Rules With Multiple Outputs in GNU Make

[article]
Mr. Make
Summary:
One problem that Makefile writers sometimes have is the need to write a single rule that produces multiple output files in order to accommodate tools that don't fit the standard one-command-one-output model generally assumed by Make. Eric Melski takes a look at a few alternatives, including the one and only way to truly capture the relationship in GNU Make syntax.

One problem that Makefile writers sometimes have is the need to write a single rule that produces multiple output files in order to accommodate tools that don't fit the standard one-command-one-output model generally assumed by Make. The classic example is bison, a parser generator used in crafting compilers and interpreters. Bison takes an input file like parser.i and generates both parser.c and parser.h. The Makefile hacker is left with a dilemna: how do you express this relationship in GNU Make syntax? In this article we'll look at the obvious answer and why it is wrong. Then we'll look at a few alternatives, including the one and only way to truly capture the relationship in GNU Make syntax.

The obvious but wrong solution
Faced with this problem, many Makefile hackers will write something like this:

all: parser.h parser.c

parser.h parser.c: parser.i
@echo Generating parser.h and parser.c from parser.i
@sleep 2
@touch parser.c parser.h

Unfortunately this Makefile does not describe a single rule with two outputs, but rather two distinct rules that each have a single output, and that happen to use the same series of commands.  In a serial build this distinction is often irrelevant and sometimes even undetectable:  although GNU Make will schedule both rules to run, the second rule will never do any work because its output file will already have been updated (by the first rule).  But try running this build in parallel with gmake -j 2:

Generating parser.h and parser.c from parser.i
Generating parser.h and parser.c from parser.i

Because there are two distinct rules which each update both output files, the files are actually updated twice.  In the best case, this just results in a little wasted work.  In the worst case, the rules both try to update the output files at the same time, resulting in corrupted output.  So, this approach will work if you only ever run serial builds, but nobody does that these days. So what's a Makefile hacker to do?

A Crude Fix

Our first attempt at fixing the problem is to add a dependency between the two output files.  This is a bit crude since there is no actual dependency between the files, but at least it will ensure that the rules run one at a time:

all: parser.h parser.c

parser.h parser.c: parser.i
@echo Generating parser.h and parser.c from parser.i
@sleep 2
@touch parser.c parser.h

parser.h: parser.c

This modification has made the makefile parallel-safe, but it has introduced a surprising side-effect: Even in a serialized build, the files are now generated twice.  Go ahead and try it yourself.  This is a result of the particular dependency graph algorithms that GNU Make employs. But here's what will really bake your noodle: f you reverse the order in which the output files are listed as prerequisites of all, suddenly the files are generated only once, because those algorithms are very sensitive to the order in which dependencies are declared.  So this approach seems to work, but it's too brittle to be considered seriously.

Another attempt
It seems that we might be able to fix some of the problems with our first crude attempt by rewriting the makefile so that there are no commands for one of the two targets:

all: parser.h parser.c

parser.c: parser.i
@echo Generating parser.h and parser.c from parser.i
@sleep 2
@touch parser.c parser.h

parser.h: parser.c

About the author

Eric Melski's picture Eric Melski

Eric is Chief Architect for ElectricAccelerator, a high-performance implementation of make from Electric Cloud, Inc.  He obtained a BS in Computer Science from the University of Wisconsin in Madison in 1999.  In 2002 Eric co-founded Electric Cloud, where he has spent more than a decade developing distributed, parallel systems designed to accelerate build processes.  He is named on seven patents related to his work on build acceleration at Electric Cloud.

CMCrossroads is one of the growing communities of the TechWell network.

Featuring fresh, insightful stories, TechWell.com is the place to go for what is happening in software development and delivery.  Join the conversation now!