Gnu makefile руководство

Время на прочтение
6 мин

Количество просмотров 415K

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

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

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

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

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

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

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

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

{Из чего делаем? (реквизиты)} ---> [Как делаем? (команды)] ---> {Что делаем? (цели)}

Несложно заметить что процессы трансляции и компиляции очень красиво ложатся на эту схему:

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

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

Простейший Makefile

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

/*
 * main.c
 */
#include <stdio.h>
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 <stdio.h>
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 представляют собой именованные строки и определяются очень просто:

	<VAR_NAME> = <value string>

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

	SRC = main.c hello.c

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

	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 и в кратчайшие сроки освоить этот провереный временем инструмент.


Все примеры на GitHub

Тем, кто вошел во вкус:
Makefile mini HOWTO на OpenNET
GNU Make Richard M. Stallman и Roland McGrath, перевод © Владимир Игнатов, 2000
Эффективное использование GNU Make

Этот файл документирует утилиту GNU make , которая автоматически определяет, какие части большой программы необходимо перекомпилировать, и выдает команды для их перекомпиляции.

Это издание 0.75,последнее обновление 17 января 2020 года.Руководство GNU Make, для GNU make версии 4.3.

Copyright © 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Free Software Foundation, Inc.

• Overview Обзор make .
• Introduction Введение в make .
• Makefiles Make-файлы сообщают программе make , что делать.
• Rules Правила описывают,когда файл должен быть переделан.
• Recipes В рецептах говорится о том,как переделать файл.
• Использование переменных Чтобы избежать повторений,можно использовать переменные.
• Conditionals Используйте или игнорируйте части makefile,основываясь на значениях переменных.
• Functions Множество мощных способов работы с текстом.
• Вызов make Как вызвать make из командной строки.
• Неявные правила Используйте неявные правила,чтобы обрабатывать множество файлов одинаково,основываясь на их именах.
• Archives Как make может обновлять архивы библиотек.
• Расширение марки Использование расширений для make .
• Интеграция Интеграция make с другими инструментами.
• Features Особенности GNU make по сравнению с другими make .
• Missing Чего GNU make хватает у других make .
• Соглашения о файлах Makefile Соглашения по написанию make-файлов для программ GNU.
• Краткий справочник Краткое руководство для опытных пользователей.
• Сообщения об ошибках Список типичных ошибок, генерируемых make .
• Сложный Makefile Реальный пример простого,но нетривиального makefile.
• Лицензия на бесплатную документацию GNU Лицензия на копирование данного руководства.
• Указатель концепций Указатель понятий.
• Указатель имен Указатель функций, переменных и директив.
 — The Detailed Node Listing —

Overview of make

• Preparing Подготовка и make .
• Reading По прочтении этого текста.
• Bugs Проблемы и ошибки.
An Introduction to Makefiles

• Введение правила Как выглядит правило.
• Простой Makefile Простой makefile.
• Как работает Make Как make обрабатывает этот файл makefile.
• Упрощение переменных Переменные упрощают работу с makefiles.
• делать выводы Позволяя make вывести рецепты.
• Комбинировать по предварительным условиям Другой стиль makefile.
• Cleanup Правила очистки каталога.
Writing Makefiles

• Содержимое Makefile Что содержат makefiles.
• Имена make-файлов Как назвать свой makefile.
• Include Как один makefile может использовать другой makefile.
• Переменная MAKEFILES В окружении могут быть указаны дополнительные makefiles.
• Переделка файлов Makefile Как переделываются makefiles.
• Переопределение файлов Makefile Как переопределить часть одного makefile с помощью другого makefile.
• Чтение Make-файлов Как считываются makefiles.
• Разбор файлов Makefile Как разбираются makefiles.
• Вторичное расширение Как и когда проводится вторичное расширение.
What Makefiles Contain

• Линии разделения Разделение длинных строк в makefiles
Writing Rules

• Пример правила Пример объясняется.
• Синтаксис правила Объяснение общего синтаксиса.
• Необходимые типы Существует два типа предпосылок.
• Wildcards Использование символов подстановки,таких как ‘*’.
• Поиск в каталоге Поиск исходных файлов в других каталогах.
• Фальшивые цели Использование цели,которая не является именем реального файла.
• Силовые цели Вы можете использовать цель без рецепта или предварительных условий,чтобы пометить другие цели как фальшивые.
• Пустые цели Когда важна только дата,а файлы пусты.
• Специальные цели Цели со специальными встроенными значениями.
• Несколько целей Когда следует использовать несколько целей в правиле.
• Несколько правил Как использовать несколько правил с одной и той же целью.
• Статический шаблон Правила статического шаблона применяются к нескольким целям и могут варьировать предварительные условия в зависимости от имени цели.
• Double-Colon Как использовать особый вид правила,чтобы разрешить несколько независимых правил для одной цели.
• Автоматические предпосылки Как автоматически генерировать правила,задающие предварительные условия,из самих исходных файлов.
Using Wildcard Characters in File Names

• Примеры подстановочных знаков Several examples.
• Ловушка с подстановочными знаками Проблемы,которых следует избегать.
• Функция подстановочного знака Как вызвать расширение подстановочного знака там,где оно обычно не происходит.
Searching Directories for Prerequisites

• Общий поиск Указание пути поиска,который применяется к каждому необходимому условию.
• Выборочный поиск Указание пути поиска для заданного класса имен.
• Алгоритм поиска Когда и как применяются пути поиска.
• Recipes/Search Как составлять рецепты,которые работают вместе с поисковыми путями.
• Implicit/Search Как пути поиска влияют на неявные правила.
• Libraries/Search Поиск библиотек ссылок в каталоге.
Static Pattern Rules

• Статическое использование Синтаксис правил статических шаблонов.
• Статический или неявный Когда они лучше,чем неявные правила?
Writing Recipes in Rules

• Синтаксис рецепта Особенности и подводные камни синтаксиса рецептов.
• Echoing Как контролировать,когда рецепты повторяются.
• Execution Как выполняются рецепты.
• Parallel Как рецепты могут выполняться параллельно.
• Errors Что происходит после ошибки при выполнении рецепта.
• Interrupts Что происходит,когда рецепт прерывается.
• Recursion Вызов make из make-файлов.
• Консервированные рецепты Определение рецептов консервов.
• Пустые рецепты Определение полезных и не полезных рецептов.
Recipe Syntax

• Разделение строк рецептов Разбивайте длинные строки рецепта для удобства чтения.
• Переменные в рецептах Использование переменных make в рецептах.
Recipe Execution

• Одна оболочка Одна оболочка для всех строк в рецепте.
• Выбор оболочки Как make выбирает оболочку, используемую для выполнения рецептов.
Parallel Execution

• Параллельный выход Обработка вывода во время параллельного выполнения
• Параллельный ввод Обработка входных данных во время параллельного выполнения
Recursive Use of make

• СДЕЛАТЬ переменную Специальные эффекты от использования ‘$(MAKE)’.
• Variables/Recursion Как передать переменные в make .
• Options/Recursion Как сообщить о вариантах make .
• Опция -w Как ‘-w‘ или же ‘—print-directory‘ помогает отлаживать использование рекурсивных команд make .
How to Use Variables

• Reference Как использовать значение переменной.
• Flavors Переменные бывают двух видов.
• Advanced Расширенные возможности для ссылки на переменную.
• Values Все способы получения переменными своих значений.
• Setting Как установить переменную в makefile.
• Appending Как добавить дополнительный текст к старому значению переменной.
• Отменить директиву Как установить переменную в makefile,даже если пользователь задал ее с помощью командного аргумента.
• Multi-Line Альтернативный способ установки переменной в многострочную строку.
• Директива отмены определения Как переопределить переменную,чтобы она выглядела так,как будто никогда не была задана.
• Environment Значения переменных могут поступать из окружающей среды.
• Target-specific Значения переменных могут быть определены для каждой цели.
• Pattern-specific Значения переменных для конкретных целей могут быть применены к группе целей,соответствующих шаблону.
• Подавление наследования Подавление наследования переменных.
• Специальные переменные Переменные с особым значением или поведением.
Advanced Features for Reference to Variables

• Замена реф. Ссылка на переменную с подстановкой значения.
• Вычисляемые имена Вычисляет имя переменной,на которую нужно сослаться.
Conditional Parts of Makefiles

• Условный пример Пример условного
• Условный синтаксис Синтаксис условных выражений.
• Флаги тестирования Условные обозначения,проверяющие флаги.
Functions for Transforming Text

• Синтаксис функций Как написать вызов функции.
• Текстовые функции Функции работы с текстом общего назначения.
• Функции имени файла Функции для работы с именами файлов.
• Условные функции Функции,реализующие условия.
• Функция Foreach Повторите некоторый текст с контролируемой вариацией.
• Функция файла Запись текста в файл.
• Функция вызова Разверните функцию,определенную пользователем.
• Функция значения Возвращает нерасширенное значение переменной.
• Функция оценки Оцените аргументы как синтаксис makefile.
• Функция происхождения Найдите,откуда переменная получила свое значение.
• Функция вкуса Узнайте вкус переменной.
• Сделать функции управления Функции,управляющие процессом производства.
• Функция оболочки Заменить вывод команды оболочки.
• Функция хитрости Используйте встроенный скриптовый язык GNU Guile.
How to Run make

• Аргументы Makefile Как указать,какой makefile использовать.
• Goals Как использовать аргументы goal,чтобы указать,какие части makefile следует использовать.
• Вместо исполнения Как использовать флаги режима,чтобы указать,что делать с рецептами в makefile,кроме простого их выполнения.
• Избегание компиляции Как избежать перекомпиляции определенных файлов.
• Overriding Как переопределить переменную,чтобы указать альтернативный компилятор и другие вещи.
• Testing Как пройти мимо некоторых ошибок,чтобы проверить компиляцию.
• Сводка параметров Резюме вариантов
Using Implicit Rules

• Использование неявного Как использовать существующее неявное правило для получения рецептов обновления файла.
• Каталог правил Список встроенных правил.
• Неявные переменные Как изменить действия предопределенных правил.
• Связанные правила Как использовать цепочку неявных правил.
• Правила шаблона Как определить новые неявные правила.
• Последнее средство Как определить рецепт для правил,которые не могут найти ни одного.
• Правила суффикса Старомодный стиль неявного правила.
• Неявный поиск правил Точный алгоритм применения неявных правил.
Defining and Redefining Pattern Rules

• Введение в шаблон Введение в правила работы с шаблонами.
• Примеры узоров Примеры правил паттерна.
• Автоматические переменные Как использовать автоматические переменные в рецепте неявных правил.
• Соответствие шаблону Как сочетаются детали.
• Правила сопоставления чего угодно Меры предосторожности,которые следует предпринять перед определением правил,которые могут соответствовать любому целевому файлу.
• Отмена правил Как переопределить или отменить встроенные правила.
Using make to Update Archive Files

• Члены архива Члены архива в качестве целей.
• Обновление архива Неявное правило для целей-членов архива.
• Подводные камни архива Опасности,которых следует остерегаться при использовании архивов.
• Правила суффикса архива Вы можете написать специальный вид суффиксного правила для обновления архивов.
Implicit Rule for Archive Member Targets

• Символы архива Как обновить каталоги архивных символов.
Extending GNU make

• Интеграция коварства Использование Guile в качестве встроенного языка сценариев.
• Загрузка объектов Загрузка динамических объектов в качестве расширений.
GNU Guile Integration

• Типы хитрости Преобразование типов Guile в make .
• Хитрый интерфейс Вызов функций make из Guile.
• Пример хитрости Пример использования Guile в make .
Loading Dynamic Objects

• Директива загрузки Загрузка динамических объектов в качестве расширений.
• Переделка загруженных объектов Как переделываются загруженные объекты.
• API загруженных объектов Программный интерфейс для загруженных объектов.
• Пример загруженного объекта Пример загруженного объекта
Integrating GNU make

• Вакансии Делитесь вакансиями с GNU make .
• Терминальный выход Выход управления на клеммы.
Sharing Job Slots with GNU make

• Сервер заданий POSIX Использование сервера заданий на POSIX-системах.
• Сервер заданий Windows Использование сервера заданий в системах Windows.

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

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

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

Это может быть сделано (и часто делается) под определенную задачу с помощью кастомных скриптов, но мы в данной статье рассмотрим, как можно удобно автоматизировать сборку, используя утилиту make и её встроенные правила.

Почему стоит использовать утилиту make

  • она работает;
  • легко настраивается как для новых, так и для существующих проектов;
  • в большинстве ОС она предустановлена, если нет — её легко скачать;
  • она крошечная и содержит мало зависимостей;
  • make-файлы всё-таки могут быть короткими, ёмкими и красивыми;
  • она не использует загадочные папки типа working или resource;
  • да и вообще темной магией не занимается — всё на виду.

Пишем make-файлы

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

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

Базовый синтаксис для определения цели (в файле makefile):

			цель: реквизит1 реквизит2 ...
команда1
команда2
...
<пустая строка>
		

Важно Индентация производится с помощью табуляции, а не пробелов.

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

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

Шаблонные правила работают на основе сопоставления расширений файлов. Например, make знает, как создавать объектные файлы *.o из исходных C-файлов *.c, компилируя их и передавая компилятору флаг -c. В make есть несколько встроенных шаблонных правил, самые известные из которых используются для компиляции кода на C и C++.

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

В большинстве случаев мы можем даже опустить пререквизиты: внутренние правила make подразумевают, что для того, чтобы, например, собрать somefile.o по принципу Исходник на C → Объектный файл, нам нужен somefile.c.

Будьте аккуратны: когда вы предлагаете make свой список команд, она будет ориентироваться только на ваш код и в данном случае не будет искать шаблонные правила для сборки цели.

Вызываем make

Запустим make в текущей директории:

Если make-файл в ней уже есть, будет создана (собрана) первая цель, которую make сможет найти. Если make-файла нет (или он есть, но в нем нет целей), make об этом сообщит.

Чтобы обратиться к конкретной цели, запустите:

Здесь цель — это название цели (без квадратных скобок).

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

Специальные цели

В большинстве make-файлов можно найти цели, называемые специальными. Вот самые распространенные:

  • all — собрать весь проект целиком;
  • clean — удалить все сгенерированные артефакты;
  • install — установить сгенерированные файлы в систему;
  • release или dist — для подготовки дистрибутивов (модули и тарболы).

Они не обязательно должны присутствовать в make-файле, но большинство сборочных процессов странно представить без хотя бы первых трех.

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

Для предотвращения этого GNU make позволяет помечать такие цели как «фиктивные» (phony), чтобы запускать их в любом случае. Сделать это можно, добавив необходимые цели в качестве пререквизитов во внутреннюю цель .PHONY следующим образом:

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

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

Переменные и функции

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

Основные операции

Определять переменные и ссылаться на них можно следующим образом:

			NAME = value
FOO = baz
bar = $(FOO) frob
		

Ссылаться на переменные можно через $(NAME) или ${NAME}. Если опустить скобки, make сочтет за имя переменной только первый символ. Присоединение осуществляется при помощи оператора +=. Можно также задать условные переменные с помощью ?= (если им еще не присвоены значения).

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

Передача аргументов встроенным шаблонным правилам

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

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

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

Это позволяет, например, запускать один и тот же make-файл с разными компиляторами, снабдив make необходимым именем бинарного файла для выполнения. Так задаётся переменная среды компилятора C:

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

  • $(CC) / $(CXX) — бинарные файлы для компиляторов C и C++, которые make использует для сборки;
  • $(CFLAGS) / $(CXXFLAGS) — флаги, передаваемые компиляторам;
  • $(LDLIBS) — присоединяемые библиотеки.

Программные переменные

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

Самая важная из них — $(MAKE), которая должна использоваться при рекурсивном вызове make из make-файла. Она принимает во внимание аргументы командной строки из исходного вызова.

В цели clean, главная задача которой — удаление файлов, безопаснее использовать переменную $(RM) вместо прямого вызова rm.

Функции нескольких переменных

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

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

Вот некоторые наиболее интересные методы:

  • $(wildcard шаблон) возвращает список с названиями файлов, соответствующих шаблону, которые в том числе могут представлять собой относительный путь. Список внутри разделен с помощью пробелов, что проблематично для работы с файлами, содержащими пробелы в названии. Лучше всего избегать таких файлов при работе с make. Шаблон может содержать универсальный символ *;
  • $(patsubst шаблон поиска, шаблон замены, список слов) заменяет все слова в списке, которые соответствуют шаблону поиска в соответствии с шаблоном замены. Оба шаблона используют % в качестве символа;
  • $(filter-out шаблон поиска, список слов) возвращает список всех слов, отфильтрованных по шаблону поиска;
  • $(notdir список слов) возвращает список слов, где имя каждой записи сокращается до основного (то есть если имя содержит название директории, то оно отфильтровывается);
  • $(shell команда) запускает команду в подпроцессоре и перехватывает стандартный вывод подобно оператору !=. Оболочка для выполнения команды определяется переменной $(SHELL).

Подробное описание функций можно найти в официальной документации.

Продвинутое использование переменных

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

			OBJECTS = $(patsubst %.c,%.o,$(wildcard *.c))
all: $(OBJECTS)
		

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

Это становится мощным инструментом в сочетании с шаблонными правилами — мы можем автоматически собирать пререквизиты, которые косвенно используются в качестве новых целей.

Замена суффиксов

Для совместимости с другими реализациями make обеспечивает альтернативный синтаксис при вызове функции $(patsubst ...), называемый «ссылка с заменой» и позволяющий заменить некоторые суффиксы в списке слов на другие.

Make-файл из предыдущего примера можно преобразовать следующим образом:

			FILES != echo *.c
OBJS = $(FILES:.c=.o)
all: $(OBJS)
		

Важно Вместо функции $(wildcard ...) используется оператор !=.

Целезависимые переменные

Это ещё одна интересная особенность, использование которой ведёт к сокращению кода: она позволяет устанавливать переменным разные значения в зависимости от текущей цели.

			FOO = bar
target1: FOO = frob
target2: FOO += baz
		

В этом примере мы бы установили $(FOO) значение bar глобально, значение frob для цели один и значение bar baz для цели два.

Можно использовать любые необходимые операторы присваивания, что позволяет, например, создавать цели с разными наборами флагов для компилятора, просто присвоив переменной $(CFLAGS) разные значения.

Интеграция с внешними процессами

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

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

  • $(DESTDIR) должна быть пустой по умолчанию и никогда не должна задаваться из make-файла (режим чтения). Используется составителями пакета для вставки пути доступа перед устанавливаемыми файлами;
  • $(PREFIX) — значение этой переменной в вашем make-файле должно соответствовать /usr/local или другому заданному вами пути. Она позволяет пользователю пакета задать желаемую директорию для установки. Задавайте значение этой переменной, только если оно не было передано окружением (используя оператор ?=).

Определение шаблонных правил

Синтаксис для написания шаблонных правил во многом похож на обычный синтаксис целей. Основное отличие состоит в использовании шаблонного символа % в основе цели (её имени до расширения), который и будет применяться для поиска соответствий.

Шаблон также может быть использован в списке пререквизитов, где он будет заменён на основу для формирования неявно определённых пререквизитов. (Список пререквизитов в случае чего можно расширить и явно определёнными.)

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

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

Динамические переменные

Все имена динамических переменных состоят из одного знака, поэтому скобки для ссылки на них не нужны. Таких переменных довольно много, рассмотрим наиболее необходимые:

  • $@ — полное название цели;
  • $< — имя первого пререквизита (в том числе косвенно сгенерированного поиском по шаблону);
  • $^ — разделенный пробелами список всех пререквизитов (версия GNU).

Шаблонное правило, конвертирующее размеченные файлы в HTML с использованием markdown, получает такой вид:

			%.htm : %.md 
    markdown $^ > $@
		

Итоги

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

			.PHONY: all clean
ARTICLES = $(patsubst %.md,%.htm,$(wildcard *.md))

%.htm : %.md index.htm
./generate_article.py $< > $@

all: $(ARTICLES)

clean:
$(RM) $(ARTICLES)
		

Скрипт generate_article.py реализует минималистичный шаблонизатор, используя index.htm в качестве базы для вставки HTML, сгенерированного из входных файлов. Присутствие шаблонизатора в пререквизитах шаблонного правила обеспечивает, что изменения в шаблонизаторе вызовут изменения всех файлов, относящихся к статье.

Для дальнейшего изучения make рекомендуем ознакомиться с официальным руководством.

This is a complete beginner’s guide to using the make command in Linux.

You’ll learn:

  • The purpose of the make command
  • Installation of the make command
  • Creating and using the makefile for a sample C project

What is the make utility?

The make utility is one of the handiest utilities for a programmer. Its primary purpose is to compile a medium-to-large software project. The make utility is so helpful and versatile that even the Linux kernel uses it!

To understand the usefulness of the make utility, one must first understand why it was needed in the first place.

As your software gets more extensive, you start relying more and more on external dependencies (i.e., libraries). Your code starts splitting into multiple files with God knows what is in each file. Compiling each file and linking them together sanely to produce necessary binaries becomes complicated.

«But I can create a Bash script for that!»

Why yes, you can! More power to you! But as your project grows, you must deal with incremental rebuilds. How will you handle it generically, such that the logic stays true even when your number of files increases?

This is all handled by the make utility. So let us not reinvent the wheel and see how to install and make good use of the make utility.

Installing the make utility

The make utility is already available in the first-party repositories of almost all Linux distributions.

To install make on Debian, Ubuntu, and their derivatives, use the apt package manager like so:

sudo apt install make

To install make on Fedora and RHEL-based Linux distributions, use the dnf package manger like so:

sudo dnf install make

To install make on Arch Linux and its derivatives, use the pacman package manager like so:

sudo pacman -Sy make

Now that the make utility is installed, you can proceed to understand it with examples.

Creating a basic makefile

The make utility compiles your code based on the instructions specified in the makefile in the top level directory of your project’s code repository.

Below is the directory structure of my project:

$ tree make-tutorial

make-tutorial
└── src
    ├── calculator.c
    ├── greeter.c
    ├── main.c
    └── userheader.h

1 directory, 4 files

Below are the contents of the main.c source file:

#include <stdio.h>

#include "userheader.h"

int main()
{
    greeter_func();

    printf("\nAdding 5 and 10 together gives us '%d'.\n", add(5, 10));
    printf("Subtracting 10 from 32 results in '%d'.\n", sub(10, 32));
    printf("If 43 is  multiplied with 2, we get '%d'.\n", mul(43, 2));
    printf("The result of dividing any even number like 78 with 2 is a whole number like '%f'.\n", div(78, 2));

    return 0;
}

Next are the contents of the greeter.c source file:

#include <stdio.h>

#include "userheader.h"

void greeter_func()
{
    printf("Hello, user! I hope you are ready for today's basic Mathematics class!\n");
}

Below are the contents of the calculator.c source file:

#include <stdio.h>

#include "userheader.h"

int add(int a, int b)
{
    return (a + b);
}

int sub(int a, int b)
{
    if (a > b)
        return (a - b);
    else if (a < b)
        return (b - a);
    else return 0;
}

int mul(int a, int b)
{
    return (a * b);
}

double div(int a, int b)
{

    if (a > b)
        return ((double)a / (double)b);
    else if (a < b)
        return ((double)b / (double)a);
    else
        return 0;
}

Finally, below are the contents of the userheader.h header file:


Basics of a makefile

Before we create a bare-bones makefile, let us take a look at the syntax of a makefile. The basic building block of a Makefile consists of one or many «rules» and «variables».

Rules in a makefile

Let us first take a look at rules in the makefile. A rule of makefile has the following syntax:

target : prerequisites
    recipe
    ...
  • A target is the name of file that will be generated by make. These are usually object files that are later on used for linking everything together.
  • A prerequisite is a file that is necessary for the target to be generated. This is where you usually specify your .c, .o and .h files.
  • Finally, a recipe is one or many steps needed to generate the target.

Macros/Variables in makefile

In C and C++, a basic language feature is variables. They allow us to store values that we might want to use in a lot of places. This helps us use the same variable name where needed. An added benefit is we only need to make one change if we need to change the value.

Similarly, a makefile can contain variables. They are sometimes referred to as macros. The syntax to declare a variable in a Makefile is as follows:

variable = value

A variable and the value(s) it holds are separated by an equals (=) sign. Multiple values are separated by spaces between each other.

In general, variables are used to store various items necessary for compilation. Let’s say that you want to enable run-time buffer overflow detection and enable full ASLR for the executable; this can be achieved by storing all the compiler flags in one variable, like CFLAGS.

Below is a demonstration doing this:

CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie

We created a variable called CFLAGS (compiler flags) and added all of our compiler flags here.

To use our variable, we can enclose it in parentheses beginning with a dollar sign, like so:

gcc $(CFLAGS) -c main.c

The above line in our makefile will add all of our specified compiler flags and compile the main.c file as we require.

Automatic variables

The make utility has a few automatic variables to help ease repetition even further. These variables are commonly used in a rule’s recipe.

Some of the automatic variables are as follows:

Automatic variables Meaning
$@ Name of the rule of target. Usually used to specify the output filename.
$< Name of the first pre-requisite.
$? Names of all pre-requisites that are newer than the target. i.e. files that have been modified after the most recent code compilation
$^ Names of all pre-requisites with spaces between them.

You can find the full list of the automatic variables on GNU Make’s official documentation.

Implicit Variables

Like the automatic variables covered above, make also has some variables that have a set use. As I previously used the CFLAGS macro/variable to store compiler flags, there are other variables that have an assumed use.

This can be thought not of as «reserved keywords» but more like the «general consensus» of naming variables.

These conventional variables are as follows:

Implicit variables Description
VPATH Make utility’s equivalent of Bash’s PATH variable. Paths are separated by the colon sign (:). This is empty by default.
AS This is the assembler. The default is the as assembler.
CC The program for compiling C files. The default is cc. (Usually, cc points to gcc.)
CXX The program for compiling C++ files. The default is the g++ compiler.
CPP The program that runs the C pre-processor. The default is set to $(CC) -E.
LEX The program that turns Lexical grammars into source code. The default is lex. (You should change this to flex.)
LINT The program that lints your source code. The default is lint.
RM The command to remove a file. The default is rm -f. (Please pay strong attention to this!)
ASFLAGS This contains all the flags for the assembler.
CFLAGS This contains all the flags for the C compiler (cc).
CXXFLAGS This contains all the flags for the C++ compiler (g++).
CPPFLAGS This contains all the flags for the C pre-processor.
.PHONY Specify targets that do not resembe name of a file. An example is the «make clean» target; where clean is a value of .PHONY

Comments in a makefile are like those in a shell script. They start with the pound/hash symbol (#) and the contents of said line (after the pound/hash symbol) are considered as a comment by the make utility and is ignored.

Below is an example demonstrating this:

CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
# The '-D_FORTIFY_SOURCE=2' flag enables run-time buffer overflow detection
# The flags '-fpie -Wl,-pie' are for enabling complete address space layout randomization

Initial draft of a makefile

Now that I have described the basic syntax of a makefile’s elements and also the dependency tree of my simple project, let us now write a very bare-bones Makefile to compile our code and link everything together.

Let us start with setting up the CFLAGS, CC and the VPATH variables that are necessary for our compilation. (This is not the complete makefile. We will be building this progressively.)

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src

With that done, let us define our rules for building. I will create 3 rules, for each .c file. My executable binary will be called make_tutorial but yours can be whatever you want!

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o $@

main.o : main.c
        $(CC) $(CFLAGS) -c $? -o $@

calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@

As you can see, I am compiling all the .c files into object files (.o) and linking them together at the end.

When we run the make command, it will start with the first rule (make_tutorial). This rule is to create a final executable binary of the same name. It has 3 prerequisite object files for each .c files.

Each consecutive rule after the make_tutorial rule is creating an object file from the source file of same the name. I can understand how complex this feels. So let us break down each of these automatic and implicit variables and understand what they mean.

  • $(CC): Calls the GNU C Compiler (gcc).
  • $(CFLAGS): An implicit variable to pass in our compiler flags like -Wall, etc.
  • $?: Names of all prerequisite files that are newer than the target. In the rule for main.o, $? will expand to main.c IF main.c has been modified after main.o had been generated.
  • $@: This is the target name. I am using this to omit typing the rule name twice. In rule for main.o, $@ expands to main.o.

Finally, the options -c and -o are gcc‘s options for compiling/assembling source files without linking and specifying an output file name respectively. You can check this by running the man 1 gcc command in your terminal.

Now let’s try and run this makefile and hope it works on first try!

$ make
gcc -Wall -Wextra -c src/main.c -o main.o
gcc -Wall -Wextra -c src/calculator.c -o calculator.o
gcc -Wall -Wextra -c src/greeter.c -o greeter.o
gcc -Wall -Wextra main.o calculator.o greeter.o -o make_tutorial

If you look closely, each step of compilation contains all the flags we specified in the CFLAGS implicit variable. We can also see that the source files were automatically sourced from the «src» directory. This occurred automatically because we specified «src» in the VPATH implicit variable.

Let’s try and run the make_tutorial binary and verify if everything works as intended.

$ ./make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.

via GIPHY

Improving the makefile

«What is there to improve?»
Let us run the ls command you can see that for yourself ;)

$ ls --group-directories-first -1
src
calculator.o
greeter.o
main.o
Makefile
make_tutorial

Do you see the build artifacts (object files)? Yeah, they can clutter things up for the worse. Let’s use our build directory and reduce this clutter.

Below is the modified makefile:

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o $@

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o $@

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@

Here, I have made one simple change: I added the build/ string before each rule that generates an object file. This will put each object file inside the «build» directory. I also added «build» to the VPATH variable.

If you look closely, our first compilation target is make_tutorial. But it will not be the target that is pedantically the first. The first target whose recipe runs is main.o (or rather build/main.o). Therefore, I added the «mkdir build» command as a recipe in the main.o target.

If I were to not create the «build» directory, I would get the following error:

$ make
gcc -Wall -Wextra -c src/main.c -o build/main.o
Assembler messages:
Fatal error: can't create build/main.o: No such file or directory
make: *** [Makefile:12: build/main.o] Error 1

Now that we have modified our makefile, let us remove the current build artifacts along with the compiled binary and rerun the make utility.

$ rm -v *.o make_tutorial
removed 'calculator.o'
removed 'greeter.o'
removed 'main.o'
removed 'make_tutorial'

$ make
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o make_tutorial

This compiled perfectly! If you look closely, we had already specified the «build» directory in the VPATH variable, making it possible for the make utility to search for our object files inside the «build» directory.

Our source and header files were automatically found from the «src» directory and the build artifacts (object files) were kept inside and linked from the «build» directory, just as we intended.

Adding .PHONY targets

We can take this improvement one step further. Let’s add the «make clean» and «make run» targets.

Below is our final makefile:

CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


build/bin/make_tutorial : main.o calculator.o greeter.o
        mkdir build/bin
        $(CC) $(CFLAGS) $? -o $@

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o $@

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o $@

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o $@


.PHONY = clean
clean :
        rm -rvf build


.PHONY = run
run: make_tutorial
        ./build/bin/make_tutorial

Everything about the build targets is the same, except for a change where I specify that I want the make_tutorial binary executable file placed inside the build/bin/ directory.

Then, I set .PHONY variable to clean, to specify that clean is not a file that the make utility needs to worry about. It is… phony. Under the clean target, I specify what must be removed to «clean everything».

I do the same for the run target. If you are a Rust developer you will like this pattern. Like the cargo run command, I use the make run command to run the compiled binary.

For us to run the make_tutorial binary, it must exist. So I added it to the prerequisite for the run target.

Let’s run make clean first and then run make run directly!

$ make clean
rm -rvf build
removed 'build/greeter.o'
removed 'build/main.o'
removed 'build/calculator.o'
removed 'build/bin/make_tutorial'
removed directory 'build/bin'
removed directory 'build'

$ make run
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
mkdir build/bin
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o build/bin/make_tutorial
./build/bin/make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.

As you see here, we did not run the make command to compile our project first. Upon running the make run, compilation was taken care of. Let’s understand how it happened.

Upon running the make run command, the make utility first looks at the run target. A prerequisite for the run target is our binary file that we compile. So our make_tutorial binary file gets compiled first.

The make_tutorial has its own prerequisites which are placed inside the build/ directory. Once those object files are compiled, our make_tutorial binary is compiled; finally, the Make utility returns back to the run target and the binary file ./build/bin/make_tutorial is executed.

such elegance much wow

Conclusion

This article covers the basics of a makefile, a file that the make utility depends on, to simplify compilation of your software repository. This is done by starting from a basic Makefile and building it as our needs grow.


Мини-руководство по созданию Makefile-ов.

Автор : Tedi Heriyanto (http://www.linuxgazette.com/authors/heriyanto.html)

Перевод : Андрей Киселев ([email protected])

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

Введение

Допустим, вы разрабатываете некую программу под названием foo, состоящую из пяти заголовочных файлов — 1.h, 2.h, 3.h, 4.h и — 5.h, и шести файлов с исходным текстом программы на языке С — 1.cpp, 2.cpp, 3.cpp, 4.cpp, 5.cpp и main.cpp. (Хочу заметить, что в реальных проектах следует избегать подобного стиля именования файлов).

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

Существует ли решение проблемы?

Не стоит беспокоиться, друзья мои! Эта проблема уже давно решена. Опытными программистами была разработана утилита make. Вместо того, чтобы производить повторную компиляцию всех файлов с исходными текстами, она обрабатывает только те файлы, которые претерпели изменения. В нашем случае будет скомпилирован только один файл — 2.cpp. Разве это не здорово!?

Кроме того [2]:

  • Утилита make значительно упрощает жизнь, когда для сборки проекта необходимо выполнение длинных и сложных команд.
  • Проект иногда требует задания редко используемых, а потому сложных для запоминания опций компилятора. make избавит вас от необходимости удерживать их в памяти.
  • Единообразие, т.к. работа с этой утилитой поддерживается многими средами разработки.
  • Процесс сборки можно автоматизировать, поскольку make может быть вызвана из сценариев или из cron.

Несмотря на все свои достоинства, утилита make ничего не знает о нашем проекте, поэтому необходимо создать простой текстовый файл, который будет содержать все необходимые инструкции по сборке. Файл с инструкциями по сборке проекта называется makefile (произносится как «мэйкфайл». прим. перев.).

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

Например, если свой makefile вы назвали bejo, то команда на сборку проекта будет выглядеть так:

make -f bejo

Структура файла

Makefile содержит разделы для «целей» [targets], зависимостей [dependencies] и правил (rules) сборки. Все это оформляется следующим образом: сначала указывается имя цели (обычно это имя исполняемого или объектного файла), после которого следует двоеточие, затем следуют имена зависимостей, т.е. файлов, необходимых для получения данной цели. И, наконец, следует список правил: т.е. команд, которые необходимо выполнить для получения указанной цели.

Простой пример структуры makefile’а:

target: dependencies
        command
        command
        ...

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

Пример Makefile.

Ниже приводится простой пример (номера строк добавлены для ясности).

1 client: conn.o
2       g++ client.cpp conn.o -o client

3 conn.o: conn.cpp conn.h
4   g++ -c conn.cpp -o conn.o

В этом примере строка, содержащая текст
client: conn.o,
называется «строкой зависимостей», а строка
g++ client.cpp conn.o -o client
называется «правилом» и описывает действие, которое необходимо выполнить.

А теперь более подробно о примере, приведенном выше:

  • Задается цель — исполняемый файл client, который зависит от объектоного файла conn.o
  • Правило для сборки данной цели
  • В третьей строке задается цель conn.o и файлы, от которых она зависит — conn.cpp и conn.h.
  • В четвертой строке описывается действие по сборке цели conn.o.

Комментарии

Строки, начинающиеся с символа «#», являются комментариями

Ниже приводится пример makefile с комментариями:

1 # Создать исполняемый файл "client"
2 client: conn.o
3    g++ client.cpp conn.o -o client
4
5 # Создать объектный файл "conn.o"
6 conn.o: conn.cpp conn.h
7    g++ -c conn.cpp -o conn.o

«Ложная» цель[1]

Обычно «ложные» [phony] цели, представляющие «мнимое» имя целевого файла, используются в случае возникновения конфликтов между именами целей и именами файлов при явном задании имени цели в командной строке.

Допустим в makefile имеется правило, которое не создает ничего, например:

clean:
        rm *.o temp

Поскольку команда rm не создает файл с именем clean, то такого файла никогда не будет существовать и поэтому команда make clean всегда будет отрабатывать.

Однако, данное правило не будет работать, если в текущем каталоге будет существовать файл с именем clean. Поскольку цель clean не имеет зависимостей, то она никогда не будет считаться устаревшей и, соответственно, команда ‘rm *.o temp’ никогда не будет выполнена. (при запуске make проверяет даты модификации целевого файла и тех файлов, от которых он зависит. И если цель оказывается «старше», то make выполняет соответствующие команды-правила — прим. ред.) Для устранения подобных проблем и предназначена специальная декларация .PHONY, объявляющая «ложную» цель. Например:

 .PHONY : clean

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

Переменные

Определить переменную в makefile вы можете следующим образом:

$VAR_NAME=value

В соответствии с соглашениями имена переменных задаются в верхнем регистре:

$OBJECTS=main.o test.o

Чтобы получить значение переменной, необходимо ее имя заключить в круглые скобки и перед ними поставить символ ‘$’, например:

$(VAR_NAME)

В makefile-ах существует два типа переменных: «упрощенно вычисляемые» и «рекурсивно вычисляемые».

В рекурсивно вычисляемых переменных все ссылки на другие переменные будут замещены их значениями, например:

TOPDIR=/home/tedi/project
SRCDIR=$(TOPDIR)/src

При обращении к переменной SRCDIR вы получите значение /home/tedi/project/src.

Однако рекурсивные переменные могут быть вычислены не всегда, например следующие определения:

CC = gcc -o
CC = $(CC) -O2

выльются в бесконечный цикл. Для разрешения этой проблемы следует использовать «упрощенно вычисляемые» переменные:

CC := gcc -o
CC += $(CC) -O2

Где символ ‘:=’ создает переменную CC и присваивает ей значение «gcc -o». А символ ‘+=’ добавляет «-O2» к значению переменной CC.

Заключение

Я надеюсь, что это краткое руководство содержит достаточно информации, чтобы начать создавать свои makefile. А за сим — успехов в работе.

Библиография

  • 1 GNU Make Documentation File, info make.
  • 2 Kurt Wall, et.al., Linux Programming Unleashed (Программирование под Linux на оперативном просторе — прим. ред.), 2001.

Понравилась статья? Поделить с друзьями:
  • Работник организации должен сообщить руководство организации ответственно на лицо
  • Дантинорм инструкция по применению для детей до года при прорезывании
  • Стол книжка с баром инструкция по сборке
  • Кофемашина philips grind brew hd7767 инструкция
  • Ветом 2 инструкция по применению для человека отзывы цена инструкция