Тарифы Услуги Сим-карты

Смотреть что такое "Make" в других словарях. Эффективное использование GNU Make

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

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

На сегодняшний день наиболее распространены три варианта утилиты, объединенные общими принципами работы, но отличающиеся синтаксисом языка и возможностями:

    GNU make - самый распространенный и функциональный вариант

    BSD make (pmake) - используется в проектах BSD, по функциональности примерно соответствует GNU make

    nmake (Microsoft make) - работает под Windows, малофункционален, только базовые принципы make.

Мы работаем с GNU make. На BSD системах (в частности, FreeBSD, он может быть доступен как gmake, на Linux - просто make).

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

Утилита make работает по правилам (rules) , записанным в специальном конфигурационном файле. Правила определяют цели (targets) , завимости между целями и набор команд для выполнения каждой цели.

Цели могут соответствовать определенным файлам. Кроме того, цели могут не соответствовать ни одному файлу и использоваться для группировки других целей или определенной последовательности команд. Такие цели называются phony targets.

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

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

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

Запуск make

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

Соответственно, команда

$ make

будет использовать файл Makefile , находящийся в текущем каталоге.

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

Явное указание цели выполняется инструкцией DEFAULT_GOAL в Makefile:

. DEFAULT_GOAL: all

Например, команда

$ make clean

вызовет обработку цели clean файла Makefile , находящегося в текущем каталоге.

Можно указать сразу несколько целей.

Выполнение целей может быть настроено с использованием переменных (о которых ниже). При запуске make можно указать значения переменных:

$ make build PREFIX =/ usr/ local

Значение переменной PREFIX будет доступно в правилах Makefile и может быть использовано при сборке.

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

    F - позволяет явно указать файл правил

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

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

Базовый синтаксис make

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

Target: dep1 dep2 ... command1 command2 ...

    target - цель

    dep1 , dep2 - цели, от которых зависит цель target

    command1 , command2 - команды, выполняемые для достижения цели target

Например:

Style. css: src/ less/ app. less lessc src/ less/ app. less > style. css

Этот фрагмент определяет, что файл style.css зависит от файла src/less/app.less и для его сборки необходимо выполнить команду lessc src/less/app.less > style.css . Перегенерация файла style.css будет выполняться только в случае,если файл src/less/app.less новее, чем файл style.css (до тех пор, пока при запуске make не будет указан ключ -B).

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

В качестве команд обработки целей используются команды shell. Текст команды выводится, для того, чтобы он не выводился, команду необходимо начать с символа @ .

Каждая команда запускается в отдельном интерпретаторе shell, таким образом, команды не связаны друг с другом. Иначе говоря, одна строка команды - один shell. Это поведение может быть переопределено с помощью специальной цели.ONESHELL .

Если команду (или список зависимостей) необходимо записать в несколько строк, используют символ переноса \ .

PHONY targets

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

.PHONY : clean clean: rm *. o temp

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

В нашем примере вызов make clean приведет к выполнению цели clean , которая безусловно выполнит удаление временных файлов.

В случае, если у phony target есть зависимость в виде другой phony target, то зависимость выполняется перед зависящей целью. Таким образом, мы получаем механизм, напоминающий подпрограммы. Например, мы можем определить цель all , собирающую все файлы проекта, и отдельные цели css , js и php , собирающие отдельной css -файлы, js -файлы и обрабатывающие php файлы.

Соответственно, в Makefile мы можем написать:

.PHONY : all css js php all: css js php css: www/ style. css ... тут команды js: www/ js/ app. js ... тут еще команды php: ... тут снова команды

В результате мы можем использовать make all для пересборки всех файлов и, скажем, make css для пересборки только CSS -файлов.

Переменные

В make-файле можно использовать переменные, хотя правильнее сказать, что можно использовать макросы.

Переменные определяются присваиванием в makefile или могут быть переданы извне.

Переменные - это макроопределения, причем вычисление переменной всегда выполняется в самый последний момент перед подстановкой. Макросы могут использовать везде в тексте makefile.

СС= gcc IDIR=../ include CFLAGS=- I$ (IDIR ) DEPS= hellomake. o hellofunc. p NAME= hellomake $ (NAME ) : $ (DEPS ) $ (CC ) - o $ (NAME ) $ (DEPS )

Подстановка выполняется конструкцией $(VAR) в отличие от shell, где используется $VAR .

Если в shell команде используется shell-переменная, необходимо квотить знак $ , дублируя его, например:

Printhome: echo $$ HOME

Помимо макропеременных существуют и более традиционные, в которых значение устанавливается сразу. Для работы с ними используется оператор:= . В наших условиях достаточно использовать обычные переменные.

Часто требуется определить переменную только в том случае, если она еще не была определена. Для этого используется оператор?= :

MYSQL_CHARSET ?= UTF8

Соответственно, если мы вызовем

make create-database

будет использована кодировка UTF8 , а в случае

Make create- database MYSQL_CHARSET= CP1251

будет использована CP1251 .

Если переменная содержит несколько строк, можно использовать синтаксис define:

Define MYSQL_APP_CONF_TEMPLATE [ mysql] host=$ (MYSQL_SERVER ) port=$ (MYSQL_PORT ) user=$ (MYSQL_USER ) password= "$(MYSQL_PASSWORD)" endef

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

Make поддерживает набор автоматических переменных, облегчающих написание правил. Например, переменная $@ соответствую текущей цели (то, что слева от:), а переменная $^ - списку зависимостей (то, что справа от:). Таким образом, например, можно написать:

Www/ js/ script. js: src/ js/ jquery. js src/ js/ plugin1. js src/ js/ plugin2. js cat $^ > $@

В результате www/js/script.js будет результатом объединения трех js-файлов.

Полный список таких переменных приведен в документации, для нас наиболее интересны:

    $@ - имя цели

    $< - имя первой зависимости

    $? - имена всех зависимостей, которые новее чем цель

    $^ - имена всех зависимостей цели

С полным списком можно ознакомиться в документации: Automatic Variables .

Условное выполнение

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

ifdef $ (APP ) setup: ... else setup: @ echo "Error, applications is not defined" endif

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

Foo: $ (objects ) ifeq ($ (CC ) , gcc) $ (CC ) - o foo $ (objects ) $ (libs_for_gcc ) else $ (CC ) - o foo $ (objects ) $ (normal_libs ) endif

Полностью с возможностями условных выражений можно ознакомиться в документации: Conditional syntax .

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

Шаблонные правила (pattern rules) позволяют указать правило преобразования одних файлов в другие на основании зависимостей между их именами. Например, мы можем указать правило для получения объектного файла из файла на языке C:

%. o: %. c $ (CC ) $< - o $@

Обратите внимание на использование переменной %< , которая в таких правилах используется для получения имени исходного файла.

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

Включение других файлов make

Файл make может подключить другие файлы make оператором include:

include make1. mk make2. mk

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

Функции

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

$ (function arg1, arg2,... )

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

Несколько примеров из hive. Получаем текущее время (обратите внимание на использование:= :

HIVE_TIME := $ (shell date +% Y/% m/% d\ % H:% M:% S)

Включение файла container.mk только в случае, если он существует:

include $ (shell find $ (HIVE_ETC_DIR ) - name container. mk)

Формирование имени MySQL базы по имени проекта:

MYSQL_DB ?= $ (subst -, _,$ (HIVE_NAME ) _$ (APP ) )

Добавление префиксов и суффиксов к именам файлов

$ (addprefix src/ less/,$ (addsuffix . less, app ui catalog) )

Подробнее о функциях можно прочитать в документации Functions .

Собственные функции

Можно создавать собственные параметризованные функции путем определения переменных, содержащих специальные переменные $1 , $2 , ..., соответствующие переданным аргументам. Вызов пользовательской функции производится специальным макросом call:

$ (call variable, param, param,... )

Очень тупой пример:

Hive_red = "\0 33 1. To cause to … The Collaborative International Dictionary of English

make - make1 vt. made, making 1. to bring into being; specif., a) to form by shaping or… … English World dictionary

Make - (engl. machen, erstellen) ist ein Computerprogramm, das Shellskript ähnlich Kommandos in Abhängigkeit von Bedingungen ausführt. Es wird hauptsächlich bei der Softwareentwicklung eingesetzt. Genutzt wird es beispielsweise, um in einem Projekt, das … Deutsch Wikipedia

Make - Cet article a pour sujet le logiciel intitulé make. Pour une définition du mot « make », voir l’article make du Wiktionnaire. make est un logiciel traditionnel d UNIX. C est un « moteur de production »: il sert à appeler … Wikipédia en Français

make - (engl. machen, erstellen) ist ein Computerprogramm, das Kommandos in Abhängigkeit von Bedingungen ausführt. Es wird hauptsächlich bei der Softwareentwicklung als Programmierwerkzeug eingesetzt. Genutzt wird es beispielsweise, um in Projekten, die … Deutsch Wikipedia

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

В этой заметке я попытаюсь рассказать как создать makefile.

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

Структуру Makefile можно представить так:

ЦЕЛЬ: ЗАВИСИМОСТЬ ДЕЙСТВИЕ

Но обычно используются более сложные правила, например:

ЦЕЛЬ: ЦЕЛЬ1 ЦЕЛЬ2 ДЕЙСТВИЕ ЦЕЛЬ1: ЗАВИСИМОСТЬ1 ДЕЙСТВИЕ1 ЦЕЛЬ2: ЗАВИСИМОСТЬ2 ДЕЙСТВИЕ2

ЦЕЛЬ — это то что мы получаем в результате ДЕЙСТВИЯ. Это может быть файл, директория или просто абстрактная ЦЕЛЬ не имеющая связи с каким-либо объектом на жестком диске. После имени цели ставится двоеточие. При запуске команды make без параметров выполнится первое найденное правило. Что бы выполнить другое правило надо указать его команде make

Make ЦЕЛЬ2

ЗАВИСИМОСТЬ — это то, от чего зависит наша ЦЕЛЬ. Это могут быть файлы, каталоги или другие ЦЕЛИ. Make сравнивает дату и время изменения ЦЕЛИ и объектов о которых зависит цель. Если объекты от которых зависит цель были изменены позже чем создана цель, то будет выполнено ДЕЙСТВИЕ. ДЕЙСТВИЕ так же выполняется если ЦЕЛЬ не является именем файла или директории.

ДЕЙСТВИЕ — это набор команд которые надо выполнить. Перед командами должен быть введен символ табуляции. Если вместо символа табуляции будут введены пробелы то при компиляции будет выведено сообщение об ошибке:

Makefile:13: *** пропущен разделитель. Останов.

Makefile:13: *** missing separator. Stop.

Пример Makefile:

all: test.elf test.elf: test1.o test2.o gcc -o test.elf test1.o test2.o test1.o test1.c gcc -c test1.c -o test1.o test2.o test2.c gcc -c test2.c -o test2.o

Рассмотри последний пример:
Первым выполняется all т.к. находится в начале Makefile. all зависит от test.elf и файла или директории с именем all не существует, поэтому всегда будет происходить проверка цели с именем test.elf.

test.elf зависит от test1.o и test2.o, по этому сначала будет проверена цель test1.o затем test2.o

При проверке цели test1.o сравниваются дата и время изменения файла test1.o и test1.c. Если файл test1.o не существует или файл test1.c был изменены позднее чем test1.o то будет выполнена команда gcc -с test1.c -o test1.o.

Аналогично будет проверена цель test2.o.

После этого сравниваются дата и время изменения файла test.elf и файлов test1.o test2.o. Если test1.o или test2.o новее то будет выполнена команда gcc -o test.elf test1.o test2.o

Таким образом отслеживаются изменения в файлах test1.с и test2.c.

P/S надеюсь данная заметка упростит создание makefile, но если есть какие-то вопросы — напишите в комментарии, постараюсь ответить.

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

  • Автоматическое построение списка файлов с исходными текстами
  • Автоматическую генерацию зависимостей от включаемых файлов (с помощью компилятора GCC )
  • "Параллельную" сборку отладочной и рабочей версий программы

Моя книга построена несколько необычным образом. Как правило, книги строятся по принципу "от простого - к сложному". Для новичков это удобно, но может вызвать затруднение у профессионалов. Опытный программист будет вынужден "продираться" сквозь книгу, пропуская главы с известной ему информацией. Я решил построить книгу по другому принципу. Вся "квинтэссенция" книги, ее "главная идея", содержится в первой главе. Остальные главы носят более или менее дополнительный характер.

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

Для работы я использовал GNU Make версии 3.79.1. Некоторые старые версии GNU Make (например, версия 3.76.1 из дистрибутива Slackware 3.5 ) могут неправильно работать с примером "традиционного" строения make-файла (по-видимому, они "не воспринимают" старую форму записи шаблонных правил).

1. Моя методика использования GNU Make

В этой главе я описываю свой способ построения make-файлов для сборки проектов с использование программы GNU Make и компилятора GCC (GNU Compiler Collection ) . Предполагается, что вы хорошо знакомы с утилитой GNU Make . Если это не так, то прочтите сначала .

1.1. Пример проекта

В качестве примера я буду использовать "гипотетический" проект - текстовой редактор. Он состоит из нескольких файлов с исходным текстом на языке C++ (main.cpp , Editor.cpp , TextLine.cpp ) и нескольких включаемых файлов (main.h ,Editor.h , TextLine.h ). Если вы имеете доступ в интернет то "электронный" вариант приводимых в книге примеров можно получить на моей домашней страничке по адресу www.geocities.com/SiliconValley/Office/6533 . Если интернет для вас недоступен, то в приведены листинги файлов, которые используются в примерах.

1.2. "Традиционный" способ построения make-файлов

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

  • example_1-traditional /
    • main.cpp
    • main.h
    • Editor.cpp
    • Editor.h
    • TextLine.cpp
    • TextLine.h
    • Makefile

Предполагается, что для компиляции программы используется компилятор GCC , и объектные файлы имеют расширение ".o" . Файл Makefile выглядит так:

# # example_1-traditional/Makefile # # Пример "традиционного" строения make-файла # iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ .cpp.o: gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.h

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

  • Требуется "явно" перечислять все объектные файлы, из которых компонуется программа
  • Требуется "явно" перечислять, от каких именно заголовочных файлов зависит тот или иной объектный файл
  • Исполняемый файл программы помещается в "текущую" директорию. Если мне нужно иметь несколько различных вариантов программы (например, отладочный и рабочий), то каждый раз при переходе от одного варианта к другому требуется полная перекомпиляция программы во избежание нежелательного "смешивания" разных версий объектных файлов.

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

1.3. Автоматическое построение списка объектных файлов

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

IEdit: *.o gcc $< -o $@

не сработает, так как будут учтены только существующие в данный момент объектные файлы. Я использую чуть более сложный способ, который основан на предположении, что все файлы с исходным текстом должны быть скомпилированы и скомпонованы в собираемую программу. Моя методика состоит из двух шагов:

  • Получить список всех файлов с исходным текстом программы (всех файлов с расширением ".cpp "). Для этого можно использовать функцию wildcard .
  • Преобразовать список исходных файлов в список объектных файлов (заменить расширение ".cpp " на расширение ".o "). Для этого можно воспользоваться функцией patsubst .

Следующий пример содержит модифицированную версию make-файла:

  • example_2-auto_obj /
    • main.cpp
    • main.h
    • Editor.cpp
    • Editor.h
    • TextLine.cpp
    • TextLine.h
    • Makefile

Файл Makefile теперь выглядит так:

# # example_2-auto_obj/Makefile # # Пример автоматического построения списка объектных файлов # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.h

Список объектных файлов программы строится автоматически. Сначала с помощью функции wildcard получается список всех файлов с расширением ".cpp ", находящихся в директории проекта. Затем, с помощью функции patsubst , полученный таким образом список исходных файлов, преобразуется в список объектных файлов. Make-файл теперь стал более универсальным - с небольшими изменениями его можно использовать для сборки разных программ.

1.4. Автоматическое построение зависимостей от заголовочных файлов

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

Перечисление зависимостей "вручную" требует довольно кропотливой работы. Недостаточно просто открыть файл с исходным текстом и перечислить имена всех заголовочных файлов, подключаемых с помощью #include . Дело в том, что одни заголовочные файлы могут, в свою очередь, включать в себя другие заголовочные файлы, так что придется отслеживать всю "цепочку" зависимостей.

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

Ключ компиляции Назначение
-M Для каждого файла с исходным текстом препроцессор будет выдавать на стандартный вывод список зависимостей в виде правила для программы make . В список зависимостей попадает сам исходный файл, а также все файлы, включаемые с помощью директив #include <имя_файла> и #include "имя_файла" . После запуска препроцессора компилятор останавливает работу, и генерации объектных файлов не происходит.
-MM Аналогичен ключу -M #include "имя_файла"
-MD Аналогичен ключу -M , но список зависимостей выдается не на стандартный вывод, а записывается в отдельный файл зависимостей. Имя этого файла формируется из имени исходного файла путем замены его расширения на ".d ". Например, файл зависимостей для файла main.cpp будет называться main.d . В отличие от ключа -M , компиляция проходит обычным образом, а не прерывается после фазы запуска препроцессора.
-MMD Аналогичен ключу -MD , но в список зависимостей попадает только сам исходный файл, и файлы, включаемые с помощью директивы #include "имя_файла"

Как видно из таблицы компилятор может работать двумя способами - в одном случае компилятор выдает только список зависимостей и заканчивает работу (опции -M и -MM ). В другом случае компиляция происходит как обычно, только в дополнении к объектному файлу генерируется еще и файл зависимостей (опции -MD и -MMD ). Я предпочитаю использовать второй вариант - он мне кажется более удобным и экономичным потому что:

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

Из двух возможных опций -MD и -MMD , я предпочитаю первую потому что:

  • С помощью директивы #include <имя_файла> я часто включаю не только "стандартные", но и свои собственные заголовочные файлы, которые могут иногда меняться (например, заголовочные файлы моей прикладной библиотеки LIB ).
  • Иногда бывает полезно взглянуть на полный список включаемых в модуль заголовочных файлов, в том числе и "стандартных".

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

Include $(wildcard *.d)

Обратите внимание на использование функции wildcard . Конструкция

Include *.d

будет правильно работать только в том случае, если в каталоге будет находиться хотя бы один файл с расширением ".d ". Если таких файлов нет, то make аварийно завершится, так как потерпит неудачу при попытке "построить" эти файлы (у нее ведь нет на этот счет ни каких инструкций!). Если же использовать функцию wildcard , то при отсутствии искомых файлов, эта функция просто вернет пустую строку. Далее, директива include с аргументом в виде пустой строки, будет проигнорирована, не вызывая ошибки. Теперь можно составить новый вариант make-файла для моего "гипотетического" проекта:

  • example_3-auto_depend /
    • main.cpp
    • main.h
    • Editor.cpp
    • Editor.h
    • TextLine.cpp
    • TextLine.h
    • Makefile

Вот как выглядит Makefile из этого примера:

# # example_3-auto_depend/Makefile # # Пример автоматического построения зависимостей от заголовочных файлов # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c -MD $< include $(wildcard *.d)

После завершения работы make директория проекта будет выглядеть так:

  • example_3-auto_depend /
    • iEdit
    • main.cpp
    • main.h
    • main.o
    • main.d
    • Editor.cpp
    • Editor.o
    • Editor.d
    • Editor.h
    • TextLine.cpp
    • TextLine.o
    • TextLine.d
    • TextLine.h
    • Makefile

Файлы с расширением ".d " - это сгенерированные компилятором GCC файлы зависимостей. Вот, например, как выглядит файл Editor.d , в котором перечислены зависимости для файла Editor.cpp

Editor.o: Editor.cpp Editor.h TextLine.h

Теперь при изменении любого из файлов - Editor.cpp , Editor.h или TextLine.h , файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o .

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

Получается что для "текущей" сборки используются файлы зависимостей, сгенерированные во время "предыдущей" сборки. Как правило, это не вызывает проблем. Сложности возникнут лишь в том случае, если какой-нибудь из заголовочных файлом по какой-либо причине прекратил свое существование. Рассмотрим простой пример. Предположим, у меня имеются файлы main.cpp и main.h :

Файл main.cpp :

#include "main.h" void main() { }

Файл main.h :

// main.h

В таком случае, сформированный компилятором файл зависимостей main.d будет выглядеть так:

Main.o: main.cpp main.h

Теперь, если я переименую файл main.h в main_2.h , и соответствующим образом изменю файл main.cpp

Файл main.cpp :

#include "main_2.h" void main() { }

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

Выходом в этой ситуации может служить удаление файла зависимостей main.d . Тогда сборка проекта пройдет нормально и будет создана новая версия этого файла, ссылающаяся уже на заголовочный файл main_2.h :

Main.o: main.cpp main_2.h

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

1.5. "Разнесение" файлов с исходными текстами по директориям

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

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

осталось работоспособным, я использую переменную VPATH , в которой перечисляются все директории, где могут располагаться исходные тексты. В следующем примере я поместил файлы Editor.cpp и Editor.h в каталог Editor , а файлы TextLine.cpp и TextLine.h в каталог TextLine :

  • example_4-multidir /
    • main.cpp
    • main.h
    • Editor /
      • Editor.cpp
      • Editor.h
    • TextLine /
      • TextLine.cpp
      • TextLine.h
    • Makefile

Вот как выглядит Makefile для этого примера:

# # example_4-multidir/Makefile # # Пример "разнесения" исходных текстов по разным директориям # source_dirs:= . Editor TextLine search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH:= $(source_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I,$(source_dirs)) $< include $(wildcard *.d)

По сравнению с предыдущим вариантом make-файла он претерпел следующие изменения:

  • Для хранения списка директорий с исходными текстами я завел отдельную переменную source_dirs , поскольку этот список понадобится указывать в нескольких местах.
  • Шаблон поиска для функции wildcard (переменная search_wildcards ) строится "динамически" исходя из списка директорий source_dirs
  • Используется переменная VPATH для того, чтобы шаблонное правило могло искать файлы исходных текстов в указанном списке директорий
  • Компилятору разрешается искать заголовочные файлы во всех директориях с исходными текстами. Для этого используется функция addprefix и флажок -I компилятора GCC .
  • При формировании списка объектных файлов, из имен исходных файлов "убирается" имя каталога, где они расположены (с помощью функции notdir )

1.6. Сборка программы с разными параметрами компиляции

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

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

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

  • example_5-multiconfig /
    • main.cpp
    • main.h
    • Editor /
      • Editor.cpp
      • Editor.h
    • TextLine /
      • TextLine.cpp
      • TextLine.h
    • Makefile
    • make_debug
    • make_release

Файлы make_debug и make_release - это командные файлы, используемые для сборки соответственно отладочной и рабочей версий программы. Вот, например, как выглядит командный файл make_release

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

Обратите внимание, что строка со значением переменной compile_flags заключена в кавычки, так как она содержит пробелы. Командный файл make_debug выглядит аналогично:

Make compile_flags="-O0 -g"

Вот как выглядит Makefile для этого примера:

# # example_5-multiconfig/Makefile # # Пример получения нескольких версий программы с помощью одного make-файла # source_dirs:= . Editor TextLine search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) override compile_flags += -pipe iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH:= $(source_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I,$(source_dirs)) $(compile_flags) $< include $(wildcard *.d)

Переменная compile_flags получает свое значение из командной строки и, далее, используется при компиляции исходных текстов. Для ускорения работы компилятора, к параметрам компиляции добавляется флажок -pipe . Обратите внимание на необходимость использования директивы override для изменения переменной compile_flags внутри make-файла.

1.7. "Разнесение" разных версий программы по отдельным директориям

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

Для решения этой проблемы я помещаю результаты компиляции каждой версии программы в свой отдельный каталог. Так, например, отладочная версия программы (включая все объектные файлы) помещается в каталог debug , а рабочая версия программы - в каталог release :

    • debug /
    • release /
    • main.cpp
    • main.h
    • Editor /
      • Editor.cpp
      • Editor.h
    • TextLine /
      • TextLine.cpp
      • TextLine.h
    • Makefile
    • make_debug
    • make_release

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

Вот, например, как выглядит командный файл make_release , собирающий рабочую версию программы (результаты компиляции помещается в каталог release ):

Mkdir release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=release \ --makefile=../Makefile

Команда mkdir введена для удобства - если удалить каталог release , то при следующей сборке он будет создан заново. В случае "составного" имени каталога (например, bin/release ) можно дополнительно использовать флажок -p . Флажок --directory заставляет make перед началом работы сделать указанную директорию release текущей. Флажок --makefile укажет программе make , где находится make-файл проекта. По отношению к "текущей" директории release , он будет располагаться в "родительском" каталоге.

Командный файл для сборки отладочного варианта программы (make_debug ) выглядит аналогично. Различие только в имени директории, куда помещаются результаты компиляции (debug ) и другом наборе флагов компиляции:

Mkdir debug make compile_flags="-O0 -g" \ --directory=debug \ --makefile=../Makefile

Вот окончательная версия make-файла для сборки "гипотетического" проекта текстового редактора:

# # example_6-multiconfig-multidir/Makefile # # Пример "разнесения" разных версий программы по отдельным директориям # program_name:= iEdit source_dirs:= . Editor TextLine source_dirs:= $(addprefix ../,$(source_dirs)) search_wildcards:= $(addsuffix /*.cpp,$(source_dirs)) $(program_name):$(notdir$(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH:= $(source_dirs) %.o: %.cpp gcc -c -MD $(compile_flags) $(addprefix -I,$(source_dirs)) $< include $(wildcard *.d)

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

После запуска командных файлов make_debug и make_release директория с последним примером выглядит так:

  • example_6-multiconfig-multidir /
    • debug /
      • iEdit
      • main.o
      • main.d
      • Editor.o
      • Editor.d
      • TextLine.o
      • TextLine.d
    • release /
      • iEdit
      • main.o
      • main.d
      • Editor.o
      • Editor.d
      • TextLine.o
      • TextLine.d
    • main.cpp
    • main.h
    • Editor /
      • Editor.cpp
      • Editor.h
    • TextLine /
      • TextLine.cpp
      • TextLine.h
    • makefile
    • make_debug
    • make_release

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

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

2. GNU Make

В этой главе я кратко опишу некоторые возможности программы GNU Make , которыми я пользуюсь при написании своих make-файлов, а также укажу на ее отличия от "традиционных" версий 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_wildcards:= $(addsuffix /*.cpp, $(source_dirs)) all: @echo $(search_wildcards)

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

Editor/*.cpp TextLine/*.cpp

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

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

  • wildcard_example /
    • Editor /
      • Editor.cpp
    • TextLine /
      • TextLine.cpp
    • makefile

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

Search_wildcards:= Editor/*.cpp TextLine/*.cpp source_files:= $(wildcard $(search_wildcards)) 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 VPAT

Написание makefile иногда становится головной болью. Однако, если разобраться, все становится на свои места, и написать мощнейший makefile длиной в 40 строк для сколь угодно большого проекта получается быстро и элегантно.

Внимание! Предполагаются базовые знания утилиты GNU make.

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

Пусть для включения заголовочных файлов в исходниках используется что-то типа #include , то есть каталог project/include делается стандартным при компиляции.

После сборки надо, чтобы получилось так:

То есть, в каталоге bin лежат рабочая (application) и отладочная (application_debug) версии, в подкаталогах Release и Debug каталога project/obj повторяется структура каталога project/src с соответствующими исходниками объектных файлов, из которых и компонуется содержимое каталога bin.

Чтобы достичь данного эффекта, создаем в каталоге project файл Makefile следующего содержания:

  1. root_include_dir:= include
  2. root_source_dir:= src
  3. source_subdirs:= . dir1 dir2
  4. compile_flags:= -Wall -MD -pipe
  5. link_flags:= -s -pipe
  6. libraries:= -ldl
  7. relative_include_dirs:= $(addprefix ../ ../ , $(root_include_dir) )
  8. relative_source_dirs:= $(addprefix ../ ../ $(root_source_dir) / , $(source_subdirs) )
  9. objects_dirs:= $(addprefix $(root_source_dir) / , $(source_subdirs) )
  10. objects:= $(patsubst ../ ../% , % , $(wildcard $(addsuffix /* .c* , $(relative_source_dirs) ) ) )
  11. objects:= $(objects:.cpp=.o)
  12. objects:= $(objects:.c=.o)
  13. all: $(program_name)
  14. $(program_name) : obj_dirs $(objects)
  15. g++ -o $@ $(objects) $(link_flags) $(libraries)
  16. obj_dirs:
  17. mkdir -p $(objects_dirs)
  18. VPATH:= ../ ../
  19. % .o: % .cpp
  20. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  21. % .o: % .c
  22. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  23. .PHONY: clean
  24. clean:
  25. rm -rf bin obj
  26. include $(wildcard $(addsuffix /* .d, $(objects_dirs) ) )

В чистом виде такой makefile полезен только для достижения цели clean, что приведет к удалению каталогов bin и obj.
Добавим еще один сценарий с именем Release для сборки рабочей версии:

Mkdir -p bin mkdir -p obj mkdir -p obj/Release make --directory=./obj/Release --makefile=../../Makefile build_flags="-O2 -fomit-frame-pointer" program_name=../../bin/application

И еще один сценарий Debug для сборки отладочной версии:

Mkdir -p bin mkdir -p obj mkdir -p obj/Debug make --directory=./obj/Debug --makefile=../../Makefile build_flags="-O0 -g3 -D_DEBUG" program_name=../../bin/application_debug

Именно вызов одного из них соберет наш проект в рабочем, либо отладочном варианте. А теперь, обо всем по-порядку.

Допустим, надо собрать отладочную версию. Переходим в каталог project и вызываем./Debug. В первых трех строках создаются каталоги. В четвертой строке утилите make сообщается, что текущим каталогом при запуске надо сделать project/obj/Debug, относительно этого далее передается путь к makefile и задаются две константы: build_flags (тут перечисляются важные для отладочной версии флаги компиляции) и program_name (для отладочной версии – это application_debug).

1: Объявляется переменная с именем корневого каталога заголовочных файлов.

2: Объявляется переменная с именем корневого каталога исходников.

3: Объявляются переменная с именами подкаталогов корневого каталога исходников.

4: Объявляется переменная с общими флагами компиляции. -MD заставляет компилятор сгенерировать к каждому исходнику одноименный файл зависимостей с расширением.d. Каждый такой файл выглядит как правило, где целью является имя исходника, а зависимостями – все исходники и заголовочные файлы, которые он включает директивой #include. Флаг -pipe заставляет компилятор пользоваться IPC вместо файловой системы, что несколько ускоряет компиляцию.

5: Объявляется переменная с общими флагами компоновки. -s заставляет компоновщик удалить из результирующего ELF файла секции.symtab, .strtab и еще кучу секций с именами типа.debug*, что значительно уменшает его размер. В целях более качественно отладки этот ключ можно убрать.

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

8: Объявляется переменная, содержащая относительные имена каталогов со стандартными заголовочными файлами. Потом такие имена напрямую передаются компилятору, предваряемые ключем -I. Для нашего случая получится../../include, потому что такой каталог у нас один. Функция addprefix добавляет свой первый аргумент ко всем целям, которые задает второй аргумент.

9: Объявляется переменная, содержащая относительные имена всех подкаталогов корневого каталога исходников. В итоге получим: ../../src/. ../../src/dir1 ../../src/dir1.

10: Объявляется переменная, содержащая имена подкаталогов каталога project/obj/Debug/src относительно текущего project/obj/Debug. То есть, этим мы перечисляем копию структуры каталога project/src. В итоге получим: /src/dir1 src/dir2.

11: Объявляется переменная, содержащая имена исходников, найденных на основе одноименных файлов *.c* (.cpp\.c), безотносительно текущего каталога. Смотрим поэтапно: результатом addsuffix будет../../src/./*.с* ../../src/dir1/*.с* ../../src/dir2/*.с*. Функция wildcard развернет шаблоны со звездочками до реальных имен файлов: ../../src/./main.сpp ../../src/dir1/file1.с../../src/dir1/file2.сpp ../../src/dir2/file3.с../../src/dir2/file4.с. Функция patsubsb уберет префикс../../ у имен файлов (она заменяет шаблон, заданный первым аргументом на шаблон во втором аргументе, а % обозначает любое количество символов). В итоге получим: src/./main.сpp src/dir1/file1.с src/dir1/file2.сpp src/dir2/file3.с src/dir2/file4.с.

12: В переменной с именами исходников расширения.cpp заменяется на.o.

13: В переменной с именами исходников расширения.c заменяется на.o.

15: Первое объявленное правило – его цель становится целью всего проекта. Зависимостью является константа, содержащая имя программы (../../bin/application_debug мы ее передали при запуске make из сценария).

17: Описание ключевой цели. Зависимоcти тоже очевидны: наличие созданных подкаталого в project/obj/Debug, повторяющих структуру каталога project/src и множество объектных файлов в них.

18: Описано действие по компоновке объектных файлов в целевой.

20: Правило, в котором цель – каталог project/obj/Debug/src и его подкаталоги.

21: Действие по достижению цели – создать соответствующие каталоги src/., src/dir1 и src/dir2. Ключ -p утилиты mkdir игнорирует ошибку, если при создании какого-либо каталога, таковой уже существуют.

23: Переменная VPATH принимает значение../../. Это необходимо для шаблонов нижеследующих правил.

25: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на.o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.cpp (то есть имена которых оканчиваются на.cpp). При этом под одноименностью понимается не только точное совпадение, но также если имя зависимости предварено содержимым переменной VPATH. Например, имена src/dir1/file2 и../../src/dir1/file2 совпадут, так как VPATH содержит../../.

26: Вызов компилятора для превращения исходника на языке С++ в объектный файл.

28: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на.o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.c (то есть имена которых оканчиваются на.c). Одноименность как в строке 23.

29: Вызов компилятора для превращения исходника на языке С в объектный файл.

31: Некоторая цель clean объявлена абстрактной. Достижение абстрактной цели происходит всегда и не зависит от существования одноименного файла.

32: Объявление абстрактной цели clean.

33: Действие по ее достижению заключается в уничтожении каталогов project/bin и project/obj со всем их содержимым.

36: Включение содержимого всех файлов зависимостей (с расширением.d), находящихся в подкаталогах текущего каталога. Данное действие утилита make делает в начале разбора makefile. Однако, файлы зависимостей создаются только послекомпиляции. Значит, при первой сборке ни один такой файл включен не будет. Но это не страшно. Цель включения этих файлов – вызвать перекомпиляцию исходников, зависящих от модифицированного заголовочного файла. При второй и последующих сборках утилита make будет включать правила, описанные во всех файлах зависимостей, и, при необходимости, достигать все цели, зависимые от модифицированного заголовочного файла.