Всем привет! Недавно я написал свою систему сборки — 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.

Комментарии (2)


  1. CoolCmd
    13.07.2026 13:07

    компилируется одним make

    тц тц тц. по канону нужно make заменить на take.


  1. SilverTrouse
    13.07.2026 13:07

    что там с поддержкой модулей с++20?