PHP Delphi CSS HTML JavaScript Perl API ASP MySQL XML С++ VBasic WEB разработка *NIX CouchDB Hack Python
Главная Статьи С++ Эффективное использование GNU Make
Главная
 Главная  Контакты
 
Программинг
Статьи Книги ЧаВО
 
xBOOKi
Fresh Books Операционки Сети
 
Поиск
-------
 
Counters
Яндекс цитирования
Rambler's Top100
-------
 
CryptDisk.4h
Программа которая позволяет создать виртуальный шифрованный логический диск.

cryptdisk.4hack.com

-------
 
 

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

(C) Владимир Игнатов, 2000
Версия 1.2.1b

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

В этой книге я описываю свой опыт работы с утилитой 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. Если это не так, то прочтите сначала главу 2 - "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 выглядит так:

    
    #
    #   example_1-traditional/Makefile
    #
    #   Пример "традиционного" строения make-файла 
    #

    iEdit: main.o Editor.o TextLine.o 
        gcc $^ -o $@

    .cpp.o:
        gcc -c $<

    main.o:     main.h Editor.h TextLine.h
    Editor.o:   Editor.h TextLine.h
    TextLine.o: TextLine.h 

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

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

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

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

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

 

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

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

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

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

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

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

    #
    #   example_2-auto_obj/Makefile
    #
    #   Пример автоматического построения списка объектных файлов
    #

    iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
        gcc $^ -o $@ 

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

    main.o:     main.h Editor.h TextLine.h
    Editor.o:   Editor.h TextLine.h
    TextLine.o: TextLine.h 

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

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

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

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

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

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

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

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

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

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

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

    include $(wildcard *.d) 

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

    include *.d 

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

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

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

    #
    #   example_3-auto_depend/Makefile
    # 
    #   Пример автоматического построения зависимостей от заголовочных файлов
    #

    iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
        gcc $^ -o $@ 

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

    include $(wildcard *.d) 

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

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

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

:

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

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

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

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

Файл main.cpp:

    #include "main.h"

    void main()
    {
    } 

Файл main.h:

    // main.h

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

    main.o: main.cpp main.h

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

,

Файл main.cpp:

    #include "main_2.h"

    void main()
    {
    } 

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

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

    main.o: main.cpp main_2.h

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

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

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

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

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

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

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

    #
    #   example_4-multidir/Makefile
    # 
    #   Пример "разнесения" исходных текстов по разным директориям
    #

    source_dirs := . Editor TextLine

    search_wildcards := $(addsuffix /*.cpp,$(source_dirs)) 

    iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards))))
        gcc $^ -o $@ 

    VPATH := $(source_dirs)
     
    %.o: %.cpp
        gcc -c -MD $(addprefix -I,$(source_dirs)) $<

    include $(wildcard *.d) 

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

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

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

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

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

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

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

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

:

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

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

    make  compile_flags="-O0 -g" 

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

    #
    #   example_5-multiconfig/Makefile
    # 
    #   Пример получения нескольких версий программы с помощью одного make-файла 
    #

    source_dirs := . Editor TextLine

    search_wildcards       := $(addsuffix /*.cpp,$(source_dirs)) 
    override compile_flags += -pipe

    iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards))))
        gcc $^ -o $@ 

    VPATH := $(source_dirs)
     
    %.o: %.cpp
        gcc -c -MD $(addprefix -I,$(source_dirs)) $(compile_flags) $<

    include $(wildcard *.d) 

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

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

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

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

  • 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
    make    compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \
          --directory=release \
          --makefile=../Makefile 

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

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

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

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

    #
#   example_6-multiconfig-multidir/Makefile
# 
#   Пример "разнесения" разных версий программы по отдельным директориям
#

program_name := iEdit 
source_dirs  := . Editor TextLine

source_dirs      := $(addprefix ../,$(source_dirs))
search_wildcards := $(addsuffix /*.cpp,$(source_dirs))

$(program_name):$(notdir$(patsubst %.cpp,%.o,$(wildcard $(search_wildcards))))
gcc $^ -o $@ 

VPATH := $(source_dirs) 

%.o: %.cpp
gcc -c -MD $(compile_flags) $(addprefix -I,$(source_dirs)) $<

include $(wildcard *.d) 

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

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

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

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

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

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

2. GNU Make

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

.

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

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

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

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

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

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

.

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

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

    var1 = one
    var2 = $(var1) two 
    var1 = three

    all: 
        @echo $(var2) 

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

    compile_flags = -pipe $(compile_flags)

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

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

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

:

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

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

    var1 := one
    var2 := $(var1) two 
    var1 := three

    all:
        @echo $(var2) 

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

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

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

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

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

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

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

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

    all:
        @echo $(src_dirs)

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

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

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

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

    source_dirs := Editor  TextLine
    search_wildcards := $(addsuffix /*.cpp, $(source_dirs))

    all:
        @echo $(search_wildcards)

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

    Editor/*.cpp  TextLine/*.cpp 

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

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

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

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

    search_wildcards := Editor/*.cpp  TextLine/*.cpp
    source_files := $(wildcard $(search_wildcards))

    all:
        @echo $(source_files)

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

    Editor/Editor.cpp  TextLine/TextLine.cpp 

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

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

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

    all:
        @echo $(source_files)

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

    Editor.cpp TextLine.cpp 

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

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

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

    all:
        @echo $(object_files) 

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

    Editor.o  TextLine.o 

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

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

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

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

    all:
        @echo $(object_files)

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

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

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

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

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

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

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

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

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

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

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

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

Переменная VPATH описывается в главе "VPATH: Search Path for All Dependencies" руководства по GNU Make. На страничке Paul D. Smith есть статья под названием "How Not to Use VPAT



Свежее
Резервное копирование rsync-ом
DNS Amplification (DNS усиление)
Алгоритм Шинглов — поиск нечетких дубликатов текста
Metasploit Framework. Обзор
Использование CouchDB
-------



 
Copyright © 2003-2009   Frikazoid.
Rambler's Top100