Make files not war: что такое утилита GNU make, зачем ее использовать и как это делать правильно

В этой статье мы поговорим о некоторых тонкостях работы с утилитой GNU make, а также научимся писать простые и аккуратные make-файлы. Последнее особенно важно — make-файлы выглядят сложно и нечитабельно, если им не уделить должного внимания. Это обеспечивает make плохую репутацию, хотя на самом деле инструмент крайне удобный, особенно для автоматизации сборки.

В большинстве проектов, будь то разработка ПО, написание книги или публикация записи в блоге, в какой-то момент приходится генерировать «конечные продукты» из вручную написанных исходных файлов, то есть осуществлять сборку.

Суть автоматической сборки состоит в том, чтобы взять за основу генерацию какого-то одного элемента, вычленить правила, по которым она происходит, и применить их к большему количеству элементов такого же типа, обновляя только изменяемые параметры.

Это может быть сделано (и часто делается) под определенную задачу с помощью кастомных скриптов, но мы в данной статье рассмотрим, как можно удобно автоматизировать сборку, используя утилиту make и её встроенные правила.

Почему стоит использовать утилиту make

  • она работает;
  • легко настраивается как для новых, так и для существующих проектов;
  • в большинстве ОС она предустановлена, если нет — её легко скачать;
  • она крошечная и содержит мало зависимостей;
  • make-файлы всё-таки могут быть короткими, ёмкими и красивыми;
  • она не использует загадочные папки типа working или resource;
  • да и вообще темной магией не занимается — всё на виду.

Пишем make-файлы

Создадим файл и назовем его makefile или Makefile. Содержание стандартного make-файла можно описать так: «если любой из файлов-пререквизитов был изменен, то целевой файл должен быть обновлен». Суть make в том, что нам нужно по определенным правилам произвести какие-то действия с пререквизитами, чтобы получить некую цель.

Правила, которые и составляют основу make-файла, по сути являются командами и могут быть сколь угодно сложными, а также могут содержать в себе вызов других инструментов, таких как компиляторы и парсеры.

Базовый синтаксис для определения цели (в файле makefile):

цель: реквизит1 реквизит2 ...
команда1
команда2
...
<пустая строка>

Важно Индентация производится с помощью табуляции, а не пробелов.

В командах описывается, что make должна сделать, чтобы создать целевой файл. Они исполняются, когда мы делаем запрос на создание или обновление целевого файла и make заключает, что пререквизиты изменились или целевой файл еще не существует.

Часто нам нужно обрабатывать несколько файлов одного вида по одним и тем же правилам. Например, при создании страниц HTML на основе размеченного текста. Это делается с помощью шаблонных правил, наличие которых является отличительной чертой make в сравнении с обычной сборкой через командную строку.

Шаблонные правила работают на основе сопоставления расширений файлов. Например, make знает, как создавать объектные файлы *.o из исходных C-файлов *.c, компилируя их и передавая компилятору флаг -c. В make есть несколько встроенных шаблонных правил, самые известные из которых используются для компиляции кода на C и C++.

Теперь мы можем упростить make-файл, избегая написания команд в случаях, когда make сама знает, что делать. Главное, не забывать ставить пустую строку после цели — make это нужно, чтобы определять, где кончается одна цель и начинается другая.

В большинстве случаев мы можем даже опустить пререквизиты: внутренние правила make подразумевают, что для того, чтобы, например, собрать somefile.o по принципу Исходник на C → Объектный файл, нам нужен somefile.c.

Будьте аккуратны: когда вы предлагаете make свой список команд, она будет ориентироваться только на ваш код и в данном случае не будет искать шаблонные правила для сборки цели.

Вызываем make

Запустим make в текущей директории:

make

Если make-файл в ней уже есть, будет создана (собрана) первая цель, которую make сможет найти. Если make-файла нет (или он есть, но в нем нет целей), make об этом сообщит.

Чтобы обратиться к конкретной цели, запустите:

make [цель]

Здесь цель — это название цели (без квадратных скобок).

make может догадаться, что делать, используя встроенные правила, без дополнительной информации от пользователя. Попробуйте создать файл test.c в пустой папке и запустить make. test.make скомпилирует test.c, используя встроенное шаблонное правило, и соберет цель с названием test. Это сработает даже через несколько шагов генерации промежуточных файлов.

Специальные цели

В большинстве make-файлов можно найти цели, называемые специальными. Вот самые распространенные:

  • all — собрать весь проект целиком;
  • clean — удалить все сгенерированные артефакты;
  • install — установить сгенерированные файлы в систему;
  • release или dist — для подготовки дистрибутивов (модули и тарболы).

Они не обязательно должны присутствовать в make-файле, но большинство сборочных процессов странно представить без хотя бы первых трех.

При сборке этих целей не будут созданы файлы с именами, например, all или clean. make обычно решает, запускать ли какие-либо процессы, основываясь на данных о том, нужно ли изменять целевой файл. Создание файлов с подобными именами не даст make произвести никаких изменений с целями.

Для предотвращения этого GNU make позволяет помечать такие цели как «фиктивные» (phony), чтобы запускать их в любом случае. Сделать это можно, добавив необходимые цели в качестве пререквизитов во внутреннюю цель .PHONY следующим образом:

.PHONY: all clean run

Цель all обычно просто содержит главный исполняемый файл в пререквизите — иногда с дополнительными командами для пост-обработки генерируемых файлов. Так как мы в большинстве случаев хотим, чтобы цель all исполнялась по умолчанию, она должна стоять первой в файле.

Для цели clean стоит добавить список команд, удаляющих все генерируемые файлы. Это может быть легко сделано с использованием переменных, о которых мы поговорим в следующем разделе.

Переменные и функции

Как и в большинстве языков программирования, make-файлы можно сделать более читабельными, используя переменные. make импортирует свою среду, позволяя нам определять переменные внутри файла через внешние источники.

Основные операции

Определять переменные и ссылаться на них можно следующим образом:

NAME = value
FOO = baz
bar = $(FOO) frob

Ссылаться на переменные можно через $(NAME) или ${NAME}. Если опустить скобки, make сочтет за имя переменной только первый символ. Присоединение осуществляется при помощи оператора +=. Можно также задать условные переменные с помощью ?= (если им еще не присвоены значения).

Наконец, большинство реализаций make позволяют нам задать выходную переменную с помощью оператора != при порождении одного подпроцесса за операцию.

Передача аргументов встроенным шаблонным правилам

Ранее мы видели некоторые шаблонные правила в действии, правда, во всех случаях они запускали одни и те же базовые команды. Конечно, требования к сборке не всегда одинаковы, и инструменты могут запрашивать разные флаги для разных задач.

Например, какому-то ПО нужно обеспечить соединение с разными библиотеками, или заданные оптимизации различаются для сборки отладки и итоговой сборки.

Поэтому встроенные правила в make включают в себя несколько распространённых переменных в важных местах команд. Мы можем установить их по желанию из make-файла или внешней среды.

Это позволяет, например, запускать один и тот же make-файл с разными компиляторами, снабдив make необходимым именем бинарного файла для выполнения. Так задаётся переменная среды компилятора C:

CC=clang make

Вот некоторые из самых известных переменных, которые вы могли видеть, если когда-нибудь заглядывали в make-файл:

  • $(CC) / $(CXX) — бинарные файлы для компиляторов C и C++, которые make использует для сборки;
  • $(CFLAGS) / $(CXXFLAGS) — флаги, передаваемые компиляторам;
  • $(LDLIBS) — присоединяемые библиотеки.

Программные переменные

make хранит некоторые распространённые программы в переменных. В основном это делается для того, чтобы при необходимости их можно было перезаписать.

Самая важная из них — $(MAKE), которая должна использоваться при рекурсивном вызове make из make-файла. Она принимает во внимание аргументы командной строки из исходного вызова.

В цели clean, главная задача которой — удаление файлов, безопаснее использовать переменную $(RM) вместо прямого вызова rm.

Функции нескольких переменных

GNU make определяет некоторые очень полезные методы, большинство которых работает со словами, то есть с разделенными пробелами строками символов. Это облегчает работу с переменными, содержащими в себе списки файлов.

Функции вызываются таким же образом, как вызывалась бы переменная с таким же именем, но с добавлением аргументов перед закрывающей скобкой.

Вот некоторые наиболее интересные методы:

  • $(wildcard шаблон) возвращает список с названиями файлов, соответствующих шаблону, которые в том числе могут представлять собой относительный путь. Список внутри разделен с помощью пробелов, что проблематично для работы с файлами, содержащими пробелы в названии. Лучше всего избегать таких файлов при работе с make. Шаблон может содержать универсальный символ *;
  • $(patsubst шаблон поиска, шаблон замены, список слов) заменяет все слова в списке, которые соответствуют шаблону поиска в соответствии с шаблоном замены. Оба шаблона используют % в качестве символа;
  • $(filter-out шаблон поиска, список слов) возвращает список всех слов, отфильтрованных по шаблону поиска;
  • $(notdir список слов) возвращает список слов, где имя каждой записи сокращается до основного (то есть если имя содержит название директории, то оно отфильтровывается);
  • $(shell команда) запускает команду в подпроцессоре и перехватывает стандартный вывод подобно оператору !=. Оболочка для выполнения команды определяется переменной $(SHELL).

Подробное описание функций можно найти в официальной документации.

Продвинутое использование переменных

Отсылки к переменным можно делать в любом контексте внутри make-файла. Можно даже соорудить имя исполняемого файла внутри списка команд с помощью соединения нескольких переменных. Это позволяет использовать переменные в качестве целей или пререквизитов и создавать простые конструкции типа:

OBJECTS = $(patsubst %.c,%.o,$(wildcard *.c))
all: $(OBJECTS)

Данный make-файл создает список всех исходных C-файлов в директории, заменяет суффикс .c на .o, используя функцию $(patsubst ...), и потом использует этот список файлов в качестве пререквизитов к цели all. При запуске make станет собирать цель all, потому что она определена первой. Так как цель зависит от нескольких объектных файлов, которые могут ещё не существовать или должны быть обновлены, а make знает, как их сделать из исходных C-файлов, все запрашиваемые файлы также будут собраны.

Это становится мощным инструментом в сочетании с шаблонными правилами — мы можем автоматически собирать пререквизиты, которые косвенно используются в качестве новых целей.

Замена суффиксов

Для совместимости с другими реализациями make обеспечивает альтернативный синтаксис при вызове функции $(patsubst ...), называемый «ссылка с заменой» и позволяющий заменить некоторые суффиксы в списке слов на другие.

Make-файл из предыдущего примера можно преобразовать следующим образом:

FILES != echo *.c
OBJS = $(FILES:.c=.o)
all: $(OBJS)

Важно Вместо функции $(wildcard ...) используется оператор !=.

Целезависимые переменные

Это ещё одна интересная особенность, использование которой ведёт к сокращению кода: она позволяет устанавливать переменным разные значения в зависимости от текущей цели.

FOO = bar
target1: FOO = frob
target2: FOO += baz

В этом примере мы бы установили $(FOO) значение bar глобально, значение frob для цели один и значение bar baz для цели два.

Можно использовать любые необходимые операторы присваивания, что позволяет, например, создавать цели с разными наборами флагов для компилятора, просто присвоив переменной $(CFLAGS) разные значения.

Интеграция с внешними процессами

В связи с тем, что дистрибутивы Linux в большинстве случаев поставляются с системой управления пакетами и многие пакеты создаются из проектов, использующих make для сборки, были приняты некоторые общие правила использования переменных.

В основном они касаются установки целей проекта, так как бинарные пакеты часто состоят из результатов запуска make install, упакованных в распространённом формате. Важно запомнить следующие переменные:

  • $(DESTDIR) должна быть пустой по умолчанию и никогда не должна задаваться из make-файла (режим чтения). Используется составителями пакета для вставки пути доступа перед устанавливаемыми файлами;
  • $(PREFIX) — значение этой переменной в вашем make-файле должно соответствовать /usr/local или другому заданному вами пути. Она позволяет пользователю пакета задать желаемую директорию для установки. Задавайте значение этой переменной, только если оно не было передано окружением (используя оператор ?=).

Определение шаблонных правил

Синтаксис для написания шаблонных правил во многом похож на обычный синтаксис целей. Основное отличие состоит в использовании шаблонного символа % в основе цели (её имени до расширения), который и будет применяться для поиска соответствий.

Шаблон также может быть использован в списке пререквизитов, где он будет заменён на основу для формирования неявно определённых пререквизитов. (Список пререквизитов в случае чего можно расширить и явно определёнными.)

Также можно перезаписать любое из встроенных правил с помощью определения правила с такой же целью и пререквизитами.

Так как шаблонное правило должно быть написано таким образом, чтобы отличаться от реальных имён файлов, с которыми оно вызывается, make предоставляет специальные переменные для определения шаблонных правил.

Динамические переменные

Все имена динамических переменных состоят из одного знака, поэтому скобки для ссылки на них не нужны. Таких переменных довольно много, рассмотрим наиболее необходимые:

  • $@ — полное название цели;
  • $< — имя первого пререквизита (в том числе косвенно сгенерированного поиском по шаблону);
  • $^ — разделенный пробелами список всех пререквизитов (версия GNU).

Шаблонное правило, конвертирующее размеченные файлы в HTML с использованием markdown, получает такой вид:

%.htm : %.md 
    markdown $^ > $@

Итоги

Ранее мы разобрали некоторые из наиболее трудных аспектов в контексте make-файлов. Перед вами относительно сложный, но тем не менее полезный make-файл, использующийся для статей на сайте автора:

.PHONY: all clean
ARTICLES = $(patsubst %.md,%.htm,$(wildcard *.md))

%.htm : %.md index.htm
./generate_article.py $&lt; &gt; $@

all: $(ARTICLES)

clean:
$(RM) $(ARTICLES)

Скрипт generate_article.py реализует минималистичный шаблонизатор, используя index.htm в качестве базы для вставки HTML, сгенерированного из входных файлов. Присутствие шаблонизатора в пререквизитах шаблонного правила обеспечивает, что изменения в шаблонизаторе вызовут изменения всех файлов, относящихся к статье.

Для дальнейшего изучения make рекомендуем ознакомиться с официальным руководством.

По материалам статьи «Make Files Not War»