Why you should consider using separate build directories

One of the grand Unix traditions is that source code is built directly inside the source tree. This is the simple approach, which has been used for decades. In fact, most people do not even consider doing something else, because this is the way things have always been done.

The alternative to an in-source build is, naturally, an out-of-source build. In this build type you create a fresh subdirectory and all files generated during the build (object files, binaries etc) are written in that directory. This very simple change brings about many advantages.

Multiple build directories with different setups

This is the main advantage of separate build directories. When developing you typically want to build and test the software under separate conditions. For most work you want to have a build that has debug symbols on and all optimizations disabled. For performance tests you want to have a build with both debug and optimizations on. You might want to compile the code with both GCC and Clang to test compatibility and get more warnings. You might want to run the code through any one of the many static analyzers available.

If you have an in-source build, then you need to nuke all build artifacts from the source tree, reconfigure the tree and then rebuild. You also need to return the old settings because you probably don’t want to run a static analyzer on your day-to-day development work, mostly because it is up to 10 times slower than a non-optimized build.

Separate build directories provide a nice solution to this problem. Since all their state is stored in a separate build directory, you can have as many build directories per one source directory as you want. They will not stomp on each other. You only need to configure your build directories once. When you want to build any specific configuration, you just run Make/Ninja/whatever in that subdirectory. Assuming your build system is good (i.e. not Autotools with AM_MAINTAINER_MODE hacks) this will always work.

No need to babysit generated files

If you look at the .bzrignore file of a common Autotools project, it typicaly has on the order of a dozen or so rules for files such as Makefiles, Makefile.ins, libtool files and all that stuff. If your build system generates .c source files which it then compiles, all those files need to be in the ignore file. You could also have a blanket rule of ‘*.c’ but that is dangerous if your source tree consists of handwritten C source. As files come and go, the ignore file needs to be updated constantly.

With build directories all this drudgery goes away. You only need to add build directory names to the ignore file and then you are set. All new source files will show up immediately as will stray files. There is no possibility of accidentally masking a file that should be checked in revision control. Things just work.

Easy clean

Want to get rid of a certain build configuration? Just delete the subdirectory it resides in. Done! There is no chance whatsoever that any state from said build setup remains in the source tree.

Separate partitions for source and build

This gets into very specific territory but may be useful sometimes. The build directory can be anywhere in the filesystem tree. It can even be on a different partition. This allows you to put the build directory on a faster drive or possibly even on ramdisk. Security conscious people might want to put the source tree on a read-only (optionally a non-execute) file system.

If the build tree is huge, deleting it can take a lot of time. If the build tree is in a BTRFS subvolume, deleting all of it becomes a constant time operation. This may be useful in continuous integration servers and the like.

Conclusion

Building in separate build directories brings about many advantages over building in-source. It might require some adjusting, though. One specific thing that you can’t do any more is cd into a random directory in your source tree and typing make to build only that subdirectory. This is mostly an issue with certain tools with poor build system integration that insist on running Make in-source. They should be fixed to work properly with out-of-source builds.

If you decide to give out-of-tree builds a try, there is one thing to note. You can’t have in-source and out-of-source builds in the same source tree at the same time (though you can have either of the two). They will interact with each other in counter-intuitive ways. The end result will be heisenbugs and tears, so just don’t do it. Most build systems will warn you if you try to have both at the same time. Building out-of-source may also break some applications, typically tests, that assume they are being run from within the source directory.

Building C/C++: what really happens and why does it take so long

A relatively large portion of software development time is not spent on writing, running, debugging or even designing code, but waiting for it to finish compiling. This is usually seen as necessary evil and accepted as an unfortunate fact of life. This is a shame, because spending some time optimizing the build system can yield quite dramatic productivity gains.

Suppose a build system takes some thirty seconds to run for even trivial changes. This means that even in theory you can do at most two changes a minute. In practice the rate is a lot lower. If the build step takes only a few seconds, trying out new code becomes a lot faster. It is easier to stay in the zone when you don’t have to pause every so often to wait for your tools to finish doing their thing.

Making fundamental changes in the code often triggers a complete rebuild. If this takes an hour or more (there are code bases that take 10+ hours to build), people try to avoid fundamental changes as much as possible. This causes loss of flexibility. It becomes very tempting to just do a band-aid tweak rather than thoroughly fix the issue at hand. If the entire rebuild could be done in five to ten minutes, this issue would become moot.

In order to make things fast, we first have to understand what is happening when C/C++ software is compiled. The steps are roughly as follows:

  1. Configuration
  2. Build tool startup
  3. Dependency checking
  4. Compilation
  5. Linking

We will now look at each step in more detail focusing on how they can be made faster.

Configuration

This is the first step when starting to build. Usually means running a configure script or CMake, Gyp, SCons or some other tool. This can take anything from one second to several minutes for very large Autotools-based configure scripts.

This step happens relatively rarely. It only needs to be run when changing configurations or changing the build configuration. Short of changing build systems, there is not much to be done to make this step faster.

Build tool startup

This is what happens when you run make or click on the build icon on an IDE (which is usually an alias for make). The build tool binary starts and reads its configuration files as well as the build configuration, which are usually the same thing.

Depending on build complexity and size, this can take anywhere from a fraction of a second to several seconds. By itself this would not be so bad. Unfortunately most make-based build systems cause make to be invocated tens to hundreds of times for every single build. Usually this is caused by recursive use of make (which is bad).

It should be noted that the reason Make is so slow is not an implementation bug. The syntax of Makefiles has some quirks that make a really fast implementation all but impossible. This problem is even more noticeable when combined with the next step.

Dependency checking

Once the build tool has read its configuration, it has to determine what files have changed and which ones need to be recompiled. The configuration files contain a directed acyclic graph describing the build dependencies. This graph is usually built during the configure step. Suppose we have a file called SomeClass.cc which contains this line of code:

#include "OtherClass.hh"

This means that whenever OtherClass.hh changes, the build system needs to rebuild SomeClass.cc. Usually this is done by comparing the timestamp of SomeClass.o against OtherClass.hh. If the object file is older than the source file or any header it includes, the source file is rebuilt.

Build tool startup time and the dependency scanner are run on every single build. Their combined runtime determines the lower bound on the edit-compile-debug cycle. For small projects this time is usually a few seconds or so. This is tolerable.

The problem is that Make scales terribly to large projects. As an example, running Make on the codebase of the Clang compiler with no changes takes over half a minute, even if everything is in cache. The sad truth is that in practice large projects can not be built fast with Make. They will be slow and there’s nothing that can be done about it.

There are alternatives to Make. The fastest of them is Ninja, which was built by Google engineers for Chromium. When run on the same Clang code as above it finishes in one second. The difference is even bigger when building Chromium. This is a massive boost in productivity, it’s one of those things that make the difference between tolerable and pleasant.

If you are using CMake or Gyp to build, just switch to their Ninja backends. You don’t have to change anything in the build files themselves, just enjoy the speed boost. Ninja is not packaged on most distributions, though, so you might have to install it yourself.

If you are using Autotools, you are forever married to Make. This is because the syntax of autotools is defined in terms of Make. There is no way to separate the two without a backwards compatibility breaking complete rewrite. What this means in practice is that Autotool build systems are slow by design, and can never be made fast.

Compilation

At this point we finally invoke the compiler. Cutting some corners, here are the approximate steps taken.

  1. Merging includes
  2. Parsing the code
  3. Code generation/optimization

Let’s look at these one at a time. The explanations given below are not 100% accurate descriptions of what happens inside the compiler. They have been simplified to emphasize the facets important to this discussion. For a more thorough description, have a look at any compiler textbook.

The first step joins all source code in use into one clump. What happens is that whenever the compiler finds an include statement like #include “somefile.h”, it finds that particular source file and replaces the #include with the full contents of that file. If that file contained other #includes, they are inserted recursively. The end result is one big self-contained source file.

The next step is parsing. This means analyzing the source file, splitting it into tokens and building an abstract syntax tree. This step translates the human understandable source code into a computer understandable unambiguous format. It is what allows the compiler to understand what the user wants the code to do.

Code generation takes the syntax tree and transforms it into machine code sequences called object code. This code is almost ready to run on a CPU.

Each one of these steps can be slow. Let’s look at ways to make them faster.

Faster #includes

Including by itself is not slow, slowness comes from the cascade effect. Including even one other file causes everything included in it to be included as well. In the worst case every single source file depends on every header file. This means that touching any header file causes the recompilation of every source file whether they use that particular header’s contents or not.

Cutting down on interdependencies is straightforward. Only #include those headers that you actually use. In addition, header files must not include any other header files if at all possible. The main tool for this is called forward declaration. Basically what it means is that instead of having a header file that looks like this:

#include "SomeClass.hh"

class MyClass {
  SomeClass s;
};

You have this:

class SomeClass;

class MyClass {
  SomeClass *s;
}

Because the definition of SomeClass is not know, you have to use pointers or references to it in the header.

Remember that #including MyClass.hh would have caused SomeClass.hh and all its #includes to be added to the original source file. Now they aren’t, so the compiler’s work has been reduced. We also don’t have to recompile the users of MyClass if SomeClass changes. Cutting the dependency chain like this everywhere in the code base can have a major effect in build time, especially when combined with the next step. For a more detailed analysis including measurements and code, see here.

Faster parsing

The most popular C++ libraries, STL and Boost, are implemented as header only libraries. That is, they don’t have a dynamically linkable library but rather the code is generated anew into every binary file that uses them. Compared to most C++ code, STL and Boost are complex. Really, really complex. In fact they are most likely the hardest pieces of code a C++ compiler has to compile. Boost is often used as a stress test on C++ compilers, because it is so difficult to compile.

It is not an exaggeration to say that for most C++ code using STL, parsing the STL headers is up to 10 times slower than parsing all the rest. This leads to massively slow build times because of class headers like this:

#include <vector>

class SomeClass {
private:
  vector<int> numbers;

public:
  ...
};

As we learned in the previous chapter, this means that every single file that includes this header must parse STL’s vector definition, which is an internal implementation detail of SomeClass and even if they would not use vector themselves. Add some other class include that uses a map, one for unordered_map, a few Boost includes and what do you end up with? A code base where compiling any file requires parsing all of STL and possibly Boost. This is a factor of 3-10 slowdown on compile times.

Getting around this is relatively simple, though takes a bit of work. It is known as the pImpl idiom. One way of achieving it is this:

---header---

struct someClassPrivate;

class SomeClass {
private:
  someClassPrivate *p;
};

---- implementation ---
#include <vector>
struct someClassPrivate {
  vector<int> numbers;
};

SomeClass::SomeClass() {
  p = new someClassPrivate;
}

SomeClass::~SomeClass() {
  delete p;
}

Now the dependency chain is cut and users of SomeClass don’t have to parse vector. As an added bonus the vector can be changed to a map or anything else without needing to recompile files that use SomeClass.

 Faster code generation

Code generation is mostly an implementation detail of the compiler, and there’s not much that can be done about it. There are a few ways to make it faster, though.

Optimizing code is slow. In every day development all optimizations should be disabled. Most build systems do this by default, but Autotools builds optimized binaries by default. In addition to being slow, this makes debugging a massive pain, because most of the time trying to print the value of some variable just prints out “value optimised out”.

Making Autotools build non-optimised binaries is relatively straightforward. You just have to run configure like this: ./configure CFLAGS=’O0 -g’ CXXFLAGS=’-O0 -g’. Unfortunately many people mangle their autotools cflags in config files so the above command might not work. In this case the only fix is to inspect all autotools config files and fix them yourself.

The other trick is about reducing the amount of generated code. If two different source files use vector<int>, the compiler has to generate the complete vector code in both of them. During linking (discussed in the next chapter) one of them is just discarded. There is a way to tell the compiler not to generate the code in the other file using a technique that was introduced in C++0x called extern templates. They are used like this.

file A:

#include <vector>
template class std::vector<int>;

void func() {
  std::vector<int> numbers;
}

file B:

#include <vector>
extern template class std::vector<int>;

void func2() {
  std::vector<int> idList;
}

This instructs the compiler not to generate vector code when compiling file B. The linker makes it use the code generated in file A.

Build speedup tools

CCache is an application that stores compiled object code into a global cache. If the same code is compiled again with the same compiler flags, it grabs the object file from the cache rather than running the compiler. If you have to recompile the same code multiple times, CCache may offer noticeable speedups.

A tool often mentioned alongside CCache is DistCC, which increases parallelism by spreading the build to many different machines. If you have a monster machine it may be worth it. On regular laptop/desktop machines the speed gains are minor (it might even be slower).

Precompiled headers

Precompiled headers is a feature of some C++ compilers that basically serializes the in-memory representation of parsed code into a binary file. This can then be read back directly to memory instead of reparsing the header file when used again. This is a feature that can provide massive speedups.

Out of all the speedup tricks listed in this post, this has by far the biggest payoff. It turns the massively slow STL includes into, effectively, no-ops.

So why is it not used anywhere?

Mostly it comes down to poor toolchain support. Precompiled headers are fickle beasts. For example with GCC they only work between two different compilation units if the compiler switches are exactly the same. Most people don’t know that precompiled headers exist, and those that do don’t want to deal with getting all the details right.

CMake does not have direct support for them. There are a few modules floating around the Internet, but I have not tested them myself. Autotools is extremely precompiled header hostile, because its syntax allows for wacky and dynamic alterations of compiler flags.

Faster Linking

When the compiler compiles a file and comes to a function call that is somewhere outside the current file, such as in the standard library or some other source file, it effectively writes a placeholder saying “at this point jump to function X”. The linker takes all these different compiled files and connects the jump points to their actual locations. When linking is done, the binary is ready to use.

Linking is surprisingly slow. It can easily take minutes on relatively large applications. As an extreme case, linking the Chromium browser on ARM takes 3 gigs of RAM and takes 18 hours.

Yes, hours.

The main reason for this is that the standard GNU linker is quite slow. Fortunately there is a new, faster linker called Gold. It is not the default linker yet, but hopefully it will be soon. In the mean time you can install and use it manually.

A different way of making linking faster is to simply cut down on these symbols using a technique called symbol visibility. The gist of it is that you hide all non-public symbols from the list of exported symbols. This means less work and memory use for the linker, which makes it faster.

Conclusions

Contrary to popular belief, compiling C++ is not actually all that slow. The STL is slow and most build tools used to compile C++ are slow. However there are faster tools and ways to mitigate the slow parts of the language.

Using them takes a bit of elbow grease, but the benefits are undeniable. Faster build times lead to happier developers, more agility and, eventually, better code.

Relative popularity of build systems

The conventional wisdom in build systems is that GNU Autotools is the one true established standard and other ones are used only rarely.

But is this really true?

I created a script that downloads all original source packages from Ubuntu’s main pool. If there were multple versions of the same project, only the newest was chosen. Then I created a second script that goes through those packages and checks what build system they actually use. Here’s the breakdown:

CMake:           348     9%
Autofoo:        1618    45%
SCons:            10     0%
Ant:             149     4%
Maven:            41     1%
Distutil:        313     8%
Waf:               8     0%
Perl:            341     9%
Make(ish):       351     9%
Customconf:       45     1%
Unknown:         361    10%

Here Make(ish) means packages that don’t have any other build system, but do have a makefile. This usually indicates building via custom makefiles. Correspondingly customconf is for projects that don’t have any other build system, but have a configure file. This is usually a handwritten shell or Python file.

This data is skewed by the fact that the pool is just a jumble of packages. It would be interesting to run this analysis separately for precise, oneiric etc to see the progression over time. For truly interesting results you would run it against the whole of Debian.

The relative popularity of CMake and Autotools is roughly 20/80. This shows that Autotools is not the sole dominant player for C/C++ it once was. It’s still far and away the most popular, though.

The unknown set contains stuff such as Rake builds. I simply did not have time to add them all. It also has a lot of fonts, which makes sense, since you don’t really build those in the traditional sense.

The scripts can be downloaded here. A word of warning: to run the analysis you need to download 13 GB of source. Don’t do it just for the heck of it. The parser script does not download anything, it just produces a list of urls. Download the packages with wget -i.

Some orig packages are compressed with xz, which Python’s tarfile module can’t handle. You have to repack them yourself prior to running the analysis script.

Speed bumps hide in places where you least expect them

The most common step in creating software is building it. Usually this means running make or equivalent and waiting. This step is so universal that most people don’t even think about it actively. If one were to see what the computer is doing during build, one would see compiler processes taking 100% of the machine’s CPUs. Thus the system is working as fast as it possibly can.

Right?

Some people working on Chromium doubted this and built their own replacement of Make called Ninja. It is basically the same as Make: you specify a list of dependencies and then tell it to build something. Since Make is one of the most used applications in the world and has been under development since the 70s, surely it is as fast as it can possibly be done.

Right?

Well, let’s find out. Chromium uses a build system called Gyp that generates makefiles. Chromium devs have created a Ninja backend for Gyp. This makes comparing the two extremely easy.

Compiling Chromium from scratch on a dual core desktop machine with makefiles takes around 90 minutes. Ninja builds it in less than an hour. A quad core machine builds Chromium in ~70 minutes. Ninja takes ~40 minutes. Running make on a tree with no changes at all takes 3 minutes. Ninja takes 3 seconds.

So not only is Ninja faster than Make, it is faster by a huge margin and especially on the use case that matters for the average developer: small incremental changes.

What can we learn from this?

There is an old (and very wise) saying that you should never optimize before you measure. In this case the measurement seemed to indicate that nothing was to be done: CPU load was already maximized by the compiler processes. But sometimes your tools give you misleading data. Sometimes they lie to you. Sometimes the “common knowledge” of the entire development community is wrong. Sometimes you just have to do the stupid, irrational, waste-of-time -thingie.

This is called progress.

PS I made quick-n-dirty packages of a Ninja git checkout from a few days ago and put them in my PPA. Feel free to try them out. There is also an experimental CMake backend for Ninja so anyone with a CMake project can easily try what kind of a speedup they would get.

Why GNU Autotools is not my favorite build system

The GNU Autotools are nothing short of an astounding engineering achievement. They can configure, compile and install software ranging from supercomputers all the way down to a SunOS 1.0 from 1983, or thereabouts. It can even do shared libraries portably, which is considered to be among the darkest of magicks on many of these platforms.

This technological triumph does not change the fact that using Autotools is awful. The feeling of using Autotools is not totally unlike trying to open a safe by hitting it repeatedly with your head. Here are just some ways they cause pain to developers.

Complexity is standard

If I were to describe Autotools with just one word, it would be this:

Ununderstandable

It’s not that Autotools is hard, a lot of systems are. It’s not that it has a weird syntax, lots of programs have that too. It’s that every single thing about Autotools seem to be designed to be as indecipherable as possible.

Suppose that a developer new to Autotools opens up a configure.ac for the first time. He might find lines such as these, which I took from a real world project:

AM_CONDITIONAL([HAVE_CHECK],[test "x$have_check" = xyes])

AM_INIT_AUTOMAKE([foreign dist-bzip2])
AM_MAINTAINER_MODE

Several questions immediately come to mind. Why are all function arguments quoted in brackets? What is AM_MAINTAINER_MODE? Why is it needed since I am not the maintainer of Automake? What is “xyes”? A misspelling of “xeyes” perhaps? Are the bracketed things arrays? Is space the splitting chracter? Why is the answer no on some locations and yes on others?

The interested reader might dig into these questions and a week or so later have answers. Most people don’t. They just get aggravated, copy existing files and hope that they work. A case in point: Autotools’ documentation states clearly that AM_MAINTAINER_MODE should not be used, yet it is in almost every Autotools project. It survives as a vestigial organ much like the appendix, because no-one wants to understand the system. And, just like the appendix, it sometimes tries to kill its host with a sudden inflammation.

My estimate is that there are less than 20 people in the entire world who truly understand Autotools. That is one hell of a small pool for something as fundamental as the most used build system in the Free software world.

A legacy of legacy

Autotools work by generating a Bourne shell (the one from the seventies) compatible configure script and makefiles (also of the type from the seventies). To do this they use a macro language called M4 (guess which decade this one is from). There is nothing inherently wrong with using tried and tested tools.

The big problem is that the designers did not do proper encapsulation. The source files to Autotools do not have one syntax. They have several all mixed together. The “xyes” thing
mentioned above is in fact a shell script snippet that gets included (eventually) to the main configure script. Make directives can also be added for extra fun.

The end result is that you have code snippets in several different languages mixed together arbitrarily. In addition to being tedious to read, they also make automatic code inspection all but impossible. For example most non-trivial Autoconf snippets give syntax errors in Eclipse’s Autotools editor due to missing and/or extraneous parentheses and so on (to be fair, most of these are bugs in Eclipse, but they are caused by the fact that parsing is so hard). The only way to find errors is to compile and run the code. Debugging the result is even harder.

Since Autotools is intertwingled with these tools and their idiosyncrasies, it can never be fixed, cleaned or substantially improved.

Surprising costs

One of the main goals of the C++ committee has been that you should never have to pay penalty for features you don’t use. If you don’t use virtual inheritance, your function calls are just as fast as in C. Don’t need RTTI? Just disable it. Don’t use templates? Then you
don’t get any extra bloat in your binaries.

Not so with Autotools.

Suppose you develop a program that uses GTK+ and D-bus. That implies a rather modern Linux program that will never run on, say, AIX 4.0. So you would probably want to throw away all the garbage dealing with that platform’s linking peculiarities (and everything else, too) from your build system. But you can’t.

Autotools is designed so that every single portion of it runs according to the lowest possible common denominator in the entire world (except when it doesn’t, we’ll come back to this). This has interesting consequences.

Bloat

The most common complaint about any piece of software is that it is bloated. For some reason this is never said of Autotools, even though it is one of the most bloated things in existance.

As an example let’s look at utouch-grail. It is a plain C library that detects multitouch gestures. It is a small to medium sized project. Analyzing it with Sloccount reveals that its configure script is three times larger than all C source (including headers) put together.

THREE! TIMES! LARGER!

This is even more astounding when you remember that the configure script is written in a high level scripting language, whereas the library is plain C.

If you look inside the configure script, one of the things you notice quite quickly is that it does not use shell functions. They are all unrolled. This is because the original plain Bourne Shell did not support functions (or maybe it did but they were broken in some versions of Ultrix or whatever). So Autotools will not use them in the code it generates. You pay the price whether you want to or not.

Slow

My friend once told me that if you have a multicore machine and update Gentoo on it, a fascinating thing happens. For most packages running configure takes a lot longer than the actual build. The reason being that configure is slow, and, even worse, unparallelizable.

A question of state

Computers are very good at remembering state. Humans are notoriously bad at it. Therefore the basic rule in interaction design is to never have the user remember state that the machine can either remember or deduce by itself. Autotools forces its user to keep all sorts of state needlessly in his head.

When the user has changed the code, he types make to compile it. This usually works. But when the files describing the build system are changed (Which ones? I really don’t know.) just running make will fail. Even worse it probably fails silently, claiming everything is fine but producing garbage.

In these cases the user has to manually run autogen.sh, autoreconf, or maybe something else, before make. Why? Why does the developer have to care?  Is it really too much for a build dependency tracker system to, you know, actually track dependencies?  To notice that a file that some other files depend on has changed and thus deduce the steps that
need taking? And take those steps automatically?

For Autotools the answer is yes. They force the user to keep state in his head needlessly. Since the human mind can only keep about 7 things in short term memory at any one time, this single choice needlessly wastes over 14% of developer brain capacity.

When are binaries not binaries?

When they are shell scripts that invoke magical incantations in hidden directories, of course. This little gem is courtesy of Libtool, which is a part of Autotools.

If your project uses shared libraries, Autotools does not actually build them until after you do “make install”. Instead it creates so-called convenience libraries and, in a step of utmost convenience, hides them from the developer. Since the actual libraries do not exist, binaries in the build tree cannot use them, ergo they are replaced with magical wrapper scripts.

By itself this would not be so bad, but what happens when you want to run the files under gdb or Valgrind? You either always run make install before debugging or you follow the instructions on this page:

http://www.delorie.com/gnu/docs/libtool/libtool_11.html

(At least they are honest, seeing that their breakpoint was put on the line with the statement ‘printf (“Welcome to GNU Hell!\n”)’.)

This decision again forces state on the user. How you run gdb, Valgrind or any other inspection tool depends on whether the program you build uses shared libraries or not. There goes another 14% of your brain. More if your project has several different kinds of
binaries.

Consistent lack of consistency

With Autotools you can never depend on anything being the same. So you have to jump through unnecessary hoops all the time. Say you want to have automated build service that does builds directly from revision control as well as release builds from tarballs.

To do this you need two different build rules, since the configure script is not in revision control you need to generate it in daily builds. But since source tarballs sometimes don’t contain autogen.sh you can’t always call that before configure. And indeed you shouldn’t,
you’re testing the release after all.

As an added bonus any patch that changes configure is guaranteed to be non-mergeable with any other patch that does the same. So be sure to specifically tell your diff program to ignore the configure file. But be sure to remember whether you did that or not every single time.

This is just one more way Autotools gives you more state to deal with. These kinds of annoying one-off things keep popping up in places where they really should not. They sap developers’ time and effort constantly.

Poor portability

Autoconf’s main claim to fame is that it is portable. The only requirement it has, as mentioned above, is the userland of SunOS from 1983.

Unfortunately there is a platform that does not provide anything even close to that. It is quite popular in some circles. For example it has over 90% market share in desktop machines (but who uses those, anyway).

You can use Autotools on Windows, but first you need to install either Cygwin or MSYS and even then you can only use variants of GCC. There is roughly zero support for Visual studio, which unfortunately is the most popular compiler on that platform.

The end result is that if you want or need to support Windows as a first class platform then Autotools can’t be used. Many projects provide both Autotools and Visual Studio projects, but that means that you have two independent build systems that will go out of sync on a
regular basis.

Portability is internal too

Autotools are not forward or backwards compatible with themselves. The developers change the API quite often. This means that if you need to support several different aged platforms, you need to support several versions of Autotools.

This can be done by programming the configure scripts to work differently in different Autotools versions. And who among us doesn’t like a bit of meta-meta-meta-metaprogramming?

Not producing garbage is better than cleaning it

As a final issue I’d like to mention build directories. This is a concept advocating source code directory hygiene. The idea is that you have a source directory, which contains all files that are checked into revision control. In addition there is the build directory. All files generated during build go there. In this way the source directory is always clean. You can also have several build directories, each using different settings, a different compiler and
so on.

Autotools do provide this functionality. If you run the configure script from a directory other than source root, it writes the build files to that directory. But this is pretty much useless, as it only works on full tarballs. Actually it probably won’t since most Autotools projects are written so that they only work when built in the source directory. Probably because the project they were copypasted from also did that.

Tarballs are used mostly by packagers and end users. Developers work on revision control checkouts. As their first step they need to run either autogen.sh or autoreconf. These commands will always vomit their files in the source directory. And there are lots of them, just look at almost any project’s revision control file ignore list.

Thus we have a really useful feature, which is completely useless to those people who need it the most.

What to use then?

That depends. Believe it or not, there are build systems that are even worse. Actually most of them are.

My personal favorite is CMake. It fixes almost all of the issues listed here. It has a couple of downsides too. Its language syntax is weird at some places. The way its state and cache values interact on repeated invocations is non-intuitive. Fortunately you usually don’t have to care about that if you just want to build your stuff.