Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

GNU Make: управляйте процессом сборки своего программного обеспечения

Оригинал: GNU Make: Manage Your Software Builds
Автор: Mihalis Tsoukalos
Дата публикации: 14 декабря 2016 г.
Перевод: А.Панин
Дата перевода: 30 января 2017 г.

Компилируете программное обеспечение из исходного кода? Если это так, вам просто необходимо разобраться с Make-файлами.

Для чего это нужно?

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

Make является мощной системой для автоматизации процесса сборки программного обеспечения, разработанной Стюартом Фельдманом из Bell Labs в апреле 1976 года. GNU Make является стандартной реализацией Make, используемой в Linux и Mac OS X с множеством улучшений, которая, в том числе, необходима для компиляции ядра Linux. Ее основной задачей является автоматическое выявление модифицированных файлов исходного кода сложных приложений и исполнение команд, направленных на их повторную компиляцию.

Для конфигурации make используются так называемые Make-файлы, которые позволяют сохранить группы команд для их последующего исполнения. Давайте рассмотрим содержимое Make-файла более подробно. Но перед этим вы должны принять к сведению тот факт, что GNU Make трактует отступы различных форматах по-разному, то есть, в представлении данного инструмента символ табуляции отличается от 4 или 8 последовательных символов пробела, следовательно, вам придется отнестись к форматированию рассматриваемых файлов с особым вниманием. Это особенно важно, так как каждая строка Make-файла с новой командой должна начинаться с символа табуляции.

Make-файлы могут управлять процессом компиляции программного обеспечения благодаря наличию зависимостей, целей и правил. Правила сообщают GNU Make о том, когда, почему и как нужно исполнять заданные последовательности команд для генерации результирующих файлов на основе файлов исходного кода. Целями являются файлы, которые должны генерироваться с участием GNU Make, причем их имена располагаются слева от символов двоеточий в описаниях правил. Чаще всего каждое из правил имеет по одной цели; однако, в рамках одного правила допускается использование сразу нескольких целей. Зависимости располагаются справа от символов двоеточий в описаниях правил и указывают на то, какие файлы или другие цели могут инициировать исполнение команд, описанных в рамках правила, по причине модификаций.

Я создал простой пример проекта, состоящий из четырех файлов исходного кода на языке C++ (вы можете загрузить исходный код проекта по ссылке www.linuxvoice.com/make23.tar.gz). Этот архив содержит исходный код всех примеров, которые будут рассматриваться в данной статье. Файлы первого проекта расположены в директории simple. А это содержимое Make-файла этого проекта:

RM = /bin/rm

program: file1.cpp file1.h file2.o file3.o file4.o
g++ file2.o file3.o file4.o file1.cpp -o program
file2.o: file2.cpp file2.h
g++ -c file2.cpp
file3.o: file3.cpp file3.h
g++ -c file3.cpp
file4.o: file4.cpp file4.h
g++ -c file4.cpp
clean:
$(RM) file2.o file3.o file4.o program
$(RM) *.gch

Впоследствии вы можете переместиться в данную директорию и просто выполнить в ней команду make.

Что же должно случиться?

Теперь вы готовы к исполнению команды make. В случае исполнения команды make program вы должны увидеть аналогичный вывод:

$ make program
g++ -c file2.cpp
g++ -c file3.cpp
g++ -c file4.cpp
g++ file2.o file3.o file4.o file1.cpp -o program
$ ls -l program
-rwxr-xr-x 1 mtsouk mtsouk 8992 Nov 10 11:35 program

Приведенный в конце вывод утилиты ls позволяет убедиться в том, что все работает в точности так, как ожидалось и был получен желаемый результат.

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

$ make clean
/bin/rm file2.o file3.o file4.o program
/bin/rm *.gch
/bin/rm: cannot remove ‘*.gch’: No such file or directory
Makefile:14: recipe for target 'clean' failed
make: *** [clean] Error 1

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

Реализация функции main() находится в файле исходного кода file1.cpp; исходя из этого, последними компилируемыми файлами должны быть файлы file1.cpp и file1.h. Рассматриваемый проект является настолько простым, что каждый файл с расширением .cpp, за исключением файла file1.cpp, содержит реализацию лишь одного класса, описанного в рамках соответствующего файла с расширением .h, которая используется в рамках файла file1.cpp. Это обстоятельство обуславливает наличие зависимостей, которые описываются в рамках Make-файла.

Важная информация: утилита Make не исследует содержимое файлов проекта для принятия решения о том, стоит ли осуществлять их повторную компиляцию - она всего лишь проверяет метки времени модификации этих файлов.

Страница руководства утилиты make является источником полезной информации

Страница руководства утилиты make является источником полезной информации

Отладка Make-файлов

Make-файл может содержать синтаксические или логические ошибки, препятствующие его корректному функционированию. Наиболее полезным в плане отладки Make-файлов параметром утилиты make является параметр -n, который позволяет утилите просто выводить предназначенные для исполнения команды, но не исполнять их. Еще одним полезным параметром является параметр -d, который позволяет утилите выводить большой объем отладочной информации в процессе обычной обработки файла (хотя эта информация и может показаться интересной, она не всегда является полезной).

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

make -p -f/dev/null

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

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

Золотые правила разработки Make-файлов

При разработке нового Make-файла в первую очередь следует описать все макросы (переменные), которые должны содержать полные пути ко всем используемым бинарным файлам системных команд. Это объясняется тем, что вы должны четко знать, какие команды исполняются в процессе сборки проекта, а не полагаться на наличие путей к директориям с необходимыми бинарными файлами системных команд в значении переменной окружения PATH.

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

Не пытайтесь использовать утилиту GNU Make в рамках крупного проекта, если вы все еще изучаете ее возможности; попробуйте внедрить ее в проекты меньшего масштаба перед тем, как начинать работу с более сложными проектами. И наконец, уясните, что переменные полезны и могут существенно сэкономить ваше время, поэтому вам определенно стоит использовать их всегда, когда это возможно.

А это пример практического использования сложного Make-файла в рамках программного проекта на языке C

А это пример практического использования сложного Make-файла в рамках программного проекта на языке C

Различные полезные параметры утилиты make

Если вам понадобится использовать Make-файл с именем, отличающимся от makefile или Makefile, вы можете воспользоваться параметром -f, передав после него имя этого файла. Параметры --file=ИМЯ_ФАЙЛА и --makefile=ИМЯ_ФАЙЛА полностью эквивалентны параметру -f. Параметр -k сообщает утилите make о необходимости осуществления сборки проекта так долго, как это возможно вне зависимости от обнаруженных ошибок. Параметр -t позволяет изменить метки времени модификации файлов вместо выполнения команд для сборки проекта; данный режим работы предназначен для симуляции полной пересборки проекта, что соответствующим образом отразится на результате последующего запуска утилиты make. Параметр --trace позволяет получить информацию о том, почему заданная цель требует пересборки проекта и какие команды будут выполнены в ходе этой операции. Параметр --trace также может использоваться для отладки файлов GNU Make.

Вы можете без каких-либо сложностей прочитать или изменить значение существующей переменной окружения, объявив ее в рамках своего Make-файла. Если вы захотите отключить данный механизм, вы можете просто вызвать утилиту make, передав ей параметр -e.

Пример вывода утилиты GNU Make с параметрами -n, -p и -d, которые используются главным образом для целей отладки Make-файлов

Пример вывода утилиты GNU Make с параметрами -n, -p и -d, которые используются главным образом для целей отладки Make-файлов

Внутри Make-файла

В первой строке рассматриваемого Make-файла описывается переменная RM, значением которой является полный путь к бинарному файлу утилиты rm, которая используется для удаления файлов. Для того, чтобы получить значение переменной RM и использовать его в рамках вашего Make-файла, вам придется использовать нотацию $(RM). Мы поговорим о переменных более подробно в следующем разделе.

В строке 3 описывается цель program, имеющая пять зависимостей, причем тремя ее зависимостями являются другие цели (file2.o, file3.o и file4.o), а остальными двумя - файл исходного кода на языке C++ и соответствующий заголовочный файл. Цель program имеет лишь одну ассоциированную команду, которая приведена в строке 4. Однако, как становится очевидно при рассмотрении цели clean (также из приведенного выше Make-файла), с любой целью может ассоциироваться более одной команды. В строках с 6 по 10 приведены инструкции по созданию объектных файлов на основе трех классов из файлов с исходным кодом на языке С++ с именами file2.cpp, file3.cpp и file4.cpp соответственно. Последнее правило предназначено для удаления всех временных файлов - использование цели clean является обычной практикой для Make-файлов, так как подобная цель, осуществляющая удаление всех временных файлов без какой-либо модификации основных файлов проекта, позволяет осуществлять полную пересборку проекта.

Параметр компилятора из набора GCC используется главным образом для обработки файлов, которые не содержат реализации функции main(), так как он сообщает компилятору C++ из состава GCC о том, что нужно осуществлять лишь предварительную обработку, компиляцию и ассемблирование исходного кода. Параметр -o <имя>, используемый в рамках цели program сообщает g++ и gcc о необходимости сохранения результирующего бинарного файла под именем <имя>. Если вы попытаетесь задействуете цель, соответствующую файлам в актуальном состоянии, вы получите аналогичный вывод:

$ make file2.o make: 'file2.o' is up to date

Давайте рассмотрим более сложный пример проекта на языке C. Файлы этого проекта находятся в директории advanced архива, доступного по ссылке www.linuxvoice.com/make23.tar.gz. Make-файл позволяет скомпилировать исходный код проекта и сгенерировать исполняемый файл таким же образом, как и в предыдущем проекте, причем в данном случае также осуществляется копирование сгенерированного исполняемого файла в другую директорию с помощью цели install. Директория /tmp используется в качестве примера для того, чтобы вам не приходилось захламлять системные директории при тестировании примера, на самом же деле бинарные файлы обычно сохраняются в директории /usr/local/bin.

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

GCC = /usr/bin/gcc

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

RELEASE_FILE = $(EXECUTABLE)-$(VERSION)-$(DATE)

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

Рассматриваемый Make-файл содержит переменную, хранящую номер версии вашей программы. Значение этой переменной может изменяться либо с помощью параметра командной строки, либо путем редактирования самого Make-файла. Если вы желаете использовать значение переменной, отличное от объявленного в Make-файле (в данном случае это переменная VERSION), вы можете осуществить аналогичный вызов GNU Make:

make VERSION=1.2 backup

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

Переменные GNU Make также называются макросами. Следующее правило является реализацией цели make all:

all: executable

Это простая цель, которая позволяет создать программный продукт на основе его исходного кода путем передачи управления другому правилу (executable). Все программы проекта GNU должны в обязательном порядке поддерживать цель all.

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

clean:
	@$(RM) a.o b.o c.o
	@$(RM) $(EXECUTABLE)*

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

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

install: executable
	mv $(RELEASE_FILE) $(INSTALLED_FILE)

Цель target должна зависеть от цели executable, так как при отсутствии исполняемого файла вы не сможете установить его в систему!

Для копирования файла в директорию, расположенную за пределами домашней директории, обычно требуются особые привилегии. Директория /tmp является исключением из данного правила; однако, директория /tmp обычно очищается после перезагрузки системы. Если вам нужно установить бинарный файл в директорию /usr/local/bin, которая обычно используется для хранения исполняемых файлов, вам придется выполнить команду make install либо от лица пользователя root, либо с помощью утилиты sudo.

Следующие команды реализуют команду make backup, генерируя архив с резервной копией файлов проекта, уникально идентифицируемый с помощью времени и даты создания:

backup:
	$(TAR) cvf $(BACKUP_FILE) .
	$(GZIP) $(BACKUP_FILE)

Хранение резервной копии файлов проекта за пределами его директории является разумной практикой. В приведенном выше Make-файле файл архива сохраняется в директории /tmp, но вы можете использовать вместо нее любую другую директорию.

Другой переменной, практически всегда используемой в процессе компиляции кода на языках C и C++, является переменная CFLAGS, которая хранит флаги, используемые в процессе компиляции. Например, вы можете активировать режим вывода всех предупреждений компилятора и включить информацию о символах в результирующий бинарный файл для упрощения процесса отладки с помощью следующих флагов:

CFLAGS = -Wall -g

Вы можете изменить значение переменной CFLAGS для активации поддержки отладчиков, механизмов оптимизации кода или генерации машинного кода для отличной архитектуры центрального процессора.

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

%.o: %.c
	$(GCC) $(CFLAGS) -c $< -o $@

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

clean:
	@$(RM) *.o
	@$(RM) $(EXECUTABLE)*

Следующая цель позволяет вывести список значений всех переменных Make-файла:

printallvariables:
	$(foreach v, $(.VARIABLES), $(info $(v) = $($(v))))

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

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

printa:
	$(foreach v, $(filter A_%,$(.VARIABLES)), $(info $(v) = $($(v))))

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

Make масштабируется практически до любого уровня сложности проекта. Даже исходный код ядра Linux содержит Make-файлы

Make масштабируется практически до любого уровня сложности проекта. Даже исходный код ядра Linux содержит Make-файлы

Несмотря на то, что основным предназначением утилиты make является автоматизация процесса сборки программных проектов, администраторы Unux-систем, а также их обычные пользователи могут использовать ее и для своих целей. Вы можете использовать данную утилиту для создания резервных копий файлов конфигурации после их обновления, установки новых версий сценариев командной оболочки, генерации новых таблиц поиска Postfix с последующим перезапуском соответствующей службы, создания заданной структуры директорий для каждого из новых пользователей системы, генерации документации и выполнения других подобных действий - ее возможности безграничны; конечно же, вы также можете выработать новые методики использования утилиты GNU Make.