Rambles around computer science
Diverting trains of thought, wasting precious time
Wed, 19 Nov 2014
How to write vaguely acceptable makefiles
It's not hard to write a clear, orderly makefile if you respect
it as a programming job in its own right, and adopt some principles.
I rarely see such principles written down.
Here's what I've gravitated towards
as I've learnt to use make more effectively.
- Get your source tree in order. No amount of build cleverness can make up
for an ill-defined directory structure.
Figure out how you want the tree to look.
However you do it, it should maximise regularity.
- Decide what is your software and what is third-party.
It's not your makefile's
job to build third-party software. Document the dependency and provide a
configuration mechanism for pointing your makefile at the third-party stuff.
- Don't write a portable makefile; use a portable make.
This is stolen verbatim (modulo memory) from the GNU make manual,
and I will be using GNU make from hereon.
The point is not about which exact version of make you use;
it's that you should pick one that runs in all the build environments you care about,
then use it wholeheartedly—instead of
trying to accommodate every potential user's make in a lowest-common-denominator fashion.
If you do accommodate others, it should be
only to the extent of using them to build GNU make
(or whatever your chosen make is) and then
using the result to continue your build.
- Use prerequisites.
If a file is input to your recipe, make it a prerequisite.
This is the one of the most basic ideas in make.
- Use targets.
If a file is built by your recipe, make it a target.
This is the other most-basic idea in make.
Note that a rule's target can include more than one output file.
- Don't use phony targets except where necessary.
This follows from
the previous two rules. A phony target is one whose target doesn't name a real file.
Some phony targets are useful (like clean and default)
but I often see makefiles with many phony targets that
could be made non-phony. (Also, mark your phony targets using .PHONY!
If you don't, you will eventually get bitten by unexpected behaviour.)
- Factor your recipes.
If a recipe is generating more than one file across more than one step,
it needs to be split up.
Separate out logically separate outputs into their own target wherever possible.
Recipes consisting of long chains of commands, generating outputs at multiple stages,
are an undesirable feature (an antipattern,
in fact).
- Use variables.
Target names and prerequisite names can be constructed by variable
expansion. This affords a lot of flexibility for custom build configurations.
(But note that variables aren't the right mechanism for everything.
Generally, they only work for things that are logically
fixed at the point where make is invoked.)
-
Keep details of your system out of makefiles.
To do this, you can include a second makefile (with whatever name you like,
but often something like config.mk) which defines variables
detailing the configuration, such as paths to various tools and codebases,
from somewhere inside your makefile.
Then your makefile can use “?=” to
supply “sensible default” definitions only for configuration details
that are otherwise unspecified.
It's best to do this “?=”-assignment near the top of the makefile.
If/when you get around to using a fancier configuration system like autoconf
it will provide a more-or-less drop-in replacement for the included files.
Alternatively with GNU make you can use a file called makefile
then includes Makefile. Doing it this way around can be neater if you're
messing with a third-party makefile that you don't want to modify.
- Know the basic functions.
Functions like patsubst, wildcard and
filter are provided for a reason.
Some others that I use a fair bit are realpath and
sort.
Get to know make's repertoire of useful functions and how they work.
- Use recursive make sparingly.
Just because something's in a subdirectory doesn't mean you
have to use recursive make, and sometimes it's better that you don't.
In general, recursive make is for logically separate subprojects,
such that you might want to build them completely independently
of your overarching project.
The lack of dependency handling across recursive make
means it can be a bad idea (hence this classic).
- Understand lazy versus eager expansion, and prefer eager. In GNU make this is
the difference between
“FOO = bar”
and “FOO := bar”.
Usually you want the latter.
- Don't clobber the environment. The environment is part of the interface between
a make-based build system and its user. If the user wants CFLAGS to include something,
they should be able to communicate this in the environment.
To set CFLAGS, append to it (“+=” in GNU make) rather than assigning to it afresh.
- Don't pass variables to sub-makes on their command line;
pass them in the environment.
The semantics of “$(MAKE) VAR=xxx target”
are one of the most surprising behaviours of make:
any assignment to VAR in the makefile will be ignored!
Usually you instead want to do "VAR=xxx $(MAKE) target”.
- Copying stuff around is an antipattern.
It generally shouldn't happen. Sometimes there can be
exceptions, but it's normally a sign that the source tree organisation isn't right,
or that there is third-party software in the tree,
or that you're doing something ill-advised to get the effect of a separate build directory (see below),
and/or that you should be using vpath
(ditto).
-
Shell scripts in the build tree are generally an antipattern.
Use make to invoke the shell commands you want.
That way, you can arrange that they're only run when necessary.
You can still run them from the command line (make -B)
and view them in isolation (make -n)
if you want to.
-
Understand depfiles.
They are a pleasingly powerful (albeit imperfect) way to
accommodate a lot of different build tools.
The mechanism by which they work—make re-making included makefiles—is
non-obvious and slightly head-twisting at first, but it works nicely.
-
Use stock rules wherever possible.
If you're just compiling a .c file to a .o file,
there should be no need to write your own make rule. Just use CFLAGS
to add any extra flags
you need, LDFLAGS for any link-time flags, and LDLIBS to add any extra libraries.
-
Use the well-known variables when writing recipes.
If you do have to write your own rule,
don't hard-code things like the C compiler.
Use the same conventions that the built-in rules use; the C compiler is always $(CC).
-
Understand vpath.
This is a mechanism to help your build to use source files that live
in another source tree.
It only works if the command that need these source files
have their input described to make, using pseudovariables like $+. Contrast
foo: blah.c bar.c
$(CC) -o foo blah.c bar.c
with
foo: blah.c bar.c
$(CC) -o foo $+
In the latter example, make would know how to find blah.c
and/or bar.c in another
directory tree, if you had told it about this using vpath.
In the former example, this won't work.
This is another reason to use prerequisites:
by allowing make to expand the prerequisite filename into the recipe's commands,
it can also account for any vpaths that are in place.
-
The working directory is the build directory.
If you want a separate build directory from your source directory,
you can do it very straightforwardly with a little discipline
and the right use of make.
Importantly, knowledge of the build directory should not pollute all your rules.
Instead, assume that make's current working directory is the build directory.
A remote source directory can be accommodated
with VPATH, vpath and/or use of a $(srcdir)
variable.
The makefile itself should exist only in the source directory.
To see how, put your favourite one-file C program in foo.c
and try the following GNU makefile (with helpful warning messages so you can see
what's happening). To use a separate build directory, use make -f
to invoke the makefile from that directory.
THIS_MAKEFILE := $(lastword $(MAKEFILE_LIST))
srcdir := $(dir $(THIS_MAKEFILE))
ifneq ($(realpath .),$(realpath $(srcdir)))
$(warning Build directory is $(realpath .))
$(warning Putting $(srcdir) in VPATH)
VPATH := $(srcdir):$(VPATH)
endif
foo: foo.c
[/devel]
permanent link
contact
validate this page