Всем привет! Недавно я написал свою систему сборки — Take (C++17, без внешних зависимостей если не считать один header only TOML‑парсер). Идея простая: взять привычную модель Make (именованные цели, зависимости между ними, шелл команды), но выкинуть табы, $@/$< и прочую магию синтаксиса Makefile, а вместо этого настраивать всё через обычный .toml‑файл.
Получилось вот так:
default = "build"[vars]CXX = "gcc"FLAGS = "-O2 -Wall -Wextra"OUT = "msin"[build]desc = "Build msin"run = [ "${CXX} ${FLAGS} -Iinclude src/main.c src/functions.c -o ${OUT}",][install]desc = "Install msin (sudo or su)"run = [ "install -d /usr/local/bin", "install -m 755 ${OUT} /usr/local/bin/msin",][clean]desc = "Clean build"run = ["rm -f ${OUT}"]
Каждая секция (кроме служебной [vars]) — это отдельная «функция»: именованный набор шагов, который можно запустить командой take <имя>. Например, take build соберёт проект, а take install — поставит бинарник в /usr/local/bin.
Переменные и подстановка
В [vars] можно объявить произвольные переменные и подставлять их через ${VAR} в любую команду run. Это просто текстовая подстановка на этапе загрузки конфига — никакой отдельной DSL для выражений нет, и это осознанное решение: чем меньше синтаксиса нужно держать в голове, тем быстрее пишется конфиг.
Кроме пользовательских переменных, Take прокидывает несколько встроенных:
-
${TAKE_FUNC}— имя текущей выполняемой функции; -
${TAKE_DIR}— директория, где лежитtake.toml; -
${ARGS}— всё, что передано после--в командной строке.
Плюс есть доступ к переменным окружения через ${env:VARNAME}.
Зависимости между функциями
Функция может зависеть от других через deps:
[build]desc = "Build msin"run = ["${CXX} ${FLAGS} -Iinclude src/main.c -o ${OUT}"][install]desc = "Install after build"deps = ["build"]run = ["install -m 755 ${OUT} /usr/local/bin/msin"]
take install сначала выполнит build, и только потом свои собственные команды. Внутри это обычный обход графа зависимостей с отслеживанием уже выполненных функций и детектом циклов — если где‑то в цепочке deps образуется цикл, Take сразу упадёт с понятной ошибкой вместо ухода в бесконечную рекурсию.
Пример ошибки при ручной остановке сборки:
roit@Debian:/sdisk/p/progect/p/msin$ take==> take build — Build msin $ gcc -O2 -Wall -Wextra -Iinclude src/main.c src/functions.c -o msinsrc/main.c: In function ‘handle_encrypt’:src/main.c:48:55: warning: unused parameter ‘filename’ [-Wunused-parameter] 48 | void handle_encrypt(const char* password, const char* filename) { | ~~~~~~~~~~~~^~~~~~~~^Ctake: error: [build] The command finished with code 1: gcc -O2 -Wall -Wextra -Iinclude src/main.c src/functions.c -o msinroit@Debian:/sdisk/p/progect/p/msin$
Команды под конкретную ОС
Если проект собирается по‑разному на Linux/macOS/Windows, вместо одного run можно расписать run_linux, run_macos, run_windows или run_unix (последний — общий вариант для всех *nix‑систем):
[build]desc = "Build for current OS"run_linux = ["gcc -O2 src/main.c -o app"]run_macos = ["clang -O2 src/main.c -o app"]run_windows = ["cl.exe /O2 src\\main.c"]
Take сам определяет текущую платформу на этапе компиляции (#if defined(_WIN32) / APPLE / linux) и подставляет нужный блок команд.
Смена рабочей директории
Для монорепозиториев или проектов с подпапками есть dir:
[frontend]desc = "Build frontend"dir = "web"run = ["npm run build"]
Команды в run выполнятся именно в указанной директории, а после — Take вернётся туда, откуда был запущен.
Для примера вот конфиг для проекта LogicGrid:
Посмотреть полный take.toml для проекта LogicGrid
# TOML build configuration for the take build system.# https://github.com/Roit-liasp/Takedefault = "build"[vars]CXX = "g++"STD = "c++20"FLAGS = "-O2 -Wall -Wextra"PROJECT = "LogicGridProject"SRC_DIR = "src"INCLUDE_DIR = "include"BUILD_DIR = "build"OUT_DIR = "out"SCRIPTS_DIR = "scripts"THIRD_PARTY = "third_party"SOL2_DIR = "third_party/sol2"SOL2_INCLUDE = "third_party/sol2/include"SOL2_TAG = "v3.5.0"SOL2_REPO = "https://github.com/ThePhD/sol2.git"LUA_LIBS_FALLBACK = "-llua5.4"SFML_LIBS_FALLBACK = "-lsfml-graphics -lsfml-window -lsfml-system"EXTRA_INCLUDE = ""EXTRA_LIB = ""[deps]desc = "Fetch third-party sources that don't ship on the system (sol2)"run = [ 'mkdir -p ${THIRD_PARTY}', 'test -f ${SOL2_INCLUDE}/sol/sol.hpp || (rm -rf ${SOL2_DIR} && git clone --depth 1 --branch ${SOL2_TAG} ${SOL2_REPO} ${SOL2_DIR})',][doctor]desc = "Check that the required tools/libraries are present"run = [ 'command -v ${CXX} >/dev/null || { echo "take: ${CXX} not found" >&2; exit 1; }', 'command -v git >/dev/null || { echo "take: git not found (needed to fetch sol2)" >&2; exit 1; }', 'command -v pkg-config >/dev/null && echo "take: pkg-config found (used to auto-detect Lua/SFML)" || echo "take: warning: pkg-config not found, will fall back to plain -l flags for Lua/SFML" >&2', 'pkg-config --exists lua5.4 || pkg-config --exists lua-5.4 || pkg-config --exists lua || echo "take: warning: no Lua 5.4 pkg-config package found, will link with ${LUA_LIBS_FALLBACK}" >&2', 'pkg-config --exists sfml-graphics || echo "take: warning: no SFML pkg-config package found, will link with ${SFML_LIBS_FALLBACK}" >&2',][build]desc = "Compile LogicGrid (auto-fetches sol2, auto-detects Lua/SFML flags)"deps = ["deps"]run = [ 'mkdir -p ${BUILD_DIR} ${OUT_DIR}', 'LUA_PKG=""; for p in lua5.4 lua-5.4 lua; do pkg-config --exists "$p" 2>/dev/null && LUA_PKG="$p" && break; done; if [ -n "$LUA_PKG" ]; then pkg-config --cflags "$LUA_PKG" > ${BUILD_DIR}/.lua_cflags; pkg-config --libs "$LUA_PKG" > ${BUILD_DIR}/.lua_libs; else : > ${BUILD_DIR}/.lua_cflags; echo "${LUA_LIBS_FALLBACK}" > ${BUILD_DIR}/.lua_libs; fi', 'if pkg-config --exists sfml-graphics sfml-window sfml-system 2>/dev/null; then pkg-config --cflags sfml-graphics sfml-window sfml-system > ${BUILD_DIR}/.sfml_cflags; pkg-config --libs sfml-graphics sfml-window sfml-system > ${BUILD_DIR}/.sfml_libs; else : > ${BUILD_DIR}/.sfml_cflags; echo "${SFML_LIBS_FALLBACK}" > ${BUILD_DIR}/.sfml_libs; fi', '${CXX} -std=${STD} ${FLAGS} -I${INCLUDE_DIR} -isystem ${SOL2_INCLUDE} ${EXTRA_INCLUDE} $(cat ${BUILD_DIR}/.lua_cflags) $(cat ${BUILD_DIR}/.sfml_cflags) ${SRC_DIR}/*.cpp -o ${OUT_DIR}/${PROJECT} ${EXTRA_LIB} $(cat ${BUILD_DIR}/.sfml_libs) $(cat ${BUILD_DIR}/.lua_libs) -ldl', 'if [ -d ${SCRIPTS_DIR} ]; then mkdir -p ${OUT_DIR}/${SCRIPTS_DIR}; cp -r ${SCRIPTS_DIR}/. ${OUT_DIR}/${SCRIPTS_DIR}/; fi',][run]desc = "Run LogicGrid"dir = "${OUT_DIR}"run_linux = ['./${PROJECT}']run_macos = ['./${PROJECT}']run_windows = ['${PROJECT}.exe'][install]desc = "Install LogicGrid (sudo or su)"deps = ["build"]run = [ 'install -d /usr/local/bin', 'install -m 755 ${OUT_DIR}/${PROJECT} /usr/local/bin/${PROJECT}',][clean]desc = "Remove build artifacts (build/ and out/)"run = ['rm -rf ${BUILD_DIR} ${OUT_DIR}'][clean-deps]desc = "Remove fetched third-party dependencies (third_party/)"run = ['rm -rf ${THIRD_PARTY}'][distclean]desc = "clean + clean-deps: wipe everything take fetched or built"deps = ["clean", "clean-deps"][rebuild]desc = "Clean and build from scratch"deps = ["clean", "build"][test]desc = "Build then run the resulting binary"deps = ["build", "run"]

Dry‑run, тихий режим и прочие мелочи
-
take -n/--dry-run— показать, какие команды будут выполнены, но не выполнять их. Удобно, когда не доверяешь конфигу (своему или чужому) и хочешь сначала посмотреть, что вообще произойдёт. -
take -q/--quiet— не печатать сами команды и заголовки функций, только их вывод. То же самое можно включить точечно для одной функции черезquiet = true. -
ignore_errors = true— не прерывать выполнение, если команда вернула ненулевой код. -
take -l/--list— вывести список всех функций с ихdesc. -
take -f other.toml— использовать другой файл конфигурации вместоtake.toml. -
take -C dir— сначала перейти в директорию, а потом уже читать конфиг и выполнять функцию. -
take build -- --verbose— всё, что после--, попадёт в${ARGS}и будет доступно внутри команд.
Вывод в терминал подсвечивается цветом (если это реально tty, isatty проверяется), но при желании отключается флагом --no-color.
Как это устроено внутри
Реализация нарочно компактная — весь main.cpp укладывается в районе 400 строк на C++17 (в следущем релизе я думаю его разделить на несколько файлов для удобства): свой TOML‑парсер вынесен в отдельный header only файл, а сама программа просто читает секции, собирает граф deps, делает подстановку ${...} через простой проход по строке и дергает std::system() для каждой команды. Никаких внешних зависимостей, никакого рантайма — компилируется одним make, ставится через sudo make install, или просто кладётся бинарник take куда нибудь в $PATH.
Название Take идет от Make, но вместо M стоит T что обозначает Toml. ¯\_(ツ)_/¯
Проект довольно молодой (сейчас версия 0.0.4) и не претендует на замену Make или CMake в больших проектах — это скорее инструмент для небольших C/C++ штук и personal проектов, где хочется читаемый конфиг без синтаксиса Makefile и без веса полноценного CMake. Буду рад issues, PR и вообще любой обратной связи.
Репозиторий: https://github.com/Roit‑liasp/Take
Лицензия: BSD 2-Clause.
ссылка на оригинал статьи https://habr.com/ru/articles/1058644/