Я фанат Makefile
и активно задействую его в большинстве своих текущих проектов. Возможно, Makefile
попадался вам на GitHub в различных проектах с открытым ПО (пример). Наверняка вы задавались вопросом, что это за инструмент и что он делает.
По Makefile
создано немалое количество обучающих материалов. Цель данного руководства — разжечь интерес к Makefile
и научить им пользоваться буквально за 5 минут. В результате вы приобретаете навык работы с этим инструментом и можете продолжать самостоятельно его изучать.
При желании пропускайте вступительную часть и сразу переходите к Уровню 1 и Уровню 2.
Определение Make и Makefile
Если кратко, то Makefile
— это файл специального формата с инструкциями для утилиты GNU Make, т.е. make
, по выполнению команд, работающих в системах *nix
. Как правило, Make
применяется для компиляции, сборки и установки ПО.
Хотя Makefile
обычно используется для компиляции C и C++, он НЕ ограничен каким-либо конкретным языком программирования. Makefile
решает разные задачи:
- выполнение цепочки команд для настройки среды разработки;
- автоматическая сборка;
- запуск наборов тестов;
- развертывание.
Аргументы в пользу Makefile
Компиляция исходного кода может стать проблематичной, особенно при необходимости включать множество исходных файлов.
По сути, Makefile
представляет собой утилиту для написания и выполнения набора инструкций командной строки для таких процедур, как компиляция кода, его тестирование, форматирование, запуск и т.д.
Он помогает автоматизировать рабочие процессы разработки в виде простых команд (make build
, make test
, make format
и make run
).
Дополнительные преимущества:
make
предустановлен на большинстве существующих систем*nix
;make
не зависит от языка программирования/фреймворка.
От слов переходим к делу!
Краткое руководство по Makefile
При последующем разборе уровней обучения рекомендую создать Makefile
и самим выполнять практические задания.
Примечание. Файл всегда должен носить имя Makefile
.
Уровень 1. “Расскажи все, что нужно знать”
На этом уровне изучаем основы Makefile
. Скорее всего, этих знаний хватит для эффективной работы с ним.
Допустим, у нас есть проект, использующий Docker. Для многократной сборки и запуска приложения с помощью Docker-контейнера вы обычно совершаете следующие действия.
- Выполняете команду
docker build
. - Убеждаетесь в отсутствии работающих контейнеров.
- Выполняете команду
docker run
. - Повторяете процедуру.
Вот как мы это делаем самостоятельно:
docker build -t image-name . --build-arg ENV_VAR=foo
docker stop container-name || true && docker rm container-name || true
docker run -d -e ANOTHER_VAR=bar--name container-name image-name
Довольно напряжно! Столько всего нужно запомнить и напечатать. Кроме того, возрастает вероятность допустить глупые ошибки.
Конечно, можно просто выполнять все три команды при каждом внесении изменений. Такой прием сработает, но об эффективности можно забыть. Вместо этого напишем Makefile
следующим образом:
all: build stop run # build -> stop -> run
build:
@docker build -t image-name . --build-arg ENV_VAR=foo
stop:
@docker stop container-name || true && docker rm container-name || true
run:
@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
Теперь для сборки и запуска нового Docker-образа потребуется всего лишь одна команда make all
. В вышеуказанном случае можно просто вызвать make
, поскольку all
является первым правилом. Обратите внимание, что первое правило выбирается по умолчанию.
Правило в Makefile
обычно выглядит так:
# запуск `make <target>` для выполнения данного правила
<target>: <prerequisite 1> <prerequisite 2> <prerequisite N> # перед блоком комментариев ставится префикс "#"
<command 1>
<command 2>
<command N>
Ключевые понятия уровня 1:
<target>
(цель) — любое имя файла.<command>
— команды/шаги*nix
для выполнения<target>
. Они должны начинаться с символа табуляцииTAB
.<prerequisite>
(пререквизиты) — необязательная часть. Указываетmake
на то, что перед запуском команд все пререквизиты должны быть в наличии. Исходя из этого, они выполняются в порядке от 1 до N, как показано в примере выше.- Назначение синтаксиса
@
. Если командная строка начинается с@
, вывод самой команды в консоль подавляется (ссылка). - Первая цель
<target>
выбирается по умолчанию при запускеmake
.
Переходим на следующий уровень и рассмотрим Makefile
в действии!
Уровень 2. “Круто, но хочется большего”
Подстановка переменных довольно часто встречается во всех аспектах программирования. В этом плане Makefile
— не исключение.
Так как же применять переменные среды (по умолчанию)?
Допустим, нужно организовать сборку приложения с разными аргументами, например ENV_VAR
. В этом случае Makefile
принимает следующий вид:
NAME := my-app
DOCKER := $(shell command -v docker 2> /dev/null)
ENV_VAR := $(shell echo $${ENV_VAR-development}) # Примечание: двойной символ $ для экранирования
.PHONY: build
build: ## сборка Docker-образа на основе shell ENV_VAR
@if [ -z $(DOCKER) ]; then echo "docker is missing."; exit 2; fi # tip
@docker build -t $(NAME) . --build-arg ENV_VAR=$(ENV_VAR)
Ключевые понятия уровня 2:
.PHONY
. По умолчаниюmake
принимает цели<target>
за файлы. Если же они таковыми не являются, то пометьте их с помощью директивы.PHONY
, особенно при совпадении имени файла и цели. Более подробная информация по ссылке.- Объявление переменной
Makefile
с помощью синтаксиса=
или:=
(ссылка). :=
означает однократное выполнение инструкции.=
указывает на выполнение инструкции в каждом случае. Например, при необходимости в новом значенииdate
при каждом вызове функции.- С помощью переменной среды можно проверить факт существования команды, как показано в инструкции
if
.
Дополнительная информация:
echo $${ENV_VAR-development}
устанавливаетENV_VAR
на основе текущей среды shell со значением по умолчанию дляdevelopment
.- Как вариант,
make
позволяет передавать переменные и переменные среды из командной строки, напримерENV_VAR=development make build
.
Теперь вы знаете достаточно, чтобы создавать Makefile
для небольших и средних проектов.
Двигаемся дальше и расширяем наши возможности с Makefile
!
Уровень 3. “Покажи, что-нибудь особенное”
Мы подошли к самой излюбленной части. Думаю, что всем по нраву содержательное сообщение help
.
Создадим полезную цель help
на тот случай, если пользователям потребуется помощь по использованию Make
в проекте:
.DEFAULT_GOAL := help
.PHONY: help
help:
@echo "Welcome to $(NAME)!"
@echo "Use 'make <target>' where <target> is one of:"
@echo ""
@echo " all run build -> stop -> run"
@echo " build build docker image based on shell ENV_VAR"
@echo " stop stop docker container"
@echo " run run docker container"
@echo ""
@echo "Go forth and make something great!"
all: build stop run
.PHONY: build
build:
@docker build -t image-name . --build-arg ENV_VAR=foo
.PHONY: stop
stop:
@docker stop container-name || true && docker rm container-name || true
.PHONY: run
run:
@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
.DEFAULT_GOAL
. Ранее упоминалось о том, что при запускеmake
первое правило выбирается по умолчанию. Инструкция.DEFAULT_GOAL
позволяет отменить это действие и явно указать цель.- С помощью
.DEFAULT_GOAL
вы просто запускаетеmake
для отображения сообщенияhelp
каждый новый раз.
Однако каждая новая цель в Makefile
должна добавляться новой строкой echo
.
Как вам идея о самодокументирующемся сообщении help
?
Меняем цель help
соответствующим образом:
.DEFAULT_GOAL := help
.PHONY: help
help: ## отображение данного сообщения help
@awk 'BEGIN {FS = ":.*##"; printf "\\nUsage:\\n make \3[36m<target>\3[0m\\n\\nTargets:\\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \3[36m%-10s\3[0m %s\\n", $$1, $$2 }' $(MAKEFILE_LIST)
Далее добавляем комментарии с помощью тега ##
, чтобы вывести их как часть сообщения help
:
all: build stop run ## run build -> stop -> run (запуск build -> stop -> run)
.PHONY: build
build: ## build docker image (сборка docker-образа)
@docker build -t image-name . --build-arg ENV_VAR=foo
.PHONY: stop
stop: ## stop running container (остановка запущенного контейнера)
@docker stop container-name || true && docker rm container-name || true
.PHONY: run
run: ## run docker container (запуск docker-контейнера)
@docker run -d -e ANOTHER_VAR=bar--name container-name image-name
В результате получаем отличное самодокументирующееся сообщение help
:
$ make Usage: make <target> Targets: help display this help all run build -> stop -> run build build docker image stop stop running container run run docker container
Уровень 4. “Статья не включает детальный разбор темы!”
Этот уровень предполагает самостоятельное погружение в тему Makefile
и расширение знаний на основе качественных обучающих руководств.
Заключение
Даже если вы пишите отличную документацию, Makefile
наверняка окажется намного более надежным инструментом. Его преимущество заключается в строгом принципе документирования выполняемых действий.
Если вы участвуете в проектах с открытым ПО, то задействуйте Makefile
для определения рабочих процессов разработки.
Послесловие. Текущее состояние Makefile
Makefile
появился в 1976 году и с тех пор прошел через взлеты и падения.
Чаще всего Make
критикуют за сложный синтаксис. Как следствие, программисты разделились на поклонников и противников Makefile
.
Думаю, тут дело вкуса. У каждого есть свои предпочтения. Я вот не вижу смысла загружать еще одну новомодную программу/инструмент/фреймворк/библиотеку по многим другим причинам.
Ведь Makefile
“работает”.
Читайте также:
- Как написать красивый и информативный README.md
- Слабо решить эти задачи по программированию?
- С этими советами у junior-разработчиков не возникнет проблем на их первой работе
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jerry Ng: 4 Levels of How To Use Makefile