Lichendust

I'm Harley, an artist, animator and programmer.
I make all kinds of useless stuff.

Build Systems

CONTENTS

My motto when writing any build system or toolchain is 'would I happily subject a fellow human to this?' If the answer is no I do not pursue it. In practice, this means no make, cmake or ninja-type scripts and as little NodeJS/NPM as humanly possible.

If I have to use these things, they're only integrated insofar as they're necessary and, if I can help it, never on the critical path for a daily build. If I'm using someone else's tools/library and I can bypass the build system entirely, I will do so.

This is what I do instead —

Build scripts done right

I like to construct single purpose build scripts for every project. This example will build and run a project, but if any step fails, the script will exit — if the compiler doesn't build, it doesn't launch the app afterwards.

Here's the simplest version of this in Bash

#!/usr/bin/bash
set -q

odin build source -out:project -o:speed \
    -define:SOME_FLAG=true

./project

Or as a multi-modal build system —

#!/usr/bin/bash
set -e

odin build source -out:project -debug -collection:forest=/mnt/x/dev/forest

[[ "$1" == "debug" ]] && <debugger> program
[[ "$1" == "run"   ]] && ./program

You can write the exact same system in Windows batch —

@echo off

odin build . -out:project -o:speed ^
    -define:SOME_FLAG=true

if %errorlevel% -neq 0 goto :eof

project

Or as a multi-modal build system —

@echo off
set mode=%1

odin build source -out:program.exe -debug -collection:forest=X:\dev\forest
if %errorlevel% neq 0 goto :eof

if %mode%==debug (
    raddbg --auto_run program.exe
)
if %mode%==run (
    blot
)

Both of these act like a makefile, in that they provide a series of possible commands in a single script. You can run the base script for a build, append debug to launch it with the RAD Debugger or run to build and run immediately directly. Technically, with Odin, you can just use odin run for this for the exact same result, but this isn't true of all compilers. It also makes it easier to insert more commands with less weird control flow.

Here's an example of a build system for a shipping Go program I make called Meander. This script builds, packages and checksums all of the release bundles for the application. I run this script every time I ship an update. The most complex part of the script is the bit where it renames macOS builds — darwin and arm64 to macOS and silicon — to put things into Mac-user language.

Some Tricks

Script location

Getting the working directory of a script in batch —

set script_path=%~dp0

Doing the same in bash —

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)

WSL overloading

If you use WSL, like me, you can also overload your Bash/SH build scripts with the Windows versions of utilities. For environment variable reasons this rarely works with compilers, but with almost anything else it renders your scripts cross-platform, now able to run on Linux, macOS and WSL on Windows.

if [[ ! -z $WSL_DISTRO_NAME ]]
then
    aseprite() {
        aseprite.exe "$@"
    }
fi

# do normal stuff with aseprite down here
aseprite -xx ...

Philosophy

I had a thousand word rant here about how I feel about the modern state of C and C++ focused build systems like make, cmake and ninja, but I've chopped it down to a simple statement.

TL;DR: I hate them.

They purport to solve the problem of 'it works on my machine', which they don't.

I use SDL a lot, and SDL's build tooling consists of no less than thirty-odd .cmake definitions across its ecosystem which have not — ever — reduced the complexity in getting it to compile for me.

I recently attempted to compile SDL_shadercross. I would have been able to easily get it to compile without cmake's interference looking for non-existent system install files like FindSDL3.cmake. This is a thing that, as far as I know, doesn't exist because SDL3 wasn't in release at the time. I, on the other hand, know exactly where SDL3 is. It's there, a lovely, fresh clean clone of the latest beta API-stable tag that I got just for you.

So can I easily redirect cmake to that bundle? Well, I had to pass four separate flags and the path as a command-line variable, which took an hour to uncover after a lot of trial and error, just to bypass this feature for one dependency.

I was passing so many flags to the cmake call that I started writing them into a build.bat file to make it easier to run. Oh look, it's the thing I wanted to just have in the first place.

Oh and I immediately started getting FindX.cmake errors for the next dependency after that and promptly gave up.

SDL_shadercross, at least at that time, shipped seventy-eight cmake files and I still had to write a script to even attempt to construct a repeatable build on my system.

It seems you're better off just writing a generic, sh/bash or batch build script that clearly defines and enumerates dependencies and allows you to just write where they are, and then have your team duplicate it for their own machines if differences arise. You could even do this with environment variables, but only if you hate yourself. Just write it down, in plain text, and execute it. You're a programmer.

Machine-specific configuration is a reality of the world — you can't avoid different OSes, package managers and just plain old user decisions from creating fundamental differences, and that's before versions and versioning enter the picture — and by accepting it and assuming there will be differences rather than trying to pave over them with abstraction, your life becomes drastically simpler. In fact, this applies to almost every problem in programming.

I'm fundamentally unconvinced that the problem of importing some header files is so complicated that we can't just write a build.sh file and go from there. If your program's build is so complicated that you just scoffed while reading that sentence, it's probably a sign that your dependency tree is ridiculous and should be chopped down to size anyway.

WORD COUNT
1043
LAST UPDATED
2026-01-03
BACKLINKS

Programming