Ode to building, part 2

posted on August 08, 2007

In the first part of this series I introduced the idea of what I termed abstract build policies — higher-level methods for describing how to produce the DAG of a build from a description of the desired inputs (sources) and results (targets). An abstract build policy allows developers to specify their sources and targets in abstract terms and let the build system sort out the details. For example, the autotools allow a developer to specify that they want to generate a shared library from a set of C source files:

lib_LTLIBRARIES = libexample.la
libexample_la_SOURCES = example.c example.h ...

But what's really going on under the hood?

Instead of requiring developers to describe the complete DAG of all sources-to-targets steps in a build, the autotools conspire to allow the developer to operate in terms of a set of higher-order abstractions:

systems:: The build is occurring on a particular kernel/OS/hardware combination (the build-system) to run on another (the host-system) and may — if producing a compiler, etc. — generate system-specific entities for another (the target-system).
variants:: The user invoking the build may choose to add optional part of the build, remove other parts, and cause yet other parts certain parts to occur in an alternative fashion.
sources:: The inputs to the build, some of which may themselves need to be generated from predecessor sources.
targets:: The kinds from a restricted set (e.g., shared library), names (libexample.so), and other properties (somewhat abstract installation location or lack thereof) of entities to result from the build.

The key aspect of the abstraction is that the developer describes her build only in terms of the details relevant for the particular build. "This set of source files produces a shared library." The autotools handle the nitty-gritty of populating the DAG with all the nasty little details only real toolchain-weenies actually care about.

In a perfect world (or for sufficiently simple/well-written code), the developer doesn't care whether the code is being built with gcc or the Sun Workshop compiler, for OpenBSD or AIX. If the code depends upon OS features which vary from system to system, then the developer needs to account for those variations, but only those variations. On Linux accessing /proc involves text-file parsing, on Solaris using a library interface, but the developer still doesn't care which compiler the build invokes or which flags the linker requires to coax it into producing a shared library.

The key limitation of the autotools is that they provide no facility for generating new abstractions. As I said in part 1, they provide a set of abstract build policies, but no a real build policy abstraction. The beauty and downfall of the autotools is that they produce truly portable build descriptions — build descriptions which depend only on the POSIX shell, POSIX utilities, POSIX make, and a toolchain for the source language. But providing even as simple a new abstract policy as one for producing a new target type or compiling a new source language is impossible without wholesale modification of the autotools core. And even then a new policy has little power to leverage intermediate abstractions, ultimately needing to itself generate portable POSIX make rules.

As originally promised for this part, part 3 will look at build tools which allow definition of new abstractions and what that implies.

Commentary most sage