Всем привет! Недавно я написал свою систему сборки — 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 msin src/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 msin roit@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/Take default = "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.
CoolCmd
тц тц тц. по канону нужно
makeзаменить наtake.