Введение
Краткое описание
Главный пример и его обсуждение
Резюме

Статья, предназначенная для начинающих программистов, описывает основные возможности утилиты make. Приведены примеры, показаны некоторые типичные приемы, используемые в make-файлах.

Введение

В процессе создания любой программы программист неоднократно вносит изменение в исходные тексты; одни изменения связаны с обнаруженными ошибками, другие - с изменившимися спецификациями, добавлением новых возможностей или необходимостью перенести программу на новую технику (либо в новую среду).

При этом возникают две проблемы. Во-первых, сохранение предыдущих версий для возможного возврата, внесение согласованных изменений в различные версии программы, журнализация исправлений и т. д. Эти задачи автоматизируются так называемыми "системами управления версиями", наиболее известны из которых - SCCS (стандартная для UNIX'а), RCS (проект GNU), PVCS (INTERSOLV, Inc.). Об этих системах будет рассказано в одной из будущих статей.

Эта же статья посвящена второй проблеме - автоматизации действий по формированию конечного продукта (программы, упакования данных, дистрибутива программного продукта...). Всякий раз после изменения исходных текстов необходимо выполнить некоторые формальные шаги - компиляцию, редактирование связей, запуск текстов с проверкой результата и т. д. Минимально необходимая последовательность шагов зависит от внесенных изменений. Утилита make автоматизирует эти действия, гарантируя, что все изменения будут учтены. Можно сказать, что если системы управления версиями управляют исходными текстами, то make управляет "исходными" файлами.

Make традиционно используется в UNIX'у, однако в последнее время стал широко доступен и на персональных компьютерах, поскольку любой производитель C-компиляторов снабжает свою систему этой полезной утилитой. В ОС UNIX, наряду с обязательно присутствующей стандартной, существуют и другие реализации, несколько отличающиеся по своим возможностям и синтаксису. наиболее известна утилита make из проекта GNU, существенно более развитая, чем стандартная.

Цель данной статьи - продемонстрировать, как и почему следует использовать make. Описываются только возможности, присутствующие в большинстве версий make'а, по крайней мере, в GNU- и в стандартной реализации. Некоторые средства, слишком сложные для первого знакомства, не упоминаются.

Краткое описание

Make позволяет задать все зависимости между файлами, составляющими программных проект, воздействие модификаций одних файлов на другие, точную последовательность действий, необходимых для порождения новой версии. Эта информация с помощью текстового редактора помещается в файл с описаниями. Опираясь на содержимое файла с описаниями, make определяет, какие команды следует передать shell'у для выполнения, чтобы гарантировать автоматическое формирование конечного продукта.

Алгоритм в общих чертах выглядит так. В файле с описаниями определяется имя целевого файла (или файлов). Проверяются все файлы, от которых зависит целевой: если какой-либо файл не существует либо устарел, он порождается заново (проверка сопровождается просмотром описаний, при котором этот файл в свою очередь трактуется как целевой.) Наконец, если одни из этих файлов модифицирован позже целевого, последний пересоздается. Подобный рекурсивный просмотр зависимостей позволяет при формировании новой версии программы порождать заново только те файлы, которые прямо или косвенно затронуты последними изменениями.

Рассмотрим простейший пример. Пусть программа prog получается из трех исходных файлов x.c и z.c путем их компиляции и редактирования связей совместно с библиотекой libm.a. В соответствии с принятыми соглашениями результат работы C-компилятора будет помещен в файлы x.c и y.c используют общие описания из включаемого файла defs.h, а z.c - не использует. Взаимосвязи и команды описываются так:

prog : x.o y.o z.o 

    cc x.o y.o z.o -lm -o prog

x.o : x.c  defs.h

     cc -c x.c

y.o : y.c defs.h

     cc -c y.c

z.o : z.c

     cc -c z.c

В первой строке утверждается, что prog зависит от трех .о файлов. Вторая строка указывает, как отредактировать связи между ними, чтобы создать prog. Третья строка гласит, что x.o зависит от x.c и defs.h; четвертая описывает, как получить файл x.o, если он отсутствует или устарел. Сходный смысл имеют и последующие строчки.

Если эту информацию поместить в файл с именем makefile, команда

make

будет выполнять операции, необходимые для перегенерации prog после любых изменений, сделанных в каком-либо из четырех исходных исходных файлов x.c, z.c или defs.h.

Если ни один из исходных или объектных файлов не был изменен с момента последнего порождения prog и все файлы в наличии, утилита make известит об этом и прекратит работу. Если, однако, файл defs.h отредактировать, x.c и y.c (но не z.c) будут перекомпилированы; затем из новых файлов x.o и y.o и уже существующего файла z.o будет заново собрана программа prog. Если изменен лишь файл y.c, только он и перекомпилируется, после чего последует пересборка prog.

Перед выполнением всякой команды утилита make распечатывает ее. Если изменен файл defs.h, будет распечатано следующее:

cc -c x.c
cc -c y.
cc x.o y.o z.o -lm -o prog

Если во второй командной строке make не задано имя целевого файла, создается первый упомянутый в описании целевой файл (в нашем случае - файл prog); в противном случае создаются специфицированные целевые файлы. Если изменен x.c или defs.h, команда

make x.o

будет перегенерировать x.o, но не prog.

Чтобы упростить формирование файлов описаний, make использует "подразумеваемые" трансформации. например, если для порождения целевого файла требуется некоторый объектный файл (.o), а в текущем каталоге имеется соответствующий ему исходный С-файл (файл с тем же именем и расширением .с), make применяет встроенное правило порождения объектного файла из исходного (то есть выполняет команду cc -c).

Поэтому наш make-файл можно упростить:

prog : x.o y.o z.o
    cc x.o y.o z.o -lm -o prog
x.o y.o : defs.h

Чтобы лучше понять поведение make'а, следует учитывать, что make читает спецификацию один раз в начале работы, после чего, проверив наличие и время последней модификации файлов, составляет "план кампании". Затем начинается реализация этого плана, причем make проверяет только коды возврата команд (успех/неуспех), всегда считая, что целевой файл в результате выполнения команды изменился (даже если у него не изменилось время последней модификации).

Синтаксис правила

Правила - основное содержимое make-файла. Рассмотрим внимательно, как они устроены:

цель 1 [цель 2..] : [зависимость 1..][; команды]
[	 команды]

Указанные в скобках компоненты могут быть опущены, цели и зависимости, которые задают имена файлов, являются цепочками из букв, цифр, символов . и /. При обработке строки интерпретируются метасимволы shell'а, такие как * и ?. Команды могут быть указаны после точки с запятой в строке зависимостей, или в строках, начинающихся с табуляции, которые следуют сразу за строкой зависимостей.

Будьте внимательны: строка с командой должна начинаться действительно с табуляции, а не с восьми пробелов; в противном случае это начало следующего правила.

Несколько целей в одном правиле указываются исключительно в целях сокращения записи. Правило

x.o y.o : defs.h

эквивалентно паре:

x.o : defs.h
y.o : defs.h

Макросы

В спецификации предусмотрен механизм макросов. Макроопределение состоит из имени (цепочка букв и/или цифр), за которым следует знак равенства, а затем цепочка символов, сопоставляемая имени. примеры конкретных макроопределений:

2 = xyz
abc = -ll -ly -lm
LIBES =

Последнее определение сопоставляет LIBES с пустой цепочкой. Макрос, нигде не определенный явным образом, имеет в качестве значения пустую цепочку.

Макроопределения можно не только включать в файл описаний, но и задавать в виде аргументов командной строки, например:

make abc="-ll -ly -lm"

Все переменные окружения (HOME, LOGNAME, TERM, PATH и т. д.) также обрабатываются как макроопределения.

При обращении к макросу перед его именем указывается символ $. Имена макросов, состоящие более чем из одного символа, должны заключаться в скобки. примеры корректных обращений к макросам (две последние строки эквиваленты):

$(LIBES)
$2
$(2)

Некоторые макроопределения являются встроенными:

MAKE=make
CFCLAGS=-0
LDFLAGS=

Следующий фрагмент демонстрирует все упомянутые способы определения макросов:

OBJECTS -x.o y.o z.o
$(HOME)/prog: $(OBJECTS)

    $(CC) $OBJECTS) $(LIBES) -o $(HOME)/prog

Команда

make LIBES="-ll -lm"

связывает три объектных файла вместе с библиотекой lex'а (-ll) и математической библиотекой (-lm).

разные способы определения макроса имеют разный приоритет. Приведем их в порядке увеличения приоритета:

  • встроенные макроопределения;
  • переменные окружения;
  • макроопределения в make-файле;
  • макроопределения в командной строке.

Таким образом, встроенный макрос СС можно переопределить в make-файле, а макрос, определенный в make-файле, можно переопределить в командной строке.

Макроопределения одного приоритета - это не последовательные присваивания,а скорее неупорядоченный набор утверждений о значениях макросов; поэтому макрос имеет одно и то же значение во всех частях make-файла. Его нельзя переопределить для части правил. Порядок макроопределений, как и порядок правил, не играет никакой роли.

$*, $@, $?, $< - четыре специальных встроенных макроса, значения которых изменяются в каждом правиле, действуя только в его теле. Макрос $@ устанавливается равным полном имени текущего целевого файла, а макрос $? - цепочке имен файлов, более свежих, чем целевой. Пример:

prog: x.c y.c z.c
   cc -c $?
    cc -o $@ x.o y.o z.o

Макросы $< и $* используются при определении неявных правил трансформации (см. далее).

Приведенное выше обращение к макросу вида $(макро) является частным случаем более мощной конструкции. В общем случае она выглядит так:

$(макро:цепочка1=цепочка2)

Подобная конструкция преобразуется следующим образом. Значение $(макро) рассматривается как набор разделенных пробелами или табуляциями цепочек символов, оканчивающихся подцепочкой цепочка1; при подстановке все вхождения цепочки1 заменяются на цепочку2. Такая форма преобразований макросов была выбрана из-за того, что make обычно имеет дело с окончаниями имен файлов. В команде

$(CC) -c $(CFLAGS) $(?:.o=.c)

значение макроса $? - цепочка имен объектных файлов, оказавшихся более свежими, чем целевой. Допустим, что эта цепочка суть "x.o y.o". Тогда обращение $(?:.o=.c) транслируется в

x.c y.c

Суффиксы и правила трансформации

Как уже упоминалось, make использует таблицу неявных правил, определяющих, как преобразовать файл с один суффиксом в файл с другим суффиксом.

Имен правил трансформации - просто конкатенация суффиксов файлов до и после трансформации. Так, правило трансформации .с-файла в .о-файл называется .с.о. Пользователь может переопределить встроенное правило трансформации. Если команда порождается при помощи одного из таких правил, посредством макроса $* можно вычислить префикс имени целевого файла; макрос $< обозначает полное имя исходного файла, к которому применяется правило трансформации.

Используемые в правилах трансформации суффиксы даются как список зависимостей для специального имени .SUFFIXES. При этом важен порядок: первое возможное имя, для которого существуют и файл и правило, используется как имя источника. Первоначально этот список выглядит примерно так:

.SUFFIXES: .o .c .y .l .h .f .sh

Более полный синтаксис

Если какая-либо строка начинается с цепочки include, за которой следует пробел или табуляция, остаток строки после подстановки макросов считается именем файла; этот файл будет прочтен и включен в make-файл.

Символ # является признаком комментария; все символы за ним до конца строки игнорируются. Комментарии могут встретиться в любом месте.

Любой длинный текст (макроопределение, список зависимостей, команду) можно записать на нескольких строках, используя в качестве признака конца продолжения символ /. Если последним символом строки является /, то он, а также перевод строки и все следующие за ним пробелы и табуляция заменяются на одиночный пробел.

Запуск утилиты make

Соответствующая командная строка выглядит так:

make [опции] [макроопределения] [целевые_файлы]

Чтобы понять, какие действия будет делать make, очень удобно использовать опцию -n. Обращение

make -n

предписывает выводить команды, порождаемые make'ом, не выполняя их на самом деле. Обращение

make -s

напротив, подавляет вывод командный строк перед их выполнением. если входящая в правило команда начинается с @, она не распечатывается.

Make обычно прекращает работу, если какая-либо команда возвращает ненулевой код завершения (чаще всего это признак ошибки). Чтобы ошибки игнорировались, в командной строке можно указать опцию -i. Ошибка игнорируется также, если командная строка в файле описаний начинается со знака минус. Если известно, что программа возвращает бессодержательное значение, перед строкой, ее запускающей, полезно указывать минус.

Часто бывает полезно указать опцию -k. В этом случае при возникновении ошибки выполнение команд, связанных с текущей зависимостью, прекращается, однако обработка других зависимостей продолжается. Например, при ошибке компиляции одного из исходных файлов редактирование связей не выполняется, но компилируются все остальные.

Опция -f имя_файла задает имя файла описаний:

make -f mymakefile

Имя файла - обозначает стандартный ввод. Если опция не указана, читается файл с именем makefile или Makefile.

Опция -p указывает make'у вывести все макроопределения, а также описания зависимостей и операций для создания целевых_файлов. Если использовать пустой make-файл, мы получим полную информацию о неявных предопределенных правилах, макросах и суффиксах:

make -pf - 2>/dev/null 

Опции не обязательно указывать в командной строке, их можно помещать в переменную окружения MAKEFLAGS. Она обрабатывается как содержащая любую из допустимых опций (естественно, кроме -f). При запуске make в нее дописываются опции, указанные в командной строке.

Главный пример и его обсуждение

Мы узнали основные возможности make; посмотрим, как их использовать на практике. Ниже приведен make-файл, при помощи которого собирается небольшая библиотека, используемая в реальном проекте, разрабатываемом несколькими программистами:

# Файл makefile.
# Если цель не указана, будет достигаться цель

# all, т. е. создаваться библиотека picro-x.a, два файла

# tst  и auto-test и результаты тестирования.

all: picro-x.a tst auto-test tests

# Заметьте, что макрос можно использовать "раньше" по

# тексту, чем ему присвоено значение. Обратите внимание

# на замену суффиксов при подстановке значения макроса.

tests: $(TESTS:.dat=.res)

# Определение макросов. Предопределенные макросы

# CC, CFLAGS, AR и AS переопределяются.

NICE=nice -15  # Уменьшение приоритета

CC=$(NICE) gcc # подразумеваемый компилятор

               # Опции для препроцессора

CPPFLAGS= -i../ETC -I. -I- -DDEBUG -UNDEBUG -U_PW_

WARNFLAGS=-Wall -Wpointer-arith -Wcast-qual -Wshadow

SIMPFLAGS= -0 -g

               # Опции для компилятора

CFLAGS= $(SIMPFLAGS) $(WARNFLAGS) $(CPPFLAGS)

CPP=$(NICE) /usr/local/lib/gcc-cpp -P # Препроцессор

AR=$(NICE) ar                         # Библиотекарь

AS=$(NICE) as -n                      # Ассемблер

               # Директивы редактора связей

LDSPECS= $(HOME) /etc/default.ld

LIBDIR=/usr/lib         # Куда помещать библиотеку

LIBINC=/usr/include/    # Куда помещать включаемый файл


# Суффиксы для подразумеваемых правил: удаляем все

# старые суффиксы, добавляем новые (порядок важен!)

.SUFFIXES:

.SUFFIXES: .o .c .S .res .dat

# Суффикс .S имеет один единственный файл alloca.S.

#Такие файлы сначала обрабатываются стандартным

# С-препроцессором, после чего ассемблируются. Конечно,

# для одного файла было бы проще указать явное правило.

.S.o:

   $(CPP) $(CPPFLAGS) $< >$*.s

   $(AS) -o $@ $*.s

   -@rm $*.s

.res.dat:                # Правило для тестирования

   auto-test < $< > $@

   if     test -f save/$@; 

     then cmp  $@ save/$@; 

     else cp   $@ save/$@;

   fi # Сравнение результата теста с сохраненным ранее

      # в каталоге save: если сохраненного результата

      # еще нет, он сохраняется; если есть, но не

      # совпадает, выдается сообщение об ошибке.

# Список тестовых файлов для auto-test'а

TESTS= test1.dat test2.dat test3. dat

# Правило с несколькими целями

# Используется правило .res.dat

$(TESTS:.dat=.res): auto-test

# Обратите внимание на зависимость от auto-test: логичнее

# было бы указать ее в подразумеваемом правиле .res.dat,

# но, к сожалению, стандартный make не допускает

# зависимостей при определении подразумеваемых правил.

# Явные правила для создания исполняемых файлов

tst: tstsem.o picro-x.a $(LDSPECS) $(LIBDIR)libttp.a

   $(CC) $(SIMPFLAGS) $(LDSPECS) -o $@ tstsem.o 

           picro-x.a -ltpp -lmalloc -lc

auto-test: auto-test.o picro-x.a $(LDSPECS)

   $(CC) $(SIMPFLAGS) $(LDSPECS) -o $@ auto-test.o 

           picro-x.a  -lmalloc -lc

# Перечисление всех С-источников для picro-x.a

LIB_C_SRCS=input.c cursor.c init.c windows.c buttons.c 

toplevel.c zagl1.c zagliniall.c datamin.c datamax.c 

miscin.c miscout.c bin.c labels.c inputstr.c eprintf.c

# Перечисление всех модулей из picro-x.a.

LIB_MODS=$(LIB_C_SRCS:.c=.o) alloca.o

# Создание библиотеки: модули упорядочиваются, чтобы

# ссылаться по возможности "вперед" для ускорения

# редактирования связей.

picro-x.a: $(LIB_MODS)

   $(AR) -r $@ 'lorder $? | tsort'

# Очистки
cleanobj: ; -rm *.o
clean: cleanobj
    -rm a.out core tst auto-test *mon.out picro-x.a *.res
fullclean: clean ; -rm save/*.res    # Самая полная
# очистка с удалением запомненных тестовых результатов.
#Формирование включаемого make-файла, содержащего
# зависимости объектных файлов от исходных, в
# том числе включаемых.
makeinclude:
    $(NICE) gcc -MM $(SIMPFLAGS) $(CPPFLAGS) *.c >inc.mak

#Файл inc.mak не следует удалять.
include inc.mak

# Запуск в другом каталоге (с другим make-файлом)
# для проверки того, что используемая версия библиотеки
# libttp.a, модифицируемая другим программистом, не 
# устарела. См. определение цепи install_export в этом 
# make-файле.
install_import:
   (cd /udd/tpp-hack; $(MAKE) install_export)

EXPORT_STAMPS=.stamppocrox.a .stamppicrox.h

# Свежие версии библиотеки picro-x.a и включаемого фай-
# ла picrox.h помещаются в подходящие каталоги при ус-
# ловии, что они изменились (это условие проверяется, 
# чтобы изменением времени последней модификации файла
# не спровоцировать лишних действий у тех, кто ис-
# пользует picro-x). Обратите внимание, что сначала бу-
# дут проверены результаты тестов, если это не было
# сделано заранее.
install_export: tests $(EXPORT_STAMPS)
.stamppicrox.a: picro-x.a
    cmp picro-x.a $(LIBDIR)libpicro-x.h >/dev/null 2>&1
      ||(cp picro-x.a $(LIBDIR)libpicro-x.h; 
         chown bin $(LIBDIR) libpirco-x.h; 
         chgrp bin $(LIBDIR)libpicro-x.h; 
         chmod 644 $(LIBDIR)libpicro-x.h)
    touch .stamppicrox.a
.stamppicrox.h: picrox.h
    cmp picrox.h $(LIBINC)picrox.h >/dev/null 2>&1
      ||(cp picro-x.a $(LIBINC)libpicro-x.h; 
         chown bin $(LIBINC) libpirco-x.h; 
         chgrp bin $(LIBINC)libpicro-x.h; 
         chmod 644 $(LIBINC)libpicro-x.h)
    touch .stamppicrox.h
 
# Цели, "защищенные" от неудачи
.PRECIOUS: .print. stamppicrox.h .stamppicrox.a

# Все исходные тексты, расположенные в этом каталоге,
# включая тесты:
SOURCE=$(LIB_C_SRCS) alloca.S tstsem.c auto-test.c 
bin.h buttons.h cursor.h datamax.h init.h 
input.h inputstr.h labels.h menu.h picrox.h miscin.h 
miscout.h toplevel.h windows.h mypal makefile $(TESTS)

# Печать исходных текстов, изменившихся со времени
# последней печати.
   .print: $(SOURCE)
      lp -c -w $?
      -touch .print
#Конец make-файла.

Содержимое файла inc.mak (он приведен не полностью, только начало и конец):

bin.o : bin.c ../etc/stdio.h ../etc/compartible.h 
  ../etc/comfort.h ../etc/ansi.h ./bin.h
buttons.o : buttons.c ../etc/stdio.h 
   ..........
   ../etc/ansi.h ./miscin.h ./datamax.h
zagl1.o : zagl1.c
zagliniall.o : zagliniall.c

Как собирать библиотеки

Как указать зависимость между объектными файлами, включенными в библиотеку сборки, и исходными текстами? Различные реализации make'а предлагают (или не предлагают) разные средства. В тех же случаях, когда объектных файлов не очень много, а свободного места на диске не очень мало, лучше поступить бесхитростно, так, как показано в примере.

Основной минус этого способа понятен: отдельные объектные файлы хранятся в текущем каталоге и их удаление вызовет полную компиляцию. Плюс - описание понятно для любой версии make'а (быть может, потребуется только сменить команду или ключи архиватора). Кроме того, если архив отсутствует, при его создании файлы можно расположить в оптимальном для редактора связей порядке (в нашем случае это делает подстановка 'loader $?|tsort': loader составляет список пар имен файлов, соответствующий ребрам графа ссылок между файлами, а tsort выдает список имен файлов, в котором ссылки по возможности будут "вперед ", что позволит при связывании обработать библиотеку за один проход).

Цели отсутствующие и пустые

В том же примере кроме понятных целевых файлов (.о .а и т. п.) встречаются цели all, install_import, makeinclude, clean, для которых нет никаких команд, способных эти файлы создать. Следовательно, если это не сделать специально, данные целевые файлы не будут создаваться при запусках make'а. Поэтому, всякий раз на запрос

make all

после того, как будут успешно достигнуты подцели picro-x.a, tst, auto-test и tests, будет выполняться команда

echo "all - OK"

поскольку файл all отсутствует.

Таким образом можно не только любым удобным способом группировать целевые файлы, указывая их, как зависимости несуществующей цели, но и задавать при этом выполнение дополнительных команд. Несуществующий целевой файл может зависеть от других несуществующих целевых файлов или вообще ни от чего не зависеть (посмотрите на правила для clean, cleanobj и fullclean).

Для утилиты make цель all ничем не отличается от любой другой: точно так же проверяется наличие файла и, если он есть, время последнего изменения, исследуются зависимости и т. д. В некоторых реализациях, например в GNU-make, можно явно указать, что данную цель всегда можно считать отсутствующей. Если же такой возможности нет, разумно вставить в правило clean команду удаления файлов с такими именами, на случай, если они случайно заведутся.

Еще один интересный прием демонстрирует целевой файл .print. Команда touch не меняет содержимое файла, к которому применяется - она только устанавливает время его последней модификации равным текущему времени и создает пустой файл, если его еще не было. Следовательно, файл .print создастся, но всегда будет оставаться пустым, если только его не заполнить специально. использоваться же будет только время последней модификации, всегда совпадающее со временем последней печати измененных исходных файлов. Вместо команды touch можно использовать строку:

echo "*** File(s) $? was printed at 'date; " >>.print

В этом случае файл .print, конечно, не остался бы пустым, а содержал бы журнал распечаток исходных текстов. Точно так же используются файлы .stamppicrox.a и .stamppicrox.h (они позволяют избежать лишних сравнений файлов).

Рекурсивный запуск

Посмотрим еще раз команду в правиле для install_import:

(cd /udd/tpp-hack; $(MAKE) install_export)

Приведенное правило содержит команду запуска нового make'а. Само по себе это не удивительно: если make может запустить компилятор или редактор связей, то почему нельзя запустить make? Такой make называют рекурсивно запущенным1. (Обратите внимание, что команда cd должна быть в той же строке, чтобы выполниться тем же shell'ом!).

Рекурсивный запуск имеет ряд особенностей: во-первых, make помещает опции, с которыми он запущен, в переменную MAKEFLAGS; поэтому легко передать опции дальше, запускаемому make'у. Второе отличие связано с опцией -n: если make запущен с этой опцией, то он только выводит на стандартный вывод, но не выполняет команды, которые должен был бы выполнять. В данном режиме make просматривает команды и, если находит подстроку $(MAKE), запускает команду несмотря на опцию -n. Поскольку n попадает в переменную MAKEFLAGS, режим печати без выполнения установится для всех рекурсивно запущенных make'ов, что весьма полезно при отладке сложных make-файлов.

Интересные возможности открывает описание в make-файле процедуры изготовления нового make-файла, с последующим его рекурсивным запуском. Этот метод используется, как правило, при установке сложных систем (например, X-window) или сборе переносимых программ, распространяемых в исходных текстах. Чаще всего, make-файл изготовляется препроцессированием некоторого универсального файла стандартным препроцессором cpp. X-window содержит специальную утилиту imake, представляющую собой надстройку над cpp.

Посмотрим детально, как это прием используется для порождения из исходных текстов командного интерпретатора bash проекта GNU. Bash переносим на множество типов компьютеров с разными версиями Unix'а и разным С-окружением (в частности, с разными библиотеками). Процесс изготовления загрузочного файла может существенно различаться, поэтому среди исходных текстов есть совсем небольшой Makefile, содержащий следующий фрагмент:

all: bash-Makefile
     $(MAKE) -f bash-Makefile

bash-Makefile: cpp-Makefile Makefile machines.h
     cp cpp-Makefile tmp-Makefile.c
      $(CPP) $(CPP_ARGS) tmp-Makefile.c | cat -s >bash-Makefile
       rm -f tmp-Makefile.c

install clean distribution backup: bash-Makefile
       $(MAKE) -f bash-Makefile $@

bash-Makefile получается препроцессированием файла cpp-Makefile, в который директивой препроцессора включается machines.h. Последний файл содержит фрагменты, подобные следующим:

       . . . . . . . . . .
#if defined (sun2)
#define M_MACHINE "sun2"
#define HAVE_SUGLIST
#define USE_GNU_MALLOC
#define HAVE_SETLINEBUF
#define HAVE_VPRINTF
       . . . . . . . . . .
#endif /*sun2*/
       . . . . . . . . . .
#if defined (pyr)
#define M_MACHINE "Pyramid"
#define M_OS Bsd
       . . . . . . . . . .
#endif /*pyr*/
       . . . . . . . . . .

описывающие конфигурацию машины. Переменные sun2, pyr и другие устанавливаются препроцессором cpp соответственно на компьютерах Sun2, Pyramid и т. д. Файл cpp-Makefile имеет примерно следующий вид:

#include "machines.h"
       . . . . . . . . . .

#if !defined (HAVE_SIGLIST)
SIGLIST=siglist.o
SIGLIST_FLAG=-DINITIALIZE_SIGLIST
#endif /*HAVE_SIGLIST*/

#if defined (NAVE_BISON)
BISON=bison -y
#else
BISON=yacc
#endif

#if defined (HAVE_VPRINTF)
VPRINTF=-D"HAVE_VPRINTF"
#endif /*HAVE_VPRINTF*/

Здесь SIGLIST, BISON, VPRINTF - это уже макросы make'а. Для сборки bash достаточно запустить make без указания make-файла и цели, будет использован файл Makefile и первая цель (ею будет all); изготовится bash-Makefile, от которого зависит all; bash-Makefile настроится на компьютер и среду, для которой порождается bash; затем выполнится команда

$(MAKE) -f bash-Makefile

Следующее правило показывает, как удобно распространить на другие цели найденный прием:

install clean distribution backup: bash-Makefile
      $(MAKE) -f bash-Makefile $@

Включаемые файлы

При изучении примера из предыдущего раздела наверняка возникает вопрос: почему все зависимости для объектных файлов вынесены в отдельный файл inc.mak? А затем еще один, связанный с предыдущим: кто составил этот файл? Другими словами, кто просмотрел все исходные тексты, нашел директивы

#include "имя-файла"

и указал все эти имена в списках зависимостей? В нашем случае ответ можно найти в правиле для цели makeinclude. Используемая версия С-компилятора, точнее С-препроцессора (ведь именно препроцессор вставляет в поток компилируемого текста содержимое включаемых файлов), сама составляет правильные зависимости для утилиты make. (Речь идет о компиляторе gcc из CNU-проекта: если его запустить с ключом -MM, то запускается только препроцессор и на стандартный вывод выдается текст, состоящий из зависимостей, содержащихся в файле inc.mak. Естественно, нужно также указать все остальные ключи, влияющие на работу препроцессора, например -I).

Заметим, что поскольку make считывает файл заранее, до выполнения каких-либо команд, обновление зависимостей и сборку целевой программы в одном запуске make'а сделать не удастся. Можно поступить так:

make makeinclude; make all

Заметим также, что файл inc.mak нельзя указывать в правилах для clean, иначе при следующем запуске утилиты make будет выдано сообщение об ошибке.

Можно было бы также завести make-файлы для сборки зависимостей и самих программ и использовать рекурсивный запуск make'а.

Если же подходящий препроцессор недоступен, остается только составлять зависимости вручную. Основная трудность в том, что включаемый файл сам может включать другие файлы и так далее. Например, пусть файл a.c включает файлы b.h и c.h, файл b.h включает файлы d.h и e.h, а файл c.h - файлы b.h и f.h. Файл b.h, таким образом, включается дважды, но ведь это не запрещено! Иногда выписать подходящие правила совсем не просто. Можно порекомендовать следующий прием (правило для включаемых файлов):

a.o: a.c b.h c.h
b.h: d.h e.h
        -touch b.h
c.h: b.h f.h
        -touch c.h

Теперь в каждом правиле участвуют только те файлы, имена которых есть в тексте одного файла. Команды touch предварены минусом, чтобы предотвратить удаление цели правила при неудачном выполнении команды, ведь в данном случае цель - это включаемый файл, т. е. исходный текст! Для большей безопасности можно указать их в списке зависимостей для специальной цели .PRECIOUS, такие файлы не удаляются при неудаче. В примере таким способом защищены пустые файлы (.print и т. п.).

Замечания напоследок

Make возвращает нулевой код, если цели успешно достигнуты, и ненулевой - в противном случае. Это полезно во многих ситуациях. Например, при отладке программы test1 после модификации исходных файлов удобно запускать команду:

make test1 && test1

или

make test1 && sdb test1

Программа test1 (или отладчик) будут выполняться только при условии успешного завершения make'а. Это также важно для рекурсивного запуска make'а.

Резюме

Язык make-файлов позволяет формально описывать зависимости и взаимные связи между файлами. Утилита make, опираясь на эту информацию, автоматически выполняет все необходимое для восстановления соответствия между измененными файлами и указанной Вами целью. Тем самым make страхует Вам от столь естественных недоразумений (например, изменили исходный текст и забыли скомпилировать).

Дополнительные средства (правила по умолчанию и их переопределение, включаемые файлы, макросы и правила со многими целями и т. п.) делают запись зависимостей и действий очень компактной и удобной.

Использование утилиты make для управления версиями программ, даже самых небольших, является весьма характерной чертой программирования в ОС UNIX. Рекомендуется не отходить от этой традиции.

Имеются еще важные причины овладеть make'ом: make используется для перегенерации самой операционной системы UNIX, а также для установки свободного программного обеспечения, распространяемого в исходных текстах.

Если Вы используете компьютер только для игры в Тетрис, то Вам совершенно не нужен make; но если Вы собираетесь что-нибудь сделать, make наверняка сможет взять на себя часть проблемы.


1 Строго говоря, непонятно, почему запуск make'ом нового make'а принято называть рекурсией? Если рассматривать make-файл, как программу на непроцедурном языке, то рекурсии здесь не больше, чем в обычном вызове функции.