В начале каждой главы я кратко описываю, о чем в ней будет вестись речь, и какими знаниями нужно обладать, чтобы успешно воспринять излагаемый в главе материал. Для тех, кто чувствует, что недостаточно хорошо ориентируется в предмете разговора, я указываю на дополнительные главы, с которыми следует предварительно ознакомиться.
Для работы я использовал GNU Make версии 3.79.1. Некоторые старые версии GNU Make (например, версия 3.76.1 из дистрибутива Slackware 3.5) могут неправильно работать с примером "традиционного" строения make-файла (по-видимому, они "не воспринимают" старую форму записи шаблонных правил).
В качестве примера я буду использовать "гипотетический" проект - текстовой редактор. Он состоит из нескольких файлов с исходным текстом на языке C++ (main.cpp, Editor.cpp, TextLine.cpp) и нескольких включаемых файлов (main.h,Editor.h, TextLine.h). Если вы имеете доступ в интернет то "электронный" вариант приводимых в книге примеров можно получить на моей домашней страничке по адресу www.geocities.com/SiliconValley/Office/6533 . Если интернет для вас недоступен, то в Приложении D приведены листинги файлов, которые используются в примерах.
В первом примере make-файл построен "традиционным" способом. Все исходные файлы собираемой программы находятся в одном каталоге:
# # 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-файла мне кажется неудобным потому что:
iEdit: *.o gcc $< -o $@
не сработает, так как будут учтены только существующие в данный момент объектные файлы. Я использую чуть более сложный способ, который основан на предположении, что все файлы с исходным текстом должны быть скомпилированы и скомпонованы в собираемую программу. Моя методика состоит из двух шагов:
# # 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-файл теперь стал более универсальным - с небольшими изменениями его можно использовать для сборки разных программ.
Перечисление зависимостей "вручную" требует довольно кропотливой работы. Недостаточно просто открыть файл с исходным текстом и перечислить имена всех заголовочных файлов, подключаемых с помощью #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, я предпочитаю первую потому что:
После того как файлы зависимостей сформированы, нужно сделать их доступными утилите make. Этого можно добиться с помощью директивы include.
include $(wildcard *.d)
Обратите внимание на использование функции wildcard. Конструкция
include *.dбудет правильно работать только в том случае, если в каталоге будет находиться хотя бы один файл с расширением ".d". Если таких файлов нет, то make аварийно завершится, так как потерпит неудачу при попытке "построить" эти файлы (у нее ведь нет на этот счет ни каких инструкций!). Если же использовать функцию wildcard, то при отсутствии искомых файлов, эта функция просто вернет пустую строку. Далее, директива include с аргументом в виде пустой строки, будет проигнорирована, не вызывая ошибки. Теперь можно составить новый вариант make-файла для моего "гипотетического" проекта:
# # example_3-auto_depend/Makefile # # Пример автоматического построения зависимостей от заголовочных файлов # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c -MD $< include $(wildcard *.d)После завершения работы make директория проекта будет выглядеть так:
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
При переименовании или удалении какого-нибудь "популярного" заголовочного файла, можно просто заново пересобрать проект, удалив предварительно все объектные файлы и файлы зависимостей.
%.o: %.cpp gcc -c $<
осталось работоспособным, я использую переменную VPATH, в которой перечисляются все директории, где могут располагаться исходные тексты. В следующем примере я поместил файлы Editor.cpp и Editor.h в каталог Editor, а файлы TextLine.cpp и TextLine.h в каталог TextLine:
# # 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-файла он претерпел следующие изменения:
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-файла.
Для решения этой проблемы я помещаю результаты компиляции каждой версии программы в свой отдельный каталог. Так, например, отладочная версия программы (включая все объектные файлы) помещается в каталог debug, а рабочая версия программы - в каталог release:
Главная сложность заключалась в том, чтобы заставить программу make помещать результаты работы в разные директории. Попробовав разные варианты, я пришел к выводу, что самый легкий путь - использование флажка --directory при вызове make. Этот флажок заставляет утилиту перед началом обработки make-файла, сделать каталог, указанный в командной строке, "текущим".
Вот, например, как выглядит командный файл make_release, собирающий рабочую версию программы (результаты компиляции помещается в каталог release):
mkdir release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=release \ --makefile=../Makefile
Команда mkdir введена для удобства - если удалить каталог release, то при следующей сборке он будет создан заново. В случае "составного" имени каталога (например, bin/release) можно дополнительно использовать флажок -p. Флажок --directory заставляет make перед началом работы сделать указанную директорию release текущей. Флажок --makefile укажет программе make, где находится make-файл проекта. По отношению к "текущей" директории release, он будет располагаться в "родительском" каталоге.
Командный файл для сборки отладочного варианта программы (make_debug) выглядит аналогично. Различие только в имени директории, куда помещаются результаты компиляции (debug) и другом наборе флагов компиляции:
mkdir debug make compile_flags="-O0 -g" \ --directory=debug \ --makefile=../MakefileВот окончательная версия make-файла для сборки "гипотетического" проекта текстового редактора:
# # 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 директория с последним примером выглядит так:
В этой главе я изложил свою методику работы с 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) - как правило, она содержит лишь отрывочную и сильно устаревшую информацию.
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" .
Утилита 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:
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.
.cpp.o: gcc $^ -o $@То есть под действие правила попадают файлы с определенными расширениями (".cpp" и ".o" в данном случае).
GNU Make поддерживает более универсальный подход - с использованием шаблонов имен файлов. Для задания шаблона используется символ '%', который означает "последовательность любых символов произвольной длины". Символ '%' в правой части правила заменяется текстом, который соответствует символу '%' в левой части. Пользуясь новой формой записи, приведенный выше пример можно записать так:
%.o: %.cpp gcc $^ -o $@
В своих make-файлах я пользуюсь новой формой записи шаблонных правил, потому что считаю ее более удобной (шаблонные и нешаблонные правила теперь имеют аналогичный синтаксис) и универсальной (можно задавать не только файлы, отличающиеся своими расширениями).
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 VPATH" ( paulandlesley.org/gmake/vpath.html), в которой обсуждается "неправильный" стиль использования переменной VPATH.
compile_flags := -pipe $(compile_flags) all: echo $(compile_flags)Предположим, что переменная compile_flags была задана через командную строку при запуске программы make:
make compile_flags="-O0 -g"В результате обработки make-файла на экран будет выведена строка:
-O0 -gТо есть попытка изменить значение переменной compile_flags внутри make-файла была проигнорирована. Если все-таки возникает необходимость в изменении переменной, которая была задана с помощью командной строки, нужно использовать директиву override. Директива помещается перед именем переменной, которая должна быть изменена:
override compile_flags := -pipe $(compile_flags) all: echo $(compile_flags)Теперь в результате обработки make-файла на экран будет выдана строка:
-pipe -O0 -g
compile_flags += -pipe compile_flags += $(flags)
При использовании этого оператора, "тип" переменной (см. раздел 2.1 "Две разновидности переменных") не меняется - "макросы" остаются "макросами", а "текстовые переменные" по-прежнему остаются таковыми.
Если переменная задана с помощью командной строки, то по-прежнему для изменения ее значения внутри make-файла нужно использовать директиву override. В следующем примере предполагается, что переменная compile_flags задана в командной строке:
override compile_flags += -pipe override compile_flags += $(flags)
В директиве include могут быть указаны одно или несколько имен файлов, разделенных пробелами. В качестве имен файлов можно использовать шаблоны:
include common.mak include main.d Editor.d TextLine.d include *.dУказанные в директиве файлы должны существовать - иначе make предпримет попытку "создать" их, а при невозможности этого достигнуть, выдаст сообщение об ошибке. Директива include с пустым списком файлов:
includeпросто игнорируется.
Имя автоматической переменной | Значение |
$@ | Имя цели обрабатываемого правила |
$< | Имя первой зависимости обрабатываемого правила |
$^ | Список всех зависимостей обрабатываемого правила |
Полный список автоматических переменных приводится в разделе "Automatic Variables" руководства по GNU Make.
TextLine.o: TextLine.cpp gcc -c $< TextLine.o: TextLine.hэквивалентны правилу:
TextLine.o: TextLine.cpp TextLine.h gcc -c $<
Шаблонные и нешаблонные правила также могут "комбинироваться":
%.o: %.cpp gcc -c $< TextLine.o: TextLine.h
Обратите внимание на то, что в обоих примерах только в одном из правил указаны исполняемые команды - именно они и будут при необходимости выполняться. При наличии команд в обоих правилах, make выдаст предупреждающее сообщение и "в расчет" будут приниматься только команды из последнего правила.
Для целей, которые являются именами действий, такой подход не очень хорош. Во-первых, имя такой цели может случайно совпасть с именем какого-либо файла или директории. И, во-вторых, make просто нерационально тратит свое время, занимаясь поиском несуществующих файлов.
В утилите GNU Make имеется способ явного объявления целей абстрактными. Для этого используется механизм "специальных целей". Специальная цель - это имя, которое имеет специальное значение, когда используется в качестве цели. Для того, например, чтобы объявить перечисленные цели абстрактными, достаточно поместить их в правило со специальной целью .PHONY. В следующем примере цель clean объявляется абстрактной:
.PHONY: clean clean: rm *.o *.dВсе возможные специальные цели описаны в главе Special Built-in Target Names" руководства по GNU Make.
При запуске программа make читает файл с описанием проекта (make-файл) и, интерпретируя его содержимое, предпринимает необходимые действия. Файл с описанием проекта представляет собой текстовой файл, где описаны отношения между файлами проекта, и действия, которые необходимо выполнить для его сборки.
Основным "строительным элементом" make-файла являются правила (rules). В общем виде правило выглядит так:
<цель_1> <цель_2> ... <цель_n>: <зависимость_1> <зависимость_2> ... <зависимость_n> <команда_1> <команда_2> ... <команда_n>Цель (target) - это некий желаемый результат, способ достижения которого описан в правиле. Цель может представлять собой имя файла. В этом случае правило описывает, каким образом можно получить новую версию этого файла. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
целью является файл iEdit (исполняемый файл программы). Правило описывает, каким образом можно получить новую версию файла iEdit (скомпоновать из перечисленных объектных файлов).
Цель также может быть именем некоторого действия. В таком случае правило описывает, каким образом совершается указанное действие. В следующем примере целью является действие clean (очистка).
clean: rm *.o iEditПодобного рода цели называются псевдоцели (pseudotargets) или абстрактные цели (phony targets).
Зависимость (dependency)- это некие "исходные данные", необходимые для достижения указанной в правиле цели. Можно сказать что зависимость - это "предварительное условие" для достижения цели. Зависимость может представлять собой имя файла. Этот файл должен существовать, для того чтобы можно было достичь указанной цели. В следующем правиле:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели - построение файла iEdit.
Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением указанной в правиле цели. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы):
clean_all: clean_obj rm iEdit clean_obj: rm *.o
Для того чтобы цель clean_all была достигнута, нужно сначала выполнить действие (достигнуть цели) clean_obj.
Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit
командой является вызов компилятора GCC. Утилита make отличает строки, содержащие команды, от прочих строк make-файла по наличию символа табуляции (символа с кодом 9) в начале строки. В приведенном выше примере строка:
gcc main.o Editor.o TextLine.o -o iEditдолжна начинаться с символа табуляции.
Главная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit):
make iEditА в этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта):
make cleanЕсли не указывать какой-либо цели в командной строке, то make выбирает в качестве главной первую, встреченную в make-файле цель. В следующем примере:
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.oиз четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit. Схематично, "верхний уровень" алгоритма работы make можно представить так:
make() { главная_цель = ВыбратьГлавнуюЦель() ДостичьЦели( главная_цель ) }
ДостичьЦели( Цель ) { правило = НайтиПравило( Цель ) ОбработатьПравило( правило ) }
ОбработатьПравило( Правило ) { ОбработатьЗависимости( Правило ) если НужноВыполнятьКоманды( Правило ) { ВыполнитьКоманды( Правило ) } }
ОбработатьЗависимости( Правило ) { цикл от i=1 до Правило.число_зависимостей { если ЕстьТакаяЦель( Правило.зависимость[ i ] ) { ДостичьЦели( Правило.зависимость[ i ] ) } иначе { ПроверитьНаличиеФайла( Правило.зависимость[ i ] ) } } }
НужноВыполнятьКоманды( Правило ) { если Правило.Цель.ЯвляетсяАбстрактной() return true // цель является именем файла если ФайлНеСуществует( Правило.Цель ) return true цикл от i=1 до Правило.Число_зависимостей { если Правило.Зависимость[ i ].ЯвляетсяАбстрактной() return true иначе // зависимость является именем файла { если ВремяМодефикации( Правило.Зависимость[ i ] ) > ВремяМодефикации( Правило.Цель ) return true } } return false }
В противном случае считается, что данное имя является либо именем несуществующего файла, либо именем действия. Различия между этими двумя вариантами не делается, поскольку оба случая обрабатываются одинаково.
Подобный подход не слишком хорош по следующим соображениям. Во-первых, утилита make не слишком рационально расходует время, занимаясь поиском несуществующих имен файлов, которые на самом деле являются именами действий. Во-вторых, при подобном подходе, имена действий не должны совпадать с именами каких-либо файлов или директорий. Иначе подобный алгоритм даст сбой, и make-файл будет работать неправильно.
Некоторые версии make предлагают свои варианты решения этой проблемы. Так, например, в утилите GNU Make имеется механизм (специальная цель .PHONY), с помощью которого можно указать, что данное имя является именем действия.
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.oПредположим, что в директории с проектом находятся следующие файлы:
makeЦель не указана в командной строке, поэтому запускается алгоритм выбора цели (функция ВыбратьГлавнуюЦель). Главной целью становится файл iEdit (первая цель из первого правила).
Цель iEdit передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. В данном случае, это первое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).
Сначала поочередно обрабатываются описанные в правиле зависимости (функция ОбработатьЗависимости). Первая зависимость - объектный файл main.o. Поскольку в make-файле есть правило с такой целью (функция ЕстьТакаяЦель возвращает true), то для цели main.o запускается процедура ДостичьЦели.
Функция ДостичьЦели ищет правило, где описана цель main.o. Эта цель описана во втором правиле make-файла. Для этого правила запускается функция ОбработатьПравило.
Функция ОбработатьПравило запускает процесс обработки зависимостей (функция ОбработатьЗависимости). Во втором правиле указана единственная зависимость - main.cpp. Такой цели в make-файле не существует, поэтому считается, что зависимость main.cpp является именем файла. Далее, проверяется наличие этого файла на диске (функция ПроверитьНаличиеФайла) - такой файл существует. На этом процесс обработки зависимостей завершается.
После обработки зависимостей, функция ОбработатьПравило принимает решение о том, нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). Цели правила (файла main.o) не существует, поэтому команды нужно выполнять. Функция ВыполнитьКоманды запускает указанную в правиле команду (компилятор GCC), в результате чего создается файл main.o.
Цель main.o достигнута (объектный файл main.o построен). Теперь make возвращается к обработке остальных зависимостей первого правила. Зависимости Editor.o и TextLine.o обрабатываются аналогично. Для них выполняются те же действия, что и для зависимости main.o.
После того, как все зависимости (main.o, Editor.o и TextLine.o) обработаны, решается вопрос о необходимости выполнения указанных в правиле команд (функция НужноВыполнятьКоманды).
Поскольку цель (iEdit) является именем файла, который в данный момент не существует, то принимается решение выполнить описанную в правиле команду (функция ВыполнитьКоманды).
Содержащаяся в правиле команда запускает компилятор GCC, в результате чего создается исполняемый файл iEdit. Главная цель (iEdit)таким образом достигнута. На этом программа make завершает свою работу.
make cleanЦель явно указана в командной строке, поэтому главной целью становится абстрактная цель clean. Цель clean передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. Это будет пятое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило).
Поскольку в правиле не указано каких-либо зависимостей, make сразу переходит к этапу обработки указанных в правиле команд. Цель является именем действия, поэтому команды нужно выполнять.
Указанные в правиле команды выполняются, и цель clean, таким образом, считается достигнутой. На этом программа make завершает работу.
obj_list = main.o Editor.o TextLine.oприсваивает переменной obj_list значение "main.o Editor.o TextLine.o" (без кавычек). Пробелы между символом '=' и началом первого слова игнорируются. Следующие за последним словом пробелы также игнорируются. Значение переменной можно использовать с помощью конструкции:
$(имя_переменной)
Например, при обработке такого make-файла:
dir_list = . .. src/include all: echo $(dir_list)на экран будет выведена строка:
. .. src/include
Переменные могут не только содержать текстовые строки, но и "ссылаться" на другие переменные. Например, в результате обработки make-файла:
optimize_flags = -O3 compile_flags = $(optimize_flags) -pipe all: echo $(compile_flags)
на экран будет выведено:
-O3 -pipe
Во многих случаях использование переменных позволяет упростить make-файл и повысить его наглядность. Для того чтобы облегчить модификацию make-файла, можно разместить "ключевые" имена и списки в отдельных переменных и поместить их в начало make-файла:
program_name = iEdit obj_list = main.o Editor.o TextLine.o $(program_name): $(obj_list) gcc $(obj_list) -o $(program_name) ...
Адаптация такого make-файла для сборки другой программы сведется к изменению нескольких начальных строк.
iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEditс использованием автоматических переменных можно записать следующим образом:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@Здесь $^ и $@ являются автоматическими переменными. Переменная $^ означает "список зависимостей". В данном случае при вызове компилятора GCC она будет ссылаться на строку "main.o Editor.o TextLine.o". Переменная $@ означает "имя цели" и будет в этом примере ссылаться на имя "iEdit".
Иногда использование автоматических переменных совершенно необходимо - например, в шаблонных правилах (о них пойдет речь в следующей главе).
Традиционные реализации make поддерживают так называемую "суффиксную" форму записи шаблонных правил:
.<расширение_файлов_зависимостей>.<расширение_файлов_целей>: <команда_1> <команда_2> ... <команда_n>Например, следующее правило говорит о том, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp":
.cpp.o: gcc -c $^Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - это единственный способ узнать для каких файлов сейчас задействуется правило.
Шаблонные правила позволяют упростить make-файл и сделать его более универсальным. Рассмотрим простой проектный файл:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ main.o: main.cpp gcc -c $^ Editor.o: Editor.cpp gcc -c $^ TextLine.o: TextLine.cpp gcc -c $^Все исходные тексты программы обрабатываются одинаково - для них вызывается компилятор GCC. С использованием шаблонных правил, этот пример можно переписать так:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ .cpp.o: gcc -c $^Когда make ищет в файле проекта правило, описывающее способ достижения искомой цели (см. главу 3.2.2. "Достижение цели", функция НайтиПравило), то в расчет принимаются и шаблонные правила. Для каждого из них проверяется - нельзя ли задействовать это правило для достижения искомой цели.
Проблема состоит в том, что принятый в Unix-подобных операционных системах формат хранения текстовых файлов, несколько отличается от формата DOS/Windows. В Unix каждая строка текстового файла заканчивается символом "перевод строки" (код 0x0A). В DOS и Windows текстовые строки разделяются парой символов - "возврат каретки", "перевод строки" (0x0D, 0x0A).
Linux-версия программы GNU Make будет нормально работать с make-файлами, написанными в среде Linux. Версия утилиты GNU Make для Windows также будет нормально работать с make-файлами, подготовленными в среде Windows. Проблема возникнет лишь в том случае, если попытаться обработать Linux-версией программы GNU Make текстовой файл, подготовленный в среде DOS или Windows. Подобная ситуация может возникнуть "нечаянно" - достаточно лишь "сохранить" make-файл для среды Linux в текстовом редакторе DOS/Windows, чтобы он оказался "испорчен".
В отличие от компилятора GCC, который просто игнорирует символы "возврат каретки", GNU Make рассматривает их как "обычные" символы, которые вполне могут быть частью имени. В результате все слова, находящиеся в конце строк, искажаются, так как сзади к ним добавляется невидимый символ "возврат каретки". В следующем примере имя файла TextLine.o будет искажено (его длина будет составлять одиннадцать символов из-за невидимого "возврата строки"):
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@
Разумеется, на диске не найдется файла с таким "странным" именем и make-файл будет работать неверно. Дело осложняется еще и тем, что выдаваемые на экран диагностические сообщения также искажаются (при выводе на экран символ "возврат каретки" возвращает курсор в начало строки) и зачастую представляют собой лишь бессмысленные "обрывки" слов.
Описанную проблему можно решить разными способами. Организационный метод решения - никогда не пытаться редактировать make-файл, находясь в "чужеродной" для него среде. Другой возможный подход - "принудительно" удалять из make-файла символы "возврат каретки" (либо "вручную" - текстовым редактором, либо с помощью подходящей программы).
В директорию doc я помещаю различные текстовые файлы - документацию, замечания, список ошибок и тому подобное. Здесь же располагается и файл README.txt.
В каталоге project находится make-файл проекта и командные файлы, используемые для сборки программы в разных конфигурациях.
В каталог src я помещаю исходные тексты программы. Внутри директории src имеется своя иерархия каталогов, отражающая логическую структуру программы.
Вот пример make-файла, который работает с подобной структурой директорий проекта:
# # example_7-complex/project/Makefile # # Пример проекта со "сложной" структурой директорий # program_name := iEdit source_dirs := . Editor TextLine include_dirs := /c/aproj/lib /c/aproj/lib/linux link_flags := -static source_dirs := $(addprefix ../../src/, $(source_dirs) ) source_files := $(wildcard $(addsuffix /*.cpp, $(source_dirs) ) ) object_files := $(notdir $(source_files) ) object_files := $(object_files:.cpp=.o) $(program_name): $(object_files) gcc $^ -o $@ $(link_flags) -pipe VPATH := $(source_dirs) %.o: %.cpp gcc $< -c $(compile_flags) $(addprefix -I, $(include_dirs)) $(addprefix -I, $(source_dirs)) -MD -pipe 2>log include $(wildcard *.d)Список директорий, где располагаются файлы с исходными текстами (source_dirs), задается относительно каталога src. Вот как выглядит командный файл, собирающий отладочную версию программы:
mkdir -p ../bin/linux_debug make compile_flags="-O0 -g" \ --directory=../bin/linux_debug \ --makefile=../../project/MakefileКомандный файл, собирающий рабочую версию программы выглядит аналогично:
mkdir -p ../bin/linux_release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=../bin/linux_release \ --makefile=../../project/Makefile
Этот компилятор является "стандартным" средством для компиляции всех программ, входящих в проект GNU. GCC также является основным компилятором операционной системы Linux - с его помощью компилируется ядро системы.
gcc -v
Для подготовки отладочной версии программы с помощью компилятора GCC, достаточно отключить оптимизацию и включить генерацию отладочной информации. Для этого я использую следующие опции компиляции:
-g | Генерировать отладочную информацию |
-O0 | Отключить оптимизацию |
Отладчик GDB имеет текстовой интерфейс командной строки. Мне этот интерфейс кажется не очень удобными, поэтому я пользуюсь графической оболочкой DataDisplayDebugger (DDD). Эта оболочка является надстройкой над "текстовыми" отладчиками, реализующей для них удобный графический интерфейс. Программа DDD также входит в проект GNU. Ее домашняя страничка находится по адресу www.gnu.org/software/ddd . DataDisplayDebugger работает в среде X-Windows.
Компилятор GCC имеет большое количество опций, управляющих процессом кодогенерации и оптимизации, с которыми вы можете экспериментировать, добиваясь максимального быстродействия программы. Для своих проектов я использую следующие настройки:
Ключ | Назначение |
-O3 | Максимальная оптимизация |
-fomit-frame-pointer | Не использовать указатель на стековый фрейм. Компилятор будет адресовать переменные в стеке с помощью регистра ESP а регистр EBP "высвобождается" для использования в качестве регистра общего назначения. |
-mcpu=pentium | Оптимизировать код для процессора Pentium (однако программа по прежнему будет работать даже на i386) |
Ключ компиляции | Назначение |
-fexceptions | Включить поддержку механизма исключительных ситуаций языка C++ |
Ключ компиляции | Назначение |
-S | Остановиться после стадии компиляции, перед стадией ассемблирования. |
-fverbose-asm | Генерировать дополнительные комментарии в ассемблерном листинге. Какие именно "дополнительные комментарии" будут помещены в текст листинга, зависит от версии компилятора. |
Обратите внимание на то, что указание флажка -S просто "останавливает" компилятор после фазы генерации ассемблерного листинга, то есть процесс компиляции прерывается. Как следствие - процесс сборки программы и процесс генерации ассемблерных листингов "несовместимы" между собой. Можно либо получать листинги, либо собирать программу, но не то и другое одновременно. Для получения листингов я обычно создаю отдельный командный файл, который среди прочих опций компиляции содержит флажки -S и -fverbose-asm.
Весьма полезная возможность компилятора - помещать в листинг список всех опций компиляции, которые были включены в данный момент. Дело в том, что включение одних опций (например -O3) может "автоматически" приводить к включению других опций, а документация к GCC не всегда точна в описании подобных зависимостей. Некоторые версии GCC всегда помещают в листинг список используемых опций, другие версии делают это только при наличии флажка -fverbose-asm.
%.o: %.cpp gcc -c $< 2>log
Файл main.cpp:
#include "main.h" void main() { }
Файл main.h:
// main.h
Файл Editor.cpp:
#include "Editor.h"
Файл Editor.h:
#include "TextLine.h"
Файл TextLine.cpp:
#include "TextLine.h"
Файл TextLine.h:
// TextLine.h