Сборка программы с помощью GNU Make. Эффективное использование GNU Make

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, но если есть какие-то вопросы — напишите в комментарии, постараюсь ответить.

Чаще всего сборка проекта в ОС Linux, с учётом зависимостей и обновлений, выполняется утилитой make , которая использует для этого заранее оформленный сценарий сборки. Мы уже неоднократно прибегали к помощи этой утилиты в предыдущих статьях, и эта статья будет посвящена исключительно вопросам использования утилиты make .

Утилита make

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

Утилита make доступна для разных ОС, и из-за особенностей выполнения наряду с «родной» реализацией во многих ОС присутствует GNU реализация gmake , и поведение этих реализаций в некоторых ОС, например, Solaris может существенно отличаться. Поэтому в сценариях сборки рекомендуется указывать имя конкретной утилиты. В ОС Linux эти два имени являются синонимами, реализованными через символическую ссылку, как показано ниже:

$ ls -l /usr/bin/*make lrwxrwxrwx 1 root root 4 Окт 28 2008 /usr/bin/gmake -> make -rwxr-xr-x 1 root root 162652 Май 25 2008 /usr/bin/make ... $ make --version GNU Make 3.81 ...

По умолчанию имя файла сценария сборки - Makefile . Утилита make обеспечивает полную сборку указанной цели , присутствующей в сценарии, например:

$ make $ make clean

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

$ make -f Makefile.my

Простейший файл Makefile состоит из синтаксических конструкций двух типов: целей и макроопределений. Описание цели состоит из трех частей: имени цели, списка зависимостей и списка команд интерпретатора оболочки, требуемых для построения цели. Имя цели - непустой список файлов, которые предполагается создать. Список зависимостей - список файлов, в зависимости от которых строится цель. Имя цели и список зависимостей составляют заголовок цели, записываются в одну строку и разделяются двоеточием (":"). Список команд записывается со следующей строки, причем все команды начинаются с обязательного символа табуляции . Многие текстовые редакторы могут быть настроены таким образом, чтобы заменять символы табуляции пробелами. Этот факт стоит учесть и проверить, что редактор, в котором редактируется Makefile , не замещает табуляции пробелами, так как подобная проблема встречается довольно часто. Любая строка в последовательности списка команд, не начинающаяся с табуляции (ещё одна команда) или символа "# " (комментарий) - считается завершением текущей цели и началом новой.

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

$ make -p >make.suffix make: *** Не заданы цели и не найден make-файл. Останов. $ cat make.suffix # GNU Make 3.81 # Copyright (C) 2006 Free Software Foundation, Inc. ... # База данных Make, напечатана Thu Apr 14 14:48:51 2011 ... CC = cc LD = ld AR = ar CXX = g++ COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c COMPILE.C = $(COMPILE.cc) ... SUFFIXES:= .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l .s .S .mod .sym \ .def .h .info .dvi .tex .texinfo .texi .txinfo .w .ch... # Implicit Rules ... %.o: %.c # команды, которые следует выполнить (встроенные): $(COMPILE.c) $(OUTPUT_OPTION) $< ...

Значения всех этих переменных: CC , LD , AR , EXTRA_CFLAGS , ... могут использоваться файлом сценария как неявные определения со значениями по умолчанию. Кроме этого, можно определить и собственные правила обработки по умолчанию для выбранных суффиксов (расширений файловых имён), как это показано на примере выше для исходных файлов кода на языке С: %.c.

Большинство интегрированных сред разработки (IDE) или пакетов для создания переносимых инсталляций (например, automake или autoconf) ставят своей задачей создание файла Makefile для утилиты make .

Как ускорить сборку make

Сборка простых проектов происходит достаточно быстро, но с учётом роста проекта по ходу его развития, время сборки, основная часть которого затрачивается на компиляцию, может значительно вырасти. Хорошо известным примером такого рода является сборка ядра Linux, которая, в зависимости от типа оборудования, может занимать от нескольких десятков минут до часов процессорного времени. Усугубляет ситуацию и то, что при работе над проектом (доработка кода, отладка, поиск ошибок, тестирование, и т.д.) может понадобиться выполнять пересборку проекта по несколько десятков раз в день. Поэтому возможности, позволяющие ускорить этот процесс, приобретают действительную актуальность.

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

$ man make ... -j , --jobs[=jobs] Specifies the number of jobs (commands) to run simultaneously. ...

Проверим преимущества, предоставляемые этой возможностью на практическом примере. В качестве эталона для сборки возьмём проект NTP-сервера, который собирается не очень долго, но и не слишком быстро:

$ pwd /usr/src/ntp-4.2.6p3

Сначала запустим сборку на 4-х ядерном процессоре Atom (не очень быстрая модель с частотой 1.66Ghz) но с очень быстрым твердотельным диском SSD:

$ cat /proc/cpuinfo | head -n10 processor: 0 vendor_id: GenuineIntel cpu family: 6 model: 28 model name: Intel(R) Atom(TM) CPU 330 @ 1.60GHz stepping: 2 cpu MHz: 1596.331 cache size: 512 KB $ make clean # запускаем сборку в четыре потока $ time make -j4 ... real 1m5.023s user 2m40.270s sys 0m16.809s $ make clean # запускаем сборку в стандартном режиме без параллелизма $ time make ... real 2m6.534s user 1m56.119s sys 0m12.193s $ make clean # запускаем сборку с автоматическим выбранным уровнем параллелизма $ time make -j ... real 1m5.708s user 2m43.230s sys 0m16.301s

Как можно заметить, использование параллелизма (явное или не явное) позволяет ускорить сборку почти в два раза – 1 минута против 2-ух. Выполним сборку этого же проекта на более быстром 2-х ядерном процессоре, но с достаточно медленным обычным диском HDD:

$ cat /proc/cpuinfo | head -n10 processor: 0 vendor_id: GenuineIntel cpu family: 6 model: 23 model name: Pentium(R) Dual-Core CPU E6600 @ 3.06GHz stepping: 10 cpu MHz: 3066.000 cache size: 2048 KB ... $ time make ... real 0m31.591s user 0m21.794s sys 0m4.303s $ time make -j2 ... real 0m23.629s user 0m21.013s sys 0m3.278s

Хотя итоговая скорость сборки и выросла в 3-4 раза, но улучшение от числа процессоров составляет только порядка 20%, так как «слабым звеном» здесь является медленный накопитель, допускающий задержку при записи большого числа мелких.obj файлов проекта.

Примечание : Хотелось бы напомнить, что не всякая сборка make , которая успешно выполняется на одном процессоре (как это имеет место по умолчанию или при указании -j1 ), будет также успешно выполняться при большем числе задействованных процессоров. Это связано с нарушениями синхронизации операций в случаях сложных сборок. Самым наглядным примером такой сборки, завершающейся с ошибкой в случае параллельного исполнения, является сборка ядра Linux для некоторых версий ядра. Возможность параллельного выполнения make нужно экспериментально проверять для собираемого проекта. Но в большинстве случаев это возможность может использоваться и позволяет в разы ускорить процесс сборки!

Если данный способ ускорения процесса сборки основан на том, что сейчас подавляющее большинство систем являются многопроцессорными (многоядерными), то следующий способ использует тот факт, что объём памяти RAM современных компьютеров (2-4-8 ГБ) значительно превышает объём памяти, необходимый для компиляции программного кода. В таком случае, компиляцию, основным сдерживающим фактором для которой является создание множества объектных файлов, можно перенести в область специального созданного диска (RAM диск, tmpfs ), расположенного в памяти:

$ free total used free shared buffers cached Mem: 4124164 1516980 2607184 0 248060 715964 -/+ buffers/cache: 552956 3571208 Swap: 4606972 0 4606972 $ df -m | grep tmp tmpfs 2014 1 2014 1% /dev/shm

Теперь можно временно перенести файлы собираемого проекта в tmpfs (мы по-прежнему используем NTP-сервер из предыдущего примера), в каталог /dev/shm :

$ pwd /dev/shm/ntp-4.2.6p3 $ make -j ... real 0m4.081s user 0m1.710s sys 0m1.149s

В данном случае одновременно используются оба способа повышения производительности, и улучшение относительно исходной компиляции достигает почти порядка. Правда, этот пример запускался на системе с медленным HDD, где параллельная сборка практически не давала выигрыша и требовала порядка 30 секунд.

Этот способ ускорения можно применить к сборке ядра Linux, для которого, как уже было сказано, параллельная сборка не работает. Чтобы воспользоваться преимуществами RAM-памяти, скопируем дерево исходных кодов ядра в каталог /dev/shm :

$ pwd /dev/shm/linux-2.6.35.i686 $ time make bzImage ... HOSTCC arch/x86/boot/tools/build BUILD arch/x86/boot/bzImage Root device is (8, 1) Setup is 13052 bytes (padded to 13312 bytes). System is 3604 kB CRC 418921f4 Kernel: arch/x86/boot/bzImage is ready (#1) real 9m23.986s user 7m4.826s sys 1m18.529s

Как видно, сборка ядра Linux заняла менее 10 минут, что является необычайно хорошим результатом.

В качестве вывода, можно посоветовать тщательно оптимизировать условия сборки проекта под используемое для этого оборудование, и, учитывая, что в процессе отладки сборка выполняется сотни раз, то можно сэкономить множество времени!

Сборка модулей ядра

Частным случаем сборки приложений является сборка модулей ядра Linux (драйверов). Начиная с версий ядра 2.6, для сборки модуля составляется Makefile , построенный на использовании макросов, и нам остаётся только записать (для файла собственного кода с именем mod_params.c ), следующий шаблон для сборки модулей:

Листинг 1. Makefile для сборки модулей ядра
CURRENT = $(shell uname -r) KDIR = /lib/modules/$(CURRENT)/build PWD = $(shell pwd) TARGET = mod_params obj-m:= $(TARGET).o default: $(MAKE) -C $(KDIR) M=$(PWD) modules ... $ make make -C /lib/modules/2.6.18-92.el5/build \ M=examples/modules-done_1/hello_printk modules make: Entering directory `/usr/src/kernels/2.6.18-92.el5-i686" CC [M] /examples/modules-done_1/hello_printk/hello_printk.o Building modules, stage 2. MODPOST CC /examples/modules-done_1/hello_printk/hello_printk.mod.o LD [M] examples/modules-done_1/hello_printk/hello_printk.ko make: Leaving directory `/usr/src/kernels/2.6.18-92.el5-i686" $ ls -l *.o *.ko -rw-rw-r-- 1 olej olej 74391 Мар 19 15:58 hello_printk.ko -rw-rw-r-- 1 olej olej 42180 Мар 19 15:58 hello_printk.mod.o -rw-rw-r-- 1 olej olej 33388 Мар 19 15:58 hello_printk.o $ file hello_printk.ko hello_printk.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ /sbin/modinfo hello_printk.ko filename: hello_printk.ko author: Oleg Tsiliuric license: GPL srcversion: 83915F228EC39FFCBAF99FD depends: vermagic: 2.6.18-92.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1

Заключение

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

В следующей статье мы начнём знакомство с библиотеками API, присутствующими в POSIX системах.

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

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

make-file

Прежде чем использовать make , необходимо создать файл, называемый make-файлом , который описывает отношения между файлами Вашей программы и содержит команды для обновления каждого файла. Обычно исполняемый файл зависит от объектных файлов, которые, в свою очередь, зависят от исходных файлов и файлов заголовков. Для имени make-файла рекомендуется название GNUmakefile , makefile или Makefile , причем поиск идет именно в указанном порядке. Если необходимо использовать нестандартное имя, то его можно передать явно через опцию -f .
Когда make-файл уже написан, достаточно выполнить в каталоге в котором он находится команду make . Простой make-файл состоит из правил(инструкций) следующего вида:


ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ...
ЦЕЛЬ... : ЗАВИСИМОСТЬ...
КОМАНДА 1
КОМАНДА 2
ПЕРЕМЕННАЯ = ЗНАЧЕНИЕ...
ЦЕЛЬ... : ЗАВИСИМОСТЬ...
КОМАНДА 1
КОМАНДА 2

и т.д.

ЦЕЛЬ обычно представляет собой имя файла, генерируемого программой make ; примерами целей являются исполняемые или объектные файлы. Цель может также быть именем выполняемого действия, как, например, "clean ".
ЗАВИСИМОСТЬ - это файл, изменение которого служит признаком необходимости цели. Часто цель зависит от нескольких файлов.
КОМАНДА - это действие, которое выполняет make . Правило может иметь более чем одну команду - каждую на своей собственной строке. Важное замечание: необходимо начинать каждую строку, содержащую команды, с символа табуляции. Длинные строки разбиваются на несколько с использованием обратного слэша, за которым следует перевод строки. Знак диез # является началом комментария. Строка с # до конца игнорируется. Комментарии могут переноситься на несколько строк с помощью обратного слэша в конце строки.

Пример makefile

Использование действий по умолчанию


#default target - file edit
edit: main.o kbd.o command.o display.o \
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

Main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
command.o: command.c defs.h command.h
cc -c command.c
display.o: display.c defs.h buffer.h
cc -c display.c
insert.o: insert.c defs.h buffer.h
cc -c insert.c
search.o: search.c defs.h buffer.h
cc -c search.c
files.o: files.c defs.h buffer.h command.h
cc -c files.c
utils.o: utils.c defs.h
cc -c utils.c
clean:
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

По умолчанию, make начинает с первого правила (не считая правил, имена целей у которых начинаются с ". "). Это называется главной целью по умолчанию. В нашем случае это правило edit . Если файл edit новее чем объектные файлы от которых он зависит, то ничего не произойдет. В противном случае, прежде чем make сможет полностью обработать это правило, он должен рекурсивно обработать правила для файлов, от которых зависит "edit ". Каждый из этих файлов обрабатывается в соответствии со своими собственным правилом. Перекомпиляция должна быть проведена, если исходный файл или любой из заголовочных файлов, упомянутых среди зависимостей, обновлен позднее, чем объектный файл, или если объектный файл не существует.
Правилу clean не соответствует никакого создаваемого файла и, соответственно, clean ни от чего не зависит и само не входит в список зависимостей. При запуске по умолчанию clean вызываться не будет. Для его выполнения необходимо явно указать цель при запуске make - make clean.
Для сокращения записи можно использовать переменные и действия по умолчанию (неявные правила)

Специальная цель .PHONY является встроенной в make и определяет свои зависимости как цели-имена, которым нет соответствия в виде файлов. Если данное правило пропустить, то создание в текущем каталоге файла с именем clean заблокирует выполнение make clean .
Использование правил по умолчанию позволяет изменить стиль записей зависимостей:

Квадратные скобки означают необязательность присутствия данной части.
Цель - имя цели, которую надо выполнить.
Переменная ="abc" -переопределение переменных. Значения переменных введенных в командной строке имеют больший приоритет, чем определения в make-файле.
Опции:
-f file - явное задание имени make-файла , если задание опущено, то ищются файлы GNUmakefile , makefile или Makefile
-n ; - имитация действий без реального выполнения, служит для отладки
-t - изменение времени модификации цели без реального выполнения
-q - проверка на необходимость обновления цели без реального выполнения