Заметки о GNU Make
Полезные ссылки
- Хорошая статья
- Другая хорошая статья
- Примеры использования make для C-проектов: 1 2 3
- Документация
Зачем нужен Make?
GNU Make представляет собой одну из первых систем автоматизации сборки. Сборки чего? - спросите вы. Ответ прост - файлов, которые являются продуктом таких программ как: компиляторы, линковщики, генераторы документации, текстовые редакторы, тестовые утилиты, шаблонизаторы и др. Все они генерируют выходные файлы на основе некоторых входных файлов.
В чем же здесь проблема? Ведь для подобной сборки достаточно обычного bash-скрипта. Действительно, это так, но такой скрипт будет запускать все инструкции сборки при каждом вызове. Например, изменив что-либо в одном из файлов и запустив скрипт, мы запустим пересборку всех файлов проекта, хотя это требовалось лишь для одного из них. Именно эту проблему решает Make.
Make принимает на вход т.н Makefile. Основным строительным блоком Makefile-а являются правила
:
main.o: main.c
gcc -c main.c -o main.o
Такие правила называются явным. Также существуют неявные правила(о них будет рассказано ниже).
Деление на явные и неявные правила влияет только на приоритетность поиска правил для цели.
Помимо целевого файла - target
и инструкций по его сборке - recipe
, перечислены зависимости цели - prerequisites
. Как правило это входные файлы для команд в recipe (main.c в примере выше). Эта информация и позволяет решить описанную выше проблему.
Make собирает цель следующим образом:
- Поиск правила для цели. Одна цель может подходить под несколько правил. При поиске первый приоритет дается явным правилам.
- Если целевой файл не существует и правило для него не найдено, то make завершается с ошибкой.
- Если целевой файл уже существует, а правила для него нет, то его обработка завершается и make продолжает свою работу. Такой вариант развития событий в большинстве случаев применяется к файлам исходного кода.
- Также есть алгоритм действий для случаев, когда для одной цели подходит несколько правил, но такого поведения следует избегать.
-
Для каждой зависимости из правила make запускает этот же алгоритм сборки цели, где целью является сама зависимость.
- Теперь самое интересное: если целевой файл не существует или хотя бы одна из уже собранных зависимостей новее целевого файла, то происходит выполнение команд recipe.
prog: main.o utils.o
ld main.o utils.o -o prog
main.o: main.c
gcc -c main.c -o main.o
utils.o: utils.c
gcc -c utils.c -o utils.o
Так при сборке цели prog
, при первом вызове будут выполнены все команды, при повторном вызове в отсутсвии изменеий в исходных файлах, не будет выполнена ни одна. При внесении изменений в один из исходных файлов, будет выполнена компиляция только для него и линковка исполняемого файла prog
.
Как можно заметить, make выстраивает ациклический граф зависимостей. Вершинами этого графа, которые не зависят от других, являются, в нашем случае, файлы исходного кода. Остальные вершины являются целями с зависимостями, которые пересобираются только при своих изменеии зависимостей.
Для небольших проектов выгода такого подхода будет едва ли заметна, но полная пересборка большого проекта может занимать часы, и без подобной системы работа с ним была бы крайне затруднена. Кстати, эта одна из причин почему код проекта следует распределять по файлам.
Также в make есть возможность задать target не как имя выходного файла, а как имя некотрого действия. Например:
.PHONY: install
install: prog
cp prog /usr/bin
Такая цель считается всегда устаревшей, а также make не будет брать во внимание возможные файлы с таким же именем как и у этой цели.
Важно отметить, что все свои операции make производит относительно текущей рабочей директории. Для её изменения нужно вызвать make следующим образом: make -C somedir
Подробнее о правилах.
Как можно заметить, в предыдущем примере правила для main.o и utils.o отличаются лишь именами соответствующих файлов. В make для обобщения подобных правил существует механизм шаблонных правил. Шаблонные правила относятся к неявным правилам
.
Пример выше будет выглядет следующим образом при исрользовании шаблонов:
prog: main.o utils.o
ld main.o utils.o -o prog
%.o: %.c
gcc -c $< -o $@
Под шаблон %.o
попадаёт любой файл оканчвающийся на .o
. Именем файла в данном случае является путь к нему относительно рабочей директории make, т.е и файл main.o и, например, build/main.o попадают под данный шаблон. Далее из имени файла make получает ту его часть, которая соответсвует шаблонному символу - %
(в нашем случае это будет строка main или build/main) и подставляет на место того же %
в шаблон списка зависимостей. Несколько подробнее об этом здесь.
При написании шаблонных правил нам неизвестны имена цели и зависимостей, поэтому для получения этих имен нужно пользоваться т.н автоматическими переменными, которые устанавливает make при выполнении правила. В примере выше это $<
- содержит имя первой зависимости (например main.c), $@
- имя цели. Полный список автоматических переменных.
Также к неявным правилам относятся несколько встроенных правил для компиляции и сборки C/C++ и некотрых других языков. Скорее всего название “неявные правила” было дано им именно поэтому, хотя логичнее,все же, было бы называть их шаблонными правилами.
Переменные и функции
Существуют и обычные переменные. Для примера выше можно записать:
CC = gcc
LD = ld
OBJS = main.o utils.o
prog: $(OBJS)
$(LD) $(OBJS) -o prog
%.o: %.c
$(CC) -c $< -o $@
Хорошей практикой считается перечисление только исходных файлов проекта, а имена соответствующих объектных файлов(и многих других файлов, производимых программами) генерируются автоматически.
Для этого в make есть ряд встроенных функций для преобразования текста.
Используем функцию pattern substitution(patsubst)
CC = gcc
LD = ld
SRCS = main.c utils.c
OBJS = $(patsubst %.c,%.o,$(SRCS))
prog: $(OBJS)
$(LD) $(OBJS) -o prog
%.o: %.c
$(CC) -c $< -o $@
Переменные и функции позволяет еще больше обобщить правила. Теперь в переменную SRCS можно добавлять новые исходные файлы проекта.