4 уровня владения Makefile

Я фанат 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-контейнера вы обычно совершаете следующие действия.

  1. Выполняете команду docker build.
  2. Убеждаетесь в отсутствии работающих контейнеров. 
  3. Выполняете команду docker run.
  4. Повторяете процедуру. 

Вот как мы это делаем самостоятельно:

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: 

  1. <target> (цель)  —  любое имя файла.  
  2. <command>  —  команды/шаги *nix для выполнения <target>. Они должны начинаться с символа табуляции TAB
  3. <prerequisite> (пререквизиты)  —  необязательная часть. Указывает make на то, что перед запуском команд все пререквизиты должны быть в наличии. Исходя из этого, они выполняются в порядке от 1 до N, как показано в примере выше. 
  4. Назначение синтаксиса @. Если командная строка начинается с @, вывод самой команды в консоль подавляется (ссылка). 
  5. Первая цель <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:

  1. .PHONY. По умолчанию make принимает цели <target> за файлы. Если же они таковыми не являются, то пометьте их с помощью директивы  .PHONY, особенно при совпадении имени файла и цели. Более подробная информация по ссылке
  2. Объявление переменной Makefile с помощью синтаксиса = или := (ссылка).
  3. := означает однократное выполнение инструкции. 
  4. = указывает на выполнение инструкции в каждом случае. Например, при необходимости в новом значении date при каждом вызове функции. 
  5. С помощью переменной среды можно проверить факт существования команды, как показано в инструкции 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 \\033[36m<target>\\033[0m\\n\\nTargets:\\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \\033[36m%-10s\\033[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 “работает”. 

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Jerry Ng: 4 Levels of How To Use Makefile

Предыдущая статьяПравило 3-х часов: сколько времени в день отводить для работы
Следующая статья12 хуков React, которые должен знать каждый разработчик