Понимание 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 оправдывает себя, когда одновременно верны три вещи:

Для горстки файлов без взаимных зависимостей 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 ведёт себя странно» оказываются на деле моментами «граф, который я ему дал, был неполным» — а это исправить гораздо проще.

По теме

Список литературы

  1. Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — собственная статья Фелдмана, представляющая Make, включая исходную мотивацию.
  2. GNU Make Manual — полный и авторитетный справочник по синтаксису и семантике GNU Make.
  3. GNU Make Manual: Automatic Prerequisites — паттерн -MMD/-include словами самих разработчиков.
  4. GCC: Preprocessor Options (-M family) — что -MMD и -MP делают "под капотом".
  5. Make (software) — Wikipedia — общая история и линия развития реализаций Make (BSD Make, GNU Make, NMAKE и др.).
  6. Stuart Feldman — Wikipedia — сведения о работе Фелдмана в Bell Labs и премии ACM Software System Award 2003 года.

Комментарии