Autark: Rethinking build systems – Integrate, Don’t Outsource
I enjoy programming in C. Over the years, I’ve built a couple dozen projects of varying complexity – from small toy programs to game engines, key-value storage systems, and even algorithmic trading platforms. Each time I start a new project, before I can begin the actual work, I have to go through a somewhat tedious phase: setting up the build system and defining dependencies on other projects and libraries.
CMake
Until recently, I used CMake. For each new project, this usually meant copying the build rules from one of my previous projects and adapting them to the current needs. While that approach made it easier to get started, the process was always painful. The imperative, BASIC-like scripting logic of CMake is too cumbersome for simple reuse or quick adaptation to changing build requirements.
Another inconvenience is that CMake requires you to constantly keep up with new features and changes in its vast ecosystem. If you ignore them for too long, one day your project simply stops building with the current stable CMake version – and you’ll need to enable some CMake policy to restore compatibility. Even if we ignore the scripting complexity, there’s still the long-term issue: eventually, after the project is released, it will stop building exactly as described in its own documentation.
As we all know, CMake doesn’t actually build your project – it’s a transpiler that converts CMakeLists.txt scripts into rules for lower-level tools like Make or Ninja. In practice, that means you depend on two separate tools (and their compatible versions) just to get a working build, not to mention the debugging headaches in complex setups.
I won’t go into a detailed analysis of CMake’s pros and cons here – it’s a popular system, and it earned that popularity for a reason. It’s far from perfect, but as my colleagues like to say, “Bad. But there’s nothing better yet.” Personally, I decided to pursue a different concept for a build system – one that aligns more closely with my own values: simplicity, stability, and lightness.
Make
So why not just use plain Make or even autotools? Take a look at the folks from suckless.org. They write great software using pure POSIX mode Make. Though, amusingly, they forgot to include Make itself in their list of software that rocks. I have my own reasons for not being too fond of Make, and I’m pretty sure I’m not alone. My short-term memory isn’t great – if I don’t write Make scripts every day, I can never remember all the quirky mechanics and syntax variations. Consider constructs like:
:
Or:
:
And let’s not forget the Automatic Variables list from section 10.5.3 of the GNU Make manual – the one you’re
apparently not allowed to ignore if you dare to use Make. Please don’t tell me “it’s all simple”. I know there are people
for whom it is, but as I said earlier, for me and many colleagues with short memory spans, it’s not.
There’s another point: I believe that when a professional uses a tool, they should understand and be able to use all of its features. But the more complex the tool, the harder it is to claim you truly master it. (That’s why I’ll never call myself a C++ expert.)
In practice, we all end up sticking to a small set of build recipes that feel comfortable and just copy-paste them between projects. Make, however, is too low-level to express build logic easily and gracefully, especially when it needs to account for things like:
- Changes in a Makefile should trigger a rebuild of all dependent targets.
- Changes in environment variables like
$CCor$CFLAGSshould cause dependent modules to rebuild. - Modifications in system library headers should trigger rebuilds of the affected parts of the project.
- There are several flavors of Make: POSIX Make, GNU Make, BSD Make and you must decide what functionality to sacrifice in order to stay compatible across environments.
And that’s just the beginning. Sure, you can make Make handle all of this, but at what cost? I’ve yet to see a handmade Make project that does it all correctly.
Autark
Realizing that it’s impossible to create a perfect build system, I focused on defining a few key characteristics I wanted to see in the final product even if that meant sacrificing some functionality elsewhere.
Here’s what I needed:
- A simple, memorable syntax for build scripts.
- Deeper dependency tracking compared to Make (see above).
- Portability – ideally, the project shouldn’t require any preinstalled build system. It should be able to build itself
using only the most basic tools, like a
Ccompiler and a system shell. - The ability to verify and adapt the project before building (a configure phase).
Dependencies
To get started, I needed some inspiration and I found it in the Redo build tool. There are several Redo implementations available on GitHub, though the concept never became particularly popular. I suspect that’s because it feels rather academic: a talented mathematician proposed a simple, elegant idea, but for it to work in real-world projects, it needed to be wrapped in something more developer-friendly.
While experimenting with Redo, I discovered that building a project often required a large set of shell scripts scattered across many directories. In many of them, I had to reimplement utility functions that really belonged inside the build system itself. However, I did take away one important concept: the idea of managing dependencies through individual files and developed it further.
Portability
To achieve true portability, I took a fairly radical approach: during the first build, the project compiles its own
build system from C, and from then on, that build system compiles the project. The build system is a small C program
embedded directly inside a build.sh script, which acts as its entry point. It only requires a C99-compatible compiler
(flags compatible with clang or gcc). The entire system is about 10K lines of code, compiles in under a second, and
caches its build artifacts. This way, everything related to the build process is already part of the project itself – no
need to worry about toolchain versions or environment differences across machines.
Script Syntax
Here I had to deal with a rather contradictory set of requirements. On one hand, I wanted a powerful declarative syntax; on the other, it needed to be simple, and the parser had to remain minimal to allow fast compilation of the build system during a cold start. I eventually settled on the following syntax:
RULE:
rule_name { RULE | LITERAL ... }
LITERAL:
word | 'single quoted words' | "double quoted words"
- A script consists of a set of rules and literals.
- Rules and literals are separated by whitespaces.
- Every rule has a body enclosed in curly braces
{}. - Rules form lists, similar to syntactic structures in Scheme or Lisp. The AST of a script can change dynamically depending on conditions. I was amused to find that a trace of Scheme somehow made its way into this project without me realizing it.
A formal syntax description in PEG format can be found here: https://github.com/Softmotions/autark/blob/master/scriptx.leg. Special thanks to Ian Piumarta for his excellent PEG parser generator which I’ve used in many of my projects.
Here’s a build script example from the demo project
cc {
.c
${CFLAGS}
${CC}
consumes {
.h
}
}
run {
exec { ${AR} rcs libhello.a ${CC_OBJS} }
consumes {
${CC_OBJS}
}
produces {
${LIBHELLO_A}
}
}
Current Status of Autark
The build system autark.dev was implemented some time ago, and since then I’ve migrated most of my C/C++ projects to it, fixing issues and polishing things along the way.
Here are a few open-source projects that have already been migrated:
And even a fork of the third-party project protobuf-c where I used macros to build and run test cases.
Conclusion
Autark started as an experiment – a personal attempt to make the build process less painful and more predictable. Over time, it grew into something I now use daily across nearly all my C projects. It’s not meant to compete with CMake or reinvent the industry standard. Instead, it tries to do one thing well: give developers a lightweight, self-contained, and reliable way to describe how their projects are built.
I don’t claim that Autark is the best build system it’s simply the one that aligns with my values: simplicity, transparency, and long-term stability. If you’ve ever struggled with build scripts that felt like they were fighting you, maybe you’ll find something familiar in this approach.
The project is still evolving, and feedback is very welcome. You can learn more or try it yourself at autark.dev