A tiny build tool that stays out of the way

Ninja has become one of those tools I reach for without thinking. It's a build system with an intentionally tiny feature set, and that turns out to be exactly what makes it useful for small projects where a full-featured build system feels like overkill.

The problem it solves is familiar: you have a set of files that need transforming, and you don't want to regenerate everything when only one input changed. For example, I'm working on a zine where each page is an .svg file that needs converting to PDF with Inkscape. That conversion is slow—around 90 seconds for the whole set—so waiting through a full rebuild after touching one page gets tedious fast.

What the build file actually looks like

The core syntax is small enough to memorize in a minute. There are really just two constructs: rule and build. A rule defines a command and a human-readable description:

rule svg2pdf
  command = inkscape $in --export-text-to-path --export-pdf=$out
  description = svg2pdf $in $out

A build statement ties outputs to inputs using that rule, with the output going into $out and inputs into $in:

build pdfs/variables.pdf: svg2pdf variables.svg

Put those in a file named build.ninja, run ninja, and it executes the command for anything out of date. Run it again and it does nothing—it tracks what's been built and skips straight to "up to date."

Generate the file instead of learning a language

The key design decision is that the build file format stays dead simple so you never need a complex build language. Anything complicated gets handled by generating the build.ninja file with whatever programming language you already know. I use Python and find that much easier than remembering make's syntax.

A typical setup for me is a small script that writes out the ninja file and then invokes ninja itself:

with open('build.ninja', 'w') as ninja_file:
    # write some rules
    ninja_file.write("""
rule svg2pdf
  command = inkscape $in --export-text-to-path --export-pdf=$out
  description = svg2pdf $in $out
""")
    
    # some for loop with every file I need to build
    for filename in things_to_convert:
        ninja_file.write(f"""
build {filename.replace('svg', 'pdf')}: svg2pdf {filename}
""")

# run ninja
import subprocess
subprocess.check_call(['ninja'])

There are presumably best practices for this kind of thing, but for small projects this straightforward approach works well. Ninja was originally written for Chromium, so even with a minimal feature set it scales to large builds.

Real-world usage: Meson as a front end

Ninja also shows up as the backend for other build systems. Building plocate, a faster alternative to locate, I noticed the instructions weren't the usual ./configure; make; make install sequence:

meson builddir
cd builddir
ninja

Meson is a build system for C/C++, Java, Rust, and Fortran that can use ninja as its backend, so you get a higher-level configuration language while ninja handles the actual build steps.

After a few months of regular use, ninja has caused me essentially zero build-related problems. For someone who found make arcane and Bazel overwhelming, that simplicity is the whole appeal.