2. GNU Make

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

GNU Make - это версия программы make распространяемая Фондом Свободного Программного Обеспечения (Free Software Foundation - FSF) в рамках проекта GNU (www.gnu.org). Получить самую свежую версию программы и документации можно на "домашней страничке" программы www.gnu.org/software/make либо на страничке Paul D. Smith - одного из авторов GNU Make (www.paulandlesley.org/gmake).

Программа GNU Make имеет очень подробную и хорошо написанную документацию, с которой я настоятельно рекомендую ознакомиться. Если у вас нет доступа в интернет, то пользуйтесь документацией в формате Info, которая должна быть в составе вашего дистрибутива Linux. Будьте осторожны с документацией в формате man-странички (man make) - как правило, она содержит лишь отрывочную и сильно устаревшую информацию.

2.1. Две разновидности переменных

GNU Make поддерживает два способа задания переменных, которые несколько различаются по смыслу. Первый способ - традиционный, с помощью оператора '=':

    compile_flags = -O3 -funroll-loops -fomit-frame-pointer

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

    #define  compile_flags  "-O3 -funroll-loops -fomit-frame-pointer"

Значение переменной, заданной с помощью оператора '=', будет вычислено в момент ее использования. Например, при обработке make-файла:

    var1 = one
    var2 = $(var1) two
    var1 = three
    all:
        @echo $(var2)

на экран будет выдана строка "three two". Значение переменной var2 будет вычислено непосредственно в момент выполнения команды echo, и будет представлять собой текущее значение переменной var1, к которому добавлена строка " two". Как следствие - одна и та же переменная не может одновременно фигурировать в левой и правой части выражения, так как это может привести к бесконечной рекурсии. GNU Make распознает подобные ситуации и прерывает обработку make-файла. Следующий пример вызовет ошибку:

    compile_flags = -pipe $(compile_flags)

GNU Make поддерживает также и второй, новый способ задания переменной - с помощью оператора ':=':

    compile_flags := -O3 -funroll-loops -fomit-frame-pointer

В этом случае переменная работает подобно "обычным" текстовым переменным в каком-нибудь из языков программирования. Вот приблизительный аналог этого выражения на языке C++:

    string   compile_flags = "-O3 -funroll-loops -fomit-frame-pointer";

Значение переменной вычисляется в момент обработки оператора присваивания. Если, например, записать

    var1 := one
    var2 := $(var1) two
    var1 := three
    all:
        @echo $(var2)

то при обработке такого make-файла на экран будет выдана строка "one two".

Переменная может "менять" свое поведение в зависимости от того, какой из операторов присваивания был к ней применен последним. Одна и та же переменная на протяжении своей жизни вполне может вести себя и как "макрос" и как "текстовая переменная".

Все свои make-файлы я пишу с применением оператора ':='. Этот способ кажется мне более удобным и надежным. Вдобавок это более эффективно, так как значение переменной не вычисляется заново каждый раз при ее использовании. Подробнее о двух способах задания переменных можно прочитать в документации на GNU Make в разделе "The Two Flavors of Variables" .

2.2. Функции манипуляции с текстом

Утилита GNU Make содержит большое число полезных функций, манипулирующих текстовыми строками и именами файлов. В частности в своих make-файлах я использую функции addprefix , addsuffix, wildcard , notdir и patsubst . Для вызова функций используется синтаксис

 $(имя_функции  параметр1, параметр2 ... )

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

    src_dirs := Editor TextLine
    src_dirs := $(addprefix ../../, $(src_dirs))
    all:
        @echo $(src_dirs)

на экран будет выведено

    ../../Editor ../../TextLine

Видно, что к каждому имени директории добавлен префикс "../../". Функция addprefix обсуждается в разделе "Functions for File Names" руководства по GNU Make.

Функция addsuffix работает аналогично функции addprefix , только добавляет указанную строку в конец каждого слова. Например, в результате выполнения make-файла:

    source_dirs := Editor  TextLine
    search_wildcard  s := $(addsuffix /*.cpp, $(source_dirs))
    all:
        @echo $(search_wildcard  s)

на экран будет выведено

    Editor/*.cpp  TextLine/*.cpp

Видно, что к каждому имени директории добавлен суффикс "/*.cpp". Функция addsuffix обсуждается в разделе "Functions for File Names" руководства по GNU Make.

Функция wildcard "расширяет" переданный ей шаблон или несколько шаблонов в список файлов, удовлетворяющих этим шаблонам. Пусть в директории Editor находится файл Editor.cpp, а в директории TextLine - файл TextLine.cpp:

Тогда в результате выполнения такого make-файла:

    search_wildcard  s := Editor/*.cpp  TextLine/*.cpp
    source_files := $(wildcard   $(search_wildcard  s))
    all:
        @echo $(source_files)

на экран будет выведено

  Editor/Editor.cpp  TextLine/TextLine.cpp

Видно, что шаблоны преобразованы в списки файлов. Функция wildcard подробно обсуждается в разделе "The Function wildcard " руководства по GNU Make.

Функция notdir позволяет "убрать" из имени файла имя директории, где он находится. Например, в результате выполнения make-файла:

    source_files := Editor/Editor.cpp  TextLine/TextLine.cpp
    source_files := $(notdir $(source_files))
    all:
        @echo $(source_files)

на экран будет выведено

    Editor.cpp TextLine.cpp

Видно, что из имен файлов убраны "пути" к этим файлам. Функция notdir обсуждается в разделе "Functions for File Names" руководства по GNU Make.

Функция patsubst позволяет изменить указанным образом слова, подходящие под шаблон. Она принимает три параметра - шаблон, новый вариант слова и исходную строку. Исходная строка рассматривается как список слов, разделенных пробелом. Каждое слово, подходящее под указанный шаблон, заменяется новым вариантом слова. В шаблоне может использоваться специальный символ '%', который означает "любое количество произвольных символов". Если символ '%' встречается в новом варианте слова (втором параметре), то он заменяется текстом, соответствующим символу '%' в шаблоне. Например, в результате выполнения make-файла:

    source_files := Editor.cpp  TextLine.cpp
    object_files := $(patsubst   %.cpp, %.o, $(source_files))
    all:
        @echo $(object_files)

на экран будет выведено

    Editor.o  TextLine.o

Видно, что во всех словах окончание ".cpp" заменено на ".o". Функция patsubst имеет второй, более короткий вариант записи для тех случаев, когда надо изменить суффикс слова (например, заменить расширение в имени файла). Более короткий вариант выглядит так:

    $(имя_переменной:.старый_суффикс=.новый_суффикс)

Применяя "короткий" вариант записи предыдущий пример можно записать так:

    source_files := Editor.cpp  TextLine.cpp
    object_files := $(source_files:.cpp=.o)
    all:
        @echo $(object_files)

Функция patsubst обсуждается в разделе "Functions for String Substitution and Analysis" руководства по GNU Make.

2.3. Новый способ задания шаблонных правил

В "традиционных" вариантах make шаблонное правило задается с помощью конструкций, наподобие:

    .cpp.o:
        gcc $^ -o $@

То есть под действие правила попадают файлы с определенными расширениями (".cpp" и ".o" в данном случае).

GNU Make поддерживает более универсальный подход - с использованием шаблонов имен файлов. Для задания шаблона используется символ '%', который означает "последовательность любых символов произвольной длины". Символ '%' в правой части правила заменяется текстом, который соответствует символу '%' в левой части. Пользуясь новой формой записи, приведенный выше пример можно записать так:

    %.o: %.cpp
        gcc $^ -o $@

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

2.4. Переменная VPATH

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

    VPATH   := Editor TextLine
    %.o: %.cpp
        gcc -c $<

make будет искать файлы с расширением ".cpp" сначала в текущем каталоге, а затем, при необходимости, в подкаталогах Editor и TextLine. Я часто использую подобную возможность, так как предпочитаю располагать исходные тексты в иерархии каталогов, отражающих логическую структуру программы.

Переменная VPATH описывается в главе "VPATH : Search Path for All Dependencies" руководства по GNU Make. На страничке Paul D. Smith есть статья под названием "How Not to Use VPATH " ( paulandlesley.org/gmake/VPATH.html), в которой обсуждается "неправильный" стиль использования переменной VPATH .

2.5. Директива override

Переменные в GNU Make могут создаваться и получать свое значение разными способами:

Последний случай считается "специальным". Если переменная задана через командную строку, то внутри make-файла нельзя изменить ее значение "обычным" способом. Рассмотрим простой make-файл:

    compile_flags := -pipe $(compile_flags)
    all:
        echo $(compile_flags)

Предположим, что переменная compile_flags была задана через командную строку при запуске программы make:

    make  compile_flags="-O0 -g"

В результате обработки make-файла на экран будет выведена строка:

    -O0 -g

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

    override compile_flags := -pipe $(compile_flags)
    all:
        echo $(compile_flags)

Теперь в результате обработки make-файла на экран будет выдана строка:

    -pipe -O0 -g

2.6. Добавление текста в строку

Часто возникает необходимость добавить текст к существующей переменной. Для этой цели служит оператор "+=". Добавляемый текст может быть как текстовой константой, так и иметь ссылки на другие переменные:

    compile_flags += -pipe
    compile_flags += $(flags)

При использовании этого оператора, "тип" переменной (см. раздел 2.1 "Две разновидности переменных") не меняется - "макросы" остаются "макросами", а "текстовые переменные" по-прежнему остаются таковыми.

Если переменная задана с помощью командной строки, то по-прежнему для изменения ее значения внутри make-файла нужно использовать директиву override . В следующем примере предполагается, что переменная compile_flags задана в командной строке:

    override compile_flags += -pipe
    override compile_flags += $(flags)

2.7. Директива include

С помощью директивы include можно включать в обрабатываемый make-файл другие файлы. Работает она аналогично директиве #include в языках C и C++. Когда встречается эта директива, обработка "текущего" make-файла приостанавливается и make временно "переключается" на обработку указанного в директиве файла. Директива include может оказаться полезной для включения в make-файл каких-либо "общих", или автоматически сгенерированных другими программами фрагментов.

В директиве include могут быть указаны одно или несколько имен файлов, разделенных пробелами. В качестве имен файлов можно использовать шаблоны:

    include common.mak
    include main.d Editor.d TextLine.d
    include *.d

Указанные в директиве файлы должны существовать - иначе make предпримет попытку "создать" их, а при невозможности этого достигнуть, выдаст сообщение об ошибке. Директива include с пустым списком файлов:

    include

просто игнорируется.

2.8. Автоматические переменные

Программа GNU Make поддерживает большое число автоматических переменных. В своих make-файлах я использую следующие автоматические переменные:

Имя автоматической переменнойЗначение
$@Имя цели обрабатываемого правила
$<Имя первой зависимости обрабатываемого правила
$^Список всех зависимостей обрабатываемого правила

Полный список автоматических переменных приводится в разделе "Automatic Variables" руководства по GNU Make.

2.9. "Комбинирование" правил

В make-файле могут встречаться несколько правил, имеющих одинаковую цель. В таком случае они как бы "комбинируются вместе". Например, следующие два правила:

    TextLine.o: TextLine.cpp
        gcc -c $<
    TextLine.o: TextLine.h

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

    TextLine.o: TextLine.cpp TextLine.h
        gcc -c $<

Шаблонные и нешаблонные правила также могут "комбинироваться":

    %.o: %.cpp
        gcc -c $<
    TextLine.o: TextLine.h

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

2.10. Make-файл, используемый по умолчанию

Если при вызове программы GNU Make не указывать явно, какой make-файл следует обрабатывать, то она пытается найти и обработать файлы GNUmakefile, Editor.h и Editor.h (именно в таком порядке). Руководство по GNU Make рекомендует имя Editor.h для make-файлов, используемых по умолчанию. При "алфавитной" сортировке имен файлов в директории, такое имя будет располагаться ближе к началу списка.

2.11. Специальная цель .PHONY

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

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

В утилите GNU Make имеется способ явного объявления целей абстрактными. Для этого используется механизм "специальных целей". Специальная цель - это имя, которое имеет специальное значение, когда используется в качестве цели. Для того, например, чтобы объявить перечисленные цели абстрактными, достаточно поместить их в правило со специальной целью .PHONY. В следующем примере цель clean объявляется абстрактной:

    .PHONY: clean
    clean:
        rm *.o *.d

Все возможные специальные цели описаны в главе Special Built-in Target Names" руководства по GNU Make.

Назад | Содержание | Вперед