shrub has a website

my adventure in autohell

19:48 28 jan 2026

gnu autotools, known by it's adoring fans as autohell, is a very popular build system for unix-like systems. it is also a shockingly bad build system. it's good at what it's designed for, which is building software portably on a bunch of obscure unix systems. unfortunately, most of these obscure unix systems do not really exist anymore. linux is basically the last man standing, with bsd on the sidelines. so, what is the point of a hyper-portable build system like autotools? well, there isn't really one. that on top of the fact that autotools is about 120,000 lines of code, written pretty much purely in m4, shell, and perl, the whole thing is pretty despicable.

so, when working on my 100% non-gnu linux distribution, dérive, i was faced with a choice; accept that gnu autotools is an unavoidable dependency that i will just have to accept, or spend hundreds of hours of my life writing a drop-in replacement. i chose the latter. it is called hell.

to explain how hell works, you need to have a basic understanding of how autotools works, so i will provide a brief synopsis:

in an autotools build, there are two seed files, configure.ac and Makefile.am.

configure.ac

Makefile.am

i'm simplifying for the sake of brevity, but in a normal autotools build you would run automake to generate a Makefile.in, which is a makefile with certain variable placeholders as @VAR@, then run autoconf to generate a configure script, which checks for the presence of certain things in your system, substitutes the correct values for @VAR@ into the Makefile.in as a Makefile. then you can pretty much run make as normal. i skipped about 5 steps here [aclocal, autoheader, libtoolize], and all in all this takes quite a while. don’t write your build systems in interpreted languages, kids.

ok, so how do we even begin to fix this? first i set out some goals:

hell consists of two tools, demiurge and satan. you can think of them as replacements for autoconf and automake respectively, although the way they work is pretty different.

demiurge is essentially an autoconf scanner. it only recognizes the 39 macros that we actually care about (like i said, skip all the bullshit), for example AC-CHECK-HEADERS. for that macro, it writes the list of all headers we need to check to a file called .demiurge. it also attempts to resolve as many undefined variables as possible. for common variables like CC etc, it sources a systemwide configuration file at /etc/satan.conf. this is in stark contrast to autoconf which favours gnu tools and always checks for them first, eg looking for bison before byacc. man these gnu guys make their software spread like cancer. for other varibales, it tries to resolve them too by looking at assignments, AC-SUBST values, and package vars from AC-INIT. all of this also gets written to .demiurge.

ok, so now we have a list of variables and things we need to probe for, now what? now it's satan's turn!satan does two main things: first, it reads the list of things to probe for and runs the probes, in parallel, much faster than the autotools-generated configure script would. and i can prove it!

here are the results for time ./configure when building libarchive:

real	0m17.151s
user	0m9.933s
sys	0m7.360s

and here are the results for the command time satan gen:

real	0m1.983s
user	0m10.881s
sys	0m7.761s

as you can see, it is slighly faster. slightly. about 8.5x faster. after running the probes, it generates config outputs, like config.h. then satan translates the Makefile.am to a build.ninja. because automake is essentially a pretty complex build DSL, we do this using what is essentially a small automake compiler. I won't go into how that works too deeply, but in short it checks subdirs for sub-Makefile.am's lexes and parses all the Makefile.am, fills the default automake variables, builds the target list, resolves sources for each target, appends the flags to the compile command, and emit's the build.ninja. to avoid libtool, it converts all targets to static. then you can use the ninja implementation of your choice to build, and that's it, you build the project!

now, this sounds like a no-brainer, why would anyone use autotools after finding out about this? well, it has it's downsides. our "dumb" configure.ac scanner avoids an m4 dependency and simplifies things significantly, but it means we don't always know the right value for a variable, which can lead to build faliures. to fix this, you can write a .satan file or use -Dkey=val flags on top of your satan gen command to define these manually. whether you will have to do this or not depends on the complexity of the project. libarchive, libexpat, and many others build with no manual twiddling. on the other hand, here is the satan command to build the eiwd wifi daemon:

satan gen \
    -DCOND_MAINTAINER_MODE=0 -DCOND_EXTERNAL_ELL=0 -DCOND_LIBEDIT=0 \
    -DCOND_DBUS_POLICY=0 -DCOND_SYSTEMD_SERVICE=0 -DCOND_MANUAL_PAGES=0 \
    -DCOND_DAEMON=1 -DCOND_OFONO=0 -DCOND_CLIENT=0 -DCOND_MONITOR=0 \
    -DCOND_WIRED=0 -DCOND_TOOLS=0 -DCOND_HWSIM=0 -DCOND_DBUS=0 \
    -DCOND_RUN_RST2MAN=0 -DCOND_GCOV=0 \
    -DDEF_HAVE_EXPLICIT_BZERO=1 -DDEF_HAVE_REALLOCARRAY=1 \
    -DDEF_DAEMON_STORAGEDIR=/var/lib/iwd -DDEF_DAEMON_CONFIGDIR=/etc/iwd \
    -DDEF_WIRED_STORAGEDIR=/var/lib/ead -DUNDEF_HAVE_BACKTRACE=1 \
    -DLIBEXECDIR=/bin -DCHECK_PKG= -DEXTRA_PKG= \
    -DLDFLAGS="$LDFLAGS" \

see, that's not so fun to have to work out. but this varies from project to project, and is pretty much unavoidable without either rewriting m4 or including m4 as a dependency, which is a no-go for me. seeing as hell is mostly designed to be able to build autotools-based projects and package them for my distro without relying on gnu software, this caveat is fine for my use case. it can also only build static libraries and binaries, which i think is a big plus, but others will probably see as a downside. these caveats are fine for me, because it fully avoids the m4 dependency by being written in C, and because the probing step is MUCH faster, and ninja is often if not always faster than make, builds are generally between 2-8x faster in my benchmarks. on top of all that, it turns autotools builds from a shaky string of m4 macros and shell into a actually quite nice declarative build DSL that generates ninja.

in terms of codebase size, autoconf+automake (without libtool, autoheader etc) is 128982 lines of code. hell on the other hand, is a total of 10830 lines.

i read more gnu documentation than any human deserves to be subjected too in the making of this project.

hell's source code is available on codeberg as well as sourcehut