Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
Скидка до 20% на услуги дата-центра. Аренда серверной стойки. Colocation от 1U!

Миграция в облако #SotelCloud. Виртуальный сервер в облаке. Выбрать конфигурацию на сайте!

Виртуальная АТС для вашего бизнеса. Приветственные бонусы для новых клиентов!

Виртуальные VPS серверы в РФ и ЕС

Dedicated серверы в РФ и ЕС

По промокоду CITFORUM скидка 30% на заказ VPS\VDS

VPS/VDS серверы. 30 локаций на выбор

Серверы VPS/VDS с большим диском

Хорошие условия для реселлеров

4VPS.SU - VPS в 17-ти странах

2Gbit/s безлимит

Современное железо!

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

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

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

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

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

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

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

Предполагается, что для компиляции программы используется компилятор 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 $@

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

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

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

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

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

Как видно из таблицы компилятор может работать двумя способами - в одном случае компилятор выдает только список зависимостей и заканчивает работу (опции -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

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

    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.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:

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

Вот как выглядит 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-файла он претерпел следующие изменения:

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

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

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

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

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

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

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

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

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

    make  compile_flags="-O0 -g"

Вот как выглядит 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:

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

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

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

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

Команда mkdir введена для удобства - если удалить каталог release, то при следующей сборке он будет создан заново. В случае "составного" имени каталога (например, bin/release) можно дополнительно использовать флажок -p. Флажок --directory заставляет make перед началом работы сделать указанную директорию release текущей. Флажок --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 директория с последним примером выглядит так:

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

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

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

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

Назад | Содержание | Вперед

 

Бесплатный конструктор сайтов и Landing Page

Хостинг с DDoS защитой от 2.5$ + Бесплатный SSL и Домен

SSD VPS в Нидерландах под различные задачи от 2.6$

✅ Дешевый VPS-хостинг на AMD EPYC: 1vCore, 3GB DDR4, 15GB NVMe всего за €3,50!

🔥 Anti-DDoS защита 12 Тбит/с!

VPS в России, Европе и США

Бесплатная поддержка и администрирование

Оплата российскими и международными картами

🔥 VPS до 5.7 ГГц под любые задачи с AntiDDoS в 7 локациях

💸 Гифткод CITFORUM (250р на баланс) и попробуйте уже сейчас!

🛒 Скидка 15% на первый платеж (в течение 24ч)

Новости мира IT:

Архив новостей

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 495 7861149
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2019 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...