Утилита make, входящая в состав практически всех Unix-подобных операционных систем - это традиционное средство, применяемое для сборки программных проектов. Она является универсальной программой для решения широкого круга задач, где одни файлы должны автоматически обновляться при изменении других файлов.
При запуске программа make читает файл с описанием проекта (make-файл) и, интерпретируя его содержимое, предпринимает необходимые действия. Файл с описанием проекта представляет собой текстовой файл, где описаны отношения между файлами проекта, и действия, которые необходимо выполнить для его сборки.
Основным "строительным элементом" make-файла являются правила (rules). В общем виде правило выглядит так:
<цель_1> <цель_2> ... <цель_n>: <зависимость_1> <зависимость_2> ... <зависимость_n> <команда_1> <команда_2> ... <команда_n>
Цель (target) - это некий желаемый результат, способ достижения которого описан в правиле. Цель может представлять собой имя файла. В этом случае правило описывает, каким образом можно получить новую версию этого файла. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
целью является файл iEdit (исполняемый файл программы). Правило описывает, каким образом можно получить новую версию файла iEdit (скомпоновать из перечисленных объектных файлов).
Цель также может быть именем некоторого действия. В таком случае правило описывает, каким образом совершается указанное действие. В следующем примере целью является действие clean (очистка).
clean: rm *.o iEdit
Подобного рода цели называются псевдоцели (pseudotargets) или абстрактные цели (phony targets).
Зависимость (dependency)- это некие "исходные данные", необходимые для достижения указанной в правиле цели. Можно сказать что зависимость - это "предварительное условие" для достижения цели. Зависимость может представлять собой имя файла. Этот файл должен существовать, для того чтобы можно было достичь указанной цели. В следующем правиле:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели - построение файла iEdit.
Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением указанной в правиле цели. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы):
clean_all: clean_obj rm iEdit clean_obj: rm *.o
Для того чтобы цель clean_all была достигнута, нужно сначала выполнить действие (достигнуть цели) clean_obj.
Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
командой является вызов компилятора GCC. Утилита make отличает строки, содержащие команды, от прочих строк make-файла по наличию символа табуляции (символа с кодом 9) в начале строки. В приведенном выше примере строка:
gcc main.o Editor.o TextLine.o -o iEdit
должна начинаться с символа табуляции.
Типичный make-файл проекта содержит несколько правил. Каждое из правил имеет некоторую цель и некоторые зависимости. Смыслом работы make является достижение цели, которую она выбрала в качестве главной цели (default goal). Если главная цель является именем действия (то есть абстрактной целью), то смысл работы make заключается в выполнении соответствующего действия. Если же главная цель является именем файла, то программа make должна построить самую "свежую" версию указанного файла.
Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit):
make iEdit
А в этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта):
make clean
Если не указывать какой-либо цели в командной строке, то make выбирает в качестве главной первую, встреченную в make-файле цель. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.o
из четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit. Схематично, "верхний уровень" алгоритма работы make можно представить так:
make() { главная_цель = ВыбратьГлавнуюЦель() ДостичьЦели( главная_цель ) }
После того как главная цель выбрана, make запускает "стандартную" процедуру достижения цели. Сначала в make-файле ищется правило, которое описывает способ достижения этой цели (функция НайтиПравило). Затем, к найденному правилу применяется обычный алгоритм обработки правил (функция ОбработатьПравило).
ДостичьЦели( Цель ) { правило = НайтиПравило( Цель ) ОбработатьПравило( правило ) }
Обработка правила разделяется на два основных этапа. На первом этапе обрабатываются все зависимости, перечисленные в правиле (функция ОбработатьЗависимости). На втором этапе принимается решение - нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). При необходимости, перечисленные в правиле команды выполняются (функция ВыполнитьКоманды).
ОбработатьПравило( Правило ) { ОбработатьЗависимости( Правило ) если НужноВыполнятьКоманды( Правило ) { ВыполнитьКоманды( Правило ) } }
Функция ОбработатьЗависимости поочередно проверяет все перечисленные в правиле зависимости. Некоторые из них могут оказаться целями каких-нибудь правил. Для этих зависимостей выполняется обычная процедура достижения цели (функция ДостичьЦели). Те зависимости, которые не являются целями, считаются именами файлов. Для таких файлов проверяется факт их наличия. При их отсутствии, make аварийно завершает работу с сообщением об ошибке.
ОбработатьЗависимости( Правило ) { цикл от i=1 до Правило.число_зависимостей { если ЕстьТакаяЦель( Правило.зависимость[ i ] ) { ДостичьЦели( Правило.зависимость[ i ] ) } иначе { ПроверитьНаличиеФайла( Правило.зависимость[ i ] ) } } }
На стадии обработки команд решается вопрос - нужно ли выполнять описанные в правиле команды или нет. Считается, что нужно выполнять команды если:
В противном случае (если ни одно из вышеприведенных условий не выполняется) описанные в правиле команды не выполняются. Алгоритм принятия решения о выполнении команд схематично можно представить так:
НужноВыполнятьКоманды( Правило ) { если Правило.Цель.ЯвляетсяАбстрактной() return true // цель является именем файла если ФайлНеСуществует( Правило.Цель ) return true цикл от i=1 до Правило.Число_зависимостей { если Правило.Зависимость[ i ].ЯвляетсяАбстрактной() return true иначе // зависимость является именем файла { если ВремяМодефикации( Правило.Зависимость[ i ] ) > ВремяМодефикации( Правило.Цель ) return true } } return false }
Каким образом make отличает имена действий от имен файлов? Традиционные варианты make поступают просто. Сначала ищется файл с таким именем. Если файл найден, то считается что цель или зависимость являются именем файла.
В противном случае считается, что данное имя является либо именем несуществующего файла, либо именем действия. Различия между этими двумя вариантами не делается, поскольку оба случая обрабатываются одинаково.
Подобный подход не слишком хорош по следующим соображениям. Во-первых, утилита make не слишком рационально расходует время, занимаясь поиском несуществующих имен файлов, которые на самом деле являются именами действий. Во-вторых, при подобном подходе, имена действий не должны совпадать с именами каких-либо файлов или директорий. Иначе подобный алгоритм даст сбой, и make-файл будет работать неправильно.
Некоторые версии make предлагают свои варианты решения этой проблемы. Так, например, в утилите GNU Make имеется механизм (специальная цель .PHONY), с помощью которого можно указать, что данное имя является именем действия.
Рассмотрим, как утилита make будет обрабатывать такой make-файл:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.o
Предположим, что в директории с проектом находятся следующие файлы:
Предположим также, что программа make была вызвана следующим образом:
make
Цель не указана в командной строке, поэтому запускается алгоритм выбора цели (функция ВыбратьГлавнуюЦель). Главной целью становится файл iEdit (первая цель из первого правила).
Цель iEdit передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. В данном случае, это первое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).
Сначала поочередно обрабатываются описанные в правиле зависимости (функция ОбработатьЗависимости). Первая зависимость - объектный файл main.o. Поскольку в make-файле есть правило с такой целью (функция ЕстьТакаяЦель возвращает true), то для цели main.o запускается процедура ДостичьЦели.
Функция ДостичьЦели ищет правило, где описана цель main.o. Эта цель описана во втором правиле make-файла. Для этого правила запускается функция ОбработатьПравило.
Функция ОбработатьПравило запускает процесс обработки зависимостей (функция ОбработатьЗависимости). Во втором правиле указана единственная зависимость - main.cpp. Такой цели в make-файле не существует, поэтому считается, что зависимость main.cpp является именем файла. Далее, проверяется наличие этого файла на диске (функция ПроверитьНаличиеФайла) - такой файл существует. На этом процесс обработки зависимостей завершается.
После обработки зависимостей, функция ОбработатьПравило принимает решение о том, нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). Цели правила (файла main.o) не существует, поэтому команды нужно выполнять. Функция ВыполнитьКоманды запускает указанную в правиле команду (компилятор GCC), в результате чего создается файл main.o.
Цель main.o достигнута (объектный файл main.o построен). Теперь make возвращается к обработке остальных зависимостей первого правила. Зависимости Editor.o и TextLine.o обрабатываются аналогично. Для них выполняются те же действия, что и для зависимости main.o.
После того, как все зависимости (main.o, Editor.o и TextLine.o) обработаны, решается вопрос о необходимости выполнения указанных в правиле команд (функция НужноВыполнятьКоманды).
Поскольку цель (iEdit) является именем файла, который в данный момент не существует, то принимается решение выполнить описанную в правиле команду (функция ВыполнитьКоманды).
Содержащаяся в правиле команда запускает компилятор GCC, в результате чего создается исполняемый файл iEdit. Главная цель (iEdit)таким образом достигнута. На этом программа make завершает свою работу.
Рассмотрим, как будет действовать утилита make, если для обработки описанного в предыдущей главе make-файла, она будет вызвана следующим образом:
make clean
Цель явно указана в командной строке, поэтому главной целью становится абстрактная цель clean. Цель clean передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. Это будет пятое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).
Поскольку в правиле не указано каких-либо зависимостей, make сразу переходит к этапу обработки указанных в правиле команд. Цель является именем действия, поэтому команды нужно выполнять.
Указанные в правиле команды выполняются, и цель clean, таким образом, считается достигнутой. На этом программа make завершает работу.
Возможность использования переменных внутри make-файла - очень удобное и часто используемое свойство make. В традиционных версиях утилиты, переменные ведут себя подобно макросам языка Си. Для задания значения переменной используется оператор присваивания. Например, выражение:
obj_list = main.o Editor.o TextLine.o
присваивает переменной obj_list значение "main.o Editor.o TextLine.o" (без кавычек). Пробелы между символом '=' и началом первого слова игнорируются. Следующие за последним словом пробелы также игнорируются. Значение переменной можно использовать с помощью конструкции:
$(имя_переменной)
Например, при обработке такого make-файла:
dir_list = . .. src/include all: echo $(dir_list)
на экран будет выведена строка:
. .. src/include
Переменные могут не только содержать текстовые строки, но и "ссылаться" на другие переменные. Например, в результате обработки make-файла:
optimize_flags = -O3 compile_flags = $(optimize_flags) -pipe all: echo $(compile_flags)
на экран будет выведено:
-O3 -pipe
Во многих случаях использование переменных позволяет упростить make-файл и повысить его наглядность. Для того чтобы облегчить модификацию make-файла, можно разместить "ключевые" имена и списки в отдельных переменных и поместить их в начало make-файла:
program_name = iEdit obj_list = main.o Editor.o TextLine.o $(program_name): $(obj_list) gcc $(obj_list) -o $(program_name) ...
Адаптация такого make-файла для сборки другой программы сведется к изменению нескольких начальных строк.
Автоматические переменные - это переменные со специальными именами, которые "автоматически" принимают определенные значения перед выполнением описанных в правиле команд. Автоматические переменные можно использовать для "упрощения" записи правил. Такое, например, правило:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
с использованием автоматических переменных можно записать следующим образом:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@
Здесь $^ и $@ являются автоматическими переменными. Переменная $^ означает "список зависимостей". В данном случае при вызове компилятора GCC она будет ссылаться на строку "main.o Editor.o TextLine.o". Переменная $@ означает "имя цели" и будет в этом примере ссылаться на имя "iEdit".
Иногда использование автоматических переменных совершенно необходимо - например, в шаблонных правилах (о них пойдет речь в следующей главе).
Шаблонные правила (implicit rules или pattern rules) - это правила, которые могут быть применены к целой группе файлов. В этом их отличие от обычных правил - описывающих отношения между конкретными файлами.
Традиционные реализации make поддерживают так называемую "суффиксную" форму записи шаблонных правил:
.<расширение_файлов_зависимостей>.<расширение_файлов_целей>: <команда_1> <команда_2> ... <команда_n>
Например, следующее правило говорит о том, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp":
.cpp.o: gcc -c $^
Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - это единственный способ узнать для каких файлов сейчас задействуется правило.
Шаблонные правила позволяют упростить make-файл и сделать его более универсальным. Рассмотрим простой проектный файл:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ main.o: main.cpp gcc -c $^ Editor.o: Editor.cpp gcc -c $^ TextLine.o: TextLine.cpp gcc -c $^
Все исходные тексты программы обрабатываются одинаково - для них вызывается компилятор GCC. С использованием шаблонных правил, этот пример можно переписать так:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ .cpp.o: gcc -c $^
Когда make ищет в файле проекта правило, описывающее способ достижения искомой цели (см. главу 3.2.2. "Достижение цели", функция НайтиПравило), то в расчет принимаются и шаблонные правила. Для каждого из них проверяется - нельзя ли задействовать это правило для достижения искомой цели.
Назад | Содержание | Вперед