Основы Make и Makefile, а также примеры их использования
Как известно, программный код например, С++ — всего лишь программно-командный синтаксис, который нельзя запустить как приложение или использовать как библиотеку. Любой исходный файл следует сначала скомпилировать в исполняемый файл, а также библиотеку — в динамическую или статическую. На больших современных проектах процесс сборки может быть сложным и многоступенчатым, что требует инструмента для автоматизации данного процесса.
Один из таких инструментов, проверенных временем, — утилита Make. Ее стандарт Makefile широко используется различными системами автоматизации сборки
Automake, CMake, imake, qmake, nmake, wmake, Apache Ant, Apache Maven, OpenMake Meister, Gradle. В этой статье мы обсудим базовые возможности, которые нужно знать всем для начала уверенной работы с Make/Makefile.
1. История возникновения Make
Утилита под названием Make возникла еще в далеком 1976 году в стенах компании Bell Labs. Появилась она в результате визита Стива Джонсона автора yacc — стандартной утилиты для BSD и AT&T Unix.
Как-то раз он ворвался в офис, где работал Стюарт Фельдман, громко проклиная все на свете за то, что все утро у него ушло на отладку довольно простой программы.
Исполняемый файл с изменениями по какой-то причине не обновлялся . Поскольку за день до этого Фельдман столкнулся с аналогичной проблемой в своем проекте, ему пришла в голову идея создать универсальный инструмент для решения подобных задач.
Вначале это была просто продуманная идея анализатора зависимостей, но в конечном итоге программа стала более простой и дружелюбной. До появления системы компиляции Make
в Unix
использовались самописные кустарные сценарии командной строки, применяемые к исходному коду приложений.
2. Варианты утилиты Make
Сейчас есть целый ряд программ автоматизаторов сборки, которые отслеживают зависимости в файлах, однако Make
— одна из самых распространенных. В первую очередь по причине того, что она включена в дистрибутивы GNU/Linux.
Вообще есть две версии этой программы. Первая — написанная под платформу BSD впоследствии перекочевавшая на FreeBSD, NetBSD и OpenBSD. Она основана на разработке Адама де Бура с возможностью параллельной сборки. Вторая версия — GNU Make, предназначена для использования в Linux и MacOS.
Для платформы Microsoft Windows имеется аналогичное приложение — nmake
.
3. Что означает Makefile
Программа Make
может понадобиться, когда перед вами стоит задача обновления в сборке определенных данных.
Этот инструмент выполняет анализ файлов и идентифицирует, какие части большой программы необходимо перекомпилировать заново, и выдает соответствующие команды для их повторной сборки.
Для функционирования этой утилиты необходим специальный служебный файл, называемый Makefile
(или makefile
). Он описывает взаимосвязи между файлами в проекте, над которым вы работаете, и идентифицирует команды для обновления каждого файла. Makefile
помещается вместе с кодом в репозиторий часто просто в директорию проекта.
Содержимое простого Makefile
может иметь вид:
edit : main1.o kbd1.o command1.o display.o \ insert1.o search1.o files1.o utils1.o cc -o edit main1.o kbd1.o command1.o display1.o \ insert1.o search1.o files1.o utils1.o main1.o : main1.c defs1.h cc -c main1.c kbd1.o : kbd1.c defs.h command1.h cc -c kbd1.c command1.o : command1.c defs.h command1.h cc -c command1.c display1.o : display1.c defs1.h buffe1r.h cc -c display1.c insert1.o : insert1.c defs1.h buffer1.h cc -c insert1.c search1.o : search1.c defs1.h buffer1.h cc -c search1.c files1.o : files1.c defs1.h buffer1.h command1.h cc -c files1.c utils1.o : utils1.c defs.h cc -c utils1.c clean : rm edit main1.o kbd1.o command1.o display1.o \ insert1.o search1.o files1.o utils1.o
После запуска GNU Make считывает файл с именем GNUmakefile
, makefile
или Makefile
. Если необходимо изменить поведение программы при обработке файла по умолчанию, например, сделать так, чтобы открывался файл с другим именем, содержащий иной набор инструкций, следует указать команду make -f other_name_makefile
.
Язык make
-файлов — это полный по Тьюрингу декларативный язык. В нем описаны необходимые конечные условия, но порядок, в котором должны выполняться действия, не важен, что иногда сбивает с толку программистов, привыкших к императивному программированию.
4. Правила языка для написания Make-файлов
Язык Make
-файлов базируется на командах. Этими командами описываются цели. В качестве цели можно выбирать имя действия, которое необходимо осуществить, скажем, clean
. Также под целью можно принимать имя исполняемого или объектного файла.
Суть построения такого файла состоит в грамотной группировке команд и в выборе понятных названий для целей. Общий синтаксис выглядит так:
# Makefile цель1: # название_цели, поддерживается kebab-case (вариант при котором вместо символа подчеркивания используется дефис) и snake_case (стиль написания составных слов, при котором они разделяются символом подчеркивания) команда1 # Обратите внимание — все команды, обязаны содержать в начале символ табуляции — так инструмент сборки отслеживает правила и другие цели команда2 # если предыдущая команда была успешно выполнена, начинает выполняться следующая команда и так далее
Для команд применяются команды оболочки shell
— они будут отображены в консоли. Когда необходимо, чтобы команды не показывались в командной строке, перед командой используется символ @
. Если не вводить имя цели, то выполнится цель с именем all
либо самая первая цель в сценарии. В качестве пререквизитов или зависимостей, как правило, используются другие цели, представляющие собой имена файлов.
5. Пишем файл Make для простого проекта
Разберем вариант сборки простого приложения на Си. Предположим, наше приложение program
состоит из пары файлов кода – main.c
и lib.c
, а также одного заголовочного файла – defines.h
, что включен в оба файла-исходника кода. Тогда для создания program
необходимо из пар (main.c defines.h)
и (lib.c defines.h)
создать объектные файлы main.o
и lib.o
, а затем слинковать их в program
. При ручной сборке следует выполнить такие команды:
cc -c main.c defines.h cc -c lib.c defines.h cc -o program main.o lib.o
Если на каком-то этапе работы над приложением файл defines.h
подвергнется редактированию, возникнет необходимость перекомпилирования обоих файлов и нового линкования. При изменении одного lib.c
, перекомпиляцию main.о
делать не нужно.
Следовательно, для каждого файла, который мы должны получить в процессе компиляции, нужно указать на основе каких файлов, а также с помощью какой команды он создается.
Используя эти сведения утилита Make
:
- собирает из этих данных корректную цепочку команд для получения необходимых конечных файлов;
- запускает создание запланированного файла только в случае, когда такой файл отсутствует либо имеет более раннее время создания по сравнению с файлами, от которых зависит.
Бывает, что при запуске утилиты Make
цель явно не задана. В такой ситуации начнет выполняться первая цель, имя которой не начинается с точки “.
“. Для нашей программы достаточно написать такой файл:
program: main.o lib.o cc -o program main.o lib.o main.o lib.o: defines.h
Есть ряд моментов, которые нужно обговорить относительно этого простого примера. В имени второй цели указаны два парных файла. Для этой же цели не указана команда компиляции. Кроме того, нигде явно не указана зависимость объектных файлов от файлов *.с
.
Дело в том, что программа Make
содержит встроенные правила для получения файлов с определенными расширениями. Так для цели – объектного файла (расширение *.o
) при нахождении соответствующего файла с расширением *.с
будет автоматически вызван компилятор «сс-с
» .
Нередко команды могут задействовать различные параметры конфигурации, определять пути, устанавливать переменные окружения. С помощью Make
можно всем этим управлять, прописав нужную переменную в команде. Формат описания переменной выглядит просто: переменная = значение
. Переменные могут быть только строками.
Вы можете использовать любые символы, включая пробелы.
# Makefile say: echo "Hello, $(HELLO)!" # Bash $ make say HELLO=CRAZY echo "Hello, CRAZY!" Hello, CRAZY! $ make say HELLO=HighloadToday echo "Hello, HighloadToday!" Hello, HighloadToday!
Теперь текст нашего примера можно модифицировать:
OBJ = main.o lib.o program: $(OBJ) cc -o program $(OBJ) $(OBJ): defines.h
Обработка значения переменных выполняется исключительно в момент их задействования применяется так называемое «ленивое вычисление». Для сборки цели
all
на дисплей будет выведен текст «Ась?
»:
foo = $(bar) bar = $(ugh) ugh = Ась? all: echo $(foo)
Если в наш проект был добавлен еще один заголовочный файл lib.h
, включаемый только в lib.c
, — структура make
-файла изменится, будет дописана еще одна строка:
lib.o: lib.h
Таким образом, один целевой файл может быть указан сразу в нескольких целях. При этом полный список связей будет составлен из списков зависимостей всех целей, в которых он принимает участие, а создание файла будет выполняться только один раз.
Заключение
Теперь вы знаете базовые принципы работы утилиты Make
и можете автоматизировать процедуру сборки. Для закрепления навыков рекомендуем вам посмотреть видео, в котором показан процесс создания Make
-файла на основе реального проекта:
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: