Written: 2023-12-28
Revised: 2024-01-08

A small, easy-to-deal-with build system for C

This is not an ad. It's more of an in-progress experiment.

You see, I've changed build systems many times for my personal projects. In the beginning, I used the suckless strategy of just using a Makefile. If you don't need to support Windows, and don't have any dependencies, and support PREFIX and DESTDIR correctly, and don't need to compile other programs in the middle of compilation, this works fine.

But at some point, I wanted to use SDL, and POSIX make doesn't support shelling out to pkg-config, nor build options. Also, the support for automatic header dependencies is not very good, and usually gmake-specific. Since I don't want to depend on gmake, I started considering other options.

I had used some systems from a user's (or packager's) point of view:

[a] Generally, vendoring dependencies with no override.
[b] Usually for the sake of working around the build system.

I also had the pleasure (or, in some cases, displeasure) of using some of these from a developer's point of view:

You can see I really don't like CMake.

Hand-written configure scripts are probably the nicest solution overall, but writing them is a bit of an art, and somewhat inconvenient at the start of a project. One variation I tried was generating a Ninja script instead of a Makefile, and it's unquestionably better: faster builds by default, automatic dependency resolution, easy out-of-source builds. I learned a lot by doing that.

Probably the biggest lesson I've learned from building things from source is that everything should be user-overridable, preferably without editing anything. Dependencies, compilers and paths should be provided by the user, not "found" by the scripts. If the build system wants to have reasonable defaults, that's okay, as long as they're well-documented and easily overridable.

Another thing is that the best way to do out-of-source builds is not to put the build system's files inside the build directory. That forces you to specify it on every build command, or switch to that directory first, which is inconvenient. It's much nicer to have the build script on the project directory, and have it know where the build directory is. This also allows final binaries to be put in the current directory, which makes them more easily accessible.

Windows support is nice, but on that platform, nothing is standard. What most other meta-build-systems seem to do is to either vendor dependencies or expect them to be installed. Or use vcpkg. vcpkg is designed for Windows, but on most build scripts it's usually set up to be used on all OSes. Don't do that. It breaks badly on Alpine, and probably other systems as well.

Ideally, I'd like to specify some dependencies, add some targets made from source files, and have the build system handle the rest. You know, like cargo and go, but without forcing any particular source directory structure.

So, I made a proof-of-concept. It's currently an AWK script (cconf) that generates a shell script (configure) that generates a Ninja build file (build.ninja). It's not perfect, or complete for that matter, but it can be improved over time.

The basic usage is to write a build.config file:

default release

bin foo
foo deps sdl2 SDL2_image
foo cc foo.c sub/bar.c sub/baz.c

And then run:

cconf build.config > configure
chmod +x configure

To get a configure script that can be used with the almost-standard:

./configure
ninja

A few interesting features:

Simplifications/anti-features:

Limitations of the current prototype:

You can get the current version here.