Эффективное использование gnu make


Содержание

Эффективное использование gnu make

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

Создание программы частенько начинается с маленького однофайлового проекта. Проходит некоторое время и проект, как снежный ком, начинает обрастать файлами, заголовками, подключаемыми библиотеками, требуемыми опциями компиляции. и для его сборки становится уже недостаточным сказать » gcc -o file file.c «. Когда же, через пару дней, однажды набранная магическая строчка, содержащая все необходимые для сборки проекта параметры компилятора, таинственно исчезает в недрах истории вашего командного интерпретатора, рождается естественное желание увековечить свои знания в виде, к примеру, шелл скрипта. Затем, возможно, захочется сделать этот скрипт управляемым параметрами, чтобы его можно было использовать для разных целей. Однако, чудо юникса состоит в том, что если вам что-то понадобилось, значит кто-нибудь это уже сделал, и пришло время вспомнить о существовании команды make .

Рассмотрим несложную программу на C . Пусть программа prog состоит из пары файлов кода main.c и supp.c и используемого в каждом из них файла заголовков defs.h . Соответственно, для создания prog необходимо из пар ( main.c defs.h ) и ( supp.c defs.h ) создать объектные файлы main.o и supp.o , а затем слинковать их в prog . При сборке вручную, выйдет что-то вроде:

Если мы в последствии изменим defs.h , нам понадобится полная перекомпиляция, а если изменим supp.c, то повторную компиляцию main.о можно и не выполнять. Казалось бы, если для каждого файла, который мы должны получить в процессе компиляции указать, на основе каких файлов и с помощью какой команды он создается, то пригодилась бы программа, которая во-первых, собирает из этой информации правильную последовательность команд для получения требуемых результирующих файлов и, во-вторых, инициирует создание требуемого файла только в случае, если такого файла не существует, или он старше, чем файлы от которых он зависит. Это именно то, что делает команда make ! Всю информацию о проекте make черпает из файла Makefile , который обычно находится в том же каталоге, что и исходные файлы проекта.

Простейший Makefile состоит из синтаксических конструкций всего двух типов: целей и макроопределений .

Цель в Makefile — это файл(ы), построение которого предполагается в процессе компиляции проекта. Описание цели состоит из трех частей: имени цели, списка зависимостей и списка команд интерпретатора sh, требуемых для построения цели. Имя цели — непустой список файлов, которые предполагается создать. Список зависимостей — список файлов, из которых строится цель. Имя цели и список зависимостей составляют заголовок цели, записываются в одну строку и разделяются двоеточием. Список команд записывается со следующей строки, причем все команды начинаются с обязательного символа табуляции. Возможна многострочная запись заголовка или команд через применение символа «\» для экранирования конца строки. При вызове команды make , если ее аргументом явно не указана цель, будет обрабатываться первая найденная в Makefile цель, имя которой не начинается с символа «.». Примером для простого Makefile может послужить уже упоминавшаяся программа prog:

В прведенном примере можно заметить ряд особенностей: в имени второй цели указаны два файла и для этой же цели не указана команда компиляции, кроме того, нигде явно не указана зависимость объектных файлов от » *.c «-файлов. Дело в том, что команда make имеет предопределенные правила для получения файлов с определенными суффиксами. Так, для цели — объектного файла (суффикс » .o «) при обнаружении соответствующего файла с суффиксом » .c «, будет вызван компилятор » сс -с » с указанием в параметрах этого » .c «-файла и всех файлов — зависимостей. Более того, в этом случае явно не указанные » .c «-файлы make самостоятельно внесет в список зависимостей и будет реагировать их изменение так же, как и для явно указанных зависимостей. Впрочем, ничто не мешает указать для данной цели альтернативную команду компиляции.

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

Макроопределение имеет вид » ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ «. ЗНАЧЕНИЕ может являться произвольной последовательностью символов, включая пробелы и обращения к значениям уже определенных переменных. В дальнейшем, в любом месте Makefile , где встретится обращение к переменной-макроопределению, вместо нее будет подставлено ее текущее значение. Обращение к значению переменной в любом месте Makefile выглядит как $(ПЕРЕМЕННАЯ) (скобки обязательны, если имя переменной длиннее одного символа). Значение еще не определенных переменных — пустая строка. С учетом сказанного, можно преобразовать наш Makefile :

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

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

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

Обычно Makefile пишется так, чтобы простой запуск make приводил к компиляции проекта, однако, помимо компиляции, Makefile может использоваться и для выполнения других вспомогательных действий, напрямую не связанных с созданием каких-либо файлов. К таким действиям относится очистка проекта от всех результатов компиляции, или вызов процедуры инсталляции проекта в системе. Для выполнения подобных действий в Makefile могут быть указаны дополнительные цели, обращение к которым будет осуществляться указанием их имени аргументом вызова make (например, » make install «). Подобные вспомогательные цели носят название фальшивых , что связанно с отсутствием в проекте файлов, соответствующих их именам. Фальшивая цель может содержать список зависимостей и должна содержать список команд для исполнения. Поскольку фальшивая цель не имеет соответствующего файла в проекте, при каждом обращении к ней make будет пытаться ее построить. Однако, возможно возникновение конфликтной ситуации, когда в каталоге проекта окажется файл с именем, соответствующим имени фальшивой цели. Если для данного имени не определены файловые зависимости, он будет всегда считаться актуальным (up to date) и цель выполняться не будет. Для предотвращения таких ситуаций make поддерживает «встроенную» переменную » .PHONY «, которой можно присвоить список имен целей, которые всегда должны считаться фальшивыми .

Теперь можно привести пример полного Makefile , пригодного для работы с проектом prog и принять во внимание некоторые часто применяемые приемы:

Итак, у нас появились три фальшивых цели: all , clean и install . Цель all обычно используется как псевдоним для сборки сложного проекта, содержащего несколько результирующих файлов (исполняемых, разделяемых библиотек, страниц документации и т.п.). Цель clean используется для полной очистки каталога проекта от результатов компиляции и «мусора» — резервных файлов, создаваемых текстовыми редакторами (они обычно заканчиваются символом «

«). Цель install используется для инсталляции проекта в операционной системе (приведенный пример расчитан на установку только исполняемых файлов). Следует отметить повсеместное использование макроопределений — помимо всего, этот прием повышает читабельность. Обратите также внимание на определение переменной $(CC) — это встроенная переменная make и она неявно «сработает» и при компиляции объектных файлов.

Изложенный материал охватывает далеко не все способности make . Например, в тексте Makefile можно применять команды условного выполнения и разнообразные функции для манипуляции со строками. Make поддерживает большой набор встроенных переменных, а также метапеременные , принимающие разные значения в зависимости от контекста применения. Например, $* соответствует имени целевого файла без суффиксов, а $^ — полному списку зависимостей для данной цели.

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

Введение в make

Назначение, история, варианты

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

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

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

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

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

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

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

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

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

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

Запуск make

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

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

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

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

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

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

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

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

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

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

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

Этот фрагмент определяет, что файл 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 может быть несколько, обычно определяют одну и прописывают туда все соответствующие цели.

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

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

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

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

Переменные

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Функции

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

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

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

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

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

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

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

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

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


Теперь можно написать:

Рекурсивный make

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

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

Вызов make из Makefile часто называют submake . Для вызова используется переменная $(MAKE) :

Значение этой переменной соответствует пути к программе make , обрабатывающей текущий файл.

Для передачи переменных в вызываемый таким образом файл их необходимо явно экспортировать:

Значение переменной PREFIX будет доступно в subsystem/Makefile .

Параллельный make

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

Параллельное выполнение может быть запрещено с помощью специальной цели .NOTPARALLEL .

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

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

Варианты использования

Чаще всего о make говорят в контексте сборки программ на C/C++, в конце концов, для этого он изначально предназначался. Однако, make ­— гораздо более универсальный инструмент. Записывая makefile, мы декларативно описываем определенное состояние отношений между файлами, которое каждый запуск make будет стараться поддерживать. Декларативный характер определения состояния очень удобен, в случае использования какого-либо императивного языка (например, shell) нам приходилось бы выполнять большое количество различных проверок, получая на выходе сложный и запутанный код.

Кроме того, использование зависимостей между phony targets, позволяющии, по сути, декларативно описывать некий (ограниченный) конечный автомат, может быть полезно для написания различных административных сценариев. Используя make в качестве каркаса для выполнения различных shell-команд, мы получаем по сути некий базовый framework для shell с готовым пользовательским интерфейсом (вызов make + передача переменных), встроенными средствами отслеживания зависимостей, параллельного выполнения, макроопределениями и т.д.

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

Конспект по GNU Make

Привет, я штудировал доки по Make (уже второй раз за несколько лет) и пожалел о том, что в прошлый раз не составил небольшой конспект. Оставлю здесь на случай, когда потребуется освежить память, может и ещё кто сочтёт полезным.

Кто первый встал, того и тапки.
У кого больше популярность, того в дистрибутиве и оставят (а другого — нет).
Ты сперва напиши.

Ну я не претендую на известность и что меня куда-нибудь включат как emacs, например. Пусть это будет мелкая софтинка, которую я дам Васе и Пете, как софтинка выживет в их системе, когда её спокойно перезапишут какие-нибудь большие и важные xserver’ы (о которых я могу не знать в момент распространения программы, вот напишут что-то завтра)?
Я понимаю, что можно поставить в /opt, но что я должен сделать, чтобы иметь возможность спокойно устанавливаться в /usr/local?

Каждый проект должен писать какие-то проверки на конфликты имён?

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

Если так, то делает ли это автоматом autoconf+automake?

Там install используется, который, вроде, всегда перезаписывает существующий файл.

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

Ребята, ведь пакетный менеджер кидает всё в /usr, за уникальностью имён/совместимостью софта следят те, кто поддерживает дистрибутив, с этим всё понятно, вопросов нет. Я же про /usr/local, т.е. то место, где пользователь ручками устанавливает софт, который качает откуда угодно. Кто там следит за уникальностью, мне не понятно. В целом ничего сложного то нет, например: make DESTDIR=tmpdir install, дальше проверка уникальности tmpdir/. и /usr/local/. если всё хорошо, то копируем. Но занимается ли этим autotools. Если нет, то это надо делать самому, я то справлюсь, думаю, на справятся ли с этим простые пользователи, которым я дам свою софтинку? Очевидно, что нет.

за уникальностью имён/совместимостью софта следят те, кто поддерживает дистрибутив

Пакетный менеджер знает списки файлов и может их проверять.

Я же сказал, что он просто вызывает install , т.е. не занимается.

Это не проблема, так как конфликт возникает редко, да и в /usr/local мало чего обычно имеется. У autotools есть: И можно изменить имена, чтобы убрать конфликты у исполняемых файлов.

Есть Meson,а вы тут все еще Make пользуетесь

Пакетный менеджер знает списки файлов и может их проверять.

Разве пакетный менеджер суётся в usr/local ? Если только собрать пакет для дистрибутива, но я ведь не про такой случай, а про стандартную установку make install.

Это не проблема, так как конфликт возникает редко, да и в /usr/local мало >>чего обычно имеется.

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

Да, я такое встречал, но это требует достаточно высокой квалификации пользователя, чтобы во-первых понять, что конфликт есть, а во-вторых справится с configure. Это не решение, когда его осилит 1 из 10 человек.

Разве пакетный менеджер суётся в usr/local ?

Если собирать пакет, то да. А их собирать обычно нетрудно и рекомендуется это делать.

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

Если Вася первую ставил, то он наверное заметит, что что-то не то произошло.

И кол-во таких малоизвестных софтин неограничено.

А количество с одинаковыми именами, которые будут установлены на одну и ту же машину, пренебрежимо мало (имён много, установок мало).

это требует достаточно высокой квалификации пользователя

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

Вроде придумал довольно дешёвый способ, который может помочь ловить совпадения имён. Но есть условие: Makefile должен быть написан правильно, придерживаясь договорённостей. Например, имеем цель install:

make INSTALL_PROGRAM=’cp -i’ INSTALL_DATA=’cp -i’ install

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

/ud/test/q/build $ make INSTALL_PROGRAM=’cp -i’ install
cp -i main /usr/local/bin
cp: недоступен для записи ‘/usr/local/bin/main’ (права 0755, rwxr-xr-x); попробовать всё равно?

Конечно лучше написать какую-нибудь спец утилиту. Может такая уже есть?

.DELETE_ON_ERRORS: и MAKEILE_LIST забыл

лучше написать какую-нибудь спец утилиту

и распространять её вместе с основным продуктом? Ну хз, на вкус и цвет, конечно. А вообще, перед установкой проверяй все конфликты и, в случае наличия таковых, выход с ошибкой без внесения изменений в систему. Как уже выше писали — все случаи не предвидеть, с некоторыми придётся разбираться кому-то другому. К тому же, если хочешь сделать идеально, нужна ещё и деинсталляция. И вот здесь совпадения отследить, чтобы не удалить чужое. ну ты понел:)

и распространять её вместе с основным продуктом

на это я уже не надеюсь, если autotools из коробки не отслеживают совпадения, то как это сделать не очень продвинутому пользователю я не знаю. Эти изыскания для сохранения собственной системы в нормальной форме. Странно, но у сp, install, mv нет ключа —pretend, было бы удобно.

как это сделать не очень продвинутому пользователю я не знаю

Копипастить не надо, я не проверял. Но как один из вариантов

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

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

Такой скрипт намного шустрее:

Кстати, тут говорили, что совпадения имён — это же так маловероятно. Даже в рамках своего теста, сравнив свой проект с /usr нашлись совпадения имён. Ну понятно, что ставил бы в /usr/local, но всё же, могу ведь и из исходников пол системы собрать.

Т.е. не стоит того, чтобы напрягаться.

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

чтобы небыло конфликтов имён с другим софтом при make install

Не твоя задача такие вещи определять. Да и мейк инсталл лучше не юзать. Разве что у тебя LFS :D

Не твоя задача такие вещи определять.

Посмеялся. Где-нибудь в сети, никому неизвестный товарищ, выложил свою софтину, которая имеет конфликты с другими не более известными поделками (ну ведь может сложиться так, что нужно поставить софт, не из репозитория дистрибутива. Да это подраздел «Development», в конце то концов, здесь свой софт пишут). Наверное Линус Торвальдс сразу же свяжется с этим товарищем и скажет: «ты зачем так назвал файлы в своей софтине? Переименуй немедленно».

Да и мейк инсталл лучше не юзать.

Если человек не умеет пользоваться, то да. Но тогда и вилкой лучше не кушать, мало ли чего. Если всё по уму делать — отслеживать конфликты имён перед установкой (простейший скрипт выше), сохранять исходники в /usr/local/src, то никаких проблем не будет, даже если это кривой Makefile с отсутствующей целью uninstall.

illumium.org

Продвинутое применение GNU Make в web-разработке

kayo — Ср, 21/09/2011 — 14:38

Давно не писал ничего в блог, показалось, надо бы это исправить. Итак, сегодня, мой читатель, как ты уже, наверно, догадался, у нас временно поменяется стилистика изложения, и разговор пойдёт снова о чудесах GNU Makefile.

Задача

Мои проекты, где используется кошмарный JavaScript и кошмарные CSS-ы, обычно состоят из нескольких подкаталогов, в каждом из которых лежат поименованные в духе разработчиков столь же великого, сколько и ужасного TinyMCE файлики вида *_src.js и *_src.css. Для уменьшения размера, их требуется компилить и сжимать в *.js и *.css соответственно, для чего уважаемые разработчики наразрабатывали уйму средств, однако мы должны использовать те, которые в данном конкретном случае более всего подходят или, накрайняк, есть в наличии. Ну как, мой дорогой читатель, парсер русского языка ещё не сломался? В общем, мне надоело каждый раз править Makefile для своих проектов и потому решил написать универсальный набор правил.

Решение

Нет, сегодня не первое апреля, и я вполне серьёзен. Файл правил написался как-то сам собой, не пришлось долго жевать сопли, наверно, это опыт. Я создал каталог Makepath, чтобы сваливать туда конфиги, которых, как вы все хорошо понимаете, будет по-минимуму. Там обязательно будет что-то типа common.mk, где зададим какие-то базовые настройки, ведь мало ли что нам придёт в голову. Также там обязательно будут настройки для ужималок/компиляторов, которые мы назовём хитрым бесхитростным образом, чтобы определить приоритетность в использовании, в виде 001- .mk. Ну и прямо сейчас давай же разберём по порядку: что, как и почему.

А теперь посмотрим, что у нас в Makepath.

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

Компилятор от Google для нас более предпочтителен для сжатия JS.

Как видим поделка Yahoo умеет не только JS.

Улучшаем решение

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

Вот посмотри, дорогой друг, что у меня получилось:

Вот собственно и всё, что мне хотелось получить. Теперь без заморочек можно юзать во всех проектах. Если тебе, мой уважаемый читатель, приведённый код кажется громоздким, запутанным и всё в этом роде, то оно гарантированно почти наверняка пройдёт после прочтения занимательного texi мануала по GNU Make (info make). И в подтверждение своих слов, напишу, как можно читать код.

Понимание

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

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

А может быть так (так удобно делать многострочные переменные):

Объявление целей и правил делается типо так:

Да-да перед каждой командой в правилах ставим ТАБ и никак иначе.

Сами цели могут быть шаблонными:


Тогда как конструкции типа define, ifeq, ifdef не вызывают почти никаких вопросов у способных хоть как-то понимать английский, выражения типа $(что-то-там ) часто недопонимаемы должным образом либо понимаемые не так. Так вот на этом и заострим своё внимание. Нет, конечно я не собираюсь пересказывать ман, однако кратко и просто объяснить могу.

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

Все функции встроены в интерпретатор, напрямую свою создать и вызвать нельзя. Вот какие функции мне довелось использовать в скрипте:

  • $(if условие,если-истинно,если-ложно) — проверка условия
  • $(foreach имя-перечислителя,набор слов,действие) — выполнение чего-то для каждого слова в наборе, в перечислитель записывается значение слова на каждом шаге.
  • $(wildcard шаблон) — выбор объектов по шаблону в текущем каталоге
  • $(filter-out фильтруемые-слова,набор) — очистка набора от фильтруемых слов
  • $(info Сообщение) — выдача информационного сообщения в консоль
  • $(error Описание-ошибки) — выдача описания ошибки и завершение выполнения
  • $(shell команды) — выполнение команд оболочки и возврат выдачи
  • $(addprefix префикс,набор) — добавление префикса к каждому слову из набора
  • $(addsuffix суффикс,набор) — добавление суффикса к каждому слову из набора
  • $(patsubst искомый-шаблон,шаблон-замены,набор) — в каждом слове набора выполняет замену по шаблону
  • $(call имя-переменной, параметр-1,параметр-2,…,параметр-n ) — обрабатывает значение переменной, по порядку заменяя все вхождения вида $(порядковый-номер) на значения параметров.
  • $(eval выражение) — вычисляет все вычисляемые выражения программы make, то есть выражения вида $(что-то-там) заменяются результатом вычисления, а $ $(что-то-там) и $ $ $ $(что-то-там) на $(что-то-там) и $ $(что-то-там) соответственно, ну то есть вы поняли ^_

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

  • $@ — цель
  • $* — изменяемая часть шаблона у шаблонных целей
  • $^ — набор зависимостей, требуемых для достижения цели, каждое значение может также быть целью
  • $

Make-файлы. Эффективное использование GNU Make

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

Прежде чем использовать make , необходимо создать файл, называемый make-файлом , который описывает отношения между файлами Вашей программы и содержит команды для обновления каждого файла. Обычно исполняемый файл зависит от объектных файлов, которые, в свою очередь, зависят от исходных файлов и файлов заголовков. Для имени make-файла рекомендуется название GNUmakefile , makefile или Makefile , причем поиск идет именно в указанном порядке. Если необходимо использовать нестандартное имя, то его можно передать явно через опцию -f .
Когда make-файл уже написан, достаточно выполнить в каталоге в котором он находится команду make . Простой make-файл состоит из правил(инструкций) следующего вида:

ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ.
ЦЕЛЬ. : ЗАВИСИМОСТЬ.
КОМАНДА 1
КОМАНДА 2
ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ.
ЦЕЛЬ. : ЗАВИСИМОСТЬ.
КОМАНДА 1
КОМАНДА 2

и т.д.

ЦЕЛЬ обычно представляет собой имя файла, генерируемого программой make ; примерами целей являются исполняемые или объектные файлы. Цель может также быть именем выполняемого действия, как, например, «clean «.
ЗАВИСИМОСТЬ — это файл, изменение которого служит признаком необходимости цели. Часто цель зависит от нескольких файлов.
КОМАНДА — это действие, которое выполняет make . Правило может иметь более чем одну команду — каждую на своей собственной строке. Важное замечание: необходимо начинать каждую строку, содержащую команды, с символа табуляции. Длинные строки разбиваются на несколько с использованием обратного слэша, за которым следует перевод строки. Знак диез # является началом комментария. Строка с # до конца игнорируется. Комментарии могут переноситься на несколько строк с помощью обратного слэша в конце строки.

Использование действий по умолчанию

#default target — file edit
edit: main.o kbd.o command.o display.o \
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

Main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
command.o: command.c defs.h command.h
cc -c command.c
display.o: display.c defs.h buffer.h
cc -c display.c
insert.o: insert.c defs.h buffer.h
cc -c insert.c
search.o: search.c defs.h buffer.h
cc -c search.c
files.o: files.c defs.h buffer.h command.h
cc -c files.c
utils.o: utils.c defs.h
cc -c utils.c
clean:
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

По умолчанию, make начинает с первого правила (не считая правил, имена целей у которых начинаются с «. «). Это называется главной целью по умолчанию. В нашем случае это правило edit . Если файл edit новее чем объектные файлы от которых он зависит, то ничего не произойдет. В противном случае, прежде чем make сможет полностью обработать это правило, он должен рекурсивно обработать правила для файлов, от которых зависит «edit «. Каждый из этих файлов обрабатывается в соответствии со своими собственным правилом. Перекомпиляция должна быть проведена, если исходный файл или любой из заголовочных файлов, упомянутых среди зависимостей, обновлен позднее, чем объектный файл, или если объектный файл не существует.
Правилу clean не соответствует никакого создаваемого файла и, соответственно, clean ни от чего не зависит и само не входит в список зависимостей. При запуске по умолчанию clean вызываться не будет. Для его выполнения необходимо явно указать цель при запуске make — make clean.
Для сокращения записи можно использовать переменные и действия по умолчанию (неявные правила)

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

Квадратные скобки означают необязательность присутствия данной части.
Цель — имя цели, которую надо выполнить.
Переменная =»abc» -переопределение переменных. Значения переменных введенных в командной строке имеют больший приоритет, чем определения в make-файле.
Опции:
-f file — явное задание имени make-файла , если задание опущено, то ищются файлы GNUmakefile , makefile или Makefile
-n ; — имитация действий без реального выполнения, служит для отладки
-t — изменение времени модификации цели без реального выполнения
-q — проверка на необходимость обновления цели без реального выполнения

Написание 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 [email protected] $(objects) $(link_flags) $(libraries)
  16. obj_dirs:
  17. mkdir -p $(objects_dirs)
  18. VPATH:= ../ ../
  19. % .o: % .cpp
  20. g++ -o [email protected] -c $

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

Прежде чем использовать make , необходимо создать файл, называемый make-файлом , который описывает отношения между файлами Вашей программы и содержит команды для обновления каждого файла. Обычно исполняемый файл зависит от объектных файлов, которые, в свою очередь, зависят от исходных файлов и файлов заголовков. Для имени make-файла рекомендуется название GNUmakefile , makefile или Makefile , причем поиск идет именно в указанном порядке. Если необходимо использовать нестандартное имя, то его можно передать явно через опцию -f .
Когда make-файл уже написан, достаточно выполнить в каталоге, где он находится, команду make . Простой make-файл состоит из правил (инструкций) следующего вида:

ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ. ЦЕЛЬ. : ЗАВИСИМОСТЬ. КОМАНДА 1
КОМАНДА 2 ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ. ЦЕЛЬ. : ЗАВИСИМОСТЬ. КОМАНДА 1 КОМАНДА 2
и т.д.

ЦЕЛЬ обычно представляет собой имя файла, генерируемого программой make . Примерами целей являются исполняемые или объектные файлы. Цель может также быть именем выполняемого действия, как, например, clean .
ЗАВИСИМОСТЬ — это файл, изменение которого служит признаком необходимости цели. Часто цель зависит от нескольких файлов. КОМАНДА — это действие, которое выполняет make . Правило может иметь более чем одну команду — каждую на своей собственной строке. Важное замечание: необходимо начинать каждую строку, содержащую команды, с символа табуляции. Длинные строки разбиваются на несколько с использованием обратного слэша, за которым следует перевод строки. Знак диез # является началом комментария. Строка с # до конца игнорируется. Комментарии могут переноситься на несколько строк с помощью обратного слэша в конце строки.

make [Опции] [Переменная=»abc»] [Цель]

Квадратные скобки означают необязательность присутствия данной части.
Цель — имя цели, которую надо выполнить.
Переменная =»abc» -переопределение переменных. Значения переменных, введенных в командной строке, имеют больший приоритет, чем определения в make-файле.
Опции:
-f file — явное задание имени make-файла , если задание опущено, то ищутся файлы GNUmakefile , makefile или Makefile .
-n — имитация действий без реального выполнения, служит для отладки.
-t — изменение времени модификации цели без реального выполнения.
-q — проверка на необходимость обновления цели без реального выполнения.

Более сложные способы применения MAKE

Порядок правил несущественен. По умолчанию главной целью make является цель первого правила в первом makeфайле . Если в первом правиле есть несколько целей, то только первая цель берется в качестве цели по умолчанию. Цель, начинающаяся с точки, не используется как цель по умолчанию, если она не содержит один или более символа «/» т.е. определяет путь к файлу; кроме того, по умолчанию не используются цели, определяющие шаблонные правила.
В качестве ЦЕЛИ или ЗАВИСИМОСТИ может использоваться список файлов через пробел или шаблон в стиле shell .
Шаблоны интерпретируются в момент выполнения правила, при присваивании переменным интерпретация шаблона не происходит, для присваивания списка файлов переменной используется специальная функция wildcard .

objects:= $(wildcard *.o
edit: *.o
cc -o edit *.o

Для автоматической генерации зависимостей от файлов заголовков в языке СИ можно использовать команду gcc -M file.c или gcc -MM file.c . Второй вариант не генерирует зависимости от системных заголовочных файлов. В КОМАНДАХ можно использовать автоматические переменные. Эти переменные имеют значения, заново вычисленные для каждого выполняемого правила на основе цели и зависимостей правила.

Автоматическая переменная Назначение
[email protected] Имя файла цели правила. В шаблонном правиле с несколькими целями,имя той цели, которая вызвала выполнение команд правила.
$ edit: main.o kbd.o command.o display.o \

cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
command.o: command.c defs.h command.h
cc -c command.c
display.o: display.c defs.h buffer.h
cc -c display.c
insert.o: insert.c defs.h buffer.h
cc -c insert.c
search.o: search.c defs.h buffer.h
cc -c search.c
files.o: files.c defs.h buffer.h command.h
cc -c files.c
utils.o: utils.c defs.h
cc -c utils.c
clean:
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

По умолчанию, make начинает с первого правила (не считая правил, имена целей у которых начинаются с «. «). Это называется главной целью по умолчанию. В нашем случае это правило edit . Если файл edit новее чем объектные файлы, от которых он зависит, то ничего не произойдет. В противном случае, прежде чем make сможет полностью обработать это правило, он должен рекурсивно обработать правила для файлов, от которых зависит edit . Каждый из этих файлов обрабатывается в соответствии со своим собственным правилом. Перекомпиляция должна быть проведена, если исходный файл или любой из заголовочных файлов, упомянутых среди зависимостей, обновлен позднее, чем объектный файл, или если объектный файл не существует.
Правилу clean не соответствует никакого создаваемого файла и, соответственно, clean ни от чего не зависит и само не входит в список зависимостей. При запуске по умолчанию clean вызываться не будет. Для его выполнения необходимо явно указать цель при запуске make: make clean
Для сокращения записи можно использовать переменные и действия по умолчанию (неявные правила)

insert.o search.o files.o utils.o

edit: $(objects)
cc -o edit $(objects)
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
display.o: defs.h buffer.h
insert.o: defs.h buffer.h
search.o: defs.h buffer.h
files.o: defs.h buffer.h command.h
utils.o: defs.h
.PHONY: clean
clean:
-rm edit $(objects)

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

file.c: file.o cc -c file.c

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

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit: $(objects)
cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o: command.h
display.o insert.o search.o files.o: buffer.h

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

Утилита 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 . Поддерживаются также несколько альтернативных имен по умолчанию, но имеет смысл использовать наиболее распространенное.

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

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

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

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

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

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

$ make build PREFIX =/ usr/ local

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

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

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

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

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

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

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

Target: dep1 dep2 . command1 command2 .

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.

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

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

Printhome: echo $$ HOME

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

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

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

будет использована кодировка 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 поддерживает набор автоматических переменных, облегчающих написание правил. Например, переменная [email protected] соответствую текущей цели (то, что слева от:), а переменная $^ — списку зависимостей (то, что справа от:). Таким образом, например, можно написать:

Www/ js/ script. js: src/ js/ jquery. js src/ js/ plugin1. js src/ js/ plugin2. js cat $^ > [email protected]

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

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

$ «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:

Смотреть что такое «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 . Поддерживаются также несколько альтернативных имен по умолчанию, но имеет смысл использовать наиболее распространенное.

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

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

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

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

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

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

$ make build PREFIX =/ usr/ local

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

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

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

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

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

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

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

Target: dep1 dep2 . command1 command2 .

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.

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

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

Printhome: echo $$ HOME

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

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

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

будет использована кодировка 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 поддерживает набор автоматических переменных, облегчающих написание правил. Например, переменная [email protected] соответствую текущей цели (то, что слева от:), а переменная $^ — списку зависимостей (то, что справа от:). Таким образом, например, можно написать:

Www/ js/ script. js: src/ js/ jquery. js src/ js/ plugin1. js src/ js/ plugin2. js cat $^ > [email protected]

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

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

$ «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 ) $ \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 сравнивает дату и время изменения ЦЕЛИ и объектов о которых зависит цель. Если объекты от которых зависит цель были изменены позже чем создана цель, то будет выполнено ДЕЙСТВИЕ. ДЕЙСТВИЕ так же выполняется если ЦЕЛЬ не является именем файла или директории.

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

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

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

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-файл построен «традиционным» способом. Все исходные файлы собираемой программы находятся в одном каталоге:

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

# # example_1-traditional/Makefile # # Пример «традиционного» строения make-файла # iEdit: main.o Editor.o TextLine.o gcc $^ -o [email protected] .cpp.o: gcc -c $ и #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 . Конструкция

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

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

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

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

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

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

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

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

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

Файлы 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)) overr >

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

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

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

Для решения этой проблемы я помещаю результаты компиляции каждой версии программы в свой отдельный каталог. Так, например, отладочная версия программы (включая все объектные файлы) помещается в каталог debug , а рабочая версия программы — в каталог 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-файла для сборки «гипотетического» проекта текстового редактора:

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

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

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

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


2. GNU Make

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

GNU Make — это версия программы make распространяемая Фондом Свободного Программного Обеспечения (Free Software FoundationFSF ) в рамках проекта 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)

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

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

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

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

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

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

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

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

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

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

Видно, что шаблоны преобразованы в списки файлов. Функция 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)

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

Видно, что из имен файлов убраны «пути» к этим файлам. Функция 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)

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

Видно, что во всех словах окончание «.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 » в данном случае).

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

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

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

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

VPATH:= Editor TextLine %.o: %.cpp gcc -c $ , то есть каталог 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 [email protected] $(objects) $(link_flags) $(libraries)
  16. obj_dirs:
  17. mkdir -p $(objects_dirs)
  18. VPATH:= ../ ../
  19. % .o: % .cpp
  20. g++ -o [email protected] -c $

Linux создание makefile. Эффективное использование GNU Make. Две разновидности переменных

Что такое Makefile? Makefile — это сценарий для утилиты make . Эта утилита помогает автоматизировать процесс компиляции проекта (проектов):

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

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

make -f prjmake.nmk

Makefile состоит из нескольких основных частей:

  • правила;
  • директивы;
  • переменные;
  • комментарии.

Начнем рассмотрение в обратном порядке списка — от простого к сложному.

Комментарии.
Комментарий обозначается символом «#». Если нужно использовать символ «#» в другом контексте, то необходимо добавить обратный слэш: «\#».

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

LIB = lib1.o lib2.o lib3.o

Название переменной задается в верхнем регистре (по негласному соглашению). Для получения параметров переменной используется смпользуются скобки и символ «
$(PATH)

Значение переменной может вычисляться рекурсивно:

Либо можно использовать другую форму записи:

Некоторые переменные являются стандартыми константами, для них нельзя вычислять значение рекусивно. Такими перменными являются: CC (имя компилятора С), СХХ (имя компилятора С++), CFLAGS (параметры С компилятора), CXXFLAGS (параметры С++ компилятора).

Помимо переменных, определенных «писателем» файла, Make-файл предполагает использование автоматических переменных, значения которых вычисляются в контексте использования таких переменных (вычисление основывается на цели и зависимости правила — о целях и зависимостях ниже):

  • [email protected] — имя цели;
  • [email protected] — имя цели, если его необходимо ввести в строке описания зависимости справа от двоеточия;
  • $? — имена всех зависимостей (с пробелами) которые новее, чем цель;
  • $^ — имена всез зависимостей с пробелами;
  • $* — имя текущего предусловия за вычетом суффикса;
  • $% — имя соответствующего.о файла, если текущей целью является файл библиотеки;
  • $** — только для nmake — внутри правила обозначаются все зависимости, оказавшиеся справа от двоеточия;
  • D — часть имени внутренних макроопределений, описывающая директорию файла (допустимые варианты применения: $(@D), $$(@D), $(
  • F — часть имени внутренних макоопределений, описывающая собственно имя файла (допустимые варианты прменения: $(@F), $$(@F), $(
  • B — часть имени внутренних макроопределений, описывающая базовое имя файла (без диска, директориии и расширения);
  • R — часть имени внутренних макроопределений, описывающая полный путь к файлу за вычетом расширения.

Директивы.
Директива — это команда для make с указанием, что делать в процессе чтения make-файла. Может выполнять такие действия:

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

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

Если фала не существует — выводиться ошибка. Для отключения сообщени об ошибке, к директиве добавлятеся префикс «-«:

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

foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
Определить значение перенной можно при помощи дериктив define. endef:

define two-lines
echo foo
echo $(bar)
endef
В этом случает значение переменной two-lines = echo foo; echo $(bar)

Правила.
Правила объясняют make , что и как нужно пересобирать. В общем виде структура выглядит так:

targets: prerequisites
command
.

Здесь targets — имена файлов-результатов, разделенные пробелами; так же может быть действие не связанное с процессом компиляции, например, clean. prerequisites — зависимости, то, от чего зависит создание targets. command — комманда, которая выполняется для получения targets; может быть не одна; перед командой обязательно ставиться табуляция.

Существуют несколько фиктивных целей:

  • all — выполнение работы по достижению всех частных целей, перечисленных ниже;
  • build — компиляция и сборка всех устаренших (невыполненных) целей/файлов;
  • clean — удаление всех файлов, кроме исходных;
  • docs — подготовка документации и размещение таких файлов в соответствующих системных директориях;
  • examples — компиляция и сборка примеров;
  • install — работа, по размещению всех готовых частей проекта в соответствующих директориях.

В именах файлов могут использоваться wildcards:

Пример использования правил:


# задаем цель — исполняемый файл mytest, зависимый от lib.o
mytest: lib.o
# задаем комманду, результатом которой будет mytest
$(CXX) mytest.cpp $^ -o [email protected]

# задаем цель — файл lib.o, зависимый от libimpl.h и libimpl.с
lib.o: libimpl*
# задаем комманду, результатом которой будет lib.o
$(CXX) -c $^ -o [email protected]

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

# все o-файлы зависят от cpp-файлов
.cpp.o:
gcc -c $^

Если бы в предыдущем примере mytest зависел от нескольких o-файлов, то шаблонное правило можно было бы записать так:

mytest: lib.o map.o vector.o
gcc $^ -o [email protected]

Некоторый ключи компилятора:
-I»path/to/include» — директория со списком хидеров.
-Wall-Werror — вывод варнингов.
-O1, -O2, -O3 — оптимизация.

Некоторые ключи сборки:
-llibrary — указывает линковшику использовать библиотеку library при сборке программы.
-s — не включает симольные таблицы и информацию о размещении функций в испольняемый файл. Использование этого ключа позволяет существенно ужать исполняемые файлы.
-L»path/to/libs» — директория с библиотеками.
-static — статическая компоновка библиотек.

В этой книге я описываю свой опыт работы с утилитой 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-файл построен «традиционным» способом. Все исходные файлы собираемой программы находятся в одном каталоге:

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

# # example_1-traditional/Makefile # # Пример «традиционного» строения make-файла # iEdit: main.o Editor.o TextLine.o gcc $^ -o [email protected] .cpp.o: gcc -c $ и #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 . Конструкция

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

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

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

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

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

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

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

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

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

Файлы 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)) overr >

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

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

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

Для решения этой проблемы я помещаю результаты компиляции каждой версии программы в свой отдельный каталог. Так, например, отладочная версия программы (включая все объектные файлы) помещается в каталог debug , а рабочая версия программы — в каталог 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-файла для сборки «гипотетического» проекта текстового редактора:

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

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

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

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

2. GNU Make

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

GNU Make — это версия программы make распространяемая Фондом Свободного Программного Обеспечения (Free Software FoundationFSF ) в рамках проекта 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)

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

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


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

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

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

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

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

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

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

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

Видно, что шаблоны преобразованы в списки файлов. Функция 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)

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

Видно, что из имен файлов убраны «пути» к этим файлам. Функция 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)

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

Видно, что во всех словах окончание «.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 » в данном случае).

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

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

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

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

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

Например, если свой makefile вы назвали bejo , то команда на сборку проекта будет выглядеть так:

Структура файла

Makefile содержит разделы для «целей» , зависимостей и правил (rules) сборки. Все это оформляется следующим образом: сначала указывается имя цели (обычно это имя исполняемого или объектного файла), после которого следует двоеточие, затем следуют имена зависимостей, т.е. файлов, необходимых для получения данной цели. И, наконец, следует список правил: т.е. команд, которые необходимо выполнить для получения указанной цели.

Простой пример структуры makefile»а:

Target: dependencies command command .

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

Пример Makefile.

Ниже приводится простой пример (номера строк добавлены для ясности).

1 client: conn.o 2g++ client.cpp conn.o -o client 3 conn.o: conn.cpp conn.h 4g++ -c conn.cpp -o conn.o

В этом примере строка, содержащая текст
client: conn.o ,
называется «строкой зависимостей», а строка
g++ client.cpp conn.o -o client
называется «правилом» и описывает действие, которое необходимо выполнить.

А теперь более подробно о примере, приведенном выше:

  • Задается цель — исполняемый файл client , который зависит от объектоного файла conn.o
  • Правило для сборки данной цели
  • В третьей строке задается цель conn.o и файлы, от которых она зависит — conn.cpp и conn.h .
  • В четвертой строке описывается действие по сборке цели conn.o .

Комментарии

Строки, начинающиеся с символа «#», являются комментариями

Ниже приводится пример makefile с комментариями:

1 # Создатьисполняемыйфайл «client» 2 client: conn.o 3g++ client.cpp conn.o -o client 4 5 # Создать объектный файл «conn.o» 6 conn.o: conn.cpp conn.h 7g++ -c conn.cpp -o conn.o

«Ложная» цель

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

Допустим в makefile имеется правило, которое не создает ничего, например:

Clean: rm *.o temp

Поскольку команда rm не создает файл с именем clean , то такого файла никогда не будет существовать и поэтому команда make clean всегда будет отрабатывать.

Однако, данное правило не будет работать, если в текущем каталоге будет существовать файл с именем clean . Поскольку цель clean не имеет зависимостей, то она никогда не будет считаться устаревшей и, соответственно, команда «rm *.o temp» никогда не будет выполнена. (при запуске make проверяет даты модификации целевого файла и тех файлов, от которых он зависит. И если цель оказывается «старше», то make выполняет соответствующие команды-правила — прим. ред.) Для устранения подобных проблем и предназначена специальная декларация .PHONY , объявляющая «ложную» цель. Например:

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

Переменные

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

В соответствии с соглашениями имена переменных задаются в верхнем регистре:

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

В makefile-ах существует два типа переменных: «упрощенно вычисляемые» и «рекурсивно вычисляемые» .

При обращении к переменной SRCDIR вы получите значение /home/tedi/project/src .

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

CC = gcc -o CC = $(CC) -O2

выльются в бесконечный цикл. Для разрешения этой проблемы следует использовать «упрощенно вычисляемые» переменные:

CC:= gcc -o CC += $(CC) -O2

Где символ «:=» создает переменную CC и присваивает ей значение «gcc -o». А символ «+=» добавляет «-O2» к значению переменной CC.

Заключение

Я надеюсь, что это краткое руководство содержит достаточно информации, чтобы начать создавать свои makefile. А за сим — успехов в работе.

Библиография

  • 1 GNU Make Documentation File, info make.
  • Kurt Wall, et.al., Linux Programming Unleashed(Программирование под Linux на оперативном просторе — прим. ред.) , 2001.

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

Сценарии Make описываются в т.н. файле проекта. Проектом называется совокупность файлов, зависящих друг от друга. Файл описания проекта перечисляет зависимости между файлами и задает команды для обновления зависимых файлов. Имя файла описания проекта задается опцией –f командной строки программы make и по умолчанию предполагается равным Makefile или makefile . Если имя файла проекта явно не задано, при запуске утилита ищет в текущем каталоге файл с указанными выше именами, и, если такой файл существует, выполняет команды из него.

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

Обычно программы на языках Си или Си++ представляют собой совокупность нескольких.c (.cpp) файлов с реализациями функций и.h файлов с прототипами функций и определениями типов данных. Как правило, каждому.c файлу соответствует.h файл с тем же именем.

Предположим, что разрабатываемая программа называется earth и состоит из файлов arthur.c, arthur.h, trillian.c, trillian.h, prosser.c, prosser.h.

Разработка программы ведется в POSIX-среде с использованием компилятора GCC.

Простейший способ скомпилировать программу — указать все исходные.c файлы в командной строке gcc:

Gcc arthur.c trillian.c prosser.c -o earth

Компилятор gcc выполнит все этапы компиляции исходных файлов программы и компоновку исполняемого файла earth. Обратите внимание, что в командной строке gcc указываются только.c файлы и никогда не указываются.h файлы.

Компиляция и компоновка при помощи перечисления всех исходных файлов в аргументах командной строки GCC допустима лишь для совсем простых программ. С ростом числа исходных файлов ситуация очень быстро становится неуправляемой. Кроме того, каждый раз все исходные файлы будут компилироваться от начала до конца, что в случае больших проектов занимает много времени. Поэтому обычно компиляция программы выолняется в два этапа: компиляция объектных файлов и компоновка исполняемой программы из объектных файлов. Каждому.c файлу теперь соответствует объектный файл, имя которого в POSIX-системах имеет суффикс.o. Таким образом, в рассматриваемом случае программа earth компонуется из объектных файлов arthur.o, trillian.o и prosser.o следующей командой:

Gcc arthur.o trillian.o prosser.o -o earth

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

Обратите внимание, что явно задавать имя выходного файла необязательно. Оно будет получено из имени компилируемого файла заменой суффикса.c на суффикс.o. Итак, для компиляции программы earth теперь необходимо выполнить четыре команды:

Gcc -c arthur.c gcc -c trillian.c gcc -c prosser.c gcc arthur.o trillian.o prosser.o -o earth

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

  • если изменение внесено в один файл, например, в файл prosser.c, нет необходимости перекомпилировать файлы trillian.o или arthur.o; достаточно перекомпилировать файл prosser.o, а затем выполнить компоновку программы earth;
  • компиляция объектных файлов arthur.o, trillian.o и prosser.o не зависит друг от друга, поэтому может выполняться параллельно на многопроцессорном (многоядерном) компьютере.

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

Файл A зависит от файла B, если для получения файла A необходимо выполнить некоторую команду над файлом B. Можно сказать, что в программе существует зависимость файла A от файла B. В нашем случае файл arthur.o зависит от файла arthur.c, а файл earth зависит от файлов arthur.o, trillian.o и prosser.o. Можно сказать, что файл earth транзитивно зависит от файла arthur.c. Зависимость файла A от файла B называется удовлетворенной , если:

  • все зависимости файла B от других файлов удовлетворены;
  • файл A существует в файловой системе;
  • файл A имеет дату последней модификации не раньше даты последней модификации файла B.

Если все зависимости файла A удовлетворены, то файл A не нуждается в перекомпиляции. В противном случае сначала удовлетворяются все зависимости файла B, а затем выполняется команда перекомпиляции файла A.


Например, если программа earth компилируется в первый раз, то в файловой системе не существует ни файла earth, ни объектных файлов arthur.o, trillian.o, prosser.o. Это значит, что зависимости файла earth от объектных файлов, а также зависимости объектных файлов от.c файлов не удовлетворены, то есть все они должны быть перекомпилированы. В результате в файловой системе появятся файлы arthur.o, trillian.o, prosser.o, даты последней модификации которых будут больше дат последней модификации соответствующих.c файлов (в предположении, что часы на компьютере идут правильно, и что в файловой системе нет файлов «из будущего»). Затем будет создан файл earth, дата последней модификации которого будет больше даты последней модификации объектных файлов.

В получившейся конфигурации все зависимости всех файлов друг от друга удовлетворены, и поэтому для компиляции программы earth не нужно выполнять никаких команд

Предположим теперь, что в процессе разработки был изменен файл prosser.c. Его время последнего изменения теперь больше времени последнего изменения файла prosser.o. Зависимость prosser.o от prosser.c становится неудовлетворенной, и, как следствие, зависимость earth от prosser.o также становится неудовлетворенной. Чтобы удовлетворить зависимости необходимо перекомпилировать файл prosser.o, а затем файл earth. Файлы arthur.o и trillian.o можно не трогать, так как зависимости этих файлов от соответствующих.c файлов удовлетворены. Такова общая идея работы программы make и, на самом деле, всех программ управления сборкой проекта: ant http://ant.apache.org/ , scons http://www.scons.org/ и др

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

Файл описания проекта может содержать описания переменных, описания зависимостей и описания команд, которые используются для компиляции. Каждый элемент файла описания проекта должен, как правило, располагаться на отдельной строке. Для размещения элемента описания проекта на нескольких строках используется символ продолжения \ точно так же, как в директивах препроцессора языка Си.

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

Использование переменной записывается в одной из двух форм:

$( ) или $ < >— Эти формы равнозначны.

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

Зависимости между компонентами определяются следующим образом:

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

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

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

Для программы earth простейший пример файла Makefile для компиляции проекта может иметь вид:

Earth: arthur.o trillian.o prosser.o gcc arthur.o trillian.o prosser.o -o earth arthur.o: arthur.c gcc -c arthur.c trillian.o: trillian.c gcc -c trillian.c prosser.o: prosser.c gcc -c prosser.c

Однако, в этом описании зависимостей не учтены.h файлы. Например, если файл arthur.h подключается в файлах arthur.c и trillian.c, то изменение файла arthur.h должно приводить к перекомпиляции как arthur.c, так и trillian.c. Получается, что.o файлы зависят не только от.c файлов, но и от.h файлов, которые включаются данными.c файлами непосредственно или косвенно. С учетом этого файл Makefile может приобрести следующий вид:

Earth: arthur.o trillian.o prosser.o gcc arthur.o trillian.o prosser.o -o earth arthur.o: arthur.c arthur.h gcc -c arthur.c trillian.o: trillian.c trillian.h arthur.h gcc -c trillian.c prosser.o: prosser.c prosser.h arthur.h gcc -c prosser.c

Первой в списке зависимостей обычно записывается «главная» зависимость, а затем записываются все остальные файлы-зависимости.

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

будет при необходимости перекомпилирован только файл prosser.o и те файлы, от которых он зависит, все прочие файлы затронуты не будут. Если в командной строке имя цели не указано, берется первая цель в файле. В нашем случае это будет цель earth.

Если придерживаться хорошего стиля написания Makefile, то каждый Makefile должен содержать как минимум два правила: all – основное правило, которое соответствует основному предназначению файла, и правило clean, которое предназначено для удаления всех рабочих файлов, создаваемых в процессе компиляции. В случае программы earth рабочими файлами можно считать сам исполняемый файл программы earth, а также все объектные файлы.

С учетом этих дополнений файл Makefile примет вид:

All: earth earth: arthur.o trillian.o prosser.o gcc arthur.o trillian.o prosser.o -o earth arthur.o: arthur.c arthur.h gcc -c arthur.c trillian.o: trillian.c trillian.h arthur.h gcc -c trillian.c prosser.o: prosser.c prosser.h arthur.h gcc -c prosser.c clean: rm -f earth *.o

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

Данный файл, безусловно, решает задачу автоматизации сборки программы earth. Теперь можно придать этому файлу более общий вид, чтобы в этот файл легче было вносить изменения.

Во-первых, можно параметризовать название используемого компилятора, а также предоставить возможность управлять параметрами командной строки компилятора. Для задания компилятора можно определить переменную CC , для задания опций командной командной строки компиляции объектных файлов — переменную CFLAGS , а для задания опций командной строки компоновки выходной программы — переменную LDFLAGS .

Получим следующий файл:

CC = gcc CFLAGS = -Wall -O2 LDFLAGS = -s all: earth earth: arthur.o trillian.o prosser.o $(CC) $(LDFLAGS) arthur.o trillian.o prosser.o -o earth arthur.o: arthur.c arthur.h $(CC) $(CFLAGS) -c arthur.c trillian.o: trillian.c trillian.h arthur.h $(CC) $(CFLAGS) -c trillian.c prosser.o: prosser.c prosser.h arthur.h $(CC) $(CFLAGS) -c prosser.c clean: rm -f earth *.o

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

Позволит для компиляции программы использовать не gcc, а Intel компилятор Си. Аналогично запуск

Make CFLAGS=»-g» LDFLAGS=»-g»

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

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

Файл deps.make зависит от всех.c и.h файлов, из которых собирается программа. Может показаться, что это правило не будет работать, так как в Makefile необходимо включить файл deps.make, для генерации которого необходимо выполнить Makefile, то есть возникает циклическая зависимость, однако GNU make умеет корректно обрабатывать такие ситуации.

Для того, чтобы не выписывать списки.c и.h файлов несколько раз, в начале Makefile можно определить переменные:

CFILES = arthur.c trillian.c prosser.c HFILES = arthur.h trillian.h prosser.h

Более того, список объектных файлов можно получать из списка.c файлов заменой суффикса.c на.o:

В итоге получили следующий Makefile:

CC = gcc CFLAGS = -Wall -O2 LDFLAGS = -s CFILES = arthur.c trillian.c prosser.c HFILES = arthur.h trillian.h prosser.h OBJECTS = $(CFILES:.c=.o) TARGET = earth all: $(TARGET) earth: $(OBJECTS) $(CC) $(LDFLAGS) $^ -o [email protected] include deps.make deps.make: $(CFILES) $(HFILES) gcc -MM $(CFILES) > deps.make clean: rm -f $(TARGET) *.o

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

Пример файла C++ проекта:

Для просмотра результирующих значений переменных полезно просматривать вывод команды: make -p

21.2. Сборочная утилита make

21.2. Сборочная утилита make

Если вы уже собирали прикладную программу из исходных кодов, то обратили внимание на стандартную последовательность команд: make; make install .

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

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

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

Цель — это метка для некоторой последовательности команд (например, install) или результирующий файл, который нужно «построить» — скомпилировать или скомпоновать.

Цели должны отделяться друг от друга хотя бы одной пустой строкой. Список необходимых файлов — это перечень файлов или других целей, которые нужны для достижения данной цели; он может быть и пустым.

Последовательность команд — это команды, которые нужно выполнить для достижения цели. Последовательность команд должна отделяться от начала строки символом табуляции, иначе вы получите ошибку «missing separator» (нет разделителя).

Make-файл может содержать комментарии — они начинаются символом #.

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

MODFLAGS:= -O3 -Wall -DLINUX -I$(PATH)

$(CC) $(MODFLAGS) -c proga.c

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

Формат запуска утилиты make:

make [-f файл] [ключи] [цель]

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

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

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

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

Листинг 21.2. Примерный make-файл

all: client server

$(CC) client.с -о client

$(CC) server.с -о server

Обычно при вызове утилиты make не нужно задавать никаких ключей. Но иногда использование ключей бывает очень кстати (таблица 21.1).

Ключи команды make Таблица 21.1

GNU make

Short Table of Contents

Table of Contents

GNU make

This file documents the GNU make utility, which determines automatically which pieces of a large program need to be recompiled, and issues the commands to recompile them.

This is Edition 0.74, last updated 21 May 2020, of The GNU Make Manual , for GNU make version 4.2.

Copyright © 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2020 Free Software Foundation, Inc.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being “A GNU Manual,” and with the Back-Cover Texts as in (a) below. A copy of the license is included in the section entitled “GNU Free Documentation License.”

(a) The FSF’s Back-Cover Text is: “You have the freedom to copy and modify this GNU manual. Buying copies from the FSF supports it in developing GNU and promoting software freedom.”

Эффективное использование gnu make

Дата и время публикации: 2020-03-17 11:30:00

Использование в простых проектах

1.Что такое GNU Make?

Утилита GNU Make является средством, позволяющим управлять генерацией исполняемых и иных файлов программы из исходных текстов (англ. source code ).

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

Сборка проекта uncurses (версии 0.2), пропущенного через сито GNU Make , проиллюстрированна на рисунке 1.1

На котором изображены файлы на входе и выходе утилиты GNU Make и цели Makefile, которые позволяют генерировать в конечном итоге исполняемые файлы w.select и m.echo , а так же файл дистрибьюции uncurses-0.2.tar.gz и его контрольную сумму.

2. Использование Makefile

В листинге 2.1 приведен Makefile проекта uncurses.

В строке 1,2 и 32 определяем переменные, в т.ч. определенные по умолчанию, такие как $(CC) , $(RM) . Переменая $(TAR) к такой относится, но не исключая того, что в будущем может ей стать, применяем условное выражение $(if) в формате, показанном в листинге 2.2

В котором проверяется переменная $(TEST) на предмет присвоения ей значения – в результате чего make повторно переназначит $(TEST) или $(нечто другое) , если установлено, что её значение отсутсвует.

В строках 5 и 7 определяем флаги компилятору GCC(1) и зависимости в виде заголовочных файлов, показанные ранее на рисунке 1.1

В строке 12 и 13 опредяляется общее правило сборки для каждого объектного файла (все файлы с расширением .o ) компилируется из файла исходного кода (все файлы с расширением .с , имеющие зависимость от заголовочных файлов, перечисленных ранее в переменной $(DEPS) , строке 7 листинга 2.1

В строке 19,20 и 22,23 определены правила для сборки целей m.echo и w.select , которые являются финальной стадией компоновки ранее собранных объектных файлов в соответсвующие исполняемые бинарные модули, описываемые $@ , и их линковку с библиотекой ncurses .

В строке 17, в .PHONY перечисляем цели, которые желательно сделать абстрактными для того, чтобы не был нарушен последовательный порядок их вызыва для цели all , которая определена в строке 15 листинга 2.1

В строках 28. 56 демонстрируется возможность использования SHELL для создания файла дистрибутива uncurses-0.2.tar.gz , показанный на рисунке 1.1

Сайт разработан в соответствии с рекомендациями консорциума W3C для языка разметки HTML5.

Илон Маск рекомендует:  Что такое код setmapperflags
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL