Зміст


0. Передмова

У цій книзі я описую свій досвід
роботи з утилітою GNU Make і, зокрема, мою методику підготовки
make-файлів. Я вважаю свою методику досить зручною, оскільки вона
передбачає: Автоматична побудова списку файлів з вихідними текстами,
Автоматичну генерацію залежностей від включаються файлів (за допомогою
компілятора GCC) І "Паралельну" складання налагоджувальної та робочої версій
програми.
Моя
книга побудована дещо незвичним чином. Як правило, книги будуються за
принципом "від простого – до складного". Для новачків це зручно, але може викликати
утруднення у професіоналів. Досвідчений програміст буде змушений "продиратися"
крізь книгу, пропускаючи голови з відомою йому інформацією. Я вирішив побудувати
книгу за іншим принципом. Вся "квінтесенція" книги, її "головна ідея",
міститься у першому розділі. Інші глави носять більш-менш додатковий
характер.

На початку кожної глави я коротко описую, про що в ній буде вестися мова, і
якими знаннями потрібно володіти, щоб успішно сприйняти висловлюваний у розділі
матеріал. Для тих, хто відчуває, що недостатньо добре орієнтується в
предмет розмови, я вказую на додаткові глави, з якими слід
попередньо ознайомитися.

Для роботи я використав GNU Make версії 3.79.1. Деякі старі
версії GNU Make (Наприклад, версія 3.76.1 з дистрибутива Slackware
3.5
) Можуть неправильно працювати з прикладом "традиційного" будови
make-файлу (мабуть, вони "не сприймають" стару форму запису шаблонних
правил).

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-файл побудований "традиційним" способом. Всі вихідні
файли збирається програми знаходяться в одному каталозі:

Передбачається, що для компіляції
програми використовується компілятор GCC, І об'єктні файли мають розширення
.o”. Файл Makefile виглядає так:

#
# example_1-traditional/Makefile
#
# Приклад "традиційного" будови make-файлу
#

iEdit: main.o Editor.o TextLine.o
gcc $^ -o $@

.cpp.o:
gcc -c $<

main.o: main.h Editor.h TextLine.h
Editor.o: Editor.h TextLine.h
TextLine.o: TextLine.h

Перше правило змушує make
перекомпоновувати програму при зміні будь-якого з об'єктних файлів. Друге
правило говорить про те, що об'єктні файли залежать від відповідних вихідних
файлів. Кожна зміна файлу з вихідним текстом буде викликати його
перекомпіляцію. Наступні кілька правил вказують, від яких заголовних
файлів залежить кожен з об'єктних файлів. Такий спосіб побудови make-файлу
мені здається незручним тому що:
Видно, що традиційний спосіб побудови
make-файлів далекий від ідеалу. Єдине, чим цей спосіб може бути зручний –
своєї "сумісністю". Мабуть, з таким make-файлом будуть нормально
працювати навіть самі "давні" або "екзотичні" версії make (Наприклад,
nmake фірми Microsoft). Якщо подібна "сумісність" не потрібна, то
можна сильно полегшити собі життя, скориставшись широкими можливостями
утиліти GNU Make. Спробуємо позбутися недоліків "традиційного"
підходу.

1.3. Автоматична побудова списку
об'єктних файлів

"Ручне" перерахування всіх об'єктних файлів, що входять до
програму – досить нудна робота, яка, на щастя, може бути
автоматизована. Зрозуміло "простий трюк" на зразок:

    iEdit: *.o
gcc $< -o $@

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

Наступний приклад містить
модифіковану версію make-файлу:
Файл Makefile тепер виглядає
так:
    #
# example_2-auto_obj/Makefile
#
# Приклад автоматичної побудови списку об'єктних файлів
#

iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
gcc $^ -o $@

%.o: %.cpp
gcc -c $<

main.o: main.h Editor.h TextLine.h
Editor.o: Editor.h TextLine.h
TextLine.o: TextLine.h


Список об'єктних файлів програми будується автоматично. Спочатку за допомогою
функції wildcard виходить список всіх файлів з розширенням
.cpp", Що знаходяться в директорії проекту. Потім, за допомогою функції
patsubst, Отриманий таким чином список вихідних файлів, перетворюється
в список об'єктних файлів. Make-файл тепер став більш універсальним – з
невеликими змінами його можна використовувати для складання різних програм.

1.4. Автоматична побудова залежностей від
заголовних файлів

"Ручне" перерахування залежностей об'єктних файлів від
заголовних файлів – заняття ще більш обтяжлива і неприємне, ніж "ручне"
перерахування об'єктних файлів. Вказувати такі залежності обов'язково треба – у
процесі розробки програми заголовні файли можуть мінятися досить часто
(Описи класів, наприклад, традиційно розміщуються в заголовних файлах).
Якщо не вказувати залежності об'єктних файлів від відповідних заголовних
файлів, то може скластися ситуація, коли різні об'єктні файли програми
будуть скомпільовані з використанням різних версії одного і того ж
заголовного файлу. А це, у свою чергу, може призвести до часткової або
повної втрати працездатності зібраної програми.

Перерахування залежностей "вручну" вимагає досить кропіткої роботи.
Недостатньо просто відкрити файл з вихідним текстом і перерахувати імена всіх
заголовних файлів, що підключаються за допомогою #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, Я віддаю перевагу першому тому
що:


Після того як файли залежностей сформовані, потрібно зробити їх доступними
утиліті make. Цього можна домогтися за допомогою директиви include.

    include $(wildcard *.d) 

Зверніть увагу на використання функції wildcard. Конструкція

    include *.d 
буде правильно працювати тільки в тому випадку, якщо в
каталозі буде перебувати хоча б один файл з розширенням ".d". Якщо
таких файлів немає, то make аварійно завершиться, тому що потерпить невдачу
при спробі "побудувати" ці файли (у неї ж немає на цей рахунок ні яких
інструкцій!). Якщо ж використовувати функцію wildcard, То за відсутності
шуканих файлів, ця функція просто поверне пустий рядок. Далі, директива
include з аргументом у вигляді пустого рядка, буде проігнорована, не
викликаючи помилки. Тепер можна скласти новий варіант make-файлу для мого
"Гіпотетичного" проекту:
Ось як виглядає Makefile з
цього прикладу:
    #
# example_3-auto_depend/Makefile
#
# Приклад автоматичної побудови залежностей від заголовних файлів
#

iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp))
gcc $^ -o $@

%.o: %.cpp
gcc -c -MD $<

include $(wildcard *.d)

Після завершення роботи make директорія
проекту буде виглядати так:
Файли з розширенням ".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

При перейменування або видалення якого-небудь "популярного" заголовного
файлу, можна просто заново перезібрати проект, видаливши попередньо всі
об'єктні файли і файли залежностей.

1.5. "Рознесення" файлів з вихідними текстами по
директорій

Наведений у попередньому параграфі make-файл цілком
працездатний і з успіхом може бути використаний для складання невеликих програм.
Проте, із збільшенням розміру програми, стає не дуже зручним зберігати всі
файли з вихідними текстами в одному каталозі. У такому випадку я вважаю за краще
"Розносити" їх по різних директоріях, що відображає логічну структуру проекту.
Для цього потрібно трохи модифікувати make-файл. Щоб неявне правило
   %.o: %.cpp
gcc -c $<

залишилося працездатним, я використовую змінну VPATH, В якій
перераховуються всі директорії, де можуть розташовуватися вихідні тексти. У
наступному прикладі я помістив файли Editor.cpp і Editor.h в каталог
Editor, А файли TextLine.cpp і TextLine.h в каталог
TextLine:

Ось як виглядає Makefile для
цього прикладу:
    #
# example_4-multidir/Makefile
#
# Приклад "рознесення" вихідних текстів з різних директорій
#

source_dirs := . Editor TextLine

search_wildcards := $(addsuffix /*.cpp,$(source_dirs))

iEdit: $ (notdir $ (patsubst%. cpp,%. o, $ (wildcard $ (search_wildcards))))
gcc $^ -o $@

VPATH := $(source_dirs)

%.o: %.cpp
gcc -c -MD $(addprefix -I,$(source_dirs)) $<

include $(wildcard *.d)


У порівнянні з попереднім варіантом make-файлу він зазнав наступні
зміни:


1.6. Збірка програми з різними параметрами
компіляції

Часто виникає необхідність в отриманні кількох варіантів
програми, які були скомпільовані по-різному. Типовий приклад – отладочная
і робоча версії програми. У таких випадках я використовую просту методику:
Для кожної
конфігурації програми я роблю маленький командний файл, який викликає
make з потрібними параметрами:
Файли make_debug і
make_release – Це командні файли, використовувані для збірки
відповідно налагоджувальної та робочої версій програми. Ось, наприклад, як
виглядає командний файл make_release:
 make compile_flags = "-O3-funroll-loops-fomit-frame-pointer" 
Зверніть
увагу, що рядок зі значенням змінної compile_flags укладена в
лапки, оскільки вона містить прогалини. Командний файл make_debug виглядає
аналогічно:
    make  compile_flags="-O0 -g" 

Ось як виглядає Makefile для цього прикладу:

    #
# example_5-multiconfig/Makefile
#
# Приклад отримання декількох версій програми за допомогою одного make-файлу
#

source_dirs := . Editor TextLine

search_wildcards: = $ (addsuffix / *. cpp, $ (source_dirs))
override compile_flags += -pipe

iEdit: $ (notdir $ (patsubst%. cpp,%. o, $ (wildcard $ (search_wildcards))))
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:


Головна складність полягала в тому, щоб змусити програму 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 поточною. Прапорець
–makefile вкаже програмі make, Де знаходиться make-файл проекту.
По відношенню до "поточної" директорії release, Він буде розташовуватися в
"Батьківському" каталозі.

Командний файл для збірки отладочного варіанту програми (make_debug)
виглядає аналогічно. Різниця тільки в імені директорії, куди містяться
результати компіляції (debug) І іншому наборі прапорів компіляції:

    mkdir   debug
make compile_flags="-O0 -g"
–directory=debug
–makefile=../Makefile
Ось остаточна версія make-файлу для
збірки "гіпотетичного" проекту текстового редактора:
    #
# example_6-multiconfig-multidir/Makefile
#
# Приклад "рознесення" різних версій програми з окремих директорій
#

program_name := iEdit
source_dirs := . Editor TextLine

source_dirs := $(addprefix ../,$(source_dirs))
search_wildcards := $(addsuffix /*.cpp,$(source_dirs))

$ (Program_name): $ (notdir $ (patsubst%. Cpp,%. O, $ (wildcard $ (search_wildcards))))
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
директорія з останнім прикладом виглядає так:

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

У цій главі я виклав свою методику роботи з make-файлами. Інші глави
носять більш-менш "додатковий" характер.


2. GNU Make

У цьому розділі я коротко опишу деякі
можливості програми GNU Make, Якими я користуюсь при написанні своїх
make-файлів, а також вкажу на її відмінності від "традиційних" версій make.
Передбачається, що ви знайомі з принципом роботи подібних програм. В іншому
випадку спочатку прочитайте главу 3 – Утиліта
make
.

GNU Make – Це версія програми make поширювана Фондом
Вільного Програмного Забезпечення
(Free Software Foundation
FSF) У рамках проекту GNU ( www.gnu.org ).
Отримати найсвіжішу версію програми та документації можна на "домашній
сторінці "програми www.gnu.org/software/make
або на сторінці Paul D. Smith – Одного з авторів GNU
Make
(
www.paulandlesley.org/gmake).

Програма GNU Make має дуже детальну і добре написану
документацію, з якою я настійно рекомендую ознайомитися. Якщо у вас немає
доступу в інтернет, то користуйтеся документацією у форматі Info, Яка
повинна бути в складі вашого дистрибутива Linux. Будьте обережні з
документацією у форматі man-сторінки (man make) – Як правило, вона
містить лише уривчасту і сильно застарілу інформацію.

2.1. Дві різновиди змінних

GNU
Make
підтримує два способи завдання змінних, які кілька
розрізняються за змістом. Перший спосіб – традиційний, за допомогою оператора
=“:
    compile_flags = -O3 -funroll-loops -fomit-frame-pointer 
Такий
спосіб підтримують всі варіанти утиліти make. Його можна порівняти,
наприклад, із завданням макросу в мові Сі.
 # Define compile_flags "-O3-funroll-loops-fomit-frame-pointer" 
Значення
змінної, заданої за допомогою оператора "=", Буде обчислено в момент її
використання. Наприклад, при обробці make-файлу:
    var1 = one
var2 = $(var1) two
var1 = three

all:
@echo $(var2)

на екран буде видана рядок "three two". Значення
змінної var2 буде обчислено безпосередньо в момент виконання
команди echo, І буде являти собою поточне значення
змінної var1, До якого додано рядок ” two”. Як наслідок
– Одна і та ж змінна не може одночасно фігурувати в лівій і правій
частині виразу, так як це може призвести до нескінченної рекурсії. GNU
Make
розпізнає подібні ситуації і перериває обробку make-файлу.
Наступний приклад викличе помилку:
    compile_flags = -pipe $(compile_flags)

GNU Make підтримує також і другий, новий спосіб завдання змінної
– За допомогою оператора ":=“:

    compile_flags := -O3 -funroll-loops -fomit-frame-pointer 
У цьому
випадку змінна працює подібно "звичайним" текстовим змінним у якому-небудь
з мов програмування. Ось приблизний аналог цього виразу мовою
C++:
 string compile_flags = "-O3-funroll-loops-fomit-frame-pointer"; 
Значення
змінної обчислюється в момент обробки оператора присвоювання. Якщо,
наприклад, записати
    var1 := one
var2 := $(var1) two
var1 := three

all:
@echo $(var2)

то при обробці такого make-файла на екран буде
видана рядок "one two".

Змінна може "міняти" свою поведінку в залежності від того, який з
операторів присвоювання був до неї застосований останнім. Одна і та ж змінна на
Протягом свого життя цілком може вести себе і як "макрос" і як "текстова
змінна ".

Всі свої make-файли я пишу з застосуванням оператора ":=". Цей спосіб
здається мені більш зручним і надійним. До того ж, це більш ефективно, так як
значення змінної не обчислюється заново кожного разу при її використанні.
Детальніше про два способи завдання змінних можна прочитати в документації на
GNU Make в розділі "The
Two Flavors of Variables
” .

2.2. Функції маніпуляції з текстом


Утиліта GNU Make містить велику кількість корисних функцій,
маніпулюють текстовими рядками й іменами файлів. Зокрема у своїх
make-файлах я використовую функції addprefix, addsuffix,
wildcard, notdir і patsubst. Для виклику функцій
використовується синтаксис

 $ (Імя_функціі параметр1, параметр2 …) 

Функція addprefix розглядає другий параметр як список слів
розділених пробілами. На початку кожного слова вона додає рядок, передану
їй в якості першого параметра. Наприклад, в результаті виконання make-файлу:

    src_dirs := Editor TextLine
src_dirs := $(addprefix ../../, $(src_dirs))

all:
@echo $(src_dirs)

на екран буде виведено
    ../../Editor ../../TextLine 

Видно, що до кожного імені директорії доданий префікс "../../“.
Функція addprefix обговорюється в розділі "Functions
for File Names "керівництва по GNU Make.

Функція addsuffix працює аналогічно функції addprefix, Тільки
додає вказаний рядок в кінець кожного слова. Наприклад, в результаті
виконання make-файлу:

    source_dirs := Editor  TextLine
search_wildcards := $(addsuffix /*.cpp, $(source_dirs))

all:
@echo $(search_wildcards)


на екран буде виведено

    Editor/*.cpp  TextLine/*.cpp 

Видно, що до кожного імені директорії доданий суфікс "/*.cpp“.
Функція addsuffix обговорюється в розділі "Functions
for File Names "керівництва по GNU Make.

Функція wildcard "Розширює" переданий їй шаблон або кілька
шаблонів у список файлів, що задовольняють цим шаблонах. Нехай у директорії
Editor знаходиться файл Editor.cpp, А в директорії TextLine
файл TextLine.cpp:

Тоді в результаті виконання такого
make-файлу:
    search_wildcards := Editor/*.cpp  TextLine/*.cpp
source_files := $(wildcard $(search_wildcards))

all:
@echo $(source_files)


на екран буде виведено

    Editor/Editor.cpp  TextLine/TextLine.cpp 

Видно, що шаблони перетворені в списки файлів. Функція wildcard
детально обговорюється в розділі "The
Function wildcard"Керівництва по GNU Make.

Функція notdir дозволяє "прибрати" з імені файлу ім'я директорії, де
він знаходиться. Наприклад, в результаті виконання make-файлу:

    source_files := Editor/Editor.cpp  TextLine/TextLine.cpp
source_files := $(notdir $(source_files))

all:
@echo $(source_files)


на екран буде виведено

    Editor.cpp TextLine.cpp 
Видно, що з імен файлів прибрані "шляху" до
цих файлів. Функція notdir обговорюється в розділі "Functions
for File Names "керівництва по GNU Make.

Функція patsubst дозволяє змінити зазначеним чином слова,
підходять під шаблон. Вона приймає три параметри – шаблон, новий варіант слова
і вихідну рядок. Вихідна рядок розглядається як список слів, розділених
пропуском. Кожне слово, що підходить під зазначений шаблон, замінюється новим
варіантом слова. У шаблоні може використовуватися спеціальний символ "%", який
означає "будь-яку кількість довільних символів". Якщо символ "%" зустрічається в
новому варіанті слова (другому параметрі), то він замінюється текстом,
відповідним символу "%" у шаблоні. Наприклад, в результаті виконання
make-файлу:

    source_files := Editor.cpp  TextLine.cpp
object_files := $(patsubst %.cpp, %.o, $(source_files))

all:
@echo $(object_files)


на екран буде виведено

    Editor.o  TextLine.o 

Видно, що у всіх словах закінчення ".cpp"Замінено на".o“.
Функція patsubst має другий, більш короткий варіант запису для тих
випадків, коли треба змінити суфікс слова (наприклад, замінити розширення в
імені файлу). Більш короткий варіант виглядає так:

 $ (Ім'я_змінної:. Старий_суффікс =. новий_суффікс) 
Застосовуючи
"Короткий" варіант запису попередній приклад можна записати так:
    source_files := Editor.cpp  TextLine.cpp
object_files := $(source_files:.cpp=.o)

all:
@echo $(object_files)


Функція patsubst обговорюється в розділі "Functions
for String Substitution and Analysis "керівництва по GNU Make.

2.3. Новий спосіб завдання шаблонних правил

У
"Традиційних" варіантах make шаблонне правило задається за допомогою
конструкцій, на зразок:
    .cpp.o:
gcc $^ -o $@
Тобто під дію правила потрапляють файли з
певними розширеннями (".cpp"І".o"В даному випадку).

GNU Make підтримує більш універсальний підхід – з використанням
шаблонів імен файлів. Для завдання шаблону використовується символ “%”,
який означає "послідовність будь-яких символів довільної довжини". Символ
“%” в правій частині правила замінюється текстом, який відповідає
символу “%” в лівій частині. Користуючись новою формою запису, наведений
вище приклад можна записати так:

    %.o: %.cpp
gcc $^ -o $@

У своїх make-файлах я користуюся новою формою запису шаблонних правил, тому
що вважаю її більш зручною (шаблонні і нешаблонний правила тепер мають
аналогічний синтаксис) і універсального (можна задавати не тільки файли,
відрізняються своїми розширеннями).

2.4. Мінлива VPATH

За допомогою змінної
VPATH можна задати список каталогів, де шаблонні правила будуть шукати
залежності. У наступному прикладі:
    VPATH := Editor TextLine

%.o: %.cpp
gcc -c $<

make буде шукати файли з розширенням
.cpp"Спочатку в поточному каталозі, а потім, при необхідності, в
підкаталогах Editor і TextLine. Я часто використовую подібну
можливість, так як вважаю за краще розташовувати вихідні тексти в ієрархії
каталогів, що відбивають логічну структуру програми.

Мінлива VPATH описується в розділі "VPATH: Search Path for All
Dependencies "керівництва по GNU Make. На сторінці Paul D. Smith
є стаття під назвою "How Not to Use VPATH" (
paulandlesley.org / gmake / vpath.html), в якій обговорюється "неправильний"
стиль використання змінної VPATH.

2.5. Директива override

Змінні в GNU
Make
можуть створюватися і отримувати своє значення різними способами:
Останній випадок вважається "спеціальним". Якщо змінна задана через
командний рядок, то всередині make-файлу не можна змінити її значення "звичайним"
способом. Розглянемо простий make-файл:
    compile_flags := -pipe $(compile_flags)

all:
echo $(compile_flags)

Припустимо, що змінна
compile_flags була задана через командний рядок при запуску програми
make:
    make  compile_flags="-O0 -g" 
У результаті обробки make-файла на
екран буде виведено рядок:
    -O0 -g 
Тобто спроба змінити значення змінної
compile_flags всередині make-файлу була проігнорована. Якщо все-таки
виникає необхідність у зміні змінної, яка була задана за допомогою
командного рядка, потрібно використовувати директиву override. Директива
поміщається перед ім'ям змінної, яка повинна бути змінено:
    override compile_flags := -pipe $(compile_flags)

all:
echo $(compile_flags)

Тепер в результаті обробки make-файла на
екран буде видана рядок:
    -pipe -O0 -g 

2.6. Додавання тексту в рядок

Часто виникає
необхідність додати текст до існуючої змінної. Для цієї мети служить
оператор "+=". Додається текст може бути як текстової константою, так
й мати посилання на інші змінні:
    compile_flags += -pipe
compile_flags += $(flags)

При використанні цього оператора, "тип" змінної (див. розділ 2.1 "Дві
різновиди змінних ") не змінюється -" макроси "залишаються" макросами ", а
"Текстові змінні" як і раніше залишаються такими.

Якщо змінна задана за допомогою командного рядка, то як і раніше для
зміни її значення всередині make-файла потрібно використовувати директиву
override. У наступному прикладі передбачається, що змінна
compile_flags задана в командному рядку:

    override compile_flags += -pipe
override compile_flags += $(flags)

2.7. Директива include

За допомогою директиви
include можна включати в оброблюваний make-файл інші файли. Працює
вона аналогічно директиві #include в мовах C і C + +. Коли зустрічається ця
директива, обробка "поточного" make-файлу припиняється і make
тимчасово "переключається" на обробку зазначеного в директиві файлу. Директива
include може виявитися корисною для включення в make-файл будь-яких
"Загальних", або які розсилаються іншими програмами фрагментів.

У директиві include можуть бути зазначені одне або декілька імен файлів,
розділених пробілами. В якості імен файлів можна використовувати шаблони:

    include common.mak

include main.d Editor.d TextLine.d

include *.d

Зазначені в директиві файли повинні існувати – інакше
make зробить спробу "створити" їх, а при неможливості цього
досягти, видасть повідомлення про помилку. Директива include з порожнім
списком файлів:
    include 
просто ігнорується.

2.8. Автоматичні змінні

Програма GNU
Make
підтримує велику кількість автоматичних змінних. У своїх
make-файлах я використовую наступні автоматичні змінні:















Ім'я автоматичної змінної Значення
$@ Ім'я мети оброблюваного правила
$< Ім'я першої залежності оброблюваного правила
$^ Список всіх залежностей оброблюваного правила

Повний список автоматичних змінних наводиться в розділі "Automatic
Variables "керівництва по GNU Make.

2.9. "Комбінування" правил

У make-файлі
можуть зустрічатися декілька правил, що мають однакову мету. У такому випадку вони
як би "комбінуються разом". Наприклад, наступні два правила:
    TextLine.o: TextLine.cpp
gcc -c $<

TextLine.o: TextLine.h

еквівалентні правилом:
    TextLine.o: TextLine.cpp TextLine.h
gcc -c $<

Шаблонні і нешаблонний правила також можуть "комбінуватися":

    %.o: %.cpp
gcc -c $<

TextLine.o: TextLine.h


Зверніть увагу на те, що в обох прикладах тільки в одному з правил
вказані виконувані команди – саме вони і будуть при необхідності виконуватися.
При наявності команд в обох правилах, make видасть попередження
повідомлення і "в розрахунок" будуть прийматися тільки команди з останнього правила.

2.10. Make-файл, використовуваний за замовчуванням

Якщо при виклику програми GNU Make не вказувати явно, який
make-файл слід обробляти, то вона намагається знайти і обробити файли
GNUmakefile, makefile і Makefile (Саме в такому порядку).
Керівництво по GNU Make рекомендує ім'я Makefile для make-файлів,
використовуваних за замовчуванням. При "алфавітній" сортування імен файлів в директорії,
таке ім'я буде розташовуватися ближче до початку списку.

2.11. Спеціальна мета .PHONY

У
традиційних реалізаціях, у програми make немає надійного способу дізнатися,
чим саме є мета, зазначена у правилі. Мета може бути як ім'ям
дії, так і ім'ям файлу. Виходячи тільки з "зовнішнього вигляду" правила,
розрізнити ці випадки неможливо. Утиліта make просто шукає на диску файл з
ім'ям, яке зазначено в якості мети. Якщо такий файл існує, то мета
вважається ім'ям файлу.

Для цілей, які є іменами дій, такий підхід не дуже хороший.
По-перше, ім'я такої мети може випадково співпасти з ім'ям якого-небудь файлу або
директорії. І, по-друге, make просто нераціонально витрачає свій час,
займаючись пошуком неіснуючих файлів.

У утиліті GNU Make є спосіб явного оголошення цілей
абстрактними. Для цього використовується механізм "спеціальних цілей".
Спеціальна мета – Це ім'я, яке має спеціальне значення, коли
використовується в якості мети. Для того, наприклад, щоб оголосити перераховані
мети абстрактними, досить помістити їх у правило зі спеціальною метою
.PHONY. У наступному прикладі мета clean оголошується абстрактною:

    .PHONY: clean

clean:
rm *.o *.d

Всі можливі спеціальні цілі описані в главі Special
Built-in Target Names "керівництва по GNU Make.

3. Утиліта make

Утиліта make, Що входить до
склад практично всіх Unix-Подібних операційних систем – це
традиційний засіб, що застосовується для складання програмних проектів. Вона є
універсальною програмою для вирішення широкого кола завдань, де одні файли повинні
автоматично оновлюватися при зміні інших файлів.

При запуску програма make читає файл з описом проекту (make-файл)
і, інтерпретуючи його вміст, робить необхідні дії. Файл з
описом проекту являє собою текстовий файл, де описані відносини між
файлами проекту, і дії, які необхідно виконати для його складання.

3.1. Правила


Основним "будівельним елементом" make-файлу є правила
(rules). У загальному вигляді правило виглядає так:

 <Цель_1> <цель_2> … <Цель_n>: <завісімость_1> <завісімость_2> … <Завісімость_n>
<Команда_1>
<Команда_2>

<Команда_n>
Мета (target) – Це своєрідний бажаний
результат, спосіб досягнення якого описаний в правилі. Мета може
являти собою ім'я файлу. У цьому випадку правило описує, яким чином
можна отримати нову версію цього файлу. У наступному прикладі:
    iEdit: main.o Editor.o TextLine.o
gcc main.o Editor.o TextLine.o -o iEdit

метою є файл iEdit (Виконуваний файл програми). Правило
описує, яким чином можна отримати нову версію файлу iEdit
(Скомпонувати з перерахованих об'єктних файлів).

Мета також може бути ім'ям певної дії. У такому випадку
правило описує, яким чином здійснюється вказане діяння. У наступному
прикладі метою є дія clean (Очищення).

    clean:
rm *.o iEdit
Подібного роду мети називаються псевдоцелі
(pseudotargets) Або абстрактні цілі (phony targets).

Залежність (dependency) – Це такі собі "вихідні дані",
необхідні для досягнення зазначеної в правилі мети. Можна сказати що
залежність – Це "попередня умова" для досягнення мети.
Залежність може представляти собою ім'я файлу. Цей файл повинен
існувати, для того щоб можна було досягти зазначеної мети. У наступному
правилі:

    iEdit: main.o Editor.o TextLine.o
gcc main.o Editor.o TextLine.o -o iEdit

файли main.o, Editor.o і TextLine.o є
залежностями. Ці файли повинні існувати для того, щоб стало
можливим досягнення мети – побудови файлу iEdit.

Залежність також може бути ім'ям певної дії. Ця дія
повинно бути попередньо виконано перед досягненням зазначеної в правилі мети.
У наступному прикладі залежність clean_obj є ім'ям дії
(Видалити об'єктні файли програми):

    clean_all:  clean_obj
rm iEdit

clean_obj:
rm *.o


Для того щоб мета clean_all була досягнута, потрібно спочатку виконати
дію (досягти мети) clean_obj.

Команди – Це дії, які необхідно виконати для оновлення
або досягнення мети. У наступному прикладі:

    iEdit: main.o Editor.o TextLine.o
gcc main.o Editor.o TextLine.o -o iEdit

командою є виклик компілятора GCC. Утиліта make
відрізняє рядки, що містять команди, від інших рядків make-файлу за наявністю
символу табуляції (символу з кодом 9) на початку рядка. У наведеному вище
прикладі рядок:

    gcc  main.o Editor.o TextLine.o -o iEdit
повинна починатися з
символу табуляції.

3.2. Алгоритм роботи make

Типовий
make-файл проекту містить кілька правил. Кожне з правил має деяку
мета і деякі залежності. Сенсом роботи make є досягнення
мети, яку вона вибрала як головної мети (default goal).
Якщо головна мета є ім'ям дії (тобто абстрактної метою), то сенс
роботи make полягає у виконанні відповідної дії. Якщо ж
головна мета є ім'ям файлу, то програма make повинна побудувати
саму "свіжу" версію вказаного файлу.

3.2.1 Вибір головної мети


Головна мета може бути прямо зазначена в командному рядку при запуску
make. У наступному прикладі make буде прагнути досягти мети
iEdit (Отримати нову версію файлу iEdit):

    make iEdit
А в цьому прикладі make повинна досягти мети
clean (Очистити директорію від об'єктних файлів проекту):
    make clean
Якщо не вказувати який-небудь мети в командному рядку,
то make вибирає в якості головної першу, зустрінуту в make-файлі
мета. У наступному прикладі:
    iEdit: main.o Editor.o TextLine.o
gcc main.o Editor.o TextLine.o -o iEdit

main.o: main.cpp
gcc -c main.cpp

Editor.o: Editor.cpp
gcc -c Editor.cpp

TextLine.o: TextLine.cpp
gcc -c TextLine.cpp

clean:
rm *.o

з чотирьох перерахованих в make-файлі цілей (iEdit,
main.o, Editor.o, TextLine.o, clean) За умовчанням в
Як головну буде обрана мета iEdit. Схематично, "верхній рівень"
алгоритму роботи make можна представити так:
    make()
{
главная_цель = ВибратьГлавнуюЦель ()

ДостічьЦелі (главная_цель)
}


3.2.2 Досягнення мети

Після того як
головна мета обрана, make запускає "стандартну" процедуру
досягнення мети. Спочатку в make-файлі шукається правило, яке описує спосіб
досягнення цієї мети (функція НайтіПравіло). Потім, до знайденого правилом
застосовується звичайний алгоритм обробки правил (функція
ОбработатьПравіло).
 ДостічьЦелі (Мета)
{
правило = НайтіПравіло (Мета)

ОбработатьПравіло (правило)
}


3.2.3 Обробка правил

Обробка
правила поділяється на два основних етапи. На першому етапі обробляються всі
залежно, Перераховані в правилі (функція
ОбработатьЗавісімості). На другому етапі приймається рішення – чи потрібно
виконувати зазначені в правилі команди (функція НужноВиполнятьКоманди).
При необхідності, перераховані в правилі команди виконуються (функція
ВиполнітьКоманди).
 ОбработатьПравіло (Правило)
{
ОбработатьЗавісімості (Правило)

якщо НужноВиполнятьКоманди (Правило)
{
ВиполнітьКоманди (Правило)
}
}


3.2.4 Обробка залежностей

Функція ОбработатьЗавісімості по черзі перевіряє всі перераховані
в правилі залежності. Деякі з них можуть виявитися цілями
яких-небудь правил. Для цих залежностей виконується звичайна процедура
досягнення мети (функція ДостічьЦелі). Ті залежності, які не
є цілями, вважаються іменами файлів. Для таких файлів перевіряється факт їх
наявності. При їх відсутності, make аварійно завершує роботу з повідомленням
про помилку.
 ОбработатьЗавісімості (Правило)
{
цикл від i = 1 до Правіло.чісло_завісімостей
{
якщо ЕстьТакаяЦель (Правіло.завісімость [i])
{
ДостічьЦелі (Правіло.завісімость [i])
}
інакше
{
ПроверітьНалічіеФайла (Правіло.завісімость [i])
}
}
}

3.2.5 Обробка команд

На стадії
обробки команд вирішується питання – чи потрібно виконувати описані в правилі
команди чи ні. Вважається, що потрібно виконувати команди якщо:
В іншому
випадку (якщо жодна з вищенаведених умов не виконується) описані в
правилі команди не виконуються. Алгоритм прийняття рішення про виконання команд
схематично можна представити так:
 НужноВиполнятьКоманди (Правило)
{
якщо Правило.Цель.ЯвляетсяАбстрактной ()
return true

/ / Мета є ім'ям файлу

якщо ФайлНеСуществует (Правіло.Цель)
return true

цикл від i = 1 до Правіло.Чісло_завісімостей
{
якщо Правіло.Завісімость [i]. ЯвляетсяАбстрактной ()
return true
інакше
/ / Залежність є ім'ям файлу
{
якщо ВремяМодефікаціі (Правіло.Завісімость [i])>
ВремяМодефікаціі (Правіло.Цель)
return true
}
}

return false
}


3.3. Абстрактні цілі та імена файлів

Яким чином make відрізняє імена дій від імен файлів?
Традиційні варіанти make надходять просто. Спочатку шукається файл з таким
ім'ям. Якщо файл знайдений, то вважається що мета чи залежність є ім'ям
файлу.

В іншому випадку вважається, що це ім'я є або ім'ям
неіснуючого файлу, або ім'ям дії. Відмінності між цими двома
варіантами не робиться, оскільки обидва випадки обробляються однаково.

Подібний підхід не дуже хороший з наступних міркувань. По-перше,
утиліта make не дуже раціонально витрачає час, займаючись пошуком
неіснуючих імен файлів, які насправді є іменами дій.
По-друге, при подібному підході, імена дій не повинні збігатися з іменами
будь-яких файлів або директорій. Інакше такий алгоритм дасть збій, і make-файл
буде працювати неправильно.

Деякі версії make пропонують свої варіанти вирішення цієї проблеми.
Так, наприклад, в утиліті GNU Make є механізм (спеціальна
мета
.PHONY), За допомогою якого можна вказати, що це ім'я
є ім'ям дії.

3.4. Приклад роботи make

Розглянемо, як
утиліта make буде обробляти такий make-файл:
    iEdit: main.o Editor.o TextLine.o
gcc main.o Editor.o TextLine.o -o iEdit

main.o: main.cpp
gcc -c main.cpp

Editor.o: Editor.cpp
gcc -c Editor.cpp

TextLine.o: TextLine.cpp
gcc -c TextLine.cpp

clean:
rm *.o

Припустимо, що в директорії з проектом знаходяться наступні
файли:
Припустимо також, що програма make була
викликана наступним чином:
    make
Мета не вказана в командному рядку, тому запускається
алгоритм вибору мети (функція ВибратьГлавнуюЦель). Головною метою
стає файл iEdit (Перша мета з першого правила).

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*