Написание makefile иногда становится головной болью. Однако, если разобраться, все становится на свои места, и написать мощнейший makefile длиной в 40 строк для сколь угодно большого проекта получается быстро и элегантно.
Внимание! Предполагаются базовые знания утилиты GNU make.
Имеем некий типичный абстрактный проект со следующей структурой каталогов:
Пусть для включения заголовочных файлов в исходниках используется что-то типа #include <dir1/file1.h>, то есть каталог project/include делается стандартным при компиляции.
После сборки надо, чтобы получилось так:
То есть, в каталоге bin лежат рабочая (application) и отладочная (application_debug) версии, в подкаталогах Release и Debug каталога project/obj повторяется структура каталога project/src с соответствующими исходниками объектных файлов, из которых и компонуется содержимое каталога bin.
Чтобы достичь данного эффекта, создаем в каталоге project файл Makefile следующего содержания:
В чистом виде такой makefile полезен только для достижения цели clean, что приведет к удалению каталогов bin и obj.
Добавим еще один сценарий с именем Release для сборки рабочей версии:
И еще один сценарий Debug для сборки отладочной версии:
Именно вызов одного из них соберет наш проект в рабочем, либо отладочном варианте. А теперь, обо всем по-порядку.
Допустим, надо собрать отладочную версию. Переходим в каталог project и вызываем ./Debug. В первых трех строках создаются каталоги. В четвертой строке утилите make сообщается, что текущим каталогом при запуске надо сделать project/obj/Debug, относительно этого далее передается путь к makefile и задаются две константы: build_flags (тут перечисляются важные для отладочной версии флаги компиляции) и program_name (для отладочной версии – это application_debug).
Далее, в игру вступает GNU make. Прокомментируем каждую строку makefile:
1: Объявляется переменная с именем корневого каталога заголовочных файлов.
2: Объявляется переменная с именем корневого каталога исходников.
3: Объявляются переменная с именами подкаталогов корневого каталога исходников.
4: Объявляется переменная с общими флагами компиляции. -MD заставляет компилятор сгенерировать к каждому исходнику одноименный файл зависимостей с расширением .d. Каждый такой файл выглядит как правило, где целью является имя исходника, а зависимостями – все исходники и заголовочные файлы, которые он включает директивой #include. Флаг -pipe заставляет компилятор пользоваться IPC вместо файловой системы, что несколько ускоряет компиляцию.
5: Объявляется переменная с общими флагами компоновки. -s заставляет компоновщик удалить из результирующего ELF файла секции .symtab, .strtab и еще кучу секций с именами типа .debug*, что значительно уменшает его размер. В целях более качественно отладки этот ключ можно убрать.
6: Объявляется переменная с именами используемых библиотек в виде ключей компоновки.
8: Объявляется переменная, содержащая относительные имена каталогов со стандартными заголовочными файлами. Потом такие имена напрямую передаются компилятору, предваряемые ключем -I. Для нашего случая получится ../../include, потому что такой каталог у нас один. Функция addprefix добавляет свой первый аргумент ко всем целям, которые задает второй аргумент.
9: Объявляется переменная, содержащая относительные имена всех подкаталогов корневого каталога исходников. В итоге получим: ../../src/. ../../src/dir1 ../../src/dir1.
10: Объявляется переменная, содержащая имена подкаталогов каталога project/obj/Debug/src относительно текущего project/obj/Debug. То есть, этим мы перечисляем копию структуры каталога project/src. В итоге получим: /src/dir1 src/dir2.
11: Объявляется переменная, содержащая имена исходников, найденных на основе одноименных файлов *.c* (.cpp\.c), безотносительно текущего каталога. Смотрим поэтапно: результатом addsuffix будет ../../src/./*.с* ../../src/dir1/*.с* ../../src/dir2/*.с*. Функция wildcard развернет шаблоны со звездочками до реальных имен файлов: ../../src/./main.сpp ../../src/dir1/file1.с ../../src/dir1/file2.сpp ../../src/dir2/file3.с ../../src/dir2/file4.с. Функция patsubsb уберет префикс ../../ у имен файлов (она заменяет шаблон, заданный первым аргументом на шаблон во втором аргументе, а % обозначает любое количество символов). В итоге получим: src/./main.сpp src/dir1/file1.с src/dir1/file2.сpp src/dir2/file3.с src/dir2/file4.с.
12: В переменной с именами исходников расширения .cpp заменяется на .o.
13: В переменной с именами исходников расширения .c заменяется на .o.
15: Первое объявленное правило – его цель становится целью всего проекта. Зависимостью является константа, содержащая имя программы (../../bin/application_debug мы ее передали при запуске make из сценария).
17: Описание ключевой цели. Зависимоcти тоже очевидны: наличие созданных подкаталого в project/obj/Debug, повторяющих структуру каталога project/src и множество объектных файлов в них.
18: Описано действие по компоновке объектных файлов в целевой.
20: Правило, в котором цель – каталог project/obj/Debug/src и его подкаталоги.
21: Действие по достижению цели – создать соответствующие каталоги src/., src/dir1 и src/dir2. Ключ -p утилиты mkdir игнорирует ошибку, если при создании какого-либо каталога, таковой уже существуют.
23: Переменная VPATH принимает значение ../../. Это необходимо для шаблонов нижеследующих правил.
25: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на .o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.cpp (то есть имена которых оканчиваются на .cpp). При этом под одноименностью понимается не только точное совпадение, но также если имя зависимости предварено содержимым переменной VPATH. Например, имена src/dir1/file2 и ../../src/dir1/file2 совпадут, так как VPATH содержит ../../.
26: Вызов компилятора для превращения исходника на языке С++ в объектный файл.
28: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на .o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.c (то есть имена которых оканчиваются на .c). Одноименность как в строке 23.
29: Вызов компилятора для превращения исходника на языке С в объектный файл.
31: Некоторая цель clean объявлена абстрактной. Достижение абстрактной цели происходит всегда и не зависит от существования одноименного файла.
32: Объявление абстрактной цели clean.
33: Действие по ее достижению заключается в уничтожении каталогов project/bin и project/obj со всем их содержимым.
36: Включение содержимого всех файлов зависимостей (с расширением .d), находящихся в подкаталогах текущего каталога. Данное действие утилита make делает в начале разбора makefile. Однако, файлы зависимостей создаются только послекомпиляции. Значит, при первой сборке ни один такой файл включен не будет. Но это не страшно. Цель включения этих файлов – вызвать перекомпиляцию исходников, зависящих от модифицированного заголовочного файла. При второй и последующих сборках утилита make будет включать правила, описанные во всех файлах зависимостей, и, при необходимости, достигать все цели, зависимые от модифицированного заголовочного файла.
Удачи!
Внимание! Предполагаются базовые знания утилиты GNU make.
Имеем некий типичный абстрактный проект со следующей структурой каталогов:
Пусть для включения заголовочных файлов в исходниках используется что-то типа #include <dir1/file1.h>, то есть каталог project/include делается стандартным при компиляции.
После сборки надо, чтобы получилось так:
То есть, в каталоге bin лежат рабочая (application) и отладочная (application_debug) версии, в подкаталогах Release и Debug каталога project/obj повторяется структура каталога project/src с соответствующими исходниками объектных файлов, из которых и компонуется содержимое каталога bin.
Чтобы достичь данного эффекта, создаем в каталоге project файл Makefile следующего содержания:
- root_include_dir := include
- root_source_dir := src
- source_subdirs := . dir1 dir2
- compile_flags := -Wall -MD -pipe
- link_flags := -s -pipe
- libraries := -ldl
- relative_include_dirs := $(addprefix ../../, $(root_include_dir))
- relative_source_dirs := $(addprefix ../../$(root_source_dir)/, $(source_subdirs))
- objects_dirs := $(addprefix $(root_source_dir)/, $(source_subdirs))
- objects := $(patsubst ../../%, %, $(wildcard $(addsuffix /*.c*, $(relative_source_dirs))))
- objects := $(objects:.cpp=.o)
- objects := $(objects:.c=.o)
- all : $(program_name)
- $(program_name) : obj_dirs $(objects)
- g++ -o $@ $(objects) $(link_flags) $(libraries)
- obj_dirs :
- mkdir -p $(objects_dirs)
- VPATH := ../../
- %.o : %.cpp
- g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs))
- %.o : %.c
- g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs))
- .PHONY : clean
- clean :
- rm -rf bin obj
- include $(wildcard $(addsuffix /*.d, $(objects_dirs)))
В чистом виде такой makefile полезен только для достижения цели clean, что приведет к удалению каталогов bin и obj.
Добавим еще один сценарий с именем Release для сборки рабочей версии:
mkdir -p bin
mkdir -p obj
mkdir -p obj/Release
make --directory=./obj/Release --makefile=../../Makefile build_flags="-O2 -fomit-frame-pointer" program_name=../../bin/application
И еще один сценарий Debug для сборки отладочной версии:
mkdir -p bin
mkdir -p obj
mkdir -p obj/Debug
make --directory=./obj/Debug --makefile=../../Makefile build_flags="-O0 -g3 -D_DEBUG" program_name=../../bin/application_debug
Именно вызов одного из них соберет наш проект в рабочем, либо отладочном варианте. А теперь, обо всем по-порядку.
Допустим, надо собрать отладочную версию. Переходим в каталог project и вызываем ./Debug. В первых трех строках создаются каталоги. В четвертой строке утилите make сообщается, что текущим каталогом при запуске надо сделать project/obj/Debug, относительно этого далее передается путь к makefile и задаются две константы: build_flags (тут перечисляются важные для отладочной версии флаги компиляции) и program_name (для отладочной версии – это application_debug).
Далее, в игру вступает GNU make. Прокомментируем каждую строку makefile:
1: Объявляется переменная с именем корневого каталога заголовочных файлов.
2: Объявляется переменная с именем корневого каталога исходников.
3: Объявляются переменная с именами подкаталогов корневого каталога исходников.
4: Объявляется переменная с общими флагами компиляции. -MD заставляет компилятор сгенерировать к каждому исходнику одноименный файл зависимостей с расширением .d. Каждый такой файл выглядит как правило, где целью является имя исходника, а зависимостями – все исходники и заголовочные файлы, которые он включает директивой #include. Флаг -pipe заставляет компилятор пользоваться IPC вместо файловой системы, что несколько ускоряет компиляцию.
5: Объявляется переменная с общими флагами компоновки. -s заставляет компоновщик удалить из результирующего ELF файла секции .symtab, .strtab и еще кучу секций с именами типа .debug*, что значительно уменшает его размер. В целях более качественно отладки этот ключ можно убрать.
6: Объявляется переменная с именами используемых библиотек в виде ключей компоновки.
8: Объявляется переменная, содержащая относительные имена каталогов со стандартными заголовочными файлами. Потом такие имена напрямую передаются компилятору, предваряемые ключем -I. Для нашего случая получится ../../include, потому что такой каталог у нас один. Функция addprefix добавляет свой первый аргумент ко всем целям, которые задает второй аргумент.
9: Объявляется переменная, содержащая относительные имена всех подкаталогов корневого каталога исходников. В итоге получим: ../../src/. ../../src/dir1 ../../src/dir1.
10: Объявляется переменная, содержащая имена подкаталогов каталога project/obj/Debug/src относительно текущего project/obj/Debug. То есть, этим мы перечисляем копию структуры каталога project/src. В итоге получим: /src/dir1 src/dir2.
11: Объявляется переменная, содержащая имена исходников, найденных на основе одноименных файлов *.c* (.cpp\.c), безотносительно текущего каталога. Смотрим поэтапно: результатом addsuffix будет ../../src/./*.с* ../../src/dir1/*.с* ../../src/dir2/*.с*. Функция wildcard развернет шаблоны со звездочками до реальных имен файлов: ../../src/./main.сpp ../../src/dir1/file1.с ../../src/dir1/file2.сpp ../../src/dir2/file3.с ../../src/dir2/file4.с. Функция patsubsb уберет префикс ../../ у имен файлов (она заменяет шаблон, заданный первым аргументом на шаблон во втором аргументе, а % обозначает любое количество символов). В итоге получим: src/./main.сpp src/dir1/file1.с src/dir1/file2.сpp src/dir2/file3.с src/dir2/file4.с.
12: В переменной с именами исходников расширения .cpp заменяется на .o.
13: В переменной с именами исходников расширения .c заменяется на .o.
15: Первое объявленное правило – его цель становится целью всего проекта. Зависимостью является константа, содержащая имя программы (../../bin/application_debug мы ее передали при запуске make из сценария).
17: Описание ключевой цели. Зависимоcти тоже очевидны: наличие созданных подкаталого в project/obj/Debug, повторяющих структуру каталога project/src и множество объектных файлов в них.
18: Описано действие по компоновке объектных файлов в целевой.
20: Правило, в котором цель – каталог project/obj/Debug/src и его подкаталоги.
21: Действие по достижению цели – создать соответствующие каталоги src/., src/dir1 и src/dir2. Ключ -p утилиты mkdir игнорирует ошибку, если при создании какого-либо каталога, таковой уже существуют.
23: Переменная VPATH принимает значение ../../. Это необходимо для шаблонов нижеследующих правил.
25: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на .o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.cpp (то есть имена которых оканчиваются на .cpp). При этом под одноименностью понимается не только точное совпадение, но также если имя зависимости предварено содержимым переменной VPATH. Например, имена src/dir1/file2 и ../../src/dir1/file2 совпадут, так как VPATH содержит ../../.
26: Вызов компилятора для превращения исходника на языке С++ в объектный файл.
28: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на .o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.c (то есть имена которых оканчиваются на .c). Одноименность как в строке 23.
29: Вызов компилятора для превращения исходника на языке С в объектный файл.
31: Некоторая цель clean объявлена абстрактной. Достижение абстрактной цели происходит всегда и не зависит от существования одноименного файла.
32: Объявление абстрактной цели clean.
33: Действие по ее достижению заключается в уничтожении каталогов project/bin и project/obj со всем их содержимым.
36: Включение содержимого всех файлов зависимостей (с расширением .d), находящихся в подкаталогах текущего каталога. Данное действие утилита make делает в начале разбора makefile. Однако, файлы зависимостей создаются только послекомпиляции. Значит, при первой сборке ни один такой файл включен не будет. Но это не страшно. Цель включения этих файлов – вызвать перекомпиляцию исходников, зависящих от модифицированного заголовочного файла. При второй и последующих сборках утилита make будет включать правила, описанные во всех файлах зависимостей, и, при необходимости, достигать все цели, зависимые от модифицированного заголовочного файла.
Удачи!