Привет, Хабр! ?
Знаете это чувство, когда ты просто хочешь написать код, но вместо этого полдня ковыряешь bash-скрипты? Или когда твой Makefile разрастается до таких размеров, что открывать его страшно, а редактировать — опасно для психики?
Если да — добро пожаловать в клуб.
Меня зовут Даниил, и я устал. Устал от того, что инструменты, которые должны помогать, начинают мешать. Устал гуглить "как передать аргументы в make target" и "почему в bash if не работает". И в один прекрасный момент я решил: хватит.
Я взял Rust, закрылся в комнате и написал Nest. И сегодня я хочу рассказать, почему вам, возможно, тоже пора выбросить свой Makefile.
Боль, депрессия и Makefile ?
Давайте честно: make — это легенда. Он с нами с 1976 года. Он собирал Unix, он собирал Linux, он собирал, наверное, вообще всё. Но давайте признаем: он не для нас.
Он для сборки C-файлов. Он для отслеживания зависимостей компиляции. Он не для того, чтобы запускать докер-контейнеры, деплоить на стейджинг или гонять линтеры с хитрыми флагами.
Каждый раз, когда я вижу это:
.PHONY: deploy
deploy:
@echo "Deploying..."
./deploy.sh $(ARGS) # А передадутся ли аргументы? А если пробелы?
...у меня дергается глаз. Табы вместо пробелов? Серьезно? В 2026 году?
А потом появляются баш-скрипты. deploy.sh, build.sh, test.sh. Они валяются в корне, в папке scripts, в папке bin. Никто не помнит, что они делают. Никто не хочет их трогать.
"Просто возьми Just!" — говорили они ?
Конечно, я не первый, кто это заметил. Есть Just.
Just — классный. Он убрал табы, добавил нормальные аргументы. Это был глоток свежего воздуха. Я пользовался им какое-то время. Но потом мой Justfile тоже начал пухнуть.
Проблема Just (и Make) в том, что они плоские. Все команды свалены в одну кучу. Хочешь сделать db reset? Пиши db-reset. Хочешь test frontend? Пиши test-frontend.
build-backend:
cargo build
build-frontend:
npm run build
deploy-prod:
...
Это не структура. Это список покупок на неделю. А мне нужна иерархия. Я хочу писать nest db reset, nest dev start, nest build frontend. Я хочу, чтобы мои задачи были организованы так же красиво, как мой код.
Nest: Иерархия, Типы и Rust ?
Так родился Nest.
Я решил, что таск-раннер должен быть:
Читаемым. Никаких скобок, никаких
$(). Чистый DSL на отступах, как в Python или YAML.Структурированным. Вложенность команд любой глубины.
Типизированным. Если я жду число, я хочу получить число, а не строку, которую надо парсить в баше.
Умным. Переменные из
.envфайлов, шаблонизация, авто-help.
И, конечно, он должен быть написан на Rust. Потому что:
Скорость. Он летает.
Один бинарник. Никаких зависимостей. Скачал и работаешь. Статическая линковка (musl) — работает везде.
Безопасность. Ну, вы знаете.
Как это выглядит?
Давайте сравним.
Задача: Запустить сервер (с опцией хот-релоада)
Make:
# Mmakefile
run:
ifdef HOT
nodemon src/index.js
else
node src/index.js
endif
# Запуск: make run HOT=true (или HOT=1? или yes?)
Just:
# Justfile
run hot='false':
if [ "{{hot}}" == "true" ]; then nodemon src/index.js; else node src/index.js; fi
# Запуск: just run true (или just run --hot true)
Nest:
# nestfile
run(!hot|h: bool = false):
> desc: Run server with optional hot reload
> script: |
#!/bin/sh
if [ "{{hot}}" = "true" ]; then
nodemon src/index.js
else
node src/index.js
fi
Запуск:
nest run # hot = false
nest run -h true # hot = true
nest run --hot # Ошибка! Ждет bool, а не просто флаг (пока что, но строгость — это хорошо)
Киллер-фича: Вложенность (Namespaces)
Вот где Nest сияет. Вы можете группировать команды логически.
database:
> desc: Database operations
reset(!force: bool = false):
> desc: Drop and recreate database
> script: ...
migrate(step: num = 1):
> desc: Run migrations
> script: ...
И вызывать это как человек:
nest database reset --force true
nest database migrate 2
Автокомплит в терминале (zsh, bash, fish) генерируется сам, так что вам не придется вспоминать, какие там команды.
Пример: Работа с массивами и циклами
Один из моих любимых примеров — когда нужно запустить что-то для списка сервисов. В make это боль (через foreach или shell-хаки). В Nest это нативно:
restart(services: arr, !force: bool = false):
> desc: Restart specific services
> script: |
#!/bin/sh
for svc in {{services}}; do
echo "Restarting $svc..."
if [ "{{force}}" = "true" ]; then
docker-compose restart -t 0 "$svc"
else
docker-compose restart "$svc"
fi
done
Запуск:
nest restart --services web,api,db
# или
nest restart api --force true
Пример: Сложный деплой с проверками
А вот что-то более реальное. Деплой с проверкой ветки, подтверждением и уведомлением.
deploy(version: str, !env|e: str = "prod"):
> desc: Deploy to environment
> env: .env.{{env}} # Подгружаем .env.prod или .env.staging
> env: DEPLOYER={{user}}
# Сначала запускаем тесты (зависимость)
> depends: test(scope="all")
> script: |
#!/bin/bash
set -e
echo "Deploying version {{version}} to {{env}}..."
echo "User: $DEPLOYER"
echo "DB Host: $DB_HOST" # Пришло из .env файла
# Простая защита от дурака
if [ "{{env}}" = "prod" ]; then
read -p "Are you sure? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
fi
./deploy-script.sh --tag {{version}} --target $SERVER_IP
Киллер-фича: Обработка ошибок (Fallback)
Сколько раз у вас падал CI, и вы узнавали об этом только через час? Nest умеет запускать fallback скрипты при ошибке.
# Определяем константы
@const SLACK_WEBHOOK = "https://hooks.slack.com/..."
backup():
> desc: Backup database
> fallback: alert_failure
> script: |
pg_dump dbname > dump.sql
# Допустим, тут кончилось место на диске и скрипт упал
alert_failure():
> desc: Send alert to Slack
> script: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Backup failed! Check server."}' \
{{SLACK_WEBHOOK}}
Если backup упадет, Nest автоматически запустит alert_failure. Никаких || ./alert.sh в каждой строке.
Глобальные переменные и функции
Если у вас монорепозиторий, вам понравятся глобальные переменные и переиспользуемые функции.
@var TAG = "latest"
@const REGISTRY = "docker.io"
# Функция (это не таск, её нельзя вызвать из CLI напрямую, только из скриптов)
@function build_image(name: str):
docker build -t {{REGISTRY}}/{{name}}:{{TAG}} .
build-front():
> script: build_image(name="frontend")
build-back():
> script: build_image(name="backend")
Это позволяет писать DRY (Don't Repeat Yourself) конфигурации.
Модульность наше всё (@include)
Если Nestfile разросся, просто разбейте его на файлы. Nest умеет собирать конфиг из кусочков.
# Основной nestfile
@include db.nest
@include docker/*.nest # Да, маски тоже работают!
@include services/ # И целые папки
# Остальные таски...
Теперь db.nest лежит отдельно, но его команды доступны как родные. Это спасение для команд, где каждый сервис хочет свой кусочек конфига.
Что под капотом?
Nest парсит свой DSL, строит граф команд и исполняет их. Все переменные шаблонизируются через {{variable}}.
Плюс, я добавил удобную работу с окружением:
deploy(version: str):
> env: .env.production # Загрузит переменные из файла
> env: USER={{user}} # Подставит текущего юзера
> env: DATE={{now}} # И дату
> script: ./deploy.sh
"А почему не..."
npm scripts? Хороши для JS, но ужасны для всего остального. Попробуйте написать там сложную логику без
&&и||ада.Gulp/Grunt? JS-only, требуют
node_modules. Nest — это один бинарник.Bash? Сложно поддерживать. Нет типов. Нет help.
И да, для этого есть VS Code Extension! ?
Потому что писать DSL в блокноте — это моветон. Я написал полноценное расширение для VS Code с подсветкой синтаксиса, навигацией и сниппетами.
В планах добавить:
Запуск команд по клику (CodeLens).
Автокомплит имен переменных.
Валидацию "на лету".
Так что опыт (DX) будет приятным. Сами файлы Nestfile выглядят в редакторе красиво и читаемо.
Итог: Это MVP, но оно живое! ?♂️
Я использую Nest каждый день в своих пет-проектах и даже начал внедрять на работе (пока никто не видит).
Да, это MVP. Там нет плагинов, нет параллельного выполнения (пока). Но оно решает главную проблему: превращает хаос скриптов в упорядоченную систему.
Проект полностью открыт (MIT). Я буду рад, если вы попробуете, напишете ишью или даже кинете PR.
А если вам тоже надоело воевать с Makefile — дайте Nest шанс. Вдруг вам понравится?
? GitHub: https://github.com/quonaro/Nest
? Установка (Linux/macOS):
curl -fsSL https://raw.githubusercontent.com/quonaro/nest/main/install.sh | sh
P.S. Если у вас тоже депрессия от тулинга — пишите в комменты, поплачем вместе.
Спасибо за внимание!
Flux82
Конечно, свой велосипед с кучей !>:|&, в каждом втором слове кажется автору более удобным... Но IMHO, глядя на Make и на Nest рядом, откровенно вообще не кажется код на Nest более "чистым" или "красивым".
Наверное, своего пользователя Nest найдёт. Мне вообще видится, что есть два равнопопулярных системных подхода: первый - все должно быть максимально просто и прямолинейно, хоть этим можно отстрелить себе ногу (C), второй - накрутим огромную гору абстракций (привет, C++) и введем огромную кучу синтаксического сахара, который уже не сахар вовсе, потому что нужен через каждые два символа (Rust). Первый мне как-то ближе. С Makefile, минималистичными bash-скриптами. И понятными правилами игры.
JBFW
"но у make был фатальный недостаток: его написали не мы!“ (с)