Сборка программы с помощью GNU Make. Введение в make

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Include $(wildcard *.d)

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

Include *.d

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

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

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

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

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

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

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

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

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

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

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

Файл main.cpp :

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

Файл main.h :

// main.h

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

Main.o: main.cpp main.h

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

Файл main.cpp :

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

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

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

Main.o: main.cpp main_2.h

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Make compile_flags="-O0 -g"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2. GNU Make

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

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

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

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

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

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

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

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

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

Var1 = one var2 = $(var1) two var1 = three all: @echo $(var2)

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

Compile_flags = -pipe $(compile_flags)

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

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

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

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

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

Var1:= one var2:= $(var1) two var1:= three all: @echo $(var2)

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

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

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

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

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

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

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

Src_dirs:= Editor TextLine src_dirs:= $(addprefix ../../, $(src_dirs)) all: @echo $(src_dirs)

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

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

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

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

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

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

Editor/*.cpp TextLine/*.cpp

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

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

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

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

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

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

Editor/Editor.cpp TextLine/TextLine.cpp

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

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

Source_files:= Editor/Editor.cpp TextLine/TextLine.cpp source_files:= $(notdir $(source_files)) all: @echo $(source_files)

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

Editor.cpp TextLine.cpp

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

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

Source_files:= Editor.cpp TextLine.cpp object_files:= $(patsubst %.cpp, %.o, $(source_files)) all: @echo $(object_files)

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

Editor.o TextLine.o

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

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

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

Source_files:= Editor.cpp TextLine.cpp object_files:= $(source_files:.cpp=.o) all: @echo $(object_files)

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

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

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

Cpp.o: gcc $^ -o $@

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

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

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

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

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

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

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

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

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

У вас, вероятно, появился вопрос: можно ли не компилировать эти файлы по отдельности, а собрать сразу всю программу одной командой? Можно.

gcc calculate.c main.c -o kalkul -lm

Вы скажете, что это удобно? Удобно для нашей программы, потому что она состоит всего из двух c-файлов. Однако профессиональная программа может состоять из нескольких десятков таких файлов. Каждый раз набирать названия их всех в одной строке было бы делом чрезмерно утомительным. Но есть возможность решить эту проблему. Названия всех исходных файлов и все команды для сборки программы можно поместить в отдельный текстовый файл. А потом считывать их оттуда одной короткой командой.

Давайте создадим такой текстовый файл и воспользуемся им. В каталоге проекта kalkul2 удалите все файлы, кроме calculate.h, calculate.c, main.c. Затем создайте в этом же каталоге новый файл, назовите его Makefile (без расширений). Поместите туда следующий текст.

Kalkul: calculate.o main.o gcc calculate.o main.o -o kalkul -lm calculate.o: calculate.c calculate.h gcc -c calculate.c main.o: main.c calculate.h gcc -c main.c clean: rm -f kalkul calculate.o main.o install: cp kalkul /usr/local/bin/kalkul uninstall: rm -f /usr/local/bin/kalkul

Обратите внимание на строки, введённые с отступом от левого края. Этот отступ получен с помощью клавиши Tab. Только так его и надо делать! Если будете использовать клавишу «Пробел», команды не будут исполняться.

Затем дадим команду, состоящую всего из одного слова:

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

Makefile является списком правил. Каждое правило начинается с указателя, называемого «Цель». После него стоит двоеточие, а далее через пробел указываются зависимости. В нашем случае ясно, что конечный файл kalkul зависит от объектных файлов calculate.o и main.o. Поэтому они должны быть собраны прежде сборки kalkul. После зависимостей пишутся команды. Каждая команда должна находиться на отдельной строке, и отделяться от начала строки клавишей Tab. Структура правила Makefile может быть очень сложной. Там могут присутствовать переменные, конструкции ветвления, цикла. Этот вопрос требует отдельного подробного изучения.

Если мы посмотрим на три первых правила, то они нам хорошо понятны. Там те же самые команды, которыми мы уже пользовались. А что же означают правила clean, install и uninstall?

В правиле clean стоит команда rm, удаляющая исполняемый и объектные файлы. Флаг -f означает, что, если удаляемый файл отсутствует, программа должна это проигнорировать, не выдавая никаких сообщений. Итак, правило clean предназначено для «очистки» проекта, приведения его к такому состоянию, в каком он был до команды make.

Запустите

Появились объектные файлы и исполняемый. Теперь

Объектные и исполняемый файлы исчезли. Остались только c-файлы, h-файл и сам Makefile. То есть, проект «очистился» от результатов команды make.

Правило install помещает исполняемый файл в каталог /usr/local/bin - стандартный каталог размещения пользовательских программ. Это значит, что её можно будет вызывать из любого места простым набором её имени. Но помещать что-либо в этот каталог можно только, зайдя в систему под «суперпользователем». Для этого надо дать команду su и набрать пароль «суперпользователя». В противном случае система укажет, что вам отказано в доступе. Выход из «суперпользователя» осуществляется командой exit. Итак,

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

Можете открыть каталог /usr/local/bin. Там должен появиться файл с названием kalkul.

Давайте теперь «уберём за собой», не будем засорять систему.

Посмотритекаталог /usr/local/bin. Файл kalkul исчез. Итак, правило uninstall удаляет программу из системного каталога.

Дмитрий Пантелеичев (dimanix2006 at rambler dot ru) - Make-файлы

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

Введение

Допустим, вы разрабатываете некую программу под названием foo , состоящую из пяти заголовочных файлов -- 1.h , 2.h , 3.h , 4.h и -- 5.h , и шести файлов с исходным текстом программы на языке С - 1.cpp , 2.cpp , 3.cpp , 4.cpp , 5.cpp и main.cpp . (Хочу заметить, что в реальных проектах следует избегать подобного стиля именования файлов).

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

Существует ли решение проблемы?

Не стоит беспокоиться, друзья мои! Эта проблема уже давно решена. Опытными программистами была разработана утилита make . Вместо того, чтобы производить повторную компиляцию всех файлов с исходными текстами, она обрабатывает только те файлы, которые претерпели изменения. В нашем случае будет скомпилирован только один файл - 2.cpp . Разве это не здорово!?

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

Для чего нужен Makefile?

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

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

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

Make -f 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 , объявляющая "ложную" цель. Например:

PHONY: clean

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

Переменные

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

$VAR_NAME=value

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

$OBJECTS=main.o test.o

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

$(VAR_NAME)

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

TOPDIR=/home/tedi/project SRCDIR=$(TOPDIR)/src

При обращении к переменной 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.

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

Утилита make

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

Утилита make доступна для разных ОС, и из-за особенностей выполнения наряду с «родной» реализацией во многих ОС присутствует GNU реализация gmake , и поведение этих реализаций в некоторых ОС, например, Solaris может существенно отличаться. Поэтому в сценариях сборки рекомендуется указывать имя конкретной утилиты. В ОС Linux эти два имени являются синонимами, реализованными через символическую ссылку, как показано ниже:

$ ls -l /usr/bin/*make lrwxrwxrwx 1 root root 4 Окт 28 2008 /usr/bin/gmake -> make -rwxr-xr-x 1 root root 162652 Май 25 2008 /usr/bin/make ... $ make --version GNU Make 3.81 ...

По умолчанию имя файла сценария сборки - Makefile . Утилита make обеспечивает полную сборку указанной цели , присутствующей в сценарии, например:

$ make $ make clean

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

$ make -f Makefile.my

Простейший файл Makefile состоит из синтаксических конструкций двух типов: целей и макроопределений. Описание цели состоит из трех частей: имени цели, списка зависимостей и списка команд интерпретатора оболочки, требуемых для построения цели. Имя цели - непустой список файлов, которые предполагается создать. Список зависимостей - список файлов, в зависимости от которых строится цель. Имя цели и список зависимостей составляют заголовок цели, записываются в одну строку и разделяются двоеточием (":"). Список команд записывается со следующей строки, причем все команды начинаются с обязательного символа табуляции . Многие текстовые редакторы могут быть настроены таким образом, чтобы заменять символы табуляции пробелами. Этот факт стоит учесть и проверить, что редактор, в котором редактируется Makefile , не замещает табуляции пробелами, так как подобная проблема встречается довольно часто. Любая строка в последовательности списка команд, не начинающаяся с табуляции (ещё одна команда) или символа "# " (комментарий) - считается завершением текущей цели и началом новой.

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

$ make -p >make.suffix make: *** Не заданы цели и не найден make-файл. Останов. $ cat make.suffix # GNU Make 3.81 # Copyright (C) 2006 Free Software Foundation, Inc. ... # База данных Make, напечатана Thu Apr 14 14:48:51 2011 ... CC = cc LD = ld AR = ar CXX = g++ COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c COMPILE.C = $(COMPILE.cc) ... SUFFIXES:= .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l .s .S .mod .sym \ .def .h .info .dvi .tex .texinfo .texi .txinfo .w .ch... # Implicit Rules ... %.o: %.c # команды, которые следует выполнить (встроенные): $(COMPILE.c) $(OUTPUT_OPTION) $< ...

Значения всех этих переменных: CC , LD , AR , EXTRA_CFLAGS , ... могут использоваться файлом сценария как неявные определения со значениями по умолчанию. Кроме этого, можно определить и собственные правила обработки по умолчанию для выбранных суффиксов (расширений файловых имён), как это показано на примере выше для исходных файлов кода на языке С: %.c.

Большинство интегрированных сред разработки (IDE) или пакетов для создания переносимых инсталляций (например, automake или autoconf) ставят своей задачей создание файла Makefile для утилиты make .

Как ускорить сборку make

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

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

$ man make ... -j , --jobs[=jobs] Specifies the number of jobs (commands) to run simultaneously. ...

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

$ pwd /usr/src/ntp-4.2.6p3

Сначала запустим сборку на 4-х ядерном процессоре Atom (не очень быстрая модель с частотой 1.66Ghz) но с очень быстрым твердотельным диском SSD:

$ cat /proc/cpuinfo | head -n10 processor: 0 vendor_id: GenuineIntel cpu family: 6 model: 28 model name: Intel(R) Atom(TM) CPU 330 @ 1.60GHz stepping: 2 cpu MHz: 1596.331 cache size: 512 KB $ make clean # запускаем сборку в четыре потока $ time make -j4 ... real 1m5.023s user 2m40.270s sys 0m16.809s $ make clean # запускаем сборку в стандартном режиме без параллелизма $ time make ... real 2m6.534s user 1m56.119s sys 0m12.193s $ make clean # запускаем сборку с автоматическим выбранным уровнем параллелизма $ time make -j ... real 1m5.708s user 2m43.230s sys 0m16.301s

Как можно заметить, использование параллелизма (явное или не явное) позволяет ускорить сборку почти в два раза – 1 минута против 2-ух. Выполним сборку этого же проекта на более быстром 2-х ядерном процессоре, но с достаточно медленным обычным диском HDD:

$ cat /proc/cpuinfo | head -n10 processor: 0 vendor_id: GenuineIntel cpu family: 6 model: 23 model name: Pentium(R) Dual-Core CPU E6600 @ 3.06GHz stepping: 10 cpu MHz: 3066.000 cache size: 2048 KB ... $ time make ... real 0m31.591s user 0m21.794s sys 0m4.303s $ time make -j2 ... real 0m23.629s user 0m21.013s sys 0m3.278s

Хотя итоговая скорость сборки и выросла в 3-4 раза, но улучшение от числа процессоров составляет только порядка 20%, так как «слабым звеном» здесь является медленный накопитель, допускающий задержку при записи большого числа мелких.obj файлов проекта.

Примечание : Хотелось бы напомнить, что не всякая сборка make , которая успешно выполняется на одном процессоре (как это имеет место по умолчанию или при указании -j1 ), будет также успешно выполняться при большем числе задействованных процессоров. Это связано с нарушениями синхронизации операций в случаях сложных сборок. Самым наглядным примером такой сборки, завершающейся с ошибкой в случае параллельного исполнения, является сборка ядра Linux для некоторых версий ядра. Возможность параллельного выполнения make нужно экспериментально проверять для собираемого проекта. Но в большинстве случаев это возможность может использоваться и позволяет в разы ускорить процесс сборки!

Если данный способ ускорения процесса сборки основан на том, что сейчас подавляющее большинство систем являются многопроцессорными (многоядерными), то следующий способ использует тот факт, что объём памяти RAM современных компьютеров (2-4-8 ГБ) значительно превышает объём памяти, необходимый для компиляции программного кода. В таком случае, компиляцию, основным сдерживающим фактором для которой является создание множества объектных файлов, можно перенести в область специального созданного диска (RAM диск, tmpfs ), расположенного в памяти:

$ free total used free shared buffers cached Mem: 4124164 1516980 2607184 0 248060 715964 -/+ buffers/cache: 552956 3571208 Swap: 4606972 0 4606972 $ df -m | grep tmp tmpfs 2014 1 2014 1% /dev/shm

Теперь можно временно перенести файлы собираемого проекта в tmpfs (мы по-прежнему используем NTP-сервер из предыдущего примера), в каталог /dev/shm :

$ pwd /dev/shm/ntp-4.2.6p3 $ make -j ... real 0m4.081s user 0m1.710s sys 0m1.149s

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

Этот способ ускорения можно применить к сборке ядра Linux, для которого, как уже было сказано, параллельная сборка не работает. Чтобы воспользоваться преимуществами RAM-памяти, скопируем дерево исходных кодов ядра в каталог /dev/shm :

$ pwd /dev/shm/linux-2.6.35.i686 $ time make bzImage ... HOSTCC arch/x86/boot/tools/build BUILD arch/x86/boot/bzImage Root device is (8, 1) Setup is 13052 bytes (padded to 13312 bytes). System is 3604 kB CRC 418921f4 Kernel: arch/x86/boot/bzImage is ready (#1) real 9m23.986s user 7m4.826s sys 1m18.529s

Как видно, сборка ядра Linux заняла менее 10 минут, что является необычайно хорошим результатом.

В качестве вывода, можно посоветовать тщательно оптимизировать условия сборки проекта под используемое для этого оборудование, и, учитывая, что в процессе отладки сборка выполняется сотни раз, то можно сэкономить множество времени!

Сборка модулей ядра

Частным случаем сборки приложений является сборка модулей ядра Linux (драйверов). Начиная с версий ядра 2.6, для сборки модуля составляется Makefile , построенный на использовании макросов, и нам остаётся только записать (для файла собственного кода с именем mod_params.c ), следующий шаблон для сборки модулей:

Листинг 1. Makefile для сборки модулей ядра
CURRENT = $(shell uname -r) KDIR = /lib/modules/$(CURRENT)/build PWD = $(shell pwd) TARGET = mod_params obj-m:= $(TARGET).o default: $(MAKE) -C $(KDIR) M=$(PWD) modules ... $ make make -C /lib/modules/2.6.18-92.el5/build \ M=examples/modules-done_1/hello_printk modules make: Entering directory `/usr/src/kernels/2.6.18-92.el5-i686" CC [M] /examples/modules-done_1/hello_printk/hello_printk.o Building modules, stage 2. MODPOST CC /examples/modules-done_1/hello_printk/hello_printk.mod.o LD [M] examples/modules-done_1/hello_printk/hello_printk.ko make: Leaving directory `/usr/src/kernels/2.6.18-92.el5-i686" $ ls -l *.o *.ko -rw-rw-r-- 1 olej olej 74391 Мар 19 15:58 hello_printk.ko -rw-rw-r-- 1 olej olej 42180 Мар 19 15:58 hello_printk.mod.o -rw-rw-r-- 1 olej olej 33388 Мар 19 15:58 hello_printk.o $ file hello_printk.ko hello_printk.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ /sbin/modinfo hello_printk.ko filename: hello_printk.ko author: Oleg Tsiliuric license: GPL srcversion: 83915F228EC39FFCBAF99FD depends: vermagic: 2.6.18-92.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1

Заключение

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

В следующей статье мы начнём знакомство с библиотеками API, присутствующими в POSIX системах.

make

Утилита автоматически определяет, какие части большой программы должны быть перекомпилированы и команды для их перекомпиляции. Наиболее часто 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 . Второй вариант не генерирует зависимости от системных заголовочных файлов. В КОМАНДАХ можно использовать автоматические переменные. Эти переменные имеют значения, заново вычисленные для каждого выполняемого правила на основе цели и зависимостей правила.

Автоматическая переменная Назначение
$@ Имя файла цели правила. В шаблонном правиле с несколькими целями,имя той цели, которая вызвала выполнение команд правила.
$< Имя первой зависимости. Если цель получила свои команды из неявного правила, то это будет первая зависимость, добавленная неявным правилом.
$? Имена всех зависимостей, которые являются более новыми, чем цель, с пробелами между ними.
$^ Имена всех зависимостей, с пробелами между ними. Если Вы для цели неоднократно укажете одну и ту же зависимость, значение переменной "$^ " будет содержать только одну копию ее имени.
$+ Эта переменная аналогична переменной "$^ ", только зависимости, указанные неоднократно дублируются в том порядке, в котором они указаны в make-файле . Это в первую очередь полезно для использования в командах компоновки, где является существенным повторение имен библиотек в определенном порядке
$* База с которой сопоставляется неявное правило (см. ниже). В шаблонном правиле база представляет собой часть имени файла, которая сопоставляется символу "% " в шаблоне цели. Если целью является файл "dir/a.foo.b ", а шаблон цели - "a.%.b ", то базой будет "dir/foo ". База полезна для создания имен файлов, связанных с правилом. В явных правилах база не определена как имя файла без расширения,если такое расширение можно выделить. Не рекомендуется использовать эту переменную в явных правилах

Неявные правила определены для многих языков программирования и применяются в соответствии с расширением исходного файла. По умолчанию список расширений такой: .out, .a, .ln, .o, .c, .cc, .C, cpp, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch, .web, .sh, .elc, .el . При использовании неявных правил используются переменные, переопределяя которые можно управлять процессом преобразования файлов, например, указывать нестандартный компилятор или передавать ему опции.

Пример MakeFile

Пример makefile

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

#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
Для сокращения записи можно использовать переменные и действия по умолчанию (неявные правила)


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