Targets are the entities that are acted upon (updated, deleted, etc.), and actions are the transformation you apply to them (update, delete, etc.). Usually, the targets are files or group of files.
The make tool
introduced so-called phony targets. For example, "
clean" or "
make touch" don't build a dependency
tree, they just perform some action on some files. This is unlike
make my_exe", which first constructs a dependency tree and
then performs some action on the nodes in this tree. The make tool puts
both the names of the targets to be built and the name of the actions in
the same namespace. This is ok for amateur software projects, but it is
a serious limitation for real products.
To start with, there is the trivial problem of the name clashes.
"clean", "depends", "test", etc. are frequent names for "targets" and
also a pain in the neck when you want to make an executable named
"clean", "depends", "test", etc. If you think that it's easy to just be
careful and not give such names to what you want to build, you are
simply wrong. Think about a code base with 10,000 C files and 100
contributors and how you would go about making sure that no file will be
named clean.c, test.c, etc. (the "etc." here really means an open-ended
set). The opposite has to be watched, too: there can be no phony target
with a name that happens to be the name of some file in the dependency
tree. This is technically possible, but it is not worth the trouble. It
is possible to separate targets and actions by, for example, "
action=clean". It would have been even better if the tool had
that already implemented for you, as in "
(because in the alternative "make action=clean" you still have some
careful implementation to make in your makefiles).
Besides the name clashes, there is a more subtle and more important
problem with all phony targets. The problem is that it is very difficult
to have the action of phony targets performed on the same files as the
non-phony targets. Is it important to act on the same files? Well,
again, it is not that important in very small projects. Let's consider
again the example of "
make clean". If this removes all
binary files with a file pattern, it is probably good enough. Even
removing the entire directory with the build output (with "rm -R") might
be ok. There are some (but not many) projects for which building from
scratch takes a whole day on a capable computing farm.
More important than wasting build time, there may be serious consistency
problems. Assume that, in some location in your code base, you can build
two targets that don't have an exact set of files. It may be "
target_A" and "
make target_B", or it may be
make target flavor=1st" and "
flavor=2nd". Now, just what is "
make clean" supposed
to do? Remove the files associated with target A, or with target B, or
with both? Again, in small C/C++ projects, this is no serious problem
(no flavors, no targets with a variable number of files, etc.), but the
same can't be said of larger projects (perhaps using something
completely different from C/C++ compilation). Contrast this with the
situation in which you could say "
make target_A --clean"
make target_B --clean". There, the tool can implement
laser sharp cleaning. As a developer, you would only have to get the
dependency tree right. Once it's correct, it's correct for all actions
-- update, clean, etc. That would be the end of files overlooked in
Despite the fact that these problems have been known since the early 1990s, we keep seeing new build tools with the same confusion between targets and actions on targets. For example, Ant has only phony targets, rake has both file targets and non-file targets, etc.
The commandline of make is ill designed. It is probably the worst commandline among all tools that have ever been used by more than one person, yet its style is mimicked today by almost all want-to-be build tools.
A first problem, quite trivial, is the fact that the make command by
itself, without any argument, commits something. Moreover, it commits
something that is impossible to revert. Why do you care? Because if
something unexpected happens, you will want to roll back and debug or
just run again in a more verbose mode. There's no such possibility with
make, and that's just to save typing 2-3 keys (like "
When run without arguments, the majority of commandline tools in this
world tell you something: "What is this?", "How do I use it?", etc. They
don't do something irreversible. Does this sound like good design to
you? Of course, you can do that with make also (as with "
action=update" and having "make" just print out which final target file
will be built), but how many make-based build systems do you know that
actually do it?
As a side effect, this "commit by default" scares away whoever may want to try a partial build. You may descend into a lower-level source directory and run "make" there, but if it builds more than you expect, it's too late. This aspect, coupled with some other shortcomings, discourages users of make from "starting small" when trying something new.
The most far-reaching bad habit of the make commandline is the fact that too much information is transmitted to the tool by choosing a directory from which to run it.
First, let's get agreement on the basic facts. You run a commandline in one location in the code base, and it builds something. You run the exact same commandline with the same shell environment in another location, and it builds something else. Some information is passed through the name of the current directory, and it is an important piece of information, not just a marginal one like, for example, the desired verbosity level.
The first question is also the most difficult to answer: Just what exactly is passed through the name of the current directory? Think about how would you describe this in a limited space, as in a ten-line usage message.
One may say that the answer is easy: "The current directory serves only
one purpose, fetching a Makefile, which tells make what else to do."
That is so incomplete that we can simply say that it is wrong. Most
importantly, the current directory still dictates a lot of things inside
that Makefile. The same Makefile in another location may do something
else (or just fail to do anything). Secondly, I can use the commandline
-f <path_to_makefile> to fetch it from
elsewhere (and some make-based build systems use that at lower levels if
you run "make" at the top of a build tree).
The fact is that the current directory may mean almost all (what to
build, from what, and which flavor) and may mean not much (for example,
with "what" and "how" taken from the shell environment). That's already
bad. It means that my make-based build system may be very different
from yours. It also makes this basically impossible to document in a
generic way (by the build tool authors, not by the build description
authors). Contrast this with a commandline like
--source_root=<dir> --output_root=<dir>. That can be
may have default values if you are keen on saving typing (even with the
current directory as the default). That syntax would make my build
system differ from yours less in total and in less fundamental ways.
In most make-based build systems, the current directory tells what to build and where to put it, but some systems take this a bit further and let you choose which flavor you build by firing make in a different subdirectory. Consider as example the following directory layout:
dir_my_exe i86_mswin32 english Makefile french Makefile german Makefile Makefile_level1 i86_linux2 english Makefile french Makefile german Makefile Makefile_level1 inc src Makefile_common
where the content of the files in the inner directories is the following:
Makefile: LANGUAGE = ENGLISH include ../Makefile_level1
Makefile_level1: PLATFORM = I86_MSWIN32 include ../Makefile_common
This describes, in fact, two variants (let's call them "platform" and "language") with, respectively, two and three allowed values. You can issue "make" in the end leaf locations, and you get a flavor of "my_exe" built. This may look extreme, but many systems are fundamentally doing just this kind of unfolding of parameters, including the good old GNU Build System (the ubiquitous one based on autoconf and GNU make).
There are many issues with this kind of approach. How do you decide what you keep as a commandline argument and what you specify through the current directory? What effort is needed to introduce a new allowed value for the language? Or a new value for the platform? Or a new variant? How would you go about documenting the available variants and the allowed values for each of them? You may place cross-linked Readme files next to the Makefiles, but how many codebases you know that have that?
Contrast this with the commandline "
language=<lang>". Such a commandline makes it easy to
document what variants are there and what values are allowed. It doesn't
introduce any arbitrary order among variants (which you will have to
learn and remember), and it is probably easier to maintain during the
lifetime of the codebase.
Sometimes this possibility is implemented as well, so that the lines:
LANGUAGE ?= ENGLISH PLATFORM ?= I86_MSWIN32
are put in
Makefile_common, along with some tricks so that
the build result goes to the same location as it would if you fired
"make" in a leaf directory. But why bother when you can just use
commandline arguments instead of the current directory information from
the beginning of your build description?
The user community quickly learned the benefit of having separate root directories for the sources and for the build output. Let's call them the source directory and the binary directory, respectively.
There are many good and bad reasons to separate. One bad reason, in the
case of make-based systems, is the fact that make's timestamp checks are
not reliable enough and the clean is not very precise (so a manual
complete cleaning is needed at times, and that's most practical with
rm -R" on the binary directory). Whatever the reasons,
the fact is that you have a source directory structure and a binary
directory structure that are somewhat similar. Usually, the binary
directory tree is "broader" in order to be able to store all derived
files in several flavors without filename clashes.
Here's the tricky question: Where do you put your makefiles? There are two opposed styles:
Mixed solutions are also frequently used. For example, you start "make" somewhere in the binary directory (you specify the output path through the shell's current directory) and you point it to a Makefile "close to the sources" (you specify the location of the sources by explicitly giving the location of the Makefile).
Do we really need all these possibilities? It's true that a good build
system must be able to accommodate almost any organization of sources,
and it must let you design any sensible binary directory structure. But
how exactly does the use of the current directory of the shell help with
that? What sensible directory structure is made impossible by a
commandline syntax like "
--output_root=<dir>"? The use of the current directory by
make is not flexibility from the make tool, it is just useless
variability in the many make-based build systems.
One last word about the myth of build descriptions "close to sources". In large real-life C/C++ projects, it is not uncommon to have hundreds of directories containing source files. What does it mean to be "close" to 500 different locations? Are you going to spread your build description over 500 small chunks? That's a possibility, but then each build description will contain very little information, probably just a very few trivial lines (often the real information is then the full path of the directory where the description is placed). Smarter make-based build systems moved away from that model at the same time they moved away from the recursive make model. That means that the entire build description is in one place, probably close to the root source directory tree (and possibly "far" from the deepest directory with sources).
I hope that you are convinced by now that using the current directory instead of commandline arguments is not a good idea. If you're still not convinced, try to count how many commandline tools you know that use the current directory to pass crucial information. Any compilers? Linkers? Debuggers? Others?
One of the first actions that one would implement on a dependency tree is the ability to print it out. Several flavors are interesting to print: all genuine input, all build output, all implicit dependencies, etc. Personally, before I would implement "incremental build", "forced build", "clean", etc., I would implement "print", and I would test whether my tree is as expected in less trivial cases.
How many systems have something like "
action=listsources"? Even worse, a depressing number of new build
tools are proposed without such a simple, basic feature. What hope then
for more elaborate, yet useful actions like "clean all files that are
Make doesn't go a long way toward modelling important concepts like the C/C++ tool chain, build platforms, target platforms, optimization levels, etc. This is presented as flexibility; you are free to model them as you see fit, with any set of shell variables, any piece of shared Makefile, etc. Ok, let's buy it as freedom. But there is one place where this freedom hurts badly: The lack of separation between the build platform and the target platform. Even proposing some weak separation (like a few conventional macros or macro prefixes) would have been better than nothing at all. It may well be that the majority of the compilations in this world are not cross-compilations, but that thought will not alleviate the pain of the embedded software engineer. It will only make him curse harder.
When designing a build system, based on make or not, it is not smart to cut yourself out of the community of embedded porting engineers. The porting engineer is usually more closely involved with the build than the average mainstream platform developer. An important part of the software porting process to embedded platforms is the change/adaptation of the build description, and it had better not be a complete rewrite of the build description (to another build system).
It is sad that most build tools since "make" have shown no courtesy
toward embedded software engineers. Moreover, the most recent build
tools only make it worse. How many of these recent build tools have an
option like "
tgtplatform=<plat>" to choose what to
Some recent build tools expect to configure everything on the fly when you start a build, and they are proud to present this as progress. They say, "Look, no configuration needed!" They actually detect installed tool chains by some investigation of the build platform they are running on. While this is a desirable feature when not cross-compiling, it will most likely be a pain for the porting engineer. His tool chain will not be detected. He will have to do some manual configuration (which is acceptable), but he will also have to implement something that allows him to switch tool chains (because any porting involves reference builds for the build platform itself). He now has more work than with his previous, "dumber" build system.
With a commandline like "
the make tool had the opportunity to introduce the world to the fact
that cross-compilation exists, but it missed it. When will the chance
I've listed only a few shortcomings of "make" that tend to survive longer than "make" itself. There are others, but the point is not to count them, not even to get a "total weight" of them. The point is to get in the habit of questioning whether some way of doing something is still the most appropriate way for the situation at hand, to compare alternatives and not choose by inertia. You may discover a new world that feels even cozier than your previous one.