Синтаксис makefile. Мини-руководство по созданию Makefile-ов. "Разнесение" файлов с исходными текстами по директориям

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

В этой заметке я попытаюсь рассказать как создать makefile.

По умолчанию правила сборки считываются из файла с именем Makefile.

Структуру Makefile можно представить так:

ЦЕЛЬ: ЗАВИСИМОСТЬ ДЕЙСТВИЕ

Но обычно используются более сложные правила, например:

ЦЕЛЬ: ЦЕЛЬ1 ЦЕЛЬ2 ДЕЙСТВИЕ ЦЕЛЬ1: ЗАВИСИМОСТЬ1 ДЕЙСТВИЕ1 ЦЕЛЬ2: ЗАВИСИМОСТЬ2 ДЕЙСТВИЕ2

ЦЕЛЬ — это то что мы получаем в результате ДЕЙСТВИЯ. Это может быть файл, директория или просто абстрактная ЦЕЛЬ не имеющая связи с каким-либо объектом на жестком диске. После имени цели ставится двоеточие. При запуске команды make без параметров выполнится первое найденное правило. Что бы выполнить другое правило надо указать его команде make

Make ЦЕЛЬ2

ЗАВИСИМОСТЬ — это то, от чего зависит наша ЦЕЛЬ. Это могут быть файлы, каталоги или другие ЦЕЛИ. Make сравнивает дату и время изменения ЦЕЛИ и объектов о которых зависит цель. Если объекты от которых зависит цель были изменены позже чем создана цель, то будет выполнено ДЕЙСТВИЕ. ДЕЙСТВИЕ так же выполняется если ЦЕЛЬ не является именем файла или директории.

ДЕЙСТВИЕ — это набор команд которые надо выполнить. Перед командами должен быть введен символ табуляции. Если вместо символа табуляции будут введены пробелы то при компиляции будет выведено сообщение об ошибке:

Makefile:13: *** пропущен разделитель. Останов.

Makefile:13: *** missing separator. Stop.

Пример Makefile:

all: test.elf test.elf: test1.o test2.o gcc -o test.elf test1.o test2.o test1.o test1.c gcc -c test1.c -o test1.o test2.o test2.c gcc -c test2.c -o test2.o

Рассмотри последний пример:
Первым выполняется all т.к. находится в начале Makefile. all зависит от test.elf и файла или директории с именем all не существует, поэтому всегда будет происходить проверка цели с именем test.elf.

test.elf зависит от test1.o и test2.o, по этому сначала будет проверена цель test1.o затем test2.o

При проверке цели test1.o сравниваются дата и время изменения файла test1.o и test1.c. Если файл test1.o не существует или файл test1.c был изменены позднее чем test1.o то будет выполнена команда gcc -с test1.c -o test1.o.

Аналогично будет проверена цель test2.o.

После этого сравниваются дата и время изменения файла test.elf и файлов test1.o test2.o. Если test1.o или test2.o новее то будет выполнена команда gcc -o test.elf test1.o test2.o

Таким образом отслеживаются изменения в файлах test1.с и test2.c.

P/S надеюсь данная заметка упростит создание makefile, но если есть какие-то вопросы — напишите в комментарии, постараюсь ответить.

Меня всегда привлекал минимализм. Идея о том, что одна вещь должна выполнять одну функцию, но при этом выполнять ее как можно лучше, вылилась в создание UNIX. И хотя UNIX давно уже нельзя назвать простой системой, да и минимализм в ней узреть не так то просто, ее можно считать наглядным примером количество- качественной трансформации множества простых и понятных вещей в одну весьма непростую и не прозрачную. В своем развитии make прошел примерно такой же путь: простота и ясность, с ростом масштабов, превратилась в жуткого монстра (вспомните свои ощущения, когда впервые открыли мэйкфайл).

Мое упорное игнорирование make в течении долгого времени, было обусловлено удобством используемых IDE, и нежеланием разбираться в этом "пережитке прошлого" (по сути - ленью). Однако, все эти надоедливые кнопочки, менюшки ит.п. атрибуты всевозможных студий, заставили меня искать альтернативу тому методу работы, который я практиковал до сих пор. Нет, я не стал гуру make, но полученных мною знаний вполне достаточно для моих небольших проектов. Данная статья предназначена для тех, кто так же как и я еще совсем недавно, желают вырваться из уютного оконного рабства в аскетичный, но свободный мир шелла.

Make- основные сведения

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

1) целями (то, что данное правило делает);
2) реквизитами (то, что необходимо для выполнения правила и получения целей);
3) командами (выполняющими данные преобразования).

В общем виде синтаксис makefile можно представить так:

# Индентация осуществляется исключительно при помощи символов табуляции, # каждой команде должен предшествовать отступ <цели>: <реквизиты> <команда #1> ... <команда #n>

То есть, правило make это ответы на три вопроса:

{Из чего делаем? (реквизиты)} ---> [Как делаем? (команды)] ---> {Что делаем? (цели)}
Несложно заметить что процессы трансляции и компиляции очень красиво ложатся на эту схему:

{исходные файлы} ---> [трансляция] ---> {объектные файлы}
{объектные файлы} ---> [линковка] ---> {исполнимые файлы}

Простейший Makefile

Предположим, у нас имеется программа, состоящая всего из одного файла:

/* * main.c */ #include int main() { printf("Hello World!\n"); return 0; }
Для его компиляции достаточно очень простого мэйкфайла:

Hello: main.c gcc -o hello main.c
Данный Makefile состоит из одного правила, которое в свою очередь состоит из цели - «hello», реквизита - «main.c», и команды - «gcc -o hello main.c». Теперь, для компиляции достаточно дать команду make в рабочем каталоге. По умолчанию make станет выполнять самое первое правило, если цель выполнения не была явно указана при вызове:

$ make <цель>

Компиляция из множества исходников

Предположим, что у нас имеется программа, состоящая из 2 файлов:
main.c
/* * main.c */ int main() { hello(); return 0; }
и hello.c
/* * hello.c */ #include void hello() { printf("Hello World!\n"); }
Makefile, выполняющий компиляцию этой программы может выглядеть так:

Hello: main.c hello.c gcc -o hello main.c hello.c
Он вполне работоспособен, однако имеет один значительный недостаток: какой - раскроем далее.

Инкрементная компиляция

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

Теперь, после изменения одного из исходных файлов, достаточно произвести его трансляцию и линковку всех объектных файлов. При этом мы пропускаем этап трансляции не затронутых изменениями реквизитов, что сокращает время компиляции в целом. Такой подход называется инкрементной компиляцией. Для ее поддержки make сопоставляет время изменения целей и их реквизитов (используя данные файловой системы), благодаря чему самостоятельно решает какие правила следует выполнить, а какие можно просто проигнорировать:

Main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c hello: main.o hello.o gcc -o hello main.o hello.o
Попробуйте собрать этот проект. Для его сборки необходимо явно указать цель, т.е. дать команду make hello.
После- измените любой из исходных файлов и соберите его снова. Обратите внимание на то, что во время второй компиляции, транслироваться будет только измененный файл.

После запуска make попытается сразу получить цель hello, но для ее создания необходимы файлы main.o и hello.o, которых пока еще нет. Поэтому выполнение правила будет отложено и make станет искать правила, описывающие получение недостающих реквизитов. Как только все реквизиты будут получены, make вернется к выполнению отложенной цели. Отсюда следует, что make выполняет правила рекурсивно.

Фиктивные цели

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

$ make $ make install
Командой make производят компиляцию программы, командой make install - установку. Такой подход весьма удобен, поскольку все необходимое для сборки и развертывания приложения в целевой системе включено в один файл (забудем на время о скрипте configure). Обратите внимание на то, что в первом случае мы не указываем цель, а во втором целью является вовсе не создание файла install, а процесс установки приложения в систему. Проделывать такие фокусы нам позволяют так называемые фиктивные (phony) цели. Вот краткий список стандартных целей:

  • all - является стандартной целью по умолчанию. При вызове make ее можно явно не указывать.
  • clean - очистить каталог от всех файлов полученных в результате компиляции.
  • install - произвести инсталляцию
  • uninstall - и деинсталляцию соответственно.
Для того чтобы make не искал файлы с такими именами, их следует определить в Makefile, при помощи директивы.PHONY. Далее показан пример Makefile с целями all, clean, install и uninstall:

PHONY: all clean install uninstall all: hello clean: rm -rf hello *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c hello: main.o hello.o gcc -o hello main.o hello.o install: install ./hello /usr/local/bin uninstall: rm -rf /usr/local/bin/hello
Теперь мы можем собрать нашу программу, произвести ее инсталлцию/деинсталляцию, а так же очистить рабочий каталог, используя для этого стандартные make цели.

Обратите внимание на то, что в цели all не указаны команды; все что ей нужно - получить реквизит hello. Зная о рекурсивной природе make, не сложно предположить как будет работать этот скрипт. Так же следует обратить особое внимание на то, что если файл hello уже имеется (остался после предыдущей компиляции) и его реквизиты не были изменены, то команда make ничего не станет пересобирать . Это классические грабли make. Так например, изменив заголовочный файл, случайно не включенный в список реквизитов, можно получить долгие часы головной боли. Поэтому, чтобы гарантированно полностью пересобрать проект, нужно предварительно очистить рабочий каталог:

$ make clean $ make
Для выполнения целей install/uninstall вам потребуются использовать sudo.

Переменные

Все те, кто знакомы с правилом DRY (Don"t repeat yourself), наверняка уже заметили неладное, а именно - наш Makefile содержит большое число повторяющихся фрагментов, что может привести к путанице при последующих попытках его расширить или изменить. В императивных языках для этих целей у нас имеются переменные и константы; make тоже располагает подобными средствами. Переменные в make представляют собой именованные строки и определяются очень просто:

=
Существует негласное правило, согласно которому следует именовать переменные в верхнем регистре, например:

SRC = main.c hello.c
Так мы определили список исходных файлов. Для использования значения переменной ее следует разименовать при помощи конструкции $(); например так:

Gcc -o hello $(SRC)
Ниже представлен мэйкфайл, использующий две переменные: TARGET - для определения имени целевой программы и PREFIX - для определения пути установки программы в систему.

TARGET = hello PREFIX = /usr/local/bin .PHONY: all clean install uninstall all: $(TARGET) clean: rm -rf $(TARGET) *.o main.o: main.c gcc -c -o main.o main.c hello.o: hello.c gcc -c -o hello.o hello.c $(TARGET): main.o hello.o gcc -o $(TARGET) main.o hello.o install: install $(TARGET) $(PREFIX) uninstall: rm -rf $(PREFIX)/$(TARGET)
Это уже посимпатичней. Думаю, теперь вышеприведенный пример для вас в особых комментариях не нуждается.

Автоматические переменные

Автоматические переменные предназначены для упрощения мейкфайлов, но на мой взгляд негативно сказываются на их читабельности. Как бы то ни было, я приведу здесь несколько наиболее часто используемых переменных, а что с ними делать (и делать ли вообще) решать вам:
  • $@ Имя цели обрабатываемого правила
  • $< Имя первой зависимости обрабатываемого правила
  • $^ Список всех зависимостей обрабатываемого правила
Если кто либо хочет произвести полную обфускацию своих скриптов - черпать вдохновение можете здесь:

Мне в свое время очень не хватило подобной методички для понимания базовых вещей о make. Думаю, будет хоть кому-нибудь интересно. Хотя эта технология и отмирает, но все равно используется в очень многих проектах. Кармы на хаб «Переводы» не хватило, как только появится возможность - добавлю и туда. Добавил в Переводы. Если есть ошибки в оформлении, то прошу указать на них. Буду исправлять.

Статья будет интересная прежде всего изучающим программирование на C/C++ в UNIX-подобных системах от самых корней, без использования IDE.

Компилировать проект ручками - занятие весьма утомительное, особенно когда исходных файлов становится больше одного, и для каждого из них надо каждый раз набивать команды компиляции и линковки. Но не все так плохо. Сейчас мы будем учиться создавать и использовать Мейкфайлы. Makefile - это набор инструкций для программы make, которая помогает собирать программный проект буквально в одно касание.

Для практики понадобится создать микроскопический проект а-ля Hello World из четырех файлов в одном каталоге:

main.cpp

#include #include "functions.h" using namespace std; int main(){ print_hello(); cout << endl; cout << "The factorial of 5 is " << factorial(5) << endl; return 0; }


hello.cpp

#include #include "functions.h" using namespace std; void print_hello(){ cout << "Hello World!"; }


factorial.cpp

#include "functions.h" int factorial(int n){ if(n!=1){ return(n * factorial(n-1)); } else return 1; }


functions.h

void print_hello(); int factorial(int n);


Все скопом можно скачать отсюда
Автор использовал язык C++, знать который совсем не обязательно, и компилятор g++ из gcc. Любой другой компилятор скорее всего тоже подойдет. Файлы слегка подправлены, чтобы собирались gcc 4.7.1
Программа make
Если запустить
make
то программа попытается найти файл с именем по умолчание Makefile в текущем каталоге и выполнить инструкции из него. Если в текущем каталоге есть несколько мейкфайлов, то можно указать на нужный вот таким образом:
make -f MyMakefile
Есть еще множество других параметров, нам пока не нужных. О них можно узнать в ман-странице.
Процесс сборки
Компилятор берет файлы с исходным кодом и получает из них объектные файлы. Затем линковщик берет объектные файлы и получает из них исполняемый файл. Сборка = компиляция + линковка.
Компиляция руками
Самый простой способ собрать программу:
g++ main.cpp hello.cpp factorial.cpp -o hello
Каждый раз набирать такое неудобно, поэтому будем автоматизировать.
Самый простой Мейкфайл
В нем должны быть такие части:
цель: зависимости команда
Для нашего примера мейкфайл будет выглядеть так:
all: g++ main.cpp hello.cpp factorial.cpp -o hello
Обратите внимание, что строка с командой должна начинаться с табуляции! Сохраните это под именем Makefile-1 в каталоге с проектом и запустите сборку командой make -f Makefile-1
В первом примере цель называется all . Это цель по умолчанию для мейкфайла, которая будет выполняться, если никакая другая цель не указана явно. Также у этой цели в этом примере нет никаких зависимостей, так что make сразу приступает к выполнению нужной команды. А команда в свою очередь запускает компилятор.
Использование зависимостей
Использовать несколько целей в одном мейкфайле полезно для больших проектов. Это связано с тем, что при изменении одного файла не понадобится пересобирать весь проект, а можно будет обойтись пересборкой только измененной части. Пример:
all: hello hello: main.o factorial.o hello.o g++ main.o factorial.o hello.o -o hello main.o: main.cpp g++ -c main.cpp factorial.o: factorial.cpp g++ -c factorial.cpp hello.o: hello.cpp g++ -c hello.cpp clean: rm -rf *.o hello
Это надо сохранить под именем Makefile-2 все в том же каталоге

Теперь у цели all есть только зависимость, но нет команды. В этом случае make при вызове последовательно выполнит все указанные в файле зависимости этой цели.
Еще добавилась новая цель clean . Она традиционно используется для быстрой очистки всех результатов сборки проекта. Очистка запускается так: make -f Makefile-2 clean

Использование переменных и комментариев
Переменные широко используются в мейкфайлах. Например, это удобный способ учесть возможность того, что проект будут собирать другим компилятором или с другими опциями.
# Это комментарий, который говорит, что переменная CC указывает компилятор, используемый для сборки CC=g++ #Это еще один комментарий. Он поясняет, что в переменной CFLAGS лежат флаги, которые передаются компилятору CFLAGS=-c -Wall all: hello hello: main.o factorial.o hello.o $(CC) main.o factorial.o hello.o -o hello main.o: main.cpp $(CC) $(CFLAGS) main.cpp factorial.o: factorial.cpp $(CC) $(CFLAGS) factorial.cpp hello.o: hello.cpp $(CC) $(CFLAGS) hello.cpp clean: rm -rf *.o hello
Это Makefile-3
Переменные - очень удобная штука. Для их использования надо просто присвоить им значение до момента их использования. После этого можно подставлять их значение в нужное место вот таким способом: $(VAR)
Что делать дальше
После этого краткого инструктажа уже можно пробовать создавать простые мейкфайлы самостоятельно. Дальше надо читать серьезные учебники и руководства. Как финальный аккорд можно попробовать самостоятельно разобрать и осознать такой универсальный мейкфайл, который можно в два касания адаптировать под практически любой проект:
CC=g++ CFLAGS=-c -Wall LDFLAGS= SOURCES=main.cpp hello.cpp factorial.cpp OBJECTS=$(SOURCES:.cpp=.o) EXECUTABLE=hello all: $(SOURCES) $(EXECUTABLE) $(EXECUTABLE): $(OBJECTS) $(CC) $(LDFLAGS) $(OBJECTS) -o $@ .cpp.o: $(CC) $(CFLAGS) $< -o $@
Makefile-4
Успехов!

Мне в свое время очень не хватило подобной методички для понимания базовых вещей о make. Думаю, будет хоть кому-нибудь интересно. Хотя эта технология и отмирает, но все равно используется в очень многих проектах. Кармы на хаб «Переводы» не хватило, как только появится возможность - добавлю и туда. Добавил в Переводы. Если есть ошибки в оформлении, то прошу указать на них. Буду исправлять.

Статья будет интересная прежде всего изучающим программирование на C/C++ в UNIX-подобных системах от самых корней, без использования IDE.

Компилировать проект ручками - занятие весьма утомительное, особенно когда исходных файлов становится больше одного, и для каждого из них надо каждый раз набивать команды компиляции и линковки. Но не все так плохо. Сейчас мы будем учиться создавать и использовать Мейкфайлы. Makefile - это набор инструкций для программы make, которая помогает собирать программный проект буквально в одно касание.

Для практики понадобится создать микроскопический проект а-ля Hello World из четырех файлов в одном каталоге:

main.cpp

#include #include "functions.h" using namespace std; int main(){ print_hello(); cout << endl; cout << "The factorial of 5 is " << factorial(5) << endl; return 0; }


hello.cpp

#include #include "functions.h" using namespace std; void print_hello(){ cout << "Hello World!"; }


factorial.cpp

#include "functions.h" int factorial(int n){ if(n!=1){ return(n * factorial(n-1)); } else return 1; }


functions.h

void print_hello(); int factorial(int n);


Все скопом можно скачать отсюда
Автор использовал язык C++, знать который совсем не обязательно, и компилятор g++ из gcc. Любой другой компилятор скорее всего тоже подойдет. Файлы слегка подправлены, чтобы собирались gcc 4.7.1
Программа make
Если запустить
make
то программа попытается найти файл с именем по умолчание Makefile в текущем каталоге и выполнить инструкции из него. Если в текущем каталоге есть несколько мейкфайлов, то можно указать на нужный вот таким образом:
make -f MyMakefile
Есть еще множество других параметров, нам пока не нужных. О них можно узнать в ман-странице.
Процесс сборки
Компилятор берет файлы с исходным кодом и получает из них объектные файлы. Затем линковщик берет объектные файлы и получает из них исполняемый файл. Сборка = компиляция + линковка.
Компиляция руками
Самый простой способ собрать программу:
g++ main.cpp hello.cpp factorial.cpp -o hello
Каждый раз набирать такое неудобно, поэтому будем автоматизировать.
Самый простой Мейкфайл
В нем должны быть такие части:
цель: зависимости команда
Для нашего примера мейкфайл будет выглядеть так:
all: g++ main.cpp hello.cpp factorial.cpp -o hello
Обратите внимание, что строка с командой должна начинаться с табуляции! Сохраните это под именем Makefile-1 в каталоге с проектом и запустите сборку командой make -f Makefile-1
В первом примере цель называется all . Это цель по умолчанию для мейкфайла, которая будет выполняться, если никакая другая цель не указана явно. Также у этой цели в этом примере нет никаких зависимостей, так что make сразу приступает к выполнению нужной команды. А команда в свою очередь запускает компилятор.
Использование зависимостей
Использовать несколько целей в одном мейкфайле полезно для больших проектов. Это связано с тем, что при изменении одного файла не понадобится пересобирать весь проект, а можно будет обойтись пересборкой только измененной части. Пример:
all: hello hello: main.o factorial.o hello.o g++ main.o factorial.o hello.o -o hello main.o: main.cpp g++ -c main.cpp factorial.o: factorial.cpp g++ -c factorial.cpp hello.o: hello.cpp g++ -c hello.cpp clean: rm -rf *.o hello
Это надо сохранить под именем Makefile-2 все в том же каталоге

Теперь у цели all есть только зависимость, но нет команды. В этом случае make при вызове последовательно выполнит все указанные в файле зависимости этой цели.
Еще добавилась новая цель clean . Она традиционно используется для быстрой очистки всех результатов сборки проекта. Очистка запускается так: make -f Makefile-2 clean

Использование переменных и комментариев
Переменные широко используются в мейкфайлах. Например, это удобный способ учесть возможность того, что проект будут собирать другим компилятором или с другими опциями.
# Это комментарий, который говорит, что переменная CC указывает компилятор, используемый для сборки CC=g++ #Это еще один комментарий. Он поясняет, что в переменной CFLAGS лежат флаги, которые передаются компилятору CFLAGS=-c -Wall all: hello hello: main.o factorial.o hello.o $(CC) main.o factorial.o hello.o -o hello main.o: main.cpp $(CC) $(CFLAGS) main.cpp factorial.o: factorial.cpp $(CC) $(CFLAGS) factorial.cpp hello.o: hello.cpp $(CC) $(CFLAGS) hello.cpp clean: rm -rf *.o hello
Это Makefile-3
Переменные - очень удобная штука. Для их использования надо просто присвоить им значение до момента их использования. После этого можно подставлять их значение в нужное место вот таким способом: $(VAR)
Что делать дальше
После этого краткого инструктажа уже можно пробовать создавать простые мейкфайлы самостоятельно. Дальше надо читать серьезные учебники и руководства. Как финальный аккорд можно попробовать самостоятельно разобрать и осознать такой универсальный мейкфайл, который можно в два касания адаптировать под практически любой проект:
CC=g++ CFLAGS=-c -Wall LDFLAGS= SOURCES=main.cpp hello.cpp factorial.cpp OBJECTS=$(SOURCES:.cpp=.o) EXECUTABLE=hello all: $(SOURCES) $(EXECUTABLE) $(EXECUTABLE): $(OBJECTS) $(CC) $(LDFLAGS) $(OBJECTS) -o $@ .cpp.o: $(CC) $(CFLAGS) $< -o $@
Makefile-4
Успехов!

Написание makefile иногда становится головной болью. Однако, если разобраться, все становится на свои места, и написать мощнейший makefile длиной в 40 строк для сколь угодно большого проекта получается быстро и элегантно.

Внимание! Предполагаются базовые знания утилиты GNU make.

Имеем некий типичный абстрактный проект со следующей структурой каталогов:

Пусть для включения заголовочных файлов в исходниках используется что-то типа #include , то есть каталог project/include делается стандартным при компиляции.

После сборки надо, чтобы получилось так:

То есть, в каталоге bin лежат рабочая (application) и отладочная (application_debug) версии, в подкаталогах Release и Debug каталога project/obj повторяется структура каталога project/src с соответствующими исходниками объектных файлов, из которых и компонуется содержимое каталога bin.

Чтобы достичь данного эффекта, создаем в каталоге project файл Makefile следующего содержания:

  1. root_include_dir:= include
  2. root_source_dir:= src
  3. source_subdirs:= . dir1 dir2
  4. compile_flags:= -Wall -MD -pipe
  5. link_flags:= -s -pipe
  6. libraries:= -ldl
  7. relative_include_dirs:= $(addprefix ../ ../ , $(root_include_dir) )
  8. relative_source_dirs:= $(addprefix ../ ../ $(root_source_dir) / , $(source_subdirs) )
  9. objects_dirs:= $(addprefix $(root_source_dir) / , $(source_subdirs) )
  10. objects:= $(patsubst ../ ../% , % , $(wildcard $(addsuffix /* .c* , $(relative_source_dirs) ) ) )
  11. objects:= $(objects:.cpp=.o)
  12. objects:= $(objects:.c=.o)
  13. all: $(program_name)
  14. $(program_name) : obj_dirs $(objects)
  15. g++ -o $@ $(objects) $(link_flags) $(libraries)
  16. obj_dirs:
  17. mkdir -p $(objects_dirs)
  18. VPATH:= ../ ../
  19. % .o: % .cpp
  20. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  21. % .o: % .c
  22. g++ -o $@ -c $< $(compile_flags) $(build_flags) $(addprefix -I, $(relative_include_dirs) )
  23. .PHONY: clean
  24. clean:
  25. rm -rf bin obj
  26. include $(wildcard $(addsuffix /* .d, $(objects_dirs) ) )

В чистом виде такой makefile полезен только для достижения цели clean, что приведет к удалению каталогов bin и obj.
Добавим еще один сценарий с именем Release для сборки рабочей версии:

Mkdir -p bin mkdir -p obj mkdir -p obj/Release make --directory=./obj/Release --makefile=../../Makefile build_flags="-O2 -fomit-frame-pointer" program_name=../../bin/application

И еще один сценарий Debug для сборки отладочной версии:

Mkdir -p bin mkdir -p obj mkdir -p obj/Debug make --directory=./obj/Debug --makefile=../../Makefile build_flags="-O0 -g3 -D_DEBUG" program_name=../../bin/application_debug

Именно вызов одного из них соберет наш проект в рабочем, либо отладочном варианте. А теперь, обо всем по-порядку.

Допустим, надо собрать отладочную версию. Переходим в каталог project и вызываем./Debug. В первых трех строках создаются каталоги. В четвертой строке утилите make сообщается, что текущим каталогом при запуске надо сделать project/obj/Debug, относительно этого далее передается путь к makefile и задаются две константы: build_flags (тут перечисляются важные для отладочной версии флаги компиляции) и program_name (для отладочной версии – это application_debug).

1: Объявляется переменная с именем корневого каталога заголовочных файлов.

2: Объявляется переменная с именем корневого каталога исходников.

3: Объявляются переменная с именами подкаталогов корневого каталога исходников.

4: Объявляется переменная с общими флагами компиляции. -MD заставляет компилятор сгенерировать к каждому исходнику одноименный файл зависимостей с расширением.d. Каждый такой файл выглядит как правило, где целью является имя исходника, а зависимостями – все исходники и заголовочные файлы, которые он включает директивой #include. Флаг -pipe заставляет компилятор пользоваться IPC вместо файловой системы, что несколько ускоряет компиляцию.

5: Объявляется переменная с общими флагами компоновки. -s заставляет компоновщик удалить из результирующего ELF файла секции.symtab, .strtab и еще кучу секций с именами типа.debug*, что значительно уменшает его размер. В целях более качественно отладки этот ключ можно убрать.

6: Объявляется переменная с именами используемых библиотек в виде ключей компоновки.

8: Объявляется переменная, содержащая относительные имена каталогов со стандартными заголовочными файлами. Потом такие имена напрямую передаются компилятору, предваряемые ключем -I. Для нашего случая получится../../include, потому что такой каталог у нас один. Функция addprefix добавляет свой первый аргумент ко всем целям, которые задает второй аргумент.

9: Объявляется переменная, содержащая относительные имена всех подкаталогов корневого каталога исходников. В итоге получим: ../../src/. ../../src/dir1 ../../src/dir1.

10: Объявляется переменная, содержащая имена подкаталогов каталога project/obj/Debug/src относительно текущего project/obj/Debug. То есть, этим мы перечисляем копию структуры каталога project/src. В итоге получим: /src/dir1 src/dir2.

11: Объявляется переменная, содержащая имена исходников, найденных на основе одноименных файлов *.c* (.cpp\.c), безотносительно текущего каталога. Смотрим поэтапно: результатом addsuffix будет../../src/./*.с* ../../src/dir1/*.с* ../../src/dir2/*.с*. Функция wildcard развернет шаблоны со звездочками до реальных имен файлов: ../../src/./main.сpp ../../src/dir1/file1.с../../src/dir1/file2.сpp ../../src/dir2/file3.с../../src/dir2/file4.с. Функция patsubsb уберет префикс../../ у имен файлов (она заменяет шаблон, заданный первым аргументом на шаблон во втором аргументе, а % обозначает любое количество символов). В итоге получим: src/./main.сpp src/dir1/file1.с src/dir1/file2.сpp src/dir2/file3.с src/dir2/file4.с.

12: В переменной с именами исходников расширения.cpp заменяется на.o.

13: В переменной с именами исходников расширения.c заменяется на.o.

15: Первое объявленное правило – его цель становится целью всего проекта. Зависимостью является константа, содержащая имя программы (../../bin/application_debug мы ее передали при запуске make из сценария).

17: Описание ключевой цели. Зависимоcти тоже очевидны: наличие созданных подкаталого в project/obj/Debug, повторяющих структуру каталога project/src и множество объектных файлов в них.

18: Описано действие по компоновке объектных файлов в целевой.

20: Правило, в котором цель – каталог project/obj/Debug/src и его подкаталоги.

21: Действие по достижению цели – создать соответствующие каталоги src/., src/dir1 и src/dir2. Ключ -p утилиты mkdir игнорирует ошибку, если при создании какого-либо каталога, таковой уже существуют.

23: Переменная VPATH принимает значение../../. Это необходимо для шаблонов нижеследующих правил.

25: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на.o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.cpp (то есть имена которых оканчиваются на.cpp). При этом под одноименностью понимается не только точное совпадение, но также если имя зависимости предварено содержимым переменной VPATH. Например, имена src/dir1/file2 и../../src/dir1/file2 совпадут, так как VPATH содержит../../.

26: Вызов компилятора для превращения исходника на языке С++ в объектный файл.

28: Описывается множество правил, для которых целями являются любые цели, соответствующие шаблону %.o (то есть имена которых оканчиваются на.o), а зависимостями для этих целей являются одноименные цели, соответствующие шаблону %.c (то есть имена которых оканчиваются на.c). Одноименность как в строке 23.

29: Вызов компилятора для превращения исходника на языке С в объектный файл.

31: Некоторая цель clean объявлена абстрактной. Достижение абстрактной цели происходит всегда и не зависит от существования одноименного файла.

32: Объявление абстрактной цели clean.

33: Действие по ее достижению заключается в уничтожении каталогов project/bin и project/obj со всем их содержимым.

36: Включение содержимого всех файлов зависимостей (с расширением.d), находящихся в подкаталогах текущего каталога. Данное действие утилита make делает в начале разбора makefile. Однако, файлы зависимостей создаются только послекомпиляции. Значит, при первой сборке ни один такой файл включен не будет. Но это не страшно. Цель включения этих файлов – вызвать перекомпиляцию исходников, зависящих от модифицированного заголовочного файла. При второй и последующих сборках утилита make будет включать правила, описанные во всех файлах зависимостей, и, при необходимости, достигать все цели, зависимые от модифицированного заголовочного файла.



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: