Что такое Makefile и как начать его использовать
Введение
В жизни многих разработчиков найдётся история про первый рабочий день с новым проектом. После клонирования основного репозитория проекта наступает этап, когда приходится вводить множество команд с определёнными флагами и в заданной последовательности. Без описания команд, в большинстве случаев, невозможно понять что происходит, например:
# Bash
touch ~/.bash_history
ufw allow 3035/tcp || echo 'cant configure ufw'
ufw allow http || echo 'cant configure ufw'
docker run \
-v /root/:/root/ \
-v /etc:/etc \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/tmp:/var/tmp \
-v /tmp:/tmp \
-v $PWD:/app \
--network host \
-w /app \
--env-file .env \
ansible ansible-playbook ansible/development.yml -i ansible/development --limit=localhost -vv
grep -qxF 'fs.inotify.max_user_watches=524288' /etc/sysctl.conf || echo fs.inotify.max_user_watches=524288 | tee -a /etc/sysctl.conf || echo 'cant set max_user_watches' && sysctl -p
sudo systemctl daemon-reload && sudo systemctl restart docker
Эти команды являются лишь частью того, что необходимо выполнить при разворачивании проекта. В приведённом примере видно, что команды сами по себе длинные, содержат много флагов, а значит, их трудно не только запомнить, но и вводить вручную. Постоянно вести документацию становится сложнее с ростом проекта, она неизбежно устаревает, а порог входа для новичков становится выше, ведь уже никто не в состоянии вспомнить всех деталей проекта. Некоторые такие команды необходимо использовать каждый день, и даже не один раз в день.
Со временем становится понятно, что нужен инструмент, способный объединить в себе подобные команды, предоставить к ним удобные шорткаты (более короткие и простые команды) и обеспечить самодокументацию проекта. Именно таким инструментом стал Makefile и утилита make
. Этот гайд расскажет, как использование этих инструментов позволит свести процесс разворачивания проекта к нескольким коротким и понятным командам:
# Bash
make setup
make start
make test
Что такое make
и Makefile
Makefile — это файл, который хранится вместе с кодом в репозитории. Его обычно помещают в корень проекта. Он выступает и как документация, и как исполняемый код. Мейкфайл скрывает за собой детали реализации и раскладывает "по полочкам" команды, а утилита make
запускает их из того мейкфайла, который находится в текущей директории.
Изначально make
предназначалась для автоматизации сборки исполняемых программ и библиотек из исходного кода. Она поставлялась по умолчанию в большинство *nix дистрибутивов, что и привело к её широкому распространению и повсеместному использованию. Позже оказалось что данный инструмент удобно использовать и при разработке любых других проектов, потому что процесс в большинстве своём сводится к тем же задачам — автоматизация и сборка приложений.
Применение мейка в проектах стало стандартом для многих разработчиков, включая крупные проекты. Примеры мейкфайла можно найти у таких проектов, как Kubernetes, Babel, Ansible и, конечно же, повсеместно на Хекслете.
Синтаксис Makefile
make
запускает цели из Makefile, которые состоят из команд:
# Makefile
цель1: # имя цели, поддерживается kebab-case и snake_case
команда1 # для отступа используется табуляция, это важная деталь
команда2 # команды будут выполняться последовательно и только в случае успеха предыдущей
Но недостаточно просто начать использовать мейкфайл в проекте. Чтобы получить эффект от его внедрения, понадобится поработать над разделением команд на цели, а целям дать семантически подходящие имена. Поначалу, перенос команд в Makefile может привести к свалке всех команд в одну цель с «размытым» названием:
# Makefile
up: # разворачивание и запуск
cp -n .env.example .env
touch database/database.sqlite
composer install
npm install
php artisan key:generate
php artisan migrate --seed
heroku local -f Procfile.dev # запуск проекта
Здесь происходит сразу несколько действий: создание файла с переменными окружения, подготовка базы данных, генерация ключей, установка зависимостей и запуск проекта. Это невозможно понять из комментариев и названия цели, поэтому будет правильно разделить эти независимые команды на разные цели:
# Makefile
env-prepare: # создать .env-файл для секретов
cp -n .env.example .env
sqlite-prepare: # подготовить локальную БД
touch database/database.sqlite
install: # установить зависимости
composer install
npm install
key: # сгенерировать ключи
php artisan key:generate
db-prepare: # загрузить данные в БД
php artisan migrate --seed
start: # запустить приложение
heroku local -f Procfile.dev
Теперь, когда команды разбиты на цели, можно отдельно установить зависимости командой make install
или запустить приложение через make start
. Но остальные цели нужны только при первом разворачивании проекта и выполнять их нужно в определённой последовательности. Говоря языком мейкфайла, цель имеет пререквизиты:
# Makefile
цель1: цель2 # такой синтаксис указывает на зависимость задач — цель1 зависит от цель2
команда2 # команда2 выполнится только в случае успеха команды из цель2
цель2:
команда1
Задачи будут выполняться только в указанной последовательности и только в случае успеха предыдущей задачи. Значит, можно добавить цель setup
, чтобы объединить в себе все необходимые действия:
# Makefile
setup: env-prepare sqlite-prepare install key db-prepare # можно ссылаться на цели, описанные ниже
env-prepare:
cp -n .env.example .env
sqlite-prepare:
touch database/database.sqlite
install:
composer install
npm install
key:
php artisan key:generate
db-prepare:
php artisan migrate --seed
start:
heroku local -f Procfile.dev
Теперь развернуть и запустить проект достаточно двумя командами:
# Bash
make setup # выполнит последовательно: env-prepare sqlite-prepare install key db-prepare
make start
Благодаря проделанной работе Makefile, команды проекта вместе с флагами сведены в Makefile. Он обеспечивает правильный порядок выполнения и не важно, какие при этом задействованы языки и технологии.
Продвинутое использование
Фальшивая цель
Использование make
в проекте однажды может привести к появлению ошибки make: <имя-цели> is up to date.
, хотя всё написано правильно. Зачастую, её появление связано с наличием каталога или файла, совпадающего с именем цели. Например:
# Makefile
test: # цель в мейкфайле
php artisan test
# Bash
$ ls
Makefile
test # в файловой системе находится каталог с именем, как у цели в мейкфайле
$ make test # попытка запустить тесты
make: `test` is up to date.
Как уже говорилось ранее, изначально make
предназначалась для сборок из исходного кода. Поэтому она ищет каталог или файл с указанным именем, и пытается собрать из него проект. Чтобы изменить это поведение, необходимо в конце мейкфайла добавить .PHONY
указатель на цель:
# Makefile
test:
php artisan test
.PHONY: test
# Bash
$ make test
✓ All tests passed!
Последовательный запуск команд и игнорирование ошибок
Запуск команд можно производить по одной: make setup
, make start
, make test
или указывать цепочкой через пробел: make setup start test
. Последний способ работает как зависимость между задачами, но без описания её в мейкфайле. Сложности могут возникнуть, если одна из команд возвращает ошибку, которую нужно игнорировать. В примерах ранее такой командой было создание .env-файла при разворачивании проекта:
# Makefile
env-prepare:
cp -n .env.example .env # если файл уже создан, то повторный запуск этой команды вернёт ошибку
Самый простой (но не единственный) способ «заглушить» ошибку — это сделать логическое ИЛИ прямо в мейкфайле:
# Makefile
env-prepare:
cp -n .env.example .env || true # теперь любой исход выполнения команды будет считаться успешным
Добавлять такие хаки стоит с осторожностью, чтобы не «выстрелить себе в ногу» в более сложных случаях.
Переменные
Зачастую в команды подставляют параметры для конфигурации, указания путей, переменные окружения и make
тоже позволяет этим управлять. Переменные можно прописать прямо в команде внутри мейкфайла и передавать их при вызове:
# Makefile
say:
echo "Hello, $(HELLO)!"
# Bash
$ make say HELLO=World
echo "Hello, World!"
Hello, World!
$ make say HELLO=Kitty
echo "Hello, Kitty!"
Hello, Kitty!
Переменные могут быть необязательными и содержать значение по умолчанию. Обычно их объявляют в начале мейкфайла.
# Makefile
HELLO?=World # знак вопроса указывает, что переменная опциональна. Значение после присвоения можно не указывать.
say:
echo "Hello, $(HELLO)!"
# Bash
$ make say
echo "Hello, World!"
Hello, World!
$ make say HELLO=Kitty
echo "Hello, Kitty!"
Hello, Kitty!
Некоторые переменные в Makefile имеют названия отличные от системных. Например, $PWD
называется $CURDIR
в мейкфайле:
# Makefile
project-env-generate:
docker run --rm -e RUNNER_PLAYBOOK=ansible/development.yml \
-v $(CURDIR)/ansible/development:/runner/inventory \ # $(CURDIR) - то же самое, что $PWD в терминале
-v $(CURDIR):/runner/project \
ansible/ansible-runner
Заключение
В рамках данного гайда было рассказано об основных возможностях Makefile и утилиты make
. Более плотное знакомство с данным инструментом откроет множество других его полезных возможностей: условия, циклы, подключение файлов. В компаниях, где имеется множество проектов, написанных разными командами в разное время, мейкфайл станет отличным подспорьем в стандартизации типовых команд: setup start test deploy ...
.
Возможность описывать в мейкфале последовательно многострочные команды позволяет использовать его как «универсальный клей» между менеджерами языков и другими утилитами. Широкая распространённость этого инструмента и общая простота позволяют внедрить его в свой проект достаточно легко, без необходимости доработок. Но мейкфайл может быть по-настоящему большим и сложным, это можно увидеть на примере реальных проектов:
Дополнительные материалы
- Руководство по современному Make — «выжимка» из документации на русском языке;
- Утилита make: полезный универсальный инструмент программиста — видео-версия данного гайда.
Мейкфайлы, использованные при составлении гайда: