Меню Рубрики

Gnu make utility for windows

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

(C) Владимир Игнатов, 2000

Оглавление

Оглавление

0. Предисловие

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Файлы с расширением «.d» — это сгенерированные компилятором GCC файлы зависимостей. Вот, например, как выглядит файл Editor.d, в котором перечислены зависимости для файла Editor.cpp: Теперь при изменении любого из файлов — Editor.cpp, Editor.h или TextLine.h, файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • example_6-multiconfig-multidir /
    • debug /
    • release /
    • main.cpp
    • main.h
    • Editor /
      • Editor.cpp
      • Editor.h
    • TextLine /
      • TextLine.cpp
      • TextLine.h
    • Makefile
    • make_debug
    • make_release

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

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

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

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

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

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

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

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

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

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

2. GNU 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 поддерживает также и второй, новый способ задания переменной — с помощью оператора ‘:=‘: В этом случае переменная работает подобно «обычным» текстовым переменным в каком-нибудь из языков программирования. Вот приблизительный аналог этого выражения на языке C++: Значение переменной вычисляется в момент обработки оператора присваивания. Если, например, записать то при обработке такого make-файла на экран будет выдана строка «one two».

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

  • Gnu grub не загружает windows
  • Gnu grub как установить windows
  • Gnu fortran windows 10
  • Gnu core utilities windows
  • Gnss data interface windows 10