Хватит страдать с Makefile и бесконечными bash-скриптами: как родился свой таск-раннер на Rust
Иногда просто хочется писать фичи. Но вместо этого день уходит на изучение того, почему в очередной раз не сработал `make deploy`, куда пропал аргумент в bash-функции и какой именно `deploy.sh` нужно запускать на этом проекте. Файлы с задачами разрастаются до монструозных размеров, а любой правке в Makefile начинает предшествовать короткий сеанс дыхательных упражнений.
В какой‑то момент я поймал себя на том, что трачу больше времени на «оркестрацию» задач, чем на саму разработку. И понял: так больше нельзя. Хотелось инструмента, который помогает, а не наказывает за каждую лишнюю пробел/табуляцию.
Так появился Nest — свой таск‑раннер, написанный на Rust. Идея была простой: сделать утилиту, которая заменит громоздкий Makefile, груду shell-скриптов и при этом будет выглядеть так же аккуратно и логично, как хорошо спроектированный код.
Почему Make больше не спасает
Make — легенда. Ему почти полвека, он пережил смену поколений ОС и языков. Но исторически он создавался под одну задачу: собирать C-проекты, отслеживать зависимости и пересобирать только то, что изменилось.
Мы же в 2026 году используем его для всего подряд:
- запуск Docker-контейнеров;
- выкатывание релизов на staging/production;
- прогоны линтеров и форматтеров с десятком флагов;
- миграции, сиды, ресеты баз данных;
- локальные dev-сервера с хот-релоадом.
Make это умеет… формально. Но это выглядит как бесконечный список одноуровневых целей со странным синтаксисом, где малейший неверный отступ ломает всё. Табы вместо пробелов, магические переменные, сложные выражения с `$(shell ...)`, которые никто не хочет трогать.
В итоге у нас либо один огромный Makefile, в который страшно заглядывать, либо хаотичный зоопарк `build.sh`, `deploy.sh`, `migrate.sh` и прочих скриптов. Каждый из них когда‑то «быстро накинули под задачу», и с тех пор все боятся их редактировать.
Just оказался не конечной остановкой
Разумеется, идея «нормального Make-подобного таск-раннера» приходила в голову не мне одному. Есть, например, Just. Он удобнее:
- не требует табов;
- адекватно работает с аргументами;
- лучше документирован;
- его синтаксис проще и человечнее.
Некоторое время я жил с Just и был доволен. Но довольно быстро столкнулся с тем же эффектом: Justfile начал пухнуть.
Главная проблема и Make, и Just — это плоская структура. Все команды лежат в одном уровне:
- `db-reset`,
- `test-frontend`,
- `deploy-production`,
- `deploy-staging`,
- `lint-backend`,
- `lint-frontend`.
Через месяц весь этот список выглядит как длинный чек-лист в супермаркет: всё в кучу, логики и иерархии почти нет.
Хотелось чего‑то более естественного:
чтобы можно было писать:
- `nest db reset`;
- `nest dev start`;
- `nest build frontend`;
- `nest deploy staging`.
То есть обращаться к задачам так же, как к модулям и функциям в коде: по логичной иерархии, а не по случайным названиям с дефисами.
Каким должен быть нормальный таск‑раннер
Сформировалось несколько чётких требований к инструменту, которым я сам буду пользоваться ежедневно:
1. Читабельный DSL без криптографии
Минимум символов вроде `$()`, `${VAR}`, `&&` и прочего шума. Конфиг должен читаться как текст, а не как результат компрессии bash-скрипта.
2. Иерархия и вложенность
Команды должны группироваться по смыслу. `dev.start`, `dev.stop`, `db.migrate`, `db.reset`, `deploy.staging`, `deploy.production` — и всё это вызывалось бы через `nest dev start` и т. п.
3. Типизированные параметры
Если задача ждёт число — это должно быть числом, а не строкой, которую надо парсить в shell. Если булево — то булево, а не `yes/no` или `0/1` в случайной интерпретации.
4. Умное поведение из коробки
- подхват переменных окружения, в том числе из `.env`;
- шаблонизация значений;
- автогенерация help по структуре файла;
- предсказуемое выполнение и понятные ошибки.
5. Независимость от окружения
Один бинарник без внешних зависимостей. Положил в проект или поставил в систему — и всё, он работает. Никакого Node.js, Python, npm-пакетов и прочей инфраструктуры «по кругу».
6. Производительность и безопасность
Отсюда выбор Rust:
- быстрота старта и выполнения;
- статическая линковка (через musl) — удобно таскать бинарь куда угодно;
- жёсткая типизация и безопасность по памяти.
Так родился Nest
Nest — это таск-раннер с собственным DSL, основанным на отступах. Вдохновлён идеями Python и YAML: структура задаётся пробелами, без лишних скобок и «магии» синтаксиса.
Конфигурация представляет собой дерево пространств имён (namespaces) и задач.
Примерно вместо этого:
```make
db-reset:
docker compose exec db dropdb ...
docker compose exec db createdb ...
docker compose exec db migrate ...
```
хочется писать нечто вроде:
```text
db:
reset:
run: |
docker compose exec db dropdb ...
docker compose exec db createdb ...
docker compose exec db migrate ...
```
А в терминале — запускать это как:
```bash
nest db reset
```
Вложенность и пространства имён
Именно иерархия — одна из ключевых фич Nest.
Вместо:
- `test-frontend`
- `test-backend`
- `test-e2e`
можно иметь:
```text
test:
frontend:
run: npm test -- --watch=false
backend:
run: cargo test
e2e:
run: playwright test
```
И запускать:
```bash
nest test frontend
nest test backend
nest test e2e
```
Автокомплит для оболочек (bash, zsh, fish) генерируется автоматически на основе структуры конфигурации: вы начинаете набирать `nest test` — и сразу видите все подкоманды. Это убирает ту самую «памятку в голове»: как же называлась команда — `test-fe`, `frontend-test` или ещё как-нибудь.
Работа со списками и циклами
Типичный кейс — монолитный проект с несколькими сервисами: `api`, `worker`, `frontend`, `scheduler`. Часто нужно прогнать одну и ту же задачу по всем сервисам: запустить, перезапустить, собрать, проверить.
В классическом Make это превращается в пляски с `foreach`, подстановками и внешними shell-конструкциями. В Nest предусмотрены нативные механизмы для работы с коллекциями и циклами: можно описать список сервисов и одну задачу, которая пробегается по ним.
Структурно это выглядит как декларация:
«для каждого сервиса сделай вот такой запуск».
В результате конфиг остаётся компактным, а логика — явной, без череды почти одинаковых целей `start-api`, `start-worker`, `start-frontend` и так далее.
Сложные сценарии деплоя с проверками
Реальный деплой — это почти никогда не одна команда. Обычно это:
1. Проверка, что мы на правильной ветке.
2. Подтверждение от человека: точно ли деплоим.
3. Сборка артефактов.
4. Бэкап базы и/или данных.
5. Заливка на сервер.
6. Прогон миграций.
7. Отправка уведомления об успехе или обвале.
В башевых скриптах это превращается в длинную портянку с `if`, `read`, `||` и ручной обработкой кодов возврата. Любая мелочь, добавленная через пару месяцев, легко ломает сценарий в неожиданном месте.
В Nest подобные вещи описываются как последовательность шагов, где явно видно:
- что выполняется;
- в каком порядке;
- какие проверки происходят между шагами;
- что делать, если какой-то шаг упал.
Fallback: автоматическая обработка ошибок
Один из самых приятных моментов — встроенные fallback-сценарии.
Частая практика в shell:
```bash
backup || ./alert_failure.sh
```
Каждую команду приходится «страховать» ручной проверкой. В Nest можно задать правило:
если этап `backup` завершился неуспешно — выполнить задачу `alert_failure`.
То есть обработка ошибок становится частью декларативной конфигурации, а не россыпью `|| ...` и `set -e` по всему скрипту. Это и проще читать, и легче поддерживать, и меньше риск случайно забыть обработать неудачный сценарий.
Глобальные переменные и функции
В реальных проектах огромное количество команд использует одни и те же значения:
- путь к репозиторию;
- адрес сервера;
- имя Docker-образа;
- версия приложения;
- имя базы и пользователя;
- общие флаги для линтеров, тестов, билдов.
В Nest есть глобальные переменные и переиспользуемые функции, которые позволяют написать всё это один раз и применять в разных задачах.
Благодаря этому конфиг остаётся DRY:
- меняете имя docker-образа в одном месте — и обновляется весь пайплайн;
- переиспользуете один и тот же шаг сборки в нескольких задачах;
- общий набор аргументов для команд не приходится повторять в каждой строке.
Модульность через include
Когда Nest-конфиг дорастает до десятков и сотен задач, держать его в одном файле — мучительно. Да и организационно это неудобно: разным командам нужны разные группы задач.
В Nest есть механизм разбиения конфигурации на модули и подключения их через `@include`.
Это позволяет, например:
- выделить `db.nest` для всего, что касается базы;
- `deploy.nest` — для сценариев выката;
- `dev.nest` — для локальной разработки;
- `ci.nest` — для прогонов в CI/CD.
Все они собираются в единое дерево команд, но логически и физически разделены. Командам проще договориться: каждый отвечает за «свой» кусок инфраструктуры, не мешая остальным.
Что происходит под капотом
Nest не просто тупо прогоняет строки. Под капотом он:
1. Парсит собственный DSL и строит из него абстрактное дерево команд.
2. На основе этого дерева формирует граф зависимостей: какие задачи требуют запуска других и в каком порядке.
3. Шаблонизирует переменные вида `{{variable}}`, подставляя значения из:
- окружения;
- `.env` файлов;
- локальных и глобальных объявлений внутри Nest-конфига.
4. Исполняет команды, контролируя успех/ошибки и при необходимости вызывая fallback-сценарии.
Rust даёт возможность сделать это очень быстро и предсказуемо: даже большой конфиг обрабатывается практически моментально, а ошибки в описании задач детектируются на стадии парсинга, с понятными сообщениями.
Почему не npm scripts, Gulp, Bash и прочие
В роли таск‑раннеров часто выступают совсем другие инструменты:
- npm scripts
Отлично подходят для маленьких JS-проектов, где всё крутится вокруг Node. Но как только нужно описать сложную оркестрацию — начинается ад из `&&`, `||`, `cross-env`, кавычек и экранирования. Плюс они жёстко привязаны к Node-миру.
- Gulp/Grunt
Мощные, но снова JS-only и требуют наличие вокруг стека Node.js, dependency management и прочих радостей. Заводить это ради того, чтобы один бинарник запускал docker-команду и пару скриптов — избыточно.
- Bash-скрипты
Максимально универсальны, но:
- слабая читаемость на больших объёмах;
- отсутствие типов;
- легко допустить неочевидную ошибку;
- бедный встроенный help;
- высокая зависимость от среды (разные версии bash/sh, разные утилиты).
Nest решает именно эту задачу: быть компактным, читаемым и в меру «умным» слоем над реальными командами, который не привязан к конкретному языку, экосистеме или фреймворку.
Расширение для редактора
Работать с DSL вслепую неудобно, поэтому у Nest есть полноценное расширение для VS Code:
- подсветка синтаксиса;
- базовая навигация по структуре;
- сниппеты для часто используемых конструкций;
- помощь при написании новых задач.
Это делает конфиг не «магическим текстом», а полноценным рабочим артефактом проекта, с которым комфортно взаимодействовать ежедневно.
Где Nest особенно полезен
Хотя таск‑раннер в целом универсален, есть несколько сценариев, где он особенно заходит:
1. Монорепозитории
Несколько сервисов, много команд, разные пайплайны — и необходимость держать всё это единым целым. Вложенные неймспейсы и include позволяют разрулить хаос.
2. Команды с разными языками и стеками
Когда в одном проекте живут Rust-сервисы, Node-приложения, Python-скрипты и, например, Terraform — нужен единый слой оркестрации, не привязанный к конкретному языку.
3. Инфраструктурные пайплайны
Миграции БД, подготовка окружений, синхронизация данных, бэкапы, деплои. Всё это удобно собирать в иерархию задач с fallback-сценариями.
4. Developer Experience
Новому разработчику достаточно открыть один конфиг, посмотреть список доступных команд и их структуру, а дальше использовать автокомплит и help. Порог входа в проект сильно снижается.
Практические советы по миграции с Make/Just на Nest
Если вы задались целью избавиться от огромного Makefile или разросшегося Justfile, полезно двигаться по шагам:
1. Выделите группы задач
Разбейте все цели на логические блоки: `dev`, `db`, `deploy`, `test`, `lint` и т. д.
2. Сначала перенесите только верхний слой
Не пытайтесь переписать весь пайплайн за один день. Начните с пары ключевых сценариев: запуск dev-сервера, базовый деплой, прогон тестов.
3. Подумайте об именовании и иерархии
Сделайте структуру такой, чтобы она была очевидна даже человеку, который впервые видит проект. Это окупится уже через пару недель.
4. Выделяйте общие части в функции и переменные
Как только увидели дублирование — выносите. Nest как раз для этого и придуман.
5. Оставьте старые скрипты как fallback
На время перехода можно держать и Makefile, и Nest-конфиг, постепенно перенося команды. Как только новая конфигурация покрывает подавляющее большинство задач — старое можно смело выкидывать.
Итог: живое MVP, которое уже можно использовать
Nest начинался как личный эксперимент «на выходные», чтобы перестать воевать с Makefile и bash-скриптами. По факту из него вырос полноценный инструмент:
- с читаемым DSL на отступах;
- с иерархией команд и неймспейсами;
- с типизированными параметрами;
- с шаблонизацией и работой с окружением;
- с fallback-сценариями при ошибках;
- с модульностью через include;
- с поддержкой автокомплита и синтаксической подсветкой.
Формально это всё ещё MVP, но уже вполне боеспособное: его можно брать и использовать в реальных проектах, постепенно выдавливая из них хрупкие Makefile и ветхие bash-скрипты.
Если вы устали открывать очередной `deploy.sh` с мыслью «лучше бы я этого не видел», возможно, вам тоже пора завести свой аккуратный таск‑раннер — или хотя бы попробовать Nest и посмотреть, как себя чувствует проект, когда инфраструктурные команды превращаются из хаотичного списка в стройное дерево задач.



