Программная инженерия и программирование
Понимание и управление зависимостями программ C с помощью Makefiles
Table of Contents
Введение
В программировании на C программа часто разделяется на несколько исходных файлов и файлов заголовков для улучшения организации, многократного использования и скорости компиляции. Однако эта модульность представляет собой проблему: когда файл заголовка изменяется, каждый исходный файл, который включает его, должен быть повторно компилируем. Делать это вручную — это подвержено ошибкам и отнимает много времени. Makefiles, управляемый инструментом автоматизации сборки , решает эту проблему путем кодирования зависимостей и автоматизации процесса сборки. Mastering Makefiles — это обряд перехода для любого серьезного разработчика C, но многие новички рассматривают их как черные ящики. В этой статье объясняется, как зависимости работают в Makefiles, как генерировать их автоматически и как структурировать Makefile для надежности и скорости.
Что такое Макефилы?
Makefile — это простой текстовый файл, который определяет набор правил для создания проекта. Утилита считывает эти правила, проверяет временные метки файлов и выполняет только команды, необходимые для обновления проекта. Основная идея проста: каждое правило имеет таргетинг (файл для создания), предпосылки (файлы, необходимые для создания цели), и рецепты (командные оболочки для запуска). Записывая, какие файлы зависят от того, , можно постепенно перестраивать только те части, которые изменились.
Makefiles были частью Unix с 1970-х годов, и GNU Make является фактическим стандартом для Linux и macOS. Синтаксис лаконичный, но может быть тонким; правильное получение зависимостей является основным навыком, необходимым разработчику C.
Понимание зависимостей
В проекте C зависимости не ограничиваются файлами . Каждый исходный файл включает в себя один или несколько файлов заголовка (например, . Если заголовок изменен, все файлы , которые включают его, должны быть перекомпилированы. Аналогично, файлы объектов зависят от соответствующих файлов , а конечный исполняемый файл зависит от всех файлов объектов.
Явные vs. неявные зависимости
В ранних Makefiles программисты перечислили все предпосылки вручную. Такой подход хрупок: забывание заголовка означает устаревшие сборки, в то время как перечисление слишком большого количества триггеров ненужных перекомпиляций. Хуже того, по мере роста проекта списки руководств становятся неподъемными. Современное решение состоит в том, чтобы компилятор автоматически генерировал зависимости, превращая их в неявные, машинно-проверенные предпосылки.
Хорошо продуманный Makefile рассматривает зависимости как первоклассную проблему. Цель никогда не состоит в том, чтобы восстановить что-либо, что не нуждается в восстановлении, и всегда перестраивать все, что делает. Это суть правильности и эффективности в постепенных построениях.
Основная структура Makefile
Типичный Makefile содержит переменные определения, правила и фальшивые цели. Вот минимальный, но функциональный пример для проекта с и :
CC = gcc
CFLAGS = -Wall -Wextra -O2
main: main.o utils.o
$(CC) $(CFLAGS) -o main main.o utils.o
main.o: main.c
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c
$(CC) $(CFLAGS) -c utils.c
clean:
rm -f main main.o utils.o
Этот Makefile имеет четыре цели: (исполняемый), два объектных файла и фальшивую цель . Линии зависимости после толстой кишки сообщают , какие файлы нужно проверить, прежде чем решить восстановить цель.
Фальшивые цели
Такие цели, как или , не представляют собой файлы., чтобы не допустить , чтобы не путать их с именами файлов, они должны быть объявлены как фальшивка :
.PHONY: clean all
Если бы не было этого, то если бы существовал файл с именем , , , он бы считал его актуальным и пропускал рецепт.
Эффективное управление зависимостями
Руководство Makefile выше имеет серьезный недостаток: зависимость от является правильной, но как насчет заголовков? Если изменяется, (который включает его) должен быть перестроен, но правило говорит, что это зависит только от .
Автоматическая генерация зависимостей с GCC
GCC (и Clang) могут генерировать информацию о зависимости с использованием семейства флагов . Наиболее практичной комбинацией для большинства проектов является :
- — пишет файл зависимостей (]) во время компиляции, перечисляя только заголовки, определённые пользователем (не заголовки системы).
- — указывает имя файла зависимости.
- — добавляет фальшивые цели для каждого заголовка, предотвращая ошибки при удалении заголовка.
Вот как включить автоматическое отслеживание зависимостей в Makefile:
CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP
SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)
all: myprogram
myprogram: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
$(OBJDIR)/%.o: $(SRCDIR)/%.c
@mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
-include $(DEPS)
.PHONY: all clean
clean:
rm -rf $(OBJDIR) myprogram
Пояснение:
- собирает все файлы в каталоге источника.
- превращает их в пути объектных файлов.
- перечисляет файлы зависимостей (например, ].
- Правило шаблона компилирует каждый файл и, благодаря , генерирует файл в качестве побочного эффекта.
- Линия читает сгенерированные файлы, превращая их в реальные предпосылки Makefile. Dash подавляет ошибки, когда файлы еще не существуют (например, на первой сборке).
Если же изменяется, то следующее обращение будет автоматически перекомпилироваться , поскольку файл включает в качестве предварительного условия.
Передовые технологии
Безопасное управление генерируемыми зависимостями
Когда заголовок удален, файл может по-прежнему ссылаться на него, в результате чего делает неисправным с недостающим целевым ошибкам. флаг обращается к этому, добавляя пустые фальшивые правила для каждого заголовка зависимостей. делает просто запускает поддельное правило (которое ничего не делает) и продолжается.
Включая файлы зависимостей после изменения источника
Одна тонкость: если исходный файл добавляет или удаляет , соответствующий файл должен быть восстановлен. Поскольку файл сам по себе является предпосылкой объектного файла, сделай заметит измененную временную метку и повторно компилируемый файл, который регенерирует файл. Эта рекурсия работает автоматически, как только первая сборка завершена.
Использование только заказов
Иногда вам нужен каталог, чтобы существовать до строительства, но вы не хотите, чтобы его временная метка запускала восстановление. Это роль предпосылок только порядка (разделено ). В правиле шаблона выше мы использовали внутри рецепта; альтернатива:
$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
$(OBJDIR):
mkdir -p $@
Это гарантирует, что каталог создается до любой компиляции, но изменение самой директории (например, новый файл внутри) не вызовет повторной компиляции.
Лучшие практики управления зависимостями
- Используйте автоматическое генерирование зависимостей с первого дня. Даже для однофайлового проекта это хорошая привычка. Ничего не стоит и предотвращает будущие ошибки.
- Держите файлы зависимостей отдельно от исходных файлов. Поместите их в каталог или . Это облегчает очистку и позволяет избежать загромождения дерева источников.
- Включите сгенерированные файлы после правил, которые их создают. Директива в порядке, но размещение ее после правила шаблона гарантирует, что сначала знает, как создавать объектные файлы, прежде чем пытаться прочитать файлы.
- Использовать фальшивые цели для ведения домашнего хозяйства. , , и — это обычное дело.
- Переносные значения для флагов компилятора, каталогов источников и списков файлов. Это делает Makefile многоразовым в проектах и упрощает настройку.
- Проверьте свой Makefile с преднамеренным изменением заголовка. Измените заголовок, запустите и подтвердите, что только файлы с затронутыми объектами перекомпилированы. Если происходит полная перестройка, что-то не так с отслеживанием зависимостей.
- Просто, но не проще, чем необходимо. Перепроектирование с расширенными функциями, такими как , может сделать отладку болезненной. Начните с шаблона, показанного в этой статье; он хорошо масштабируется до десятков файлов.
Внешние ресурсы
Чтобы углубить свое понимание, обратитесь к этим авторитетным ссылкам:
- GNU Make Manual — окончательное руководство по созданию синтаксиса, функций и расширенных функций.
- GCC Preprocessor Options — Документация для , , и связанных с ними флагов.
- Makefile Tutorial by Chase Lambert — практичное, хорошо структурированное учебное пособие, охватывающее многие реальные случаи использования.
Заключение
Управление зависимостями программы C с помощью Makefiles не роскошь, а необходимость для любого проекта, который перерастает один файл. Комбинируя правила шаблона, автоматическое генерирование зависимостей с и тщательное включение сгенерированных файлов , вы можете создать систему сборки, которая является одновременно быстрой и правильной. Приведенные здесь методы устраняют ручное отслеживание, сокращают время сборки и предотвращают тонкие ошибки, вызванные устаревшими объектными файлами. После того, как вы усвоите эти шаблоны, вы никогда не будете писать список зависимостей вручную. Инвестиции в обучение правильному управлению зависимостью окупаются каждый раз, когда вы запускаете и смотрите только измененные файлы компиляции.