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.



