|
| ||||||||||||
| ||||||||||||
1. Моя методика использования GNU MakeВ этой главе я описываю свой способ построения make-файлов для сборки проектов с использование программы GNU Make и компилятора GCC (GNU Compiler Collection) . Предполагается, что вы хорошо знакомы с утилитой GNU Make. Если это не так, то прочтите сначала главу 2 - "GNU Make" . 1.1. Пример проектаВ качестве примера я буду использовать "гипотический" проект - текстовой редактор. Он состоит из нескольких файлов с исходным текстом на языке C++ (main.cpp, Editor.cpp, TextLine.cpp) и нескольких включаемых файлов (main.h,Editor.h, TextLine.h). Если вы имеете доступ в интернет то "электронный" вариант приводимых в книге примеров можно получить на моей домашней страничке по адресу www.geocities.com/SiliconValley/Office/6533 . Если интернет для вас недоступен, то в Приложении D приведены листинги файлов, которые используются в примерах. 1.2. "Традиционный" способ построения make-файловВ первом примере make-файл построен "традиционным" способом. Все исходные файлы собираемой программы находятся в одном каталоге:
Предполагается, что для компиляции программы используется компилятор GCC, и объектные файлы имеют расширение ".o". Файл Editor.h выглядит так: iEdit: main.o Editor.o TextLine.o
gcc $^ -o $@
%.o: %.cpp
gcc -c $<
main.o: main.h Editor.hTextLine.h
Editor.o: Editor.hTextLine.h
TextLine.o: TextLine.h
Первое правило заставляет make перекомпоновывать программу при изменении любого из объектных файлов. Второе правило говорит о том, что объектные файлы зависят от соответствующих исходных файлов. Каждое изменение файла с исходным текстом будет вызывать его перекомпиляцию. Следующие несколько правил указывают, от каких заголовочных файлов зависит каждый из объектных файлов. Такой способ построения make-файла мне кажется неудобным потому что:
Видно, что традиционный способ построения make-файлов далек от идеала. Единственно чем этот способ может быть удобен - своей "совместимостью". По-видимому, с таким make-файлом будут нормально работать даже самые "древние" или "экзотические" версии make (например, nmake фирмы Microsoft). Если подобная "совместимость" не нужна, то можно сильно облегчить себе жизнь, воспользовавшись широкими возможностями утилиты GNU Make. Попробуем избавиться от недостатков "традиционного" подхода. 1.3. Автоматическое построение списка объектных файлов"Ручное" перечисление всех объектных файлов, входящих в программу - достаточно нудная работа, которая, к счастью, может быть автоматизирована. Разумеется "простой трюк" вроде:
iEdit: *.o
gcc $< -o $@
не сработает, так как будут учтены только существующие в данный момент объектные файлы. Я использую чуть более сложный способ, который основан на предположении, что все файлы с исходным текстом должны быть скомпилированы и скомпонованы в собираемую программу. Моя методика состоит из двух шагов:
Следующий пример содержит модифицированную версию make-файла:
Файл Editor.h теперь выглядит так:
iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
gcc $^ -o $@
%.o: %.cpp
gcc -c $<
main.o: main.h Editor.hTextLine.h
Editor.o: Editor.hTextLine.h
TextLine.o: TextLine.h
Список объектных файлов программы строится автоматически. Сначала с помощью функции wildcard получается список всех файлов с расширением ".cpp", находящихся в директории проекта. Затем, с помощью функции patsubst , полученный таким образом список исходных файлов, преобразуется в список объектных файлов. Make-файл теперь стал более универсальным - с небольшими изменениями его можно использовать для сборки разных программ. 1.4. Автоматическое построение зависимостей от заголовочных файлов"Ручное" перечисления зависимостей объектных файлов от заголовочных файлов - занятие еще более утомительное и неприятное чем "ручное" перечисление объектных файлов. Указывать такие зависимости обязательно нужно - в процессе разработки программы заголовочные файлы могут меняться довольно часто (описания классов, например, традиционно размещаются в заголовочных файлах). Если не указывать зависимости объектных файлов от соответствующих заголовочных файлов, то может сложиться ситуация, когда разные объектные файлы программы будут скомпилированы с использованием разных версии одного и того же заголовочного файла. А это, в свою очередь, может привести к частичной или полной потере работоспособности собранной программы. Перечисление зависимостей "вручную" требует довольно кропотливой работы. Недостаточно просто открыть файл с исходным текстом и перечислить имена всех заголовочных файлов, подключаемых с помощью #include. Дело в том, что одни заголовочные файлы могут, в свою очередь, включать в себя другие заголовочные файлы, так что придется отслеживать всю "цепочку" зависимостей. Утилита GNU Make не сможет самостоятельно построить список зависимостей, поскольку для этого придется "заглядывать" внутрь файлов с исходным текстом - а это, разумеется, лежит уже за пределами ее "компетенции". К счастью, трудоемкий процесс построения зависимостей можно автоматизировать, если воспользоваться помощью компилятора GCC. Для совместной работы с make компилятор GCC имеет несколько опций:
Как видно из таблицы компилятор может работать двумя способами - в одном случае компилятор выдает только список зависимостей и заканчивает работу (опции -M и -MM). В другом случае компиляция происходит как обычно, только в дополнении к объектному файлу генерируется еще и файл зависимостей (опции -MD и -MMD). Я предпочитаю использовать второй вариант - он мне кажется более удобным и экономичным потому что:
Из двух возможных опций -MD и -MMD, я предпочитаю первую потому что:
После того как файлы зависимостей сформированы, нужно сделать их доступными утилите make. Этого можно добиться с помощью директивы include.
include $(wildcard *.d)
Обратите внимание на использование функции wildcard . Конструкция
include *.d
будет правильно работать только в том случае, если в каталоге будет находиться хотя бы один файл с расширением ".d". Если таких файлов нет, то make аварийно завершится, так как потерпит неудачу при попытке "построить" эти файлы (у нее ведь нет на этот счет ни каких инструкций!). Если же использовать функцию wildcard , то при отсутствии искомых файлов, эта функция просто вернет пустую строку. Далее, директива include с аргументом в виде пустой строки, будет проигнорирована, не вызывая ошибки. Теперь можно составить новый вариант make-файла для моего "гипотического" проекта:
Вот как выглядит Editor.h из этого примера:
iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
gcc $^ -o $@
%.o: %.cpp
gcc -c -MD $<
include $(wildcard *.d)
После завершения работы make директория проекта будет выглядеть так:
Файлы с расширением ".d" - это сгенерированные компилятором GCC файлы зависимостей. Вот, например, как выглядит файл Editor.d, в котором перечислены зависимости для файла Editor.cpp: Editor.o: Editor.cpp Editor.hTextLine.h Теперь при изменении любого из файлов - Editor.cpp, Editor.hили TextLine.h, файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o. 1.5. "Разнесение" файлов с исходными текстами по директориямПриведенный в предыдущем параграфе make-файл вполне работоспособен и с успехом может быть использован для сборки небольших программ. Однако, с увеличением размера программы, становится не очень удобным хранить все файлы с исходными текстами в одном каталоге. В таком случае я предпочитаю "разносить" их по разным директориям, отражающим логическую структуру проекта. Для этого нужно немного модифицировать make-файл. Чтобы неявное правило
%.o: %.cpp
gcc -c $<
осталось работоспособным, я использую переменную VPATH , в которой перечисляются все директории, где могут располагаться исходные тексты. В следующем примере я поместил файлы Editor.cpp и Editor.hв каталог Editor, а файлы TextLine.cpp и TextLine.h в каталог TextLine:
Вот как выглядит Editor.h для этого примера:
source_dirs := Editor TextLine
search_wildcard s := $(addsuffix /*.cpp,$(source_dirs))
iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcard s))))
gcc $^ -o $@
VPATH := $(source_dirs)
%.o: %.cpp
gcc -c -MD $(addprefix -I ,$(source_dirs)) $<
include $(wildcard *.d)
По сравнению с предыдущим вариантом make-файла он претерпел следующие изменения:
1.6. Сборка программы с разными параметрами компиляцииЧасто возникает необходимость в получении нескольких вариантов программы, которые были скомпилированы по-разному. Типичный пример - отладочная и рабочая версии программы. В таких случаях я использую простую методику:
Для каждой конфигурации программы я делаю маленький командный файл, который вызывает make с нужными параметрами:
Файлы make_debug и make_release - это командные файлы, используемые для сборки соответственно отладочной и рабочей версий программы. Вот, например, как выглядит командный файл make_release: make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" Обратите внимание, что строка со значением переменной compile_flags заключена в кавычки, так как она содержит пробелы. Командный файл make_debug выглядит аналогично: make compile_flags="-O0 -g" Вот как выглядит Editor.h для этого примера: source_dirs := Editor TextLine
search_wildcard s := $(addsuffix /*.cpp,$(source_dirs))
override compile_flags += -pipe
iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcard s))))
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:
Главная сложность заключалась в том, чтобы заставить программу 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 текущей. Флажок --Editor.h укажет программе make, где находится make-файл проекта. По отношению к "текущей" директории release, он будет располагаться в "родительском" каталоге. Командный файл для сборки отладочного варианта программы (make_debug) выглядит аналогично. Различие только в имени директории, куда помещаются результаты компиляции (debug) и другом наборе флагов компиляции:
mkdir debug
make compile_flags="-O0 -g" \
--directory=debug \
--makefile=../makefile
Вот окончательная версия make-файла для сборки "гипотического" проекта текстового редактора:
program_name := iEdit
source_dirs := Editor TextLine
source_dirs := $(addprefix ../..,$(source_dirs))
search_wildcard s := $(addsuffix /*.cpp,$(source_dirs))
$(program_name): $(notdir $(patsubst %.cpp,%.o, $(wildcard $(search_wildcard s) ) ) )
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-файлами. Остальные главы носят более или менее "дополнительный" характер.
Назад | Содержание | Вперед
|
|
CITForum © 1997–2025