Каким образом make отличает имена действий от имен файлов

Обновлено: 07.07.2024

Цель (target) – это некоторый желаемый результат, способ достижения которого описан в правиле. Цель может представлять собой имя файла. В этом случае правило описывает, каким образом можно получить новую версию этого файла.

В следующем примере целью является файл iEdit (исполняемый файл программы некоторого гипотетического проекта текстового редактора, с главным файлом проекта main.cpp и дополнительными Editor.cpp, TextLine.cpp). Правило описывает, каким образом можно получить новую версию бинарного файла iEdit (скомпоновать из перечисленных объектных файлов)

iEdit: main.o Editor.o TextLine.o

gcc main.o Editor.o TextLine.o -o iEdit

Если необходимо скомпилировать проект, написанный на C++, то можно использовать компилятор g++. Следует также отметить, что ключ o компилятора GCC указывает имя конечно бинарного файла.

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

Подобного рода цели называют псевдоцелями (pseudo targets) или абстрактными целями (phony targets).

Зависимость (dependency) – это некие «исходные данные», необходимые для достижения указанной в правиле цели, некоторое «предварительное условие» для достижения цели. Зависимость может представлять собой имя файла. Для того чтобы успешно достичь указанной цели, этот файл должен существовать.

В следующем правиле файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели – построение файла iEdit

iEdit: main.o Editor.o TextLine.o

gcc main.o Editor.o TextLine.o -o iEdit

Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением цели, указанной в правиле. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы)

Для того, чтобы достичь цели clean_all, необходимо сначала выполнить действие (достигнуть цели) clean_obj.

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

iEdit: main.o Editor.o TextLine.o

gcc main.o Editor.o TextLine.o -o iEdit

В приведенном выше примере строка gcc main.o Editor.o TextLine.o -o iEdit должна начинаться с символа табуляции.

Общий алгоритм работы make

Типичный make-файл проекта содержит несколько правил. Каждое из правил имеет некоторую цель и некоторые зависимости. Смыслом работы make является достижение цели, которую она выбрала в качестве главной цели (default goal). Если главная цель является именем действия (т. е. абстрактной целью), то смысл работы make заключается в выполнении соответствующего действия. Если же главная цель является именем файла, то программа make должна построить самую «свежую» версию указанного файла.

Выбор главной цели

Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit)

В этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта)

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

В следующем примере из четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit

iEdit: main.o Editor.o TextLine.o

gcc main.o Editor.o TextLine.o -o iEdit

gcc -c Editor.cpp

gcc -c TextLine.cpp

Схематично «верхний уровень» алгоритма работы make можно представить так

После того как главная цель выбрана, make запускает «стандартную» процедуру достижения цели. Сначала в make- файле выполняется поиск правила, которое описывает способ достижения этой цели (функция «НайтиПравило»). Затем к найденному правилу применяется обычный алгоритм обработки правил (функция «ОбработатьПравило»)

правило = НайтиПравило (Цель)

Обработка правила разделяется на два основных этапа. На первом этапе обрабатываются все зависимости, перечисленные в правиле (функция «ОбработатьЗависимости»). На втором этапе принимается решение о том, нужно ли выполнять указанные в правиле команды (функция «НужноВыполнятьКоманды»). При необходимости перечисленные в правиле команды выполняются (функция «ВыполнитьКоманды»)




если НужноВыполнятьКоманды (Правило)

цикл от i=1 до Правило.число_зависимостей

если ЕстьТакаяЦель (Правило.зависимость[ i ])

ДостичьЦели (Правило.зависимость[ i ])

ПроверитьНаличиеФайла (Правило.зависимость[ i ])

На стадии обработки команд решается вопрос о том, следует ли выполнять описанные в правиле команды или нет. Считается, что нужно выполнять команды в таких случаях, как:

- цель является именем действия (абстрактной целью);

- цель является именем файла и этого файла не существует;

- какая-либо из зависимостей является абстрактной целью;

- цель является именем файла и какая-либо из зависимостей, являющихся именем файла, имеет более позднее время модификации, чем цель.

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

// цель является именем файла

если ФайлНеСуществует (Правило.Цель)

цикл от i=1 до Правило.Число_зависимостей

если Правило.Зависимость[ i ].ЯвляетсяАбстрактной ()

// зависимость является именем файла

если ВремяМодификации(Правило.Зависимость[ i ]) >

Абстрактные цели и имена файлов

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

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

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

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

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

3.1. Правила

Основным "строительным элементом" make-файла являются правила (rules). В общем виде правило выглядит так:

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

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

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

Подобного рода цели называются псевдоцели (pseudotargets) или абстрактные цели (phony targets).

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

файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели - построение файла iEdit.

Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением указанной в правиле цели. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы):

Для того чтобы цель clean_all была достигнута, нужно сначала выполнить действие (достигнуть цели) clean_obj.

Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. В следующем примере:

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

должна начинаться с символа табуляции.

3.2. Алгоритм работы make

Типичный make-файл проекта содержит несколько правил. Каждое из правил имеет некоторую цель и некоторые зависимости. Смыслом работы make является достижение цели, которую она выбрала в качестве главной цели (default goal). Если главная цель является именем действия (то есть абстрактной целью), то смысл работы make заключается в выполнении соответствующего действия. Если же главная цель является именем файла, то программа make должна построить самую "свежую" версию указанного файла.

3.2.1 Выбор главной цели

Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit):

А в этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта):

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

из четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit. Схематично, "верхний уровень" алгоритма работы make можно представить так:

3.2.2 Достижение цели

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

3.2.3 Обработка правил

Обработка правила разделяется на два основных этапа. На первом этапе обрабатываются все зависимости, перечисленные в правиле (функция ОбработатьЗависимости). На втором этапе принимается решение - нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). При необходимости, перечисленные в правиле команды выполняются (функция ВыполнитьКоманды).

3.2.4 Обработка зависимостей

3.2.5 Обработка команд

  • Цель является именем действия (абстрактной целью)
  • Цель является именем файла и этого файла не существует
  • Какая-либо из зависимостей является абстрактной целью
  • Цель является именем файла и какая-либо из зависимостей, являющихся именем файла, имеет более позднее время модификации чем цель.

В противном случае (если ни одно из вышеприведенных условий не выполняется) описанные в правиле команды не выполняются. Алгоритм принятия решения о выполнении команд схематично можно представить так:

3.3. Абстрактные цели и имена файлов

Каким образом make отличает имена действий от имен файлов? Традиционные варианты make поступают просто. Сначала ищется файл с таким именем. Если файл найден, то считается что цель или зависимость являются именем файла.

В противном случае считается, что данное имя является либо именем несуществующего файла, либо именем действия. Различия между этими двумя вариантами не делается, поскольку оба случая обрабатываются одинаково.

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

Некоторые версии make предлагают свои варианты решения этой проблемы. Так, например, в утилите GNU Make имеется механизм (специальная цель .PHONY), с помощью которого можно указать, что данное имя является именем действия.

3.4. Пример работы make

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

  • main.cpp
  • Editor.cpp
  • TextLine.cpp

Предположим также, что программа 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 завершает свою работу.

3.5. Еще один пример работы make

Рассмотрим, как будет действовать утилита make, если для обработки описанного в предыдущей главе make-файла, она будет вызвана следующим образом:

Цель явно указана в командной строке, поэтому главной целью становится абстрактная цель clean. Цель clean передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. Это будет пятое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).

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

Указанные в правиле команды выполняются, и цель clean, таким образом, считается достигнутой. На этом программа make завершает работу.

3.6. Переменные

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

присваивает переменной obj_list значение "main.o Editor.o TextLine.o" (без кавычек). Пробелы между символом '=' и началом первого слова игнорируются. Следующие за последним словом пробелы также игнорируются. Значение переменной можно использовать с помощью конструкции:

Например, при обработке такого make-файла:

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

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

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

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

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

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

Автоматические переменные - это переменные со специальными именами, которые "автоматически" принимают определенные значения перед выполнением описанных в правиле команд. Автоматические переменные можно использовать для "упрощения" записи правил. Такое, например, правило:

с использованием автоматических переменных можно записать следующим образом:

Здесь $^ и $@ являются автоматическими переменными. Переменная $^ означает "список зависимостей". В данном случае при вызове компилятора GCC она будет ссылаться на строку "main.o Editor.o TextLine.o". Переменная $@ означает "имя цели" и будет в этом примере ссылаться на имя "iEdit".

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

3.8. Шаблонные правила

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

Традиционные реализации make поддерживают так называемую "суффиксную" форму записи шаблонных правил:

Например, следующее правило говорит о том, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp":

Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - это единственный способ узнать для каких файлов сейчас задействуется правило.

Шаблонные правила позволяют упростить make-файл и сделать его более универсальным. Рассмотрим простой проектный файл:

Все исходные тексты программы обрабатываются одинаково - для них вызывается компилятор GCC. С использованием шаблонных правил, этот пример можно переписать так:

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

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

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

3.1. Правила

Основным "строительным элементом" make-файла являются правила (rules). В общем виде правило выглядит так:

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

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

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

Подобного рода цели называются псевдоцели (pseudotargets) или абстрактные цели (phony targets).

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

файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели - построение файла iEdit.

Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением указанной в правиле цели. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы):

Для того чтобы цель clean_all была достигнута, нужно сначала выполнить действие (достигнуть цели) clean_obj.

Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. В следующем примере:

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

должна начинаться с символа табуляции.

3.2. Алгоритм работы make

Типичный make-файл проекта содержит несколько правил. Каждое из правил имеет некоторую цель и некоторые зависимости. Смыслом работы make является достижение цели, которую она выбрала в качестве главной цели (default goal). Если главная цель является именем действия (то есть абстрактной целью), то смысл работы make заключается в выполнении соответствующего действия. Если же главная цель является именем файла, то программа make должна построить самую "свежую" версию указанного файла.

3.2.1 Выбор главной цели

Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit):

А в этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта):

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

из четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit. Схематично, "верхний уровень" алгоритма работы make можно представить так:

3.2.2 Достижение цели

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

3.2.3 Обработка правил

Обработка правила разделяется на два основных этапа. На первом этапе обрабатываются все зависимости, перечисленные в правиле (функция ОбработатьЗависимости). На втором этапе принимается решение - нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). При необходимости, перечисленные в правиле команды выполняются (функция ВыполнитьКоманды).

3.2.4 Обработка зависимостей

3.2.5 Обработка команд

  • Цель является именем действия (абстрактной целью)
  • Цель является именем файла и этого файла не существует
  • Какая-либо из зависимостей является абстрактной целью
  • Цель является именем файла и какая-либо из зависимостей, являющихся именем файла, имеет более позднее время модификации чем цель.

В противном случае (если ни одно из вышеприведенных условий не выполняется) описанные в правиле команды не выполняются. Алгоритм принятия решения о выполнении команд схематично можно представить так:

3.3. Абстрактные цели и имена файлов

Каким образом make отличает имена действий от имен файлов? Традиционные варианты make поступают просто. Сначала ищется файл с таким именем. Если файл найден, то считается что цель или зависимость являются именем файла.

В противном случае считается, что данное имя является либо именем несуществующего файла, либо именем действия. Различия между этими двумя вариантами не делается, поскольку оба случая обрабатываются одинаково.

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

Некоторые версии make предлагают свои варианты решения этой проблемы. Так, например, в утилите GNU Make имеется механизм (специальная цель .PHONY), с помощью которого можно указать, что данное имя является именем действия.

3.4. Пример работы make

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

  • main.cpp
  • Editor.cpp
  • TextLine.cpp

Предположим также, что программа 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 завершает свою работу.

3.5. Еще один пример работы make

Рассмотрим, как будет действовать утилита make, если для обработки описанного в предыдущей главе make-файла, она будет вызвана следующим образом:

Цель явно указана в командной строке, поэтому главной целью становится абстрактная цель clean. Цель clean передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. Это будет пятое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).

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

Указанные в правиле команды выполняются, и цель clean, таким образом, считается достигнутой. На этом программа make завершает работу.

3.6. Переменные

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

присваивает переменной obj_list значение "main.o Editor.o TextLine.o" (без кавычек). Пробелы между символом '=' и началом первого слова игнорируются. Следующие за последним словом пробелы также игнорируются. Значение переменной можно использовать с помощью конструкции:

Например, при обработке такого make-файла:

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

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

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

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

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

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

Автоматические переменные - это переменные со специальными именами, которые "автоматически" принимают определенные значения перед выполнением описанных в правиле команд. Автоматические переменные можно использовать для "упрощения" записи правил. Такое, например, правило:

с использованием автоматических переменных можно записать следующим образом:

Здесь $^ и $@ являются автоматическими переменными. Переменная $^ означает "список зависимостей". В данном случае при вызове компилятора GCC она будет ссылаться на строку "main.o Editor.o TextLine.o". Переменная $@ означает "имя цели" и будет в этом примере ссылаться на имя "iEdit".

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

3.8. Шаблонные правила

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

Традиционные реализации make поддерживают так называемую "суффиксную" форму записи шаблонных правил:

Например, следующее правило говорит о том, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp":

Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - это единственный способ узнать для каких файлов сейчас задействуется правило.

Шаблонные правила позволяют упростить make-файл и сделать его более универсальным. Рассмотрим простой проектный файл:

Все исходные тексты программы обрабатываются одинаково - для них вызывается компилятор GCC. С использованием шаблонных правил, этот пример можно переписать так:

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

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

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

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

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

Почему стоит использовать утилиту make

  • она работает;
  • легко настраивается как для новых, так и для существующих проектов;
  • в большинстве ОС она предустановлена, если нет — её легко скачать;
  • она крошечная и содержит мало зависимостей;
  • make-файлы всё-таки могут быть короткими, ёмкими и красивыми;
  • она не использует загадочные папки типа working или resource ;
  • да и вообще темной магией не занимается — всё на виду.

Пишем make-файлы

Создадим файл и назовем его makefile или Makefile . Содержание стандартного make-файла можно описать так: «если любой из файлов-пререквизитов был изменен, то целевой файл должен быть обновлен». Суть make в том, что нам нужно по определенным правилам произвести какие-то действия с пререквизитами, чтобы получить некую цель.

ABBYY , Удалённо , От 150 000 до 330 000 ₽

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

Базовый синтаксис для определения цели (в файле makefile ):

Важно Индентация производится с помощью табуляции, а не пробелов.

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

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

Шаблонные правила работают на основе сопоставления расширений файлов. Например, make знает, как создавать объектные файлы *.o из исходных C-файлов *.c , компилируя их и передавая компилятору флаг -c . В make есть несколько встроенных шаблонных правил, самые известные из которых используются для компиляции кода на C и C++.

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

В большинстве случаев мы можем даже опустить пререквизиты: внутренние правила make подразумевают, что для того, чтобы, например, собрать somefile.o по принципу Исходник на C → Объектный файл, нам нужен somefile.c .

Будьте аккуратны: когда вы предлагаете make свой список команд, она будет ориентироваться только на ваш код и в данном случае не будет искать шаблонные правила для сборки цели.

Вызываем make

Запустим make в текущей директории:

Если make-файл в ней уже есть, будет создана (собрана) первая цель, которую make сможет найти. Если make-файла нет (или он есть, но в нем нет целей), make об этом сообщит.

Чтобы обратиться к конкретной цели, запустите:

Здесь цель — это название цели (без квадратных скобок).

make может догадаться, что делать, используя встроенные правила, без дополнительной информации от пользователя. Попробуйте создать файл test.c в пустой папке и запустить make . test.make скомпилирует test.c , используя встроенное шаблонное правило, и соберет цель с названием test . Это сработает даже через несколько шагов генерации промежуточных файлов.

Специальные цели

В большинстве make-файлов можно найти цели, называемые специальными. Вот самые распространенные:

  • all — собрать весь проект целиком;
  • clean — удалить все сгенерированные артефакты;
  • install — установить сгенерированные файлы в систему;
  • release или dist — для подготовки дистрибутивов (модули и тарболы).

Они не обязательно должны присутствовать в make-файле, но большинство сборочных процессов странно представить без хотя бы первых трех.

При сборке этих целей не будут созданы файлы с именами, например, all или clean . make обычно решает, запускать ли какие-либо процессы, основываясь на данных о том, нужно ли изменять целевой файл. Создание файлов с подобными именами не даст make произвести никаких изменений с целями.

Для предотвращения этого GNU make позволяет помечать такие цели как «фиктивные» (phony), чтобы запускать их в любом случае. Сделать это можно, добавив необходимые цели в качестве пререквизитов во внутреннюю цель .PHONY следующим образом:

Цель all обычно просто содержит главный исполняемый файл в пререквизите — иногда с дополнительными командами для пост-обработки генерируемых файлов. Так как мы в большинстве случаев хотим, чтобы цель all исполнялась по умолчанию, она должна стоять первой в файле.

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

Переменные и функции

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

Основные операции

Определять переменные и ссылаться на них можно следующим образом:

Ссылаться на переменные можно через $(NAME) или $ . Если опустить скобки, make сочтет за имя переменной только первый символ. Присоединение осуществляется при помощи оператора += . Можно также задать условные переменные с помощью ?= (если им еще не присвоены значения).

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

Передача аргументов встроенным шаблонным правилам

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

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

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

Это позволяет, например, запускать один и тот же make-файл с разными компиляторами, снабдив make необходимым именем бинарного файла для выполнения. Так задаётся переменная среды компилятора C:

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

  • $(CC) / $(CXX) — бинарные файлы для компиляторов C и C++, которые make использует для сборки;
  • $(CFLAGS) / $(CXXFLAGS) — флаги, передаваемые компиляторам;
  • $(LDLIBS) — присоединяемые библиотеки.

Программные переменные

make хранит некоторые распространённые программы в переменных. В основном это делается для того, чтобы при необходимости их можно было перезаписать.

Самая важная из них — $(MAKE) , которая должна использоваться при рекурсивном вызове make из make-файла. Она принимает во внимание аргументы командной строки из исходного вызова.

В цели clean , главная задача которой — удаление файлов, безопаснее использовать переменную $(RM) вместо прямого вызова rm .

Функции нескольких переменных

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

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

Вот некоторые наиболее интересные методы:

  • $(wildcard шаблон) возвращает список с названиями файлов, соответствующих шаблону, которые в том числе могут представлять собой относительный путь. Список внутри разделен с помощью пробелов, что проблематично для работы с файлами, содержащими пробелы в названии. Лучше всего избегать таких файлов при работе с make . Шаблон может содержать универсальный символ * ;
  • $(patsubst шаблон поиска, шаблон замены, список слов) заменяет все слова в списке, которые соответствуют шаблону поиска в соответствии с шаблоном замены. Оба шаблона используют % в качестве символа;
  • $(filter-out шаблон поиска, список слов) возвращает список всех слов, отфильтрованных по шаблону поиска;
  • $(notdir список слов) возвращает список слов, где имя каждой записи сокращается до основного (то есть если имя содержит название директории, то оно отфильтровывается);
  • $(shell команда) запускает команду в подпроцессоре и перехватывает стандартный вывод подобно оператору != . Оболочка для выполнения команды определяется переменной $(SHELL) .

Подробное описание функций можно найти в официальной документации.

Продвинутое использование переменных

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

Данный make-файл создает список всех исходных C-файлов в директории, заменяет суффикс .c на .o , используя функцию $(patsubst . ) , и потом использует этот список файлов в качестве пререквизитов к цели all . При запуске make станет собирать цель all , потому что она определена первой. Так как цель зависит от нескольких объектных файлов, которые могут ещё не существовать или должны быть обновлены, а make знает, как их сделать из исходных C-файлов, все запрашиваемые файлы также будут собраны.

Это становится мощным инструментом в сочетании с шаблонными правилами — мы можем автоматически собирать пререквизиты, которые косвенно используются в качестве новых целей.

Замена суффиксов

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

Make-файл из предыдущего примера можно преобразовать следующим образом:

Важно Вместо функции $(wildcard . ) используется оператор != .

Целезависимые переменные

Это ещё одна интересная особенность, использование которой ведёт к сокращению кода: она позволяет устанавливать переменным разные значения в зависимости от текущей цели.

В этом примере мы бы установили $(FOO) значение bar глобально, значение frob для цели один и значение bar baz для цели два.

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

Интеграция с внешними процессами

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

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

  • $(DESTDIR) должна быть пустой по умолчанию и никогда не должна задаваться из make-файла (режим чтения). Используется составителями пакета для вставки пути доступа перед устанавливаемыми файлами;
  • $(PREFIX) — значение этой переменной в вашем make-файле должно соответствовать /usr/local или другому заданному вами пути. Она позволяет пользователю пакета задать желаемую директорию для установки. Задавайте значение этой переменной, только если оно не было передано окружением (используя оператор ?= ).

Определение шаблонных правил

Синтаксис для написания шаблонных правил во многом похож на обычный синтаксис целей. Основное отличие состоит в использовании шаблонного символа % в основе цели (её имени до расширения), который и будет применяться для поиска соответствий.

Шаблон также может быть использован в списке пререквизитов, где он будет заменён на основу для формирования неявно определённых пререквизитов. (Список пререквизитов в случае чего можно расширить и явно определёнными.)

Также можно перезаписать любое из встроенных правил с помощью определения правила с такой же целью и пререквизитами.

Так как шаблонное правило должно быть написано таким образом, чтобы отличаться от реальных имён файлов, с которыми оно вызывается, make предоставляет специальные переменные для определения шаблонных правил.

Динамические переменные

Все имена динамических переменных состоят из одного знака, поэтому скобки для ссылки на них не нужны. Таких переменных довольно много, рассмотрим наиболее необходимые:

  • $@ — полное название цели;
  • $< — имя первого пререквизита (в том числе косвенно сгенерированного поиском по шаблону);
  • $^ — разделенный пробелами список всех пререквизитов (версия GNU).

Шаблонное правило, конвертирующее размеченные файлы в HTML с использованием markdown , получает такой вид:

Итоги

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

Скрипт generate_article.py реализует минималистичный шаблонизатор, используя index.htm в качестве базы для вставки HTML, сгенерированного из входных файлов. Присутствие шаблонизатора в пререквизитах шаблонного правила обеспечивает, что изменения в шаблонизаторе вызовут изменения всех файлов, относящихся к статье.

Для дальнейшего изучения make рекомендуем ознакомиться с официальным руководством.

Читайте также: