Сборочные файлы в Linux: обзор

ОГЛАВЛЕНИЕ

Данная статья делает упор на приложениях C и на том, как использовать команду make и makefile(сборочный файл) для их компоновки.

• Скачать примеры в формате ZIP - 9.45 Кб

Введение

Маленькие приложения C/C++ с парой модулей просты в управлении. Разработчики могут легко перекомпилировать их путем прямого вызова компилятора, передав исходные файлы как аргументы - это простой подход. Однако если проект становится слишком сложным с множеством исходных файлов, требуется инструмент, позволяющий разработчику управлять проектом.

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

Данная статья – не полное руководство, она делает упор на приложениях C и на том, как использовать команду make и makefile(сборочный файл) для их компоновки. Есть файл zip с множеством примеров в структуре каталога. Самые важные файлы в примерах – сборочные файлы, а не исходный код C. Скачайте файл с примерами и разархивируйте его с помощью команды unzip или любого другого излюбленного инструмента. Итак, для лучшего понимания этой статьи:

1. Скачайте make_samples.zip.
2. Откройте сеанс работы с терминалом.
3. Создайте каталог в вашем личном каталоге.
4. Переместите make_samples.zip в созданный каталог.
5. Разархивируйте его: unzip make_samples.zip.

Инструмент Make: обзор синтаксиса

Синтаксис команды make такой:

make [параметры] [цель]

Можно ввести с клавиатуры make –help, чтобы увидеть все параметры, поддерживаемые командой make. Объяснение всех этих параметров не входит в статью. Главный вопрос - структура сборочного файла, и как он работает. Цель - метка (или определенное имя), имеющаяся в сборочном файле. Она будет описана позже в статье.

make требует сборочного файла, сообщающего, как приложение должно быть скомпоновано. Сборочный файл часто хранится в том же каталоге, что и другие исходные файлы, и может иметь любое желаемое имя. Например, если сборочный файл называется run.mk, то для выполнения команды make введите с клавиатуры:

make -f run.mk

Параметр -f сообщает команде make имя сборочного файла, который надо обработать.

Есть два специальных имени, делающих параметр –f ненужным: makefile и Makefile. При выполнении make без передачи имени файла сначала она будет искать файл по имени makefile. Если он не существует, она будет искать файл по имени Makefile. Если в каталоге есть два файла – один по имени makefile, а второй по имени Makefile, и с клавиатуры вводится:

make <enter>

Команда make обработает файл по имени makefile. В таком случае надо использовать параметр –f, чтобы команда make обработала Makefile.


Базовый синтаксис сборочных файлов

Рисунок 1: Основной синтаксис сборочного файла

Сборочный файл состоит из набора целей, зависимостей и правил. Цель большей частью является файлом, подлежащим созданию или обновлению. Цель зависит от набора исходных файлов или даже от других целей, описанных в списке зависимостей. Правила – необходимые команды для создания целевого файла с помощью списка зависимостей.

Как видно на рисунке 1, каждая команда в части Правила должна быть в строках, начинающихся с символа табуляции. Пробел дает ошибки. Также пробел в конце строки правила может заставить make выдать сообщение об ошибке.

Сборочный файл читается командой make, определяющей целевые файлы, подлежащие компоновке, путем сравнения дат и времен (метка времени) исходных файлов в списке зависимостей. Если у какой-либо зависимости изменилась метка времени с момента последней компоновки, команда make выполнит правило, связанное с целью.

Тестирование sample1

Пора сделать простой тест. Исходный код содержит каталог по имени sample1. Есть четыре файла:

• app.c и inc_a.h: простое приложение на C.
• mkfile.r: Правильный сборочный файл (расширение .r означает правильный).
• mkfile.w: Неполный или плохо написанный сборочный файл (расширение .w означает неверный). Это не значит, что есть ошибки синтаксиса.

Итак, имеется:

mkfile.r

# Это очень простой сборочный файл

app: app.c inc_a.h
cc -o app app.c

и

mkfile.w

# Это очень простой сборочный файл

app: app.c
cc -o app app.c

Видимо, разница между ними кажется неважной, но в ряде случаев она важна. Сначала проверяется часть сборочного файла:

• Символ # применяется для вставки комментариев в сборочные файлы. Весь текст от символа комментария до конца строки игнорируется.
• app - цель, исполнимый файл, который должен быть скомпонован.
• app.c и inc_a.h являются зависимостями целевого app (inc_a.h присутствует только в mkfile.r).
• cc -o app app.c – правило, используемое для компоновки цели, учитывающее любые изменения файлов в списке зависимостей.

Следующая последовательность команд демонстрирует их работу (Рисунок 2):

Рисунок 2: Последовательность команд sample1

1. Команда make вызывается для обработки mkfile.r, и правило выполняется для создания исполнимого файла app.
2. app удаляется, чтобы заставить команду make снова скомпоновать app.
3. Теперь команда make вызывается для обработки mkfile.w, и получается такой же результат, как в пункте 1.
4. Команда make вновь вызывается с использованием mkfile.r, но ничего не выполняется, потому что цель новейшая.
5. Такой же результат, как в пункте 4, на этот раз обрабатывается mkfile.w.
6. Команда touch используется для изменения метки времени для inc_a.h, имитируя изменение, сделанное в файле.
7. mkfile.w не узнал изменение в модуле inc_a.h.
8. mkfile.r обрабатывается, как ожидалось.
9. Команда touch вызывается для изменения времени доступа для app.c, имитируя изменение, сделанное в файле.
10. mkfile.w обрабатывается, как ожидалось.
11. Команда touch вызывается для изменения времени доступа для app.c, имитируя изменение, сделанное в файле.
12. mkfile.r обрабатывается, как ожидалось.

Теперь ясно, почему mkfile.w считается плохим или неполным сборочным файлом. Он не учитывает модуль inc_a.h, потому что он не описан в списке зависимостей цели app.

Тестирование sample2

sample2 – еще один пример простого сборочного файла, но на этот раз есть несколько целей. Опять имеются 2 сборочных файла: mkfile.r и mkfile.w, чтобы показать верных и неверный способ написания сборочного файла.

Конечный исполнимый файл (цель app) формируется тремя объектными файлами: main.o, mod_a.o и mod_b.o. Каждый является целью со своими исходными файлами, представляющими его список зависимостей.

Цель app – главная цель, или цель, которая породит главный исполнимый файл. Обратите внимание на список зависимостей app. Они являются именами других целей.

Оба сборочных файла полные. Главное отличие – порядок помещения целей в сборочный файл.

Итак, имеется:

mkfile.r

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

main.o: main.c inc_a.h inc_b.h
cc -c main.c

mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c

mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c

и

mkfile.w

main.o: main.c inc_a.h inc_b.h
cc -c main.c

mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c

mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c

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

Выполняется следующая последовательность команд (Рисунок 3):

Рисунок 3:Последовательность команд sample2

1. Команда make вызывается для обработки mkfile.w, и выполняется только первое правило.
2. Все объектные файлы, полученные в результате предыдущих компоновок, были удалены, чтобы заставить команду make выполнить полную компоновку.
3. Команда make вызывается для обработки mkfile.r, и все модули правильно создаются.
4. app выполняется.
5. Все объекты и исполнимые файлы были удалены, чтобы заставить команду make выполнить полную компоновку.
6. Команда make вновь вызывается для обработки mkfile.w. Но на этот раз цель app передается как аргумент, и все модули правильно создаются.

Что неправильно в mkfile.w? Технически – ничего, если информируется главная цель (рисунок 3 - пункт 6). Но если не информировать цель, команда make прочитает сборочный файл с начала, чтобы найти первую цель для обработки. В случае mkfile.w этой целью является main.o. Цель main.o только говорит make скомпоновать main.o из main.c, inc_a.h и inc_b.h – не надо делать больше ничего связанного. Make не прочитает следующую цель.

Замечание: Прочтение первой цели определяет, как make должна интерпретировать все остальные цели и какого порядка она должна придерживаться в ходе процесса компоновки. Итак, первая цель должна быть главной целью, и она может быть связана с одной и более вторичными целями, чтобы выполнить компоновку.

Цель app размещена в разных строках в обоих файлах компоновки, но их синтаксис идентичен. Пункт 3 и пункт 6 рисунка 3 дадут одинаковый результат:

• цель app говорит команде make, что ей надо сначала обработать три зависимости: main.o, mod_a.o и mob_b.o перед компоновкой конечного исполнимого файла (app).
• Затем make начинает искать цель main.o и обрабатывает ее.
• Затем она находит и обрабатывает mod_a.o.
• В конце обрабатывается mod_b.o.
• Когда все три цели скомпонованы, обрабатывается правило цели app, и создается исполнимый файл app.


Ложные цели, макрос и специальные символы

Иногда target – это не файл, а действие, подлежащее выполнению. Цель, не относящаяся к файлу, называется ложной целью.

Например:

getobj:
mv obj/*.o . 2>/dev/null

Цель getobj перемещает все файлы с расширением .o из каталога obj в текущий каталог – ничего особенного. Но что если в obj нет файла? В этом случае команда mv вернет ошибку, которая передастся команде make.

Замечание: Стандартное поведение команды make – прервать обработку при обнаружении ошибки при выполнении команд в правилах.

Конечно, бывают случаи, когда каталог obj пустой. Как не допустить прерывания команды make при появлении ошибки?

Можно использовать специальный символ - (минус) перед командой mv. Итак:

getobj:
-mv obj/*.o . 2>/dev/null

- Приказывает make игнорировать ошибки. Есть еще один специальный символ: @ приказывает make не печатать команду в стандартный вывод перед выполнением. Можно объединять оба символа всегда перед командой:

getobj:
-@mv obj/*.o . 2>/dev/null

Есть специальная ложная цель по имени all(все), позволяющая сгруппировать несколько главных целей и ложных целей. Ложная цель all часто используется, чтобы направлять команду make при чтении сборочного файла.

Например:

all: getobj app install putobj

Команда make выполнит цели в последовательности: getobj, app, install и putobj. Также команда make поддерживает макросы в сборочных файлах. Можно определить макрос, написав:

MACRONAME=value

и обратиться к значению MACRONAME(макроимя), написав $(MACRONAME) или ${MACRONAME}.

Например:

EXECPATH=./bin

INCPATH=./include

OBJPATH=./obj

CC=cc

CFLAGS=-g -Wall -I$(INCPATH)

При выполнении make заменяет $(MACRONAME) на выделенное определение. Узнав, что такое ложные цели и макросы, переходим к следующему примеру.

Тестирование sample3

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

• include – тут хранятся все файлы .h.
• obj – каталог, куда все объектные файлы перемещаются после компоновки, и откуда они перемещаются перед началом новой компоновки.
• bin – сюда копируются конечные исполнимые файлы.

Исходники .c хранятся вместе со сборочным файлом в корне sample3. Если имя сборочного файла - makefile, то не надо использовать параметр –f в команде make.

Файлы разделены по каталогам для повышения реалистичности примера. Листинг сборочного файла идет ниже:

Рисунок 4:Листинг сборочного файла sample3

Конечно, номеров строк нет в сборочном файле. Они используются здесь для облегчения чтения исходника.

Итак, имеется:

• Строки 7 - 13: определение некоторого макроса:
      - INSTPATH, INCPATH и OBJPATH ссылаются на подкаталоги.
      - CC и CFLAGS - компилятор и наиболее распространенные параметры компилятора соответственно. Если хотите, можете изменить CC, чтобы он указывал на компилятор gcc.
      - COND1 и COND2 – выполняемые команды, затем обращается к макросу.

• Строка 17: ложная цель all как самая первая цель в сборочном файле. Цель all определяет порядок выполнения целей слева направо:
      - За getobj первым идет app (main.o, mod_a.o и mod_b.o являются зависимостями app и будут вызваны при необходимости), затем make вызывает цель install, и в конце выполняется putobj.

• Строки 19-29: перечисляют цели, отвечающие за компоновку самого приложения. Обратите внимание на использование макросов CC и CFLAGS. Помните, макросы заменяются значениями. Так что $(CC) читается как cc, а CFLAGS читается как -g -Wall -I./include. Затем строка 20 интерпретируется как:
      - -g -Wall -I./include -o app main.o mod_a.o mod_b.o

• Строки 31-34: перечисляют цели getobj и putobj. Это ложные цели, помогающие или организующие процесс компоновки:
      - getobj указан в цели all как выполняемый первым. Он отвечает за извлечение объектных файлов из каталога obj ($(OBJPATH)) перед началом компоновки. Команда make может сравнить метки времени у объектов с метками времени у файлов исходного кода и заново скомпоновать или нет объектный файл согласно изменениям в исходных файлах.
      - putobj делает противоположное. После успешной компоновки он перемещает все объектные файлы обратно в каталог obj.

• Строка 38: цель install – еще одна ложная цель, показывающая использование оператора командного процессора if(если). Программирование командного процессора не входит в данную статью. Если вам нужно больше информации, гуглите. Далее в статье объяснено, что делает цель install(установить).

• Строка 47: цель cleanall удаляет все файлы, чтобы заставить команду make скомпоновать все заново. Она не вызывается во время процесса компоновки. Ее можно вызвать путем передачи ее в качестве аргумента команде make:

make cleanall

Также обратите внимание на использование специальных символов (- и @) перед командами в getobj, putobj, install и cleanall. Как сказано выше, - приказывает make продолжить обработку даже при появлении ошибки, а @ приказывает make не печатать команду перед ее выполнением.

Замечание: В цели install каждая строка завершается "\", и символ "\" должен быть последним символом в строке (никаких пробелов после него), иначе make может выдать следующую ошибку:

строка xxx: синтаксическая ошибка: неожиданный конец файла

Где xxx – строка, считающаяся началом блока.

Всегда при группировке команд с помощью \ он должен быть последним символом в строке.

Выполняется следующая последовательность команд (рисунок 5):

Рисунок 5: Последовательность команд sample3.

1. Команда make вызывается без аргументов, так как имя сборочного файла - makefile.
      - Если нет файлов в каталоге obj, getobj выдаст ошибку. Однако символ - (минус) перед командой mv (строка 32) не дает make прервать обработку.
      - Это сообщение печатается, только если условие истинно (строка 39). Обратите внимание на символ @ перед оператором if. Оно запрещает печать всего блока. Условие сравнивает две строки, являющиеся результатом двух команд, определенных макросами COND1 и COND2 (строки 12-13). Условие использует сочетание команд командного процессора, чтобы проверить, различаются ли метки времени app и ./bin/myapp. Если истина – app копируется в ./bin с именем myapp, и его полномочия доступа к файлу меняются, чтобы разрешить доступ к нему только владельцу файла. Если никакого условия не было наложено, то цель install выполняется при каждом вызове make.
2. Команда make вызывается снова, но выполняются только getobj и putobj. Не происходит компоновка и, следовательно, не выполняется install.
3. Команда touch меняет метку времени для inc_a.h
4. Команда make вызывается, и перекомпоновываются только цели с зависимостью inc_a.h.
5. Перечисляется содержимое ./bin. Обратите внимание на полномочия myapp.
6. Пример использования цели cleanall.


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

Для сложного проекта с множеством исходных файлов неудобно создавать цели, представляющие каждый из исходных файлов. Например, если в проекте один исполнимый файл компонуется из 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 был сделан таким, потому что так удобней, и ошибки нет. Если хотите, можете разделить заголовки по модулю.

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


Сборочный файл, вызывающий другие сборочные файлы

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

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

Но есть хитрость, которую надо знать при принуждении команды make переключиться на другой каталог. Например, испытаем простой сборочный файл:

target1:
@pwd
cd dir_test
@pwd

Результат вызова команды make:

Рисунок 11: Неправильный способ изменения текущего каталога

Команда pwd печатает текущий каталог. В данном случае это каталог /root. Заметьте, что вторая команда pwd печатает тот же самый каталог даже после выполнения cd dir_test. Что это значит?

Вы должны знать, что большинство команд командного процессора, таких как cp, mv и так далее, заставляют команду make:

• открыть новый экземпляр командного процессора;
• выполнить команду;
• закрыть экземпляр командного процессора;

По сути, make создает три разных экземпляра командного процессора для обработки каждой из тех команд.

cd dir_test выполнился успешно только во втором экземпляре командного процессора, созданного make.

Решение имеет вид:

target1:
(pwd;cd dir_test;pwd)

Смотрите на результат:

Рисунок 12: Правильный способ изменения текущего каталога

Квадратные скобки гарантируют, что все команды обрабатываются одним командным процессором – команда make запускает только один командный процессор для выполнения всех трех команд.

Представьте, что произошло бы, если бы не использовались квадратные скобки, и сборочный файл был бы:

target1:
cd dir_test
make

Смотрите на результат:

Рисунок 13: Рекурсивный вызов make

При использовании команды make без квадратных скобок она рекурсивно вызывает один и тот же сборочный файл. Например, make[37] означает 37-й экземпляр команды make.

Тестирование sample5

sample5 показывает, как вызвать другие сборочные файлы посредством главного сборочного файла. В каталоге sample5 есть:

• каталог tstlib – содержит исходный код простой библиотеки (tlib.a).
• каталог application(приложение) – содержит исходный код приложения, связанный с tlib.a.
• makefile – главный сборочный файл.
• runmk – командный процессор, вызывающий главный сборочный файл. На самом деле этот командный процессор не требуется, но когда команда make обрабатывает сборочный файл, переключающийся на другой каталог, make показывает раздражающее сообщение, сообщающее, что она входит в или покидает каталог. Используйте --no-print-directory, чтобы избавиться от этих сообщений.

Листинг главного сборочного файла:

COND1=`stat app 2>/dev/null | grep Modify`
COND2=`stat ./application/app 2>/dev/null | grep Modify`

all: buildall getexec

buildall:
@echo "****** Invoking tstlib/makefile"
(cd tstlib; $(MAKE))
@echo "****** Invoking application/makefile"

(cd application; $(MAKE))

getexec:
@if [ "$(COND1)" != "$(COND2)" ];\
then\
echo "Getting new app!";\
cp -p ./application/app . 2>/dev/null;\
chmod 700 app;\
else\
echo "Nothing done!";\
fi

cleanall:
-rm -f app
@echo "****** Invoking tstlib/makefile"
@(cd tstlib; $(MAKE) cleanall)

@echo "****** Invoking appl/makefile"
@(cd application; $(MAKE) cleanall)

Это несложно. 4 ложные цели. Цель all target начинает вызывать цель buildall, и команде make приказывают войти и попытаться скомпоновать два разных проекта. Обратите внимание на макрос $(MAKE), заменяемый словом make. Макрос $(MAKE) стандартный и не требует определения.

Цель cleanall неявно используется для удаления всех объектов и исполнимых файлов. Символ @ перед открывающими квадратными скобками заставляет make не печатать команды.

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

Рисунок 14: Последовательность команд sample5

1. Команда make вызывается через командный процессор runmk для обработки главного сборочного файла.
      - Цель all вызывает цель buildall, и она всегда выполняется, так как является ложной целью. Сначала вызывается сборочный файл в каталоге tstlib, и компонуется tlib.a. Смотрите листинг сборочного файла:

TLIB=tlib.a
OBJS=tstlib_a.o tstlib_b.o
CC=cc
INCPATH=.
CFLAGS=-Wall -I$(INCPATH)

.c.o:
$(CC) $(CFLAGS) -c $<

$(TLIB): $(OBJS)
ar cr $(TLIB) $(OBJS)

$(OBJS): $(INCPATH)/tstlib.h

cleanall:
-rm -f *.o *.a

       - Затем вызывается сборочный файл в каталоге application, и компонуется app. Смотрите листинг сборочного файла:

OBJS=main.o mod_a.o mod_b.o
CC=cc
INCLIB=../tstlib
LIBS=$(INCLIB)/tlib.a
CFLAGS=-Wall -I. -I$(INCLIB)

.c.o:
$(CC) $(CFLAGS) -c $<

app: $(OBJS) $(LIBS)
$(CC) $(CFLAGS) -o app $(OBJS) $(LIBS)

$(OBJS): inc_a.h inc_b.h $(INCLIB)/tstlib.h

cleanall:
-rm -f *.o app

Заметьте, что tlib.a является зависимостью в цели app. Следовательно, всегда, когда tlib.a перекомпоновывается, app приходится снова связывать с ней.

       - Цель getexec была вызвана в главном сборочном файле. Так как исполнимый файл app отсутствует в каталоге sample5, он копируется из каталога application.

2. Команда touch используется для изменения метки времени tstlib/tstlib_b.c, имитируя изменение, сделанное в файле.
3. Команда make вызывается через командный процессор runmk для обработки главного сборочного файла.
       - tlib.a перекомпоновывается.
       - app снова связывается с tlib.a, так как она изменилась.
       - Была вызвана цель getexec в главном сборочном файле. Так как application/app отличается от app, оно обновляется в каталоге sample5.
4. Команда touch используется для изменения метки времени application/inc_a.h, имитируя изменение, сделанное в файле.
5. Команда make вызывается через командный процессор runmk для обработки главного сборочного файла.
       - tlib.a не обрабатывается, потому что она не изменилась.
       - app перекомпоновывается (все модули), потому что inc_a.h является зависимостью всех исходников .c.
       - Была вызвана цель getexec в главном сборочном файле. Так как application/app отличается от app, оно обновляется в каталоге sample5.
6. Цель cleanall выполняется в главном сборочном файле.

Заключение

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