Урок 10. Makefile: цель-файл — make сам отслеживает изменения

Помните из урока про историю, зачем Фелдман придумывал make? Пересобирать только то, что устарело. До этого урока наши цели были просто «именами команд» — make выполнял их без вопросов. Настало время увидеть ту самую главную идею: цель может быть файлом.

Исходник и результат

Создадим исходный конфиг config/base.conf:

# базовый конфиг
app_name = my-app
port = 8080

И опишем правило «файл из файла»:

out/app.conf: config/base.conf
	@mkdir -p out
	@echo "Пересобираю: $< -> $@"
	@cat $< > $@

Читается так: цель — файл out/app.conf, его исходник — config/base.conf. Зависимости мы знаем по уроку про цепочки целей — только теперь зависимость не «другая цель», а файл.

Разбор рецепта: $@ и $

В рецепте появились две автоматические переменные — make заполняет их сам, для каждого правила:

  • $@ — имя цели (у нас out/app.conf);
  • $< — первая зависимость, то есть первое имя в списке после двоеточия. В нашем правиле зависимость — файл-исходник config/base.conf, поэтому $< подставляет его.

Подстановка работает точно так же, как с обычными переменными из урока про переменные: перед запуском make заменяет их текстом. Наш рецепт превращается в:

@echo "Пересобираю: config/base.conf -> out/app.conf"
@cat config/base.conf > out/app.conf

Первый шаг — сообщение для человека (с уже знакомым «тихим» @ из урока про тихий вывод), второй — собственно сборка: содержимое исходника уходит в файл цели.

Смотрим на магию

Первый запуск всё собирает:

make out/app.conf
Пересобираю: config/base.conf -> out/app.conf

А второй — не делает ничего:

make out/app.conf
make: 'out/app.conf' is up to date.

«Up to date» — «актуально»: make сравнил время изменения файлов, увидел, что результат свежее исходника, и не стал работать. Изменим исходник — цель это заметит и пересоберётся:

touch config/base.conf
make out/app.conf
Пересобираю: config/base.conf -> out/app.conf

(команда touch обновляет время изменения файла, не трогая содержимое — удобно для экспериментов)

Когда make выполняет рецепт

Правило такое:

  • файл с именем цели существует — make сравнивает время его изменения с исходниками: рецепт выполнится, только если цель устарела;
  • файла нет — цель считается «всегда устаревшей», и рецепт выполняется при каждом запуске.

Именно поэтому наши прежние цели-команды — hello, help, clean — честно срабатывали каждый раз: файлов с такими именами в папке нет.

Отсюда же известные грабли: если рядом с Makefile окажется файл, случайно совпавший именем с целью-командой (скажем, файл clean), make сочтёт цель «актуальной» и откажется выполнять рецепт. Лечится это директивой .PHONY — явным обещанием «эта цель никогда не файл»; она ждёт нас в финальном уроке.

Задание

  1. Соберите цепочку из трёх файлов: a.conf ← b.conf ← c.conf (каждый собирается из следующего cat-ом). Поменяйте самый первый исходник и посмотрите, какие цели пересоберутся.
  2. Добавьте в зависимость цели второй файл и проверьте, что попадёт в $< — подсказка: только первый исходник. Как взять «все исходники сразу», узнаем в следующем уроке.
  3. Создайте файл, совпадающий именем с одной из ваших целей-команд (touch clean), и попробуйте её запустить — снимите «грабли» и объясните себе, что произошло.

Следующий урок — финальный проект: соберём конфиг приложения из базовой части и оверлея окружения dev/prod, соединив всё пройденное. Полный план — в списке уроков курса.

Пример этого урока лежит в репозитории: lesson10-file-target.