Сборка из исходников редко бывает узким местом на маленькой программе, но в дереве на тысячи файлов разница между минутами и десятками секунд заметна ежедневно. Ninja решает одну задачу: максимально быстро выполнить уже известный граф зависимостей. Она не ищет исходники и не разбирает CMakeLists.txt — этим занимается генератор, создающий build.ninja. Разделение обязанностей кажется неудобным до первого инкрементального билда, когда тысячи целей проверяются за доли секунды.
Зачем отдельный сборщик
make для каждой цели делает немало системных вызовов: сравнивает время модификации, перечитывает правила, разворачивает рекурсию. У Ninja граф известен заранее, проверки сводятся к чтению журнала и статистике файлов, а найденные компилятором зависимости лежат в отдельной базе. Поэтому сборщик почти не тратит время на себя и хорошо масштабируется на десятки параллельных задач. Плата за скорость — отсутствие удобного языка описания: файл генерируют CMake, Meson, GN или собственный скрипт, и без генератора Ninja почти не используют.
Установка
В Debian, Ubuntu, Linux Mint и Fedora пакет называется ninja-build, в Arch — ninja. Бинарник везде называется одинаково. Заодно проверьте версию CMake: старые выпуски могут не знать части возможностей.
sudo apt install ninja-build
ninja --version
cmake --version
command -v ninja
Если версия в репозитории отстаёт, соберите Ninja из исходников, но не смешивайте копии в PATH: генератор и сборщик должны быть предсказуемо одной версии.
Минимальный build.ninja
Файл состоит из правил, целей и переменных. Правило описывает команду, цель связывает её с конкретными файлами. Отступ в два пробела обязателен, табуляция ломает разбор.
cflags = -O2 -g -Wall -Wextra
rule cc
command = cc $cflags -MMD -MF $out.d -c $in -o $out
depfile = $out.d
deps = gcc
description = CC $out
rule link
command = cc -o $out $in
description = LD $out
build obj/main.o: cc src/main.c
build obj/util.o: cc src/util.c
build app: link obj/main.o obj/util.o
default app
$in и $out подставляет сам Ninja: первая переменная содержит входы, вторая — выход. Строки depfile и deps = gcc означают, что зависимости из файла $out.d будут учтены при следующем запуске, иначе правка заголовка не пересоберёт объект. Директива default задаёт цели для запуска без аргументов. Псевдоцель phony полезна для имён вроде clean, которым не соответствует реальный файл.
Переменные, области видимости и pool
Переменные вычисляются в момент использования, поэтому правило может ссылаться на значение, заданное позже. Область видимости — сверху вниз: объявление внутри блока build действует только для этой цели. Директивы include и subninja подключают другие файлы, причём вторая создаёт отдельную область. Для тяжёлых задач нужен pool: он ограничивает параллелизм независимо от -j.
pool codegen
depth = 2
rule gen
command = ./tools/gen.py --input $in --output $out
pool = codegen
restat = 1
generator = 1
build generated/schema.h: gen schema/schema.yaml
restat = 1 заставляет перепроверить время модификации после команды: если содержимое не изменилось, зависимые цели не пересобираются. generator = 1 исключает правило из очистки и говорит, что команда порождает сам граф.
Генерация из CMake и Meson
Практически всегда файл создаёт другая система. У CMake генератор выбирается ключом -G Ninja, у Meson Ninja используется по умолчанию. После изменения списка файлов перегенерируйте каталог, а не правьте результат: следующая генерация всё равно перезапишет файл.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8
meson setup build --buildtype=release
meson compile -C build
Для clangd полезен compile_commands.json: CMake создаёт его с CMAKE_EXPORT_COMPILE_COMMANDS=ON, а Ninja умеет собрать базу командой ninja -t compdb.
Ежедневная работа
-C задаёт каталог с build.ninja, -j ограничивает число задач. По умолчанию Ninja подбирает потоки по числу процессоров с небольшим запасом, но при малом объёме памяти значение лучше задать вручную. -k 0 продолжает сборку после ошибок и показывает все сломанные цели, а -n с -d explain печатает план и причины.
ninja -C build -j 8
ninja -C build -k 0
ninja -C build -n -d explain
NINJA_STATUS="[%f/%t %es] " ninja -C build
NINJA_STATUS меняет строку прогресса: %f — выполнено, %t — всего, %es — прошедшее время.
Диагностика
Встроенные инструменты отвечают на большинство вопросов. targets показывает цели, commands — точные командные строки, deps — записанные зависимости, missingdeps ищет цели, использующие заголовки без объявленной связи. graph печатает граф в формате Graphviz.
ninja -C build -t targets all
ninja -C build -t commands obj/main.o
ninja -C build -t deps obj/main.o
ninja -C build -t missingdeps
ninja -C build -t graph | dot -Tsvg > build/graph.svg
Если сборка пересобирает всё подряд, запустите -d explain: Ninja напишет, какой файл новее цели. Частые виновники — сгенерированный файл с меняющейся меткой времени и правило без restat.
Журнал и очистка
В каталоге сборки лежат .ninja_log и .ninja_deps: история команд и зависимости, найденные компилятором. Удалять их вручную нужно редко, но после смены компилятора или переезда каталога это допустимо — Ninja просто выполнит полную сборку.
ls -la build/.ninja_log build/.ninja_deps
ninja -C build -t restat
ninja -C build -t clean
ninja -C build -t cleandead
cleandead удаляет файлы, которые больше не являются выходами ни одной цели, — полезно после удаления исходников. clean стирает всё, что перечислено в графе, но не трогает созданное вручную.
Практический чек-лист
- build.ninja генерируется, а не правится руками; каталог сборки исключён из Git.
- Для каждого правила компиляции заданы depfile и deps, иначе инкрементальность ломается на заголовках.
- Долгие генераторы вынесены в pool с небольшим depth.
- Правила, создающие граф, помечены generator = 1.
- На CI число задач ограничено, а лог сборки сохраняется как артефакт.
- При странных пересборках первым делом запускается ninja -d explain.
Итог
Ninja выигрывает не за счёт магии, а за счёт разделения труда: генератор описывает граф, сборщик максимально быстро его исполняет. Освоив несколько десятков строк синтаксиса и инструменты с ключом -t, вы получаете предсказуемую сборку, которую удобно запускать и в терминале, и на сервере непрерывной интеграции. Главное — не редактировать сгенерированный файл и следить за зависимостями от заголовков.