Понимание Make
August 1, 2026 · – посетителей
make встречается почти в каждом встраиваемом и системном проекте, с которым я сталкивался — в рецептах Yocto, сборках ядра, U-Boot, небольших репозиториях прошивок. Это также один из тех инструментов, которые люди годами используют по подражанию, копируя Makefile из предыдущего проекта, так и не разобравшись до конца в модели, лежащей в его основе. Вот эта модель, изложенная напрямую.
Что такое Make?
Make — это инструмент автоматизации сборки: он читает файл (Makefile), описывающий набор целей (targets), предпосылок (prerequisites), от которых зависит каждая цель, и рецепт (команды shell), необходимый для получения этой цели из её предпосылок. Имея такое описание, make определяет, какие цели устарели, и запускает только те рецепты, которые нужны для их обновления — не больше.
main: main.o utils.o
gcc -o main main.o utils.o
main.o: main.c
gcc -c main.c
Запустив make, вы получите пересборку main.o только если main.c новее, чем main.o (или если main.o ещё не существует), а затем компоновку main только если изменился main.o или utils.o. Ничего не пересобирается без необходимости.
Для кого он предназначен?
Make был написан Стюартом Фелдманом (Stuart Feldman) в Bell Labs в апреле 1976 года. По его собственному рассказу, толчком послужил коллега — Стив Джонсон (Steve Johnson), автор yacc, — потерявший всё утро на отладку программы, которая на самом деле была верна; просто бинарник не был перелинкован после изменения исходного кода. За это Фелдман получил премию ACM Software System Award 2003 года. Инструмент, родившийся из того дня, — отслеживать зависимости один раз и позволить машине самой решать, что нужно переделать, — стал одной из самых широко скопированных идей в инструментарии разработки ПО. GNU Make, версия, которой сегодня пользуется практически каждый, — это свободная переработка от проекта GNU (1) (5) (6).
На практике он предназначен для всех, кто собирает нетривиальный артефакт — скомпилированный бинарник, документ, набор сгенерированных файлов — из более мелких частей, где пересборка всего с нуля при каждом изменении была бы расточительной или медленной. Это касается проектов на C/C++, сборок ядра и загрузчика, документов LaTeX и множества специальной автоматизации, не имеющей никакого отношения к компиляции кода.
Когда стоит к нему обращаться?
Make оправдывает себя, когда одновременно верны три вещи:
- Существует настоящий граф зависимостей — одни выходные данные зависят от других, и пересборка — это не просто «перегенерировать всё».
- Пересборки достаточно дороги, чтобы пропуск ненужных имел значение (двухсекундная компиляция в этом не нуждается; двадцатиминутная сборка ядра — нуждается).
- Правила достаточно стабильны — модель Make, основанная на временных метках файлов, не справляется с крайне динамичными конвейерами так же изящно, как это мог бы сделать task runner с явной инвалидацией.
Для горстки файлов без взаимных зависимостей shell-скрипт зачастую проще и честнее. Make начинает окупаться, когда вопрос «а действительно ли это нужно пересобирать?» становится вопросом, который стоит задавать автоматически, а не вручную.
Где он находится в цепочке инструментов?
Make обычно располагается на слой выше компилятора и на слой ниже того, что запускает человек. В типичном C-проекте: исходные файлы → (компилятор, запускаемый Make) → объектные файлы → (компоновщик, запускаемый Make) → итоговый бинарник. Сам Make ничего не компилирует и не компонует — он вызывает инструменты, которые это делают, в виде shell-команд в каждом рецепте.
Он также обычно находится под более высокоуровневыми инструментами конфигурации сборки. CMake, Autotools и BitBake из Yocto, например, не заменяют Make, а скорее генерируют Makefile (или управляют его аналогом) в качестве своего реального бэкенда исполнения. Так что даже проекты, которые никогда напрямую не показывают вам Makefile, часто где-то в конвейере его всё же запускают.
Почему он работает именно так?
Ключевое проектное решение — сравнивать время модификации файлов, чтобы определить, что устарело, — это то, что делает Make быстрым на больших деревьях: проверка временной метки дёшева, поэтому пересборка огромного проекта после однострочного изменения может пропустить почти всё и при этом остаться корректной, при условии, что переданный ему граф зависимостей точен.
Именно эта последняя оговорка — источник почти всех жалоб вида «Make сломан», с которыми сталкиваются люди, и стоит на ней задержаться: Make настолько корректен, насколько корректен граф, о котором ему сообщили. Если файл .c подключает через #include заголовок, который не указан как предпосылка соответствующего файла .o, редактирование этого заголовка не вызовет пересборку — не потому, что Make запутался, а потому, что ему никто никогда не сообщил о существовании этой зависимости. Именно поэтому реальные Makefile почти всегда сочетаются с этапом генерации зависимостей компилятором (-MMD -MP у GCC/Clang), а не со списками заголовков, написанными вручную, — компилятор уже знает истинный граф включений, так что он единственный надёжный источник для этого (4).
Как он на самом деле работает?
Цели, предпосылки, рецепты. Базовая единица выглядит так:
target: prerequisite1 prerequisite2
recipe line 1
recipe line 2
Строки рецепта должны быть с отступом в виде табуляции, а не пробелов — знаменитая историческая шероховатость, которая до сих пор сбивает людей с толку.
Автоматические переменные снижают повторение:
%.o: %.c
gcc -c $< -o $@
$@ — это цель, $< — первая предпосылка: одно это шаблонное правило может собрать каждый .o из соответствующего .c без единой строки на файл.
Цели .PHONY отмечают имена, которые не соответствуют реальным файлам — clean, all, test, — чтобы Make не запутался, если файл с таким именем вдруг существует, и всегда выполнял рецепт вместо проверки временных меток: (2)
.PHONY: clean
clean:
rm -f *.o main
Переменные избавляют от повторения флагов повсюду и облегчают переиспользование между проектами:
CC = gcc
CFLAGS = -Wall -O2
main.o: main.c
$(CC) $(CFLAGS) -c main.c
Автоматическая генерация зависимостей закрывает описанный выше пробел:
CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)
%.o: %.c
$(CC) $(CFLAGS) -c $<
-MMD заставляет компилятор выдавать файл .d для каждого объекта, перечисляя каждый заголовок, действительно подключённый при этой компиляции. -include подмешивает эти файлы обратно в Makefile, так что граф зависимостей остаётся точным по мере изменения заголовков — без того, чтобы кто-то поддерживал его вручную (3).
Заключительная мысль
Большая часть того, что выглядит как причуды Make, — сравнения временных меток, настойчивое требование табуляции, то, как отсутствующая зависимость приводит к тихому, а не громкому сбою, — проистекает из одного проектного решения, принятого в 1976 году: держать модель простой, а проверку дешёвой, и доверять тому, что Makefile точно описывает реальность. Стоит принять эту точку зрения, и большинство моментов «Make ведёт себя странно» оказываются на деле моментами «граф, который я ему дал, был неполным» — а это исправить гораздо проще.
По теме
- Git: удаление веток, слияние и отмена неудачного push — повседневные команды веток и push
Список литературы
- Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — собственная статья Фелдмана, представляющая Make, включая исходную мотивацию.
- GNU Make Manual — полный и авторитетный справочник по синтаксису и семантике GNU Make.
- GNU Make Manual: Automatic Prerequisites — паттерн
-MMD/-includeсловами самих разработчиков. - GCC: Preprocessor Options (
-Mfamily) — что-MMDи-MPделают "под капотом". - Make (software) — Wikipedia — общая история и линия развития реализаций Make (BSD Make, GNU Make, NMAKE и др.).
- Stuart Feldman — Wikipedia — сведения о работе Фелдмана в Bell Labs и премии ACM Software System Award 2003 года.
Комментарии