Эффективное использование GNU 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, я предпочитаю первую потому что:
  • С помощью директивы #include я часто включаю не только "стандартные", но и свои собственные заголовочные файлы, которые могут иногда меняться (например, заголовочные файлы моей прикладной библиотеки LIB).
  • Иногда бывает полезно взглянуть на полный список включаемых в модуль заголовочных файлов, в том числе и "стандартных".
После того как файлы зависимостей сформированы, нужно сделать их доступными утилите make. Этого можно добиться с помощью директивы include. include $(wildcard *.d) Обратите внимание на использование функции wildcard. Конструкция include *.d будет правильно работать только в том случае, если в каталоге будет находиться хотя бы один файл с расширением ".d". Если таких файлов нет, то make аварийно завершится, так как потерпит неудачу при попытке "построить" эти файлы (у нее ведь нет на этот счет ни каких инструкций!). Если же использовать функцию wildcard, то при отсутствии искомых файлов, эта функция просто вернет пустую строку. Далее, директива include с аргументом в виде пустой строки, будет проигнорирована, не вызывая ошибки. Теперь можно составить новый вариант make-файла для моего "гипотического" проекта:
  • example_3-auto_depend /
    • main.cpp
    • main.h
    • Editor.cpp
    • Editor.h
    • TextLine.cpp
    • TextLine.h
    • Makefile
Вот как выглядит Makefile из этого примера: # # example_3-auto_depend/Makefile # # Пример автоматического построения зависимостей от заголовочных файлов # iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c -MD $< include $(wildcard *.d) После завершения работы make директория проекта будет выглядеть так:
  • example_3-auto_depend /
    • iEdit
    • main.cpp
    • main.h
    • main.o
    • main.d
    • Editor.cpp
    • Editor.o
    • Editor.d
    • Editor.h
    • TextLine.cpp
    • TextLine.o
    • TextLine.d
    • TextLine.h
    • Makefile
Файлы с расширением ".d" - это сгенерированные компилятором GCC файлы зависимостей.


Вот, например, как выглядит файл Editor.d, в котором перечислены зависимости для файла Editor.cpp: Editor.o: Editor.cpp Editor.h TextLine.h Теперь при изменении любого из файлов - Editor.cpp, Editor.h или TextLine.h, файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o. Имеет ли описанная методика недостатки? Да, к сожалению, имеется один недостаток. К счастью, на мой взгляд, не слишком существенный. Дело в том, что утилита make обрабатывает make-файл "в два приема". Сначала будет обработана директива include и в make-файл будут включены файлы зависимостей, а затем, на "втором проходе", будут уже выполняться необходимые действия для сборки проекта. Получается что для "текущей" сборки используются файлы зависимостей, сгенерированные во время "предыдущей" сборки. Как правило, это не вызывает проблем. Сложности возникнут лишь в том случае, если какой-нибудь из заголовочных файлом по какой-либо причине прекратил свое существование. Рассмотрим простой пример. Предположим, у меня имеются файлы main.cpp и main.h: Файл main.cpp: #include "main.h" void main() { } Файл main.h: // main.h В таком случае, сформированный компилятором файл зависимостей main.d будет выглядеть так: main.o: main.cpp main.h Теперь, если я переименую файл main.h в main_2.h, и соответствующим образом изменю файл main.cpp, Файл main.cpp: #include "main_2.h" void main() { } то очередная сборка проекта окончится неудачей, поскольку файл зависимостей main.d будет ссылаться на не существующий более заголовочный файл main.h. Выходом в этой ситуации может служить удаление файла зависимостей main.d. Тогда сборка проекта пройдет нормально и будет создана новая версия этого файла, ссылающаяся уже на заголовочный файл main_2.h: main.o: main.cpp main_2.h При переименовании или удалении какого-нибудь "популярного" заголовочного файла, можно просто заново пересобрать проект, удалив предварительно все объектные файлы и файлы зависимостей.

Содержание раздела