Вы можете более эффективно запускать и компилировать свои программы с помощью этого удобного средства автоматизации.
Если при обновлении некоторых файлов вы хотите запускать или обновлять весь программный модуль, то для вас может оказаться полезной утилита make. Для нее требуется файл Makefile (или makefile), который определяет набор задач для выполнения. Вы могли уже использовать make, когда компилировали программу из исходного кода. Большая часть проектов с открытым исходным кодом использует make для того, чтобы скомпилировать конечный исполняемый бинарный файл, который в последствии можно будет установить с помощью команды make install.
В этой статье мы посмотрим, что же такое make и Makefile. Для этого приведем несколько простых и усложненных примерных задач. Прежде чем мы начнем, убедитесь, что make уже установлен в вашей системе.
Простые примеры
Давайте начнем с того, что напечатаем в терминале простое «Hello World!». Создайте пустой каталог myproject, который будет содержать файл Makefile, внутри которого будет записано следующее:
say_hello:
echo "Hello World"
А теперь запустите файл, набрав make внутри каталога myproject. Результат будет следующий:
$ make
echo "Hello World"
Hello World
В приведенном выше примере say_hello выступает в роли имени функции, подобно любому другому языку программирования. Это называется целью (target). Предварительные условия (prerequisites) или зависимости (dependencies) соответствуют цели. Для простоты мы не определили никаких предварительных условий в данном примере. Команда echo "Hello World" называется рецептом (recipe). Рецепт использует предварительные условия для формирования цели. Цель, предварительные условия и рецепты вместе формируют правило (rule).
Подытожим сказанное. Ниже представлен синтаксис обычного правила:
target: prerequisites
<TAB> recipe
Скажем, целью может быть двоичный файл, который зависит от предварительных условий (файлов с исходным кодом). В свою очередь, предварительное условие тоже может быть целью, которая имеет другие зависимости:
final_target: sub_target final_target.c
Recipe_to_create_final_target
sub_target: sub_target.c
Recipe_to_create_sub_target
Цель – это не обязательно файл; это может быть просто название рецепта, как в нашем примере. Мы это называем «абстрактными целями».
Возвращаясь к примеру выше, при выполнении make отображалась целая команда echo "Hello World", а только потом уже фактический вывод команды. Зачастую нам такое не нужно. Чтобы сама команда не отображалась, ввод команды echo нужно начать с @:
say_hello:
@echo "Hello World"
Теперь попробуйте запустить make снова. В выводе останется только следующее:
$ make
Hello World
Давайте добавим несколько абстрактных целей (generate и clean) в Makefile:
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
Если мы попробуем запустить make после того, как внесли в него изменения, то увидим, что выполнится только цель say_hello. Это происходит по той причине, что только первая цель в make-файле является целью по умолчанию. То, что называется целью по умолчанию (default goal), послужило причиной для того, чтобы в качестве первой цели в большинстве проектов использовалась цель all. all ответственна за то, чтобы вызывать другие цели. При этом такое развитие событий можно переиграть с помощью специальной абстрактной цели под названием .DEFAULT_GOAL.
Давайте добавим ее в начало нашего make-файла:
.DEFAULT_GOAL := generate
Это запустит цель generate в качестве цели по умолчанию:
$ make
Creating empty text files...
touch file-{1..10}.txt
Как можно понять из названия, абстрактная цель .DEFAULT_GOAL может запустить только одну цель за раз. Именно поэтому большинство make-файлов в качестве цели используют all, которая может вызывать столько целей, сколько необходимо.
Давайте добавим абстрактную цель all и уберем .DEFAULT_GOAL:
all: say_hello generate
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
Прежде чем запустить make, давайте добавим еще одну абстрактную цель .PHONY, в которой мы определим все цели, которые не являются файлами. make запустит свой рецепт вне зависимости от того, есть ли файл с таким именем или каково время его последнего изменения. Вот так будет выглядеть готовый make-файл:
.PHONY: all say_hello generate clean
all: say_hello generate
say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txt
make вызовет say_hello и generate:
$ make
Hello World
Creating empty text files...
touch file-{1..10}.txt
Это обычная практика не вызывать clean в all или ставить его в качестве первой цели. clean должен вызываться вручную только тогда, когда в качестве первого аргумента для make нужна очистка:
$ make clean
Cleaning up...
rm *.txt
Теперь, когда вы имеете представление о том, как работает простой make-файл и как его можно написать, давайте рассмотрим несколько более сложных примеров.
Усложненные примеры
Переменные
В примере выше большая часть целевых и предварительных переменных являются неизменяемыми, но в реальных проектах они заменяются переменными и шаблонами.
Самый простой способ определить переменную в make-файле – использовать оператор =. Например, вот так выглядит присваивание команды gcc переменной CC:
CC = gcc
Также она называется рекурсивной расширенной переменной (recursive expanded variable), и ее можно использовать в правиле:
hello: hello.c
${CC} hello.c -o hello
И как вы уже, наверное, догадались, при передаче в терминал в рецепте появляется дополнительный текст:
gcc hello.c -o hello
И ${CC}, и $(CC)являются допустимыми ссылками для вызова gcc. Но если кто-то попробует переприсвоить переменную самой себе, то получится бесконечный цикл. Давайте проверим:
CC = gcc
CC = ${CC}
all:
@echo ${CC}
При запуске make получим следующее:
$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.
Чтобы избежать такой ситуации, можно воспользоваться оператором := (это будет называться простой расширенной переменной (simply expanded variable)). И у нас не должно возникнуть проблем при запуске следующего make-файла:
CC := gcc
CC := ${CC}
all:
@echo ${CC}
Шаблоны и функции
Следующий make-файл может скомпилировать любую программу на C с помощью переменных, шаблонов и функций. Давайте рассмотрим его, строчку за строчкой:
# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
LINKERFLAG = -lm
SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)
all: ${BINS}
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@
%.o: %.c
@echo "Creating object.."
${CC} -c $<
clean:
@echo "Cleaning up..."
rm -rvf *.o ${BINS}
- Строки начинаются с #. Это комментарии.
- Строка .PHONY = all clean определяет абстрактные цели all и clean.
- Переменная LINKERFLAG определяет флаги, которые будут использоваться в рецепте вместе с gcc.
- SRCS := $(wildcard *.c): $(wildcard pattern) - это одна из функций для файловых имен. В данной случае, все файлы с расширением .c будут хранится в переменной SRCS.
- BINS := $(SRCS:%.c=%) : это называется ссылкой с заменой. В данной случае, если SRCS будет иметь значение 'foo.c bar.c', то значение для BINS будет следующее: 'foo bar'.
- Строка all: ${BINS}: абстрактная цель all вызывает значения в ${BINS} как отдельные цели.
- Правило:
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@
Чтобы понять это правило, давайте взглянем на пример. Предположим, что foo - это одно из значений ${BINS}. Отождествим % с foo (% можно отождествить с любым именем цели). Ниже приведено правило в полной форме:
foo: foo.o
@echo "Checking.."
gcc -lm foo.o -o foo
Как показано выше, % заменяется foo. $< заменяется foo.o. $< выполняет соответствие по образцу с предварительными условиями, а $@ отождествляется с целью. Это правило будет вызвано для каждого значения в ${BINS}.
- Правило:
%.o: %.c
@echo "Creating object.."
${CC} -c $<
Каждое предварительное условие в предыдущем правиле считается целью для этого правила. Ниже приведено правило в полной форме:
foo.o: foo.c
@echo "Creating object.."
gcc -c foo.c
- И наконец, мы удаляем все двоичные и объектные файлы в цели clean.
Ниже приведен переписанный make-файл, который мы рассматривали выше, из расчета, что он находится в каталоге с единственным файлом foo.c:
# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
LINKERFLAG = -lm
SRCS := foo.c
BINS := foo
all: foo
foo: foo.o
@echo "Checking.."
gcc -lm foo.o -o foo
foo.o: foo.c
@echo "Creating object.."
gcc -c foo.c
clean:
@echo "Cleaning up..."
rm -rvf foo.o foo