Сборочные файлы в Linux: обзор - Правила суффикса: упрощение синтаксиса сборочных файлов

ОГЛАВЛЕНИЕ

Правила суффикса: упрощение синтаксиса сборочных файлов

Для сложного проекта с множеством исходных файлов неудобно создавать цели, представляющие каждый из исходных файлов. Например, если в проекте один исполнимый файл компонуется из 20 файлов .c, и каждый исходник использует один и тот же набор флагов компилятора при компоновке, то должен быть способ приказать команде make выполнить одну и ту же команду для всех исходных правил.

Такой способ называется правила суффикса, или правила, основанные на расширении файла. Например, следующее правило суффикса

.c.o:
cc -c $<

говорит команде make: если дан файл цели с расширением .o, то должен быть файл зависимости с расширением .c (такое же имя – меняется лишь расширение), подлежащий компоновке. Файл main.c породит файл main.o. Заметьте, что .c.o не цель, а два расширения (.c и .o).

Синтаксис правила суффикса таков:

Рисунок 6: Синтаксис правила суффикса

Специальный символ $< объяснен далее в статье.

Тестирование sample4 - mkfile1

Опробуем sample4. Есть несколько сборочных файлов для тестирования. Первый - mkfile1:

.c.o:
cc -c $<

Он содержит только определение правила суффикса, ничего больше.

Опробуем следующую последовательность команд (рисунок 7):

Рисунок 7: Последовательность команд sample4 - mkfile1

1. Команда make вызывается для обработки сборочного файла mkfile1. Ничего не происходит, потому что нет определенной цели.
2. Команда make снова вызывается, но на сей раз с передачей цели main.o. В этот раз команда make компилирует main.c, следуя правилу суффикса .c.o.

Здесь надо понять один момент. mkfile1 определяет только правило суффикса, никаких целей. Так почему main.c компилируется?

Команда make обработала main.o, словно следующая цель была определена в mkfile1:

main.o: main.c
cc -c main.c

Итак, правило суффикса .c.o говорит команде make: "для каждой цели xxxxx.o должна быть зависимость xxxxx.c для компоновки".

Если бы команда была

make -f mkfile1 mod_x.o

make вернула бы ошибку, потому что нет mod_x.c в каталоге.

3. Команда make вызывается с передачей двух целей.
4. Команда make вызывается с передачей трех целей. Так как эти цели уже были скомпонованы, она не компонует их снова.

Дополнительные специальные символы

Что значит $<, определенный в правиле суффикса? Он означает имя текущей зависимости. В случае правила суффикса .c.o $< заменяется файлом xxxxx.c при выполнении правила. Есть другие:

$? список зависимостей, измененных позже текущей цели
$@ имя текущей цели
$< имя текущей зависимости
$* имя текущей зависимости без расширения

Следующие примеры показывают использование этих символов.

Тестирование sample4 - mkfile2

mkfile2 показывает другой способ использования правил суффикса. Сейчас он только переименовывает файл file.txt в file.log:

.SUFFIXES: .txt .log

.txt.log:
@echo "Converting " $< " to " $*.log
mv $< $*.log

Ключевое слово .SUFFIXES: говорит команде make, какие расширения файлов будут использоваться в сборочном файле. В случае mkfile2 это .txt и .log. Некоторые расширения, такие как .c и .o, являются стандартными и не нуждаются в объявлении с помощью ключевого слова .SUFFIXES:.

Опробуем следующую последовательность команд (рисунок 8):

Рисунок 8: Последовательность команд sample4 - mkfile2

1. Сначала создается file.txt.
2. Команда make вызывается с передачей цели file.log, и выполняется правило.

Правило суффикса .txt.log говорит команде make: "для каждой цели xxxxx.log должна быть зависимость xxxxx.txt для компоновки (в этом случае для переименования)". Это работает, словно цель file.log была определена в mkfile2:

file.log: file.txt
mv file.txt file.log

3. Показывается, что file.txt был переименован в file.log.

Тестирование sample4 - mkfile3 и mkfile4

До сих пор показывалось, как определить правила суффикса, но они не использовались в реальной ситуации. Ниже рассмотрена более реалистичная ситуация с использованием исходного кода C в каталоге sample4.

Сначала опробуем mkfile3:

.c.o: 
@echo "Compiling" $< "..."
cc -c $<

app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."

cc -o app main.o mod_a.o mod_b.o

В начале видно правило суффикса и цель app.

Опробуем следующую последовательность команд (рисунок 9):

Рисунок 9: Последовательность команд sample4 - mkfile3

1. Команда make вызывается для обработки сборочного файла mkfile3. Команда make читает цель app и обрабатывает зависимости: main.o, mod_a.o и mod_b.o, следуя правилу суффикса .c.o, говорящему команде make, что "для каждого xxxxx.o есть зависимость xxxxx.c для компоновки" .
2. Команда make снова вызывается, но ничего не обрабатывается, потому что цель app новейшая.
3. Время доступа main.c обновляется, чтобы заставить команду make перекомпилировать его.
4. Команда make перекомпилировала только main.c, как ожидалось.
5. Команда make вызывается, но ничего не обрабатывается, потому что цель app новейшая.
6. inc_a.h (включенный с помощью main.c и mod_a.c) обновляется, чтобы заставить команду make перекомпилировать эти модули.
7. Команда make вызывается, но ничего не происходит

Что пошло не так в пункте 7? Ничто не говорит команде make, что inc_a.h является зависимостью main.c или mod_a.c.

Решение – прописать зависимости для каждой цели объекта:

.c.o: 
@echo "Compiling" $< "..."
cc -c $<

app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."

cc -o app main.o mod_a.o mod_b.o

main.o: inc_a.h inc_b.h
mod_a.o: inc_a.h
mod_b.o: inc_b.h

Можно изменить и добавить последние три строки и снова опробовать последовательность команд рисунка 9. Не забудьте удалить объекты перед его повторным тестированием:

rm -f *.o app

Добавлять зависимости для каждого модуля, указывающие точный включаемый файл, включаемый каждым отдельным модулем, полезно, но неудобно. Представьте проект с 50 .c и 30 .h. Придется много вводить с клавиатуры!

Более практичное решение показано в mkfile4:

OBJS=main.o mod_a.o mod_b.o

.c.o:
@echo "Compiling" $< "..."
cc -c $<

app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."

cc -o app main.o mod_a.o mod_b.o

$(OBJS): inc_a.h inc_b.h

Опробуем следующую последовательность команд и увидим, что произойдет (рисунок 10):

Рисунок 10: Последовательность команд sample4 - mkfile4

1. Команда make вызывается для обработки сборочного файла mkfile4.
2. Метка времени > mod_a.c обновляется, чтобы заставить команду make перекомпилировать его.
3. Команда make перекомпилировала только mod_a.c, как ожидалось.
4. inc_a.h (включенный с помощью main.c и mod_a.c) обновляется, чтобы заставить команду make перекомпилировать эти модули.
5. Команда make перекомпилировала все модули

Почему mod_b.c перекомпилировался в пункте 5? mkfile4 определяет inc_a.h как зависимость mod_b.c, но это не так. inc_a.h не включается с помощью mod_b.c. По сути, сборочный файл говорит команде make, что inc_a.h и inc_b.h являются зависимостями всех модулей. mkfile4 был сделан таким, потому что так удобней, и ошибки нет. Если хотите, можете разделить заголовки по модулю.

Совет: При работе над большими проектами указывайте в качестве зависимостей только главные заголовочные файлы, то есть заголовки, включаемые с помощью всех (или большинства) модулей.