Время на прочтение
6 мин
Количество просмотров 397K
Меня всегда привлекал минимализм. Идея о том, что одна вещь должна выполнять одну функцию, но при этом выполнять ее как можно лучше, вылилась в создание 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
, а также научимся писать простые и аккуратные make-файлы. Последнее особенно важно — make-файлы выглядят сложно и нечитабельно, если им не уделить должного внимания. Это обеспечивает make
плохую репутацию, хотя на самом деле инструмент крайне удобный, особенно для автоматизации сборки.
В большинстве проектов, будь то разработка ПО, написание книги или публикация записи в блоге, в какой-то момент приходится генерировать «конечные продукты» из вручную написанных исходных файлов, то есть осуществлять сборку.
Суть автоматической сборки состоит в том, чтобы взять за основу генерацию какого-то одного элемента, вычленить правила, по которым она происходит, и применить их к большему количеству элементов такого же типа, обновляя только изменяемые параметры.
Это может быть сделано (и часто делается) под определенную задачу с помощью кастомных скриптов, но мы в данной статье рассмотрим, как можно удобно автоматизировать сборку, используя утилиту make
и её встроенные правила.
Почему стоит использовать утилиту make
- она работает;
- легко настраивается как для новых, так и для существующих проектов;
- в большинстве ОС она предустановлена, если нет — её легко скачать;
- она крошечная и содержит мало зависимостей;
- make-файлы всё-таки могут быть короткими, ёмкими и красивыми;
- она не использует загадочные папки типа
working
илиresource
; - да и вообще темной магией не занимается — всё на виду.
Создадим файл и назовем его 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
об этом сообщит.
Чтобы обратиться к конкретной цели, запустите:
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
следующим образом:
.PHONY: all clean run
Цель all
обычно просто содержит главный исполняемый файл в пререквизите — иногда с дополнительными командами для пост-обработки генерируемых файлов. Так как мы в большинстве случаев хотим, чтобы цель all
исполнялась по умолчанию, она должна стоять первой в файле.
Для цели clean
стоит добавить список команд, удаляющих все генерируемые файлы. Это может быть легко сделано с использованием переменных, о которых мы поговорим в следующем разделе.
Переменные и функции
Как и в большинстве языков программирования, make-файлы можно сделать более читабельными, используя переменные. make
импортирует свою среду, позволяя нам определять переменные внутри файла через внешние источники.
Основные операции
Определять переменные и ссылаться на них можно следующим образом:
NAME = value
FOO = baz
bar = $(FOO) frob
Ссылаться на переменные можно через $(NAME)
или ${NAME}
. Если опустить скобки, make
сочтет за имя переменной только первый символ. Присоединение осуществляется при помощи оператора +=
. Можно также задать условные переменные с помощью ?=
(если им еще не присвоены значения).
Наконец, большинство реализаций make
позволяют нам задать выходную переменную с помощью оператора !=
при порождении одного подпроцесса за операцию.
Передача аргументов встроенным шаблонным правилам
Ранее мы видели некоторые шаблонные правила в действии, правда, во всех случаях они запускали одни и те же базовые команды. Конечно, требования к сборке не всегда одинаковы, и инструменты могут запрашивать разные флаги для разных задач.
Например, какому-то ПО нужно обеспечить соединение с разными библиотеками, или заданные оптимизации различаются для сборки отладки и итоговой сборки.
Поэтому встроенные правила в make
включают в себя несколько распространённых переменных в важных местах команд. Мы можем установить их по желанию из make-файла или внешней среды.
Это позволяет, например, запускать один и тот же make-файл с разными компиляторами, снабдив make
необходимым именем бинарного файла для выполнения. Так задаётся переменная среды компилятора C:
CC=clang make
Вот некоторые из самых известных переменных, которые вы могли видеть, если когда-нибудь заглядывали в 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
рекомендуем ознакомиться с официальным руководством.
По материалам статьи «Make Files Not War»
Introduction | |
Установка | |
Проверить версию | |
Для чего используются Makefiles | |
Формат | |
.PHONY: | |
Посмотреть цели Make-файла | |
Пример из C++ | |
Переменные | |
Docker из Makefile | |
Параметризация Make | |
BUILD_ID | |
USER_ID | |
Альтернативы | |
$$: Вызов bash команд (например whoami) | |
—: Игнорировать ошибки | |
Цель из других целей | |
Несколько make-файлов в одной директории | |
Связанные статьи |
Установить make
sudo apt install make
или для rpm
sudo yum install make
Так как make входит в состав build-essentials можно установить вместе с этим пакетом
sudo apt install build-essentials
Проверить версию make
/usr/bin/make —version
GNU Make 4.2.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Для чего используются Makefiles
Make-файлы используются, чтобы помочь решить, какие части большой
программы должны быть перекомпилированы.
В подавляющем большинстве случаев компилируются файлы
C
или
C++
.
Другие языки обычно имеют свои собственные инструменты, которые служат той же цели, что и Make.
Его можно использовать и за пределами программ, когда вам нужна серия инструкций для запуска
в зависимости от того, какие файлы изменились.
В этой статье вы узнаете про использование компиляции C/C++.
Вот пример графика зависимостей, который вы можете построить с помощью Make.
Если какие-либо зависимости файла изменятся, то файл будет перекомпилирован:
wikipedia.org
Формат
Makefile состоит из правил (rules).
Первым указывается название цели (target), затем зависимости (prerequisites)
и действие (recipe — набор действий/команд), которое нужно выполнить.
Зависимости нужны не всегда и указываются по необходимости. Для простоты на первом этапе можно
думать о зависимостях как о файлах, которые нужно проверить: если ни один не изменился — заново
компилировать не нужно.
Отступы по умолчанию нужно ставить табуляцией. Если хотите поменять на другой символ — задайте
.RECIPEPREFIX
target: prerequisites
recipe
На русский обычно переводят так
цель: зависимости
команды
Типичное применение: какая-то зависимость изменилась → выполнятеся действие в результате которого
создаётся таргет файл.
output: main.o message.o
g++ main.o message.o -o output
clean:
rm *.o output
Как и в статье
Configure, make, install
в примере выше используются стандартные цели (target)
Про опции -o и -c
читайте статью
«Компиляция в C++
Опция | Назначение |
---|---|
-c | Указывает компилятору не делать линковку и создавать .o файлы для каждого исходника |
-o filename | Меняет название output файла со стадартного на указанный |
-S | Directs the compiler to produce an assembly source file but not to assemble the program. |
Дополнительная информация (на
английском
):
gnu.org: Rule-Introduction
Если файл вам не нужен, например, вы просто хотите выполнить какие-то команды — можно
использовать .PHONY
.PHONY
.PHONY: site
site:
echo "HeiHei.ru"
Если теперь выполнить
make site
echo «HeiHei.ru»
HeiHei.ru
Удалите site из первой строки, а всё остальное не трогайте
make site
echo «HeiHei.ru»
HeiHei.ru
Вроде бы ничего не изменилось, но теперь создайте файл
site
рядом с
Makefile
touch site
make site
make: ‘site’ is up to date.
Так как таргет теперь реальный — make не нашёл изменений и ничего не сделал. Из-за такого простого
совпадения имени цели (target) и какого-то файла в директории может перестать работать скрипт.
Для защиты от таких неприятностей и применяют PHONY
Также PHONY удобен тем, что можно перечислить все цели в самом начале файла.
Если не злоупотреблять этой возможностью — можно улучшить читаемость кода, особенно в небольших файлах.
Посмотреть цели Make-файла
Если вы создали Make-файл с большим количеством PHONY целей и забыли название нужно — не обязательно продираться через весь файл
Чтобы получить списко всех целей воспользуйтесь
grep
и выполните
cat GNUmakefile | grep PHONY:
Пример из C++
Рассмотрим пример из статьи о
заголовочных файлах .h
Есть три файла
ls
Functions.cpp Functions.h Main.cpp
Main.cpp
#include <iostream>
#include "Functions.h"
int main() {
double b = add(1.3, 4.5);
cout << "1.3 + 4.5 is " << b << "n";
return 0;
}
Functions.cpp
double add(double x, double y)
{
return x + y;
}
Functions.h
#pragma once
double add(double x, double y);
Если один из этих файлов изменился — нужно перекомпилировать проект. Для начала будем пользоваться командой
g++ -o output Main.cpp Functions.cpp
Эта команда сначала вызывает компиляцию, затем линковку
Создайте
Makefile
и откройте его в текстовом редакторе. Например, в
Vim
touch Makefile
vi Makefile
Makefile
будет выглядеть следующим образом
output: Main.cpp Functions.cpp Functions.h
g++ -o output Main.cpp Functions.cpp
Теперь для компиляции достаточно выполнить
make output
Или просто
make
В результате появится исполняемый файл
output
В этот пример можно добавить ещё два шага: отдельно следить за компиляцией и убираться после работы.
Если вам не понятно что происходит в этом файле — изучите статью
«Компиляция в C++
.PHONY: clean
output: Main.o Functions.o
g++ Main.o Functions.o -o output
Main.o: Main.cpp
g++ -c Main.cpp
Functions.o: Functions.cpp
g++ -c Functions.cpp
clean:
rm *.o output
To запустить скрипт, достаточно выполнить
make
g++ -c Main.cpp
g++ -c Functions.cpp
g++ -o output Main.o Functions.o
Если нужно скомпилировать Main execute
make Main.o
g++ -c Main.cpp
ls
Появится файл
Main.o
но не появятся остальные (Functions.o, output)
Functions.cpp Functions.h Main.cpp Main.o Makefile
На примере команды make Main.o можно понять почему в Make-файлах используется термин цели (target)
make
Main.o
говорит — создай файл
Main.o
а инструкция в Makefile определяет правило по которому это нужно сделать.
Если теперь выполнить make
Main.o
не будет перекомпилироваться. Будут выполнены только последние два шага.
g++ -c Functions.cpp
g++ -o output Main.o Functions.o
Выполните make если ещё не выполняли и не делайте после этого clean
Добавим ещё одну функцию в наш проект. Нужно указать её в файлах Functions.*
Вызывать пока не будет, поэтому
Main.cpp
остаётся без изменений
Functions.cpp
bool test(bool x)
{
return x;
}
Functions.h
bool test(bool x);
make
g++ -c Functions.cpp
g++ -o output Main.o Functions.o
Обратите внимание:
Main.cpp
не был перекомпилирован так как в нём нет изменений.
Таже посмотрите на время изменения файла
output
оно должно измениться.
Не вносите никаких изменений в файлы и execute
make
make: ‘output’ is up to date.
Перекомпиляция не нужна и поэтому не выполнена
Переменные
Подробнее про переменные в Makefile читайте в статье
Работа с переменными в GNUmakefile
В этом примере вы можете увидеть как названия файлов сохранены в переменную для сокращения кода.
.PHONY: clean
objects = Main.o Functions.o
output: $(objects)
g++ -o output $(objects)
Main.o: Main.cpp
g++ -c Main.cpp
Functions.o: Functions.cpp
g++ -c Functions.cpp
clean:
rm *.o output
Запустить Docker container из Makefile
.PHONY: docker
docker:
docker-compose -f docker/dev/docker-compose.yml build
Параметризация Make
?= позволяет переменным быть перезаписанными на существующие переменные окружения
:= перезаписывает значение переменной
PROJECT_NAME ?= myproject
ORG_NAME ?= heihei
REPO_NAME ?= myproject
#Filenames
DEV_COMPOSE_FILE := docker/dev/docker-compose.yml
REL_COMPOSE_FILE := docker/release/docker-compose.yml
.PHONY: test release
test:
docker-compose -f $(DEV_COMPOSE_FILE) build
docker-compose -f $(DEV_COMPOSE_FILE) up agent
docker-compose -f $(DEV_COMPOSE_FILE) up test
release:
docker-compose -f $(REL_COMPOSE_FILE) build
docker-compose -f $(REL_COMPOSE_FILE) up agent
docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py collectstatic --noinput
docker-compose -f $(REL_COMPOSE_FILE) run --rm app manage.py migrate --noinput
docker-compose -f $(REL_COMPOSE_FILE) up test
clean:
docker-compose -f $(DEV_COMPOSE_FILE) kill
docker-compose -f $(DEV_COMPOSE_FILE) rm -f
docker-compose -f $(REL_COMPOSE_FILE) kill
docker-compose -f $(DEV_COMPOSE_FILE) rm -f
BUILD_ID
To добавить переменным уникальности используют BUILD_ID
# Docker Compose Project Names
REL_PROJECT := $(PROJECT_NAME)$(BUILD_ID)
DEV_PROJECT := $(REL_PROJECT)dev
USER_ID
To получить ID пользователя запустившего GNUmakefile
USER_ID = $(shell id -u ${USER})
Какие альтернативы Make существуют
Популярными альтернативными системами сборки C/C++ являются
SCons, CMake, Bazel и Ninja. Некоторые редакторы кода, такие как
Microsoft Visual Studio
, имеют свои собственные встроенные инструменты сборки.
Для
Java
есть Ant,
Maven
и Gradle.
Другие языки, такие как
Go
и Rust, имеют свои собственные инструменты сборки.
Интерпретируемые языки, такие как
Python
,
Ruby
и
JavaScript
, не требуют аналога для создания файлов.
Цель Makefile состоит в том, чтобы скомпилировать любые файлы, которые
должны быть скомпилированы, основываясь на том, какие файлы изменились.
Но когда файлы на интерпретируемых языках меняются, ничего не нужно перекомпилировать.
При запуске программы используется самая последняя версия файла.
Что означает cc -c
cc это C compiler
Существует несколько общедоступных компиляторов C
В этой статье использовался
gcc
-c это опция, которую разбирали
здесь
whoami
В обычном
Bash скрипте
достаточно написать $(whoami) и это будет равносильно подстановке вывода whoami
В Make файле это может не получиться. Есть два варианта решить проблему
`whoami`
И
$$(whoami)
Игнорировать ошибки
Если какая-то команда выполнена с ошибкой выполнение сценария прерывается.
Рассмотрим пример
RPM_DIR=/home/$$(whoami)/rpms/
.PHONY: clean-repo
clean-repo:
@sudo rm $(RPM_DIR)release/*
@sudo rm $(RPM_DIR)master/*
Если в …release/ пусто, то удалять в …master/ make уже не будет.
Вместо этого появится ошибка:
sudo rm /home/$(whoami)/rpms/release/*
rm: cannot remove ‘/home/andrei/rpms/release/*’: No such file or directory
make: *** [clean-repo] Error 1
Избежать этой проблемы можно поставив — перед командой
RPM_DIR=/home/$$(whoami)/rpms/
.PHONY: clean-repo
clean-repo:
@-sudo rm $(RPM_DIR)release/*
@-sudo rm $(RPM_DIR)master/*
[andrei@localhost ~]$ make clean-repo
rm: cannot remove ‘/home/andrei/rpms/release/*’: No such file or directory
make: [clean-repo] Error 1 (ignored)
make жалуется, но переходит ко второй команде и чистит директорию.
Цель из других целей
Если нужно запустить несколько целей сразу, можно вызывать из новой цели
all-targets: target1 target2 target3
Несколько make-файлов в одной директории
Если в одной директории находится два и более make-файлов с совпадающими целями, вызывать
из нужного файла помогает опция -f
Пример проекта
make
├── GNUmakefile.beget
└── GNUmakefile.heihei
# GNUmakefile.beget
.PHONY: url
url:
echo «https://beget.com»
# GNUmakefile.heihei
.PHONY: url
url:
echo «https://heihei.ru»
make -f GNUmakefile.beget url
echo «https://beget.com»
https://beget.com
make -f GNUmakefile.heihei url
echo «https://heihei.ru»
https://heihei.ru
make | |
Основы make | |
PHONY | |
CURDIR | |
shell | |
wget + make | |
Переменные в Make файлах | |
ifeq: Условные операторы | |
filter | |
-c: Компиляция | |
Linux | |
Bash | |
C | |
C++ | |
C++ Header файлы | |
Configure make install | |
DevOps | |
Docker | |
OpenBSD | |
Errors 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 is a popular and commonly used program for building C language software. It is used when building the Linux
kernel
and other frequently used GNU/Linux programs and software libraries.
Most embedded software developers will work with GNU Make at some point in their career, either using it to compile small libraries or building an entire project. Though there are many, many
alternatives to Make,
it’s still commonly chosen as the build system for new software given its feature set and wide support.
This article explains general concepts and features of GNU Make and includes
recommendations for getting the most out of a Make build! Consider it a brief
guided tour through some of my favorite/most used Make concepts and features 🤗.
If you feel like you already know Make pretty well, feel free to skip the tutorial portion and jump to my personal recommendations.
Table of Contents
- What is GNU Make?
- When to choose Make
-
Invoking Make
- Parallel Invocation
-
Anatomy of a Makefile
- Variables
- Targets (Goals)
- Prerequisites
- Recipe
-
Advanced Topics
- Functions
- Conditionals
include
Directive- Sub-make
- Metaprogramming with
eval
- VPATH
touch
-
Debugging Makefiles
- Profiling
- Using a Verbose Flag
- Full Example
- Recommendations
- Outro
- References
What is GNU Make?
GNU Make is a program that automates the
running of shell commands and helps with repetitive tasks. It is typically used to transform files into some other form, e.g. compiling
source code files into programs or libraries.
It does this by tracking prerequisites and executing a hierarchy of commands to
produce targets.
Although the GNU Make manual is lengthy, I suggest giving it a read as it is the best reference I’ve found:
https://www.gnu.org/software/make/manual/html_node/index.html
Let’s dive in!
When to choose Make
Make is suitable for building small C/C++ projects or libraries that would
be included in another project’s build system. Most build systems will have a
way to integrate Make-based sub-projects.
For larger projects, you will find a more modern build system easier to work with.
I would suggest a build system other than Make in the following situations:
- When the number of targets (or files) being built is (or will eventually be) in the hundreds.
- A “configure” step is desired, which sets up and persists variables, target definitions, and environment configurations.
- The project is going to remain internal or private and will not need to be built by end users.
- You find debugging a frustrating exercise.
- You need the build to be cross platform that can build on macOS, Linux, and Windows.
In these situations, you might find using
CMake, Bazel,
Meson, or another modern build system a more pleasurable experience.
Invoking Make
Running make
will load a file named Makefile
from the current directory
and attempt to update the default goal (more on goals later).
Make will search for files named
GNUmakefile
,makefile
, andMakefile
, in that
order
You can specify a particular makefile with the -f/--file
argument:
You can specify any number of goals by listing them as positional arguments:
# typical goals
$ make clean all
You can pass Make a directory with the -C
argument, and this will run Make as if it first cd
‘d into that directory.
$ make -C some/sub/directory
Fun fact:
git
also can be run with-C
for the same effect!
Parallel Invocation
Make can run jobs in parallel if you provide the -j
or -l
options. A
guideline I’ve been told is to set the job limit to 1.5 times the number of
processor cores you have:
# a machine with 4 cores:
$ make -j 6
Anecdotally, I’ve seen slightly better CPU utilization with the -l
“load
limit” option, vs. the -j
“jobs” option. YMMV though!
There are a few ways to programmatically find the CPU count for the current
machine. One easy option is to use the python multiprocessing.cpu_count()
function to get the number of threads supported by the system (note on a system
with hyper-threading, this will use up a lot of your machine’s resources,
but is probably preferable to letting Make spawn an unlimited number of jobs).
# call the python cpu_count() function in a subshell
$ make -l $(python -c "import multiprocessing; print(multiprocessing.cpu_count())")
Output During Parallel Invocation
If you have a lot of output from the commands Make is executing in parallel, you
might see output interleaved on stdout
. To handle this, Make has the option --ouput-sync
.
I recommend using --output-sync=recurse
, which will print the entire
output of each target’s recipe when it completes, without interspersing other
recipe output.
It also will output an entire recursive Make’s output together if your recipe
is using recursive make.
Anatomy of a Makefile
A Makefile contains rules used to produce targets. Some basic components of a Makefile are shown below:
# Comments are prefixed with the '#' symbol
# A variable assignment
FOO = "hello there!"
# A rule creating target "test", with "test.c" as a prerequisite
test: test.c
# The contents of a rule is called the "recipe", and is
# typically composed of one or more shell commands.
# It must be indented from the target name (historically with
# tabs, spaces are permitted)
# Using the variable "FOO"
echo $(FOO)
# Calling the C compiler using a predefined variable naming
# the default C compiler, '$(CC)'
$(CC) test.c -o test
Let’s take a look at each part of the example above.
Variables
Variables are used with the syntax $(FOO)
, where FOO
is the variable name.
Variables contain purely strings as Make does not have other data types. Appending to a variable will add a space and the new content:
FOO = one
FOO += two
# FOO is now "one two"
FOO = one
FOO = $(FOO)two
# FOO is now "onetwo"
Variable Assignment
In GNU Make syntax, variables are assigned with two “flavors”:
-
recursive expansion:
variable = expression
The expression on the right hand side is assigned verbatim to the variable-
this behaves much like a macro in C/C++, where the expression is evaluated
when the variable is used:FOO = 1 BAR = $(FOO) FOO = 2 # prints BAR=2 $(info BAR=$(BAR))
-
simple expansion:
variable := expression
This assigns the result of an expression to a variable; the expression is
expanded at the time of assignment:FOO = 1 BAR := $(FOO) FOO = 2 # prints BAR=1 $(info BAR=$(BAR))
Note: the
$(info ...)
function is being used above to print expressions and
can be handy when debugging makefiles!*`
Variables which are not explicitly,
implicitly,
nor
automatically
set will evaluate to an empty string.
Environment Variables
Environment variables are carried into the Make execution environment. Consider
the following makefile for example:
$(info YOLO variable = $(YOLO))
If we set the variable YOLO
in the shell command when running make
, we’ll set the value:
$ YOLO="hello there!" make
YOLO variable = hello there!
make: *** No targets. Stop.
Note: Make prints the “No targets” error because our makefile had no targets
listed!
If you use the ?=
assignment syntax, Make will only assign that value if the
variable doesn’t already have a value:
Makefile:
# default CC to gcc
CC ?= gcc
We can then override $(CC)
in that makefile:
Another common pattern is to allow inserting additional flags. In the makefile,
we would append to the variable instead of directly assigning to it.
This permits passing extra flags in from the environment:
$ CFLAGS='-Werror=conversion -Werror=double-promotion' make
This can be very useful!
Overriding Variables
A special category of variable usage is called overriding variables. Using
this command-line option will override the value set ANYWHERE ELSE in the
environment or Makefile!
Makefile:
# the value passed in the make command will override
# any value set elsewhere
YOLO = "not overridden"
$(info $(YOLO))
Command:
# setting "YOLO" to different values in the environment + makefile + overriding
# variable, yields the overriding value
$ YOLO="environment set" make YOLO='overridden!!'
overridden!!
make: *** No targets. Stop.
Overriding variables can be confusing, and should be used with caution!
Target-Specific Variables
These variables are only available in the recipe context. They also apply to any prerequisite recipe!
# set the -g value to CFLAGS
# applies to the prog.o/foo.o/bar.o recipes too!
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
echo $(CFLAGS) # will print '-g'
Implicit Variables
These are pre-defined by Make (unless overridden with any other variable type of
the same name). Some common examples:
-
$(CC)
— the C compiler (gcc
) -
$(AR)
— archive program (ar
) -
$(CFLAGS)
— flags for the C compiler
Full list here:
https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html
Automatic Variables
These are special variables always set by Make and available in recipe context.
They can be useful to prevent duplicated names (Don’t Repeat Yourself).
A few common automatic variables:
# $@ : the target name, here it would be "test.txt"
test.txt:
echo HEYO > $@
# $^ : name of all the prerequisites
all.zip: foo.txt test.txt
# run the gzip command with all the prerequisites "$^", outputting to the
# name of the target, "$@"
gzip -c $^ > $@
See more at:
https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
Targets (Goals)
Targets are the left hand side in the rule syntax:
target: prerequisite
recipe
Targets almost always name files. This is because Make uses last-modified
time to track if a target is newer or older than its prerequisites and whether
it needs to be rebuilt!
When invoking Make, you can specify which target(s) you want to build as the
goal
s by specifying it as a positional argument:
# make the 'test.txt' and 'all.zip' targets
make test.txt all.zip
If you don’t specify a goal in the command, Make uses the first target specified
in the makefile, called the “default goal” (you can also
override the
default goal if you need to).
Phony Targets
Sometimes it’s useful to have meta-targets like all
, clean
, test
, etc. In these cases, you don’t want Make to check for a file named all
/clean
etc.
Make provides the .PHONY
target syntax to mark a target as not pointing to a
file:
# Say our project builds a program and a library 'foo' and 'foo.a'; if we want
# to build both by default we might make an 'all' rule that builds both
.PHONY: all
all: foo foo.a
If you have multiple phony targets, a good pattern might be to append each to
.PHONY
where it’s defined:
# the 'all' rule that builds and tests. Note that it's listed first to make it
# the default rule
.PHONY: all
all: build test
# compile foo.c into a program 'foo'
foo: foo.c
$(CC) foo.c -o foo
# compile foo-lib.c into a library 'foo.a'
foo.a: foo-lib.c
# compile the object file
$(CC) foo-lib.c -c foo-lib.o
# use ar to create a static library containing our object file. using the
# '$@' variable here to specify the rule target 'foo.a'
$(AR) rcs $@ foo-lib.o
# a phony rule that builds our project; just contains a prerequisite of the
# library + program
.PHONY: build
build: foo foo.a
# a phony rule that runs our test harness. has the 'build' target as a
# prerequisite! Make will make sure (pardon the pun) the build rule executes
# first
.PHONY: test
test: build
./run-tests.sh
NOTE!!!
.PHONY
targets are ALWAYS considered out-of-date, so Make will
ALWAYS run the recipe for those targets (and therfore any target that has a
.PHONY
prerequisite!). Use with caution!!
Implicit Rules
Implicit rules are provided by Make. I find using them to be confusing since
there’s so much behavior happening behind the scenes. You will occasionally
encounter them in the wild, so be aware.
Here’s a quick example:
# this will compile 'test.c' with the default $(CC), $(CFLAGS), into the program
# 'test'. it will handle prerequisite tracking on test.c
test: test.o
Full list of implicit rules here:
https://www.gnu.org/software/make/manual/html_node/Catalogue-of-Rules.html
Pattern Rules
Pattern rules let you write a generic rule that applies to multiple targets via
pattern-matching:
# Note the use of the '$<' automatic variable, specifying the first
# prerequisite, which is the .c file
%.o: %.c
$(CC) -c $< -o $@
The rule will then be used to make any target matching the pattern, which above
would be any file matching %.o
, e.g. foo.o
, bar.o
.
If you use those .o
files mentioned above to build a program:
OBJ_FILES = foo.o bar.o
# Use CC to link foo.o + bar.o into 'program'. Note the use of the '$^'
# automatic variable, specifying ALL the prerequisites (all the OBJ_FILES)
# should be part of the link command
program: $(OBJ_FILES)
$(CC) -o $@ $^
Prerequisites
As seen above, these are targets that Make will check before running a rule. They
can be files or other targets.
If any prerequisite is newer (modified-time) than the target, Make will run the
target rule.
In C projects, you might have a rule that converts a C file to an object file,
and you want the object file to rebuild if the C file changes:
foo.o: foo.c
# use automatic variables for the input and output file names
$(CC) $^ -c $@
Automatic Prerequisites
A very important consideration for C language projects is to trigger
recompilation if an #include
header files change for a C file. This is done
with the -M
compiler flag for gcc/clang, which will output a .d
file you
will then import with the Make include
directive.
The .d
file will contain the necessary prerequisites for the .c
file so any
header change causes a rebuild. See more details here:
https://www.gnu.org/software/make/manual/html_node/Automatic-Prerequisites.html
http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
The basic form might be:
# these are the compiler flags for emitting the dependency tracking file. Note
# the usage of the '$<' automatic variable
DEPFLAGS = -MMD -MP -MF $<.d
test.o: test.c
$(CC) $(DEPFLAGS) $< -c $@
# bring in the prerequisites by including all the .d files. prefix the line with
# '-' to prevent an error if any of the files do not exist
-include $(wildcard *.d)
Order-Only Prerequisites
These prerequisites will only be built if they don’t exist; if they are newer
than the target, they will not trigger a target re-build.
A typical use is to create a directory for output files; emitting files to a
directory will update its mtime
attribute, but we don’t want that to trigger a
rebuild.
OUTPUT_DIR = build
# output the .o to the build directory, which we add as an order-only
# prerequisite- anything right of the | pipe is considered order-only
$(OUTPUT_DIR)/test.o: test.c | $(OUTPUT_DIR)
$(CC) -c $^ -o $@
# rule to make the directory
$(OUTPUT_DIR):
mkdir -p $@
Recipe
The “recipe” is the list of shell commands to be executed to create the target. They are
passed into a sub-shell (/bin/sh
by default). The rule is considered
successful if the target is updated after the recipe runs (but is not an error
if this doesn’t happen).
foo.txt:
# a simple recipe
echo HEYO > $@
If any line of the recipe returns a non-zero exit code, Make will terminate and
print an error message. You can tell Make to ignore non-zero exit codes by
prefixing with the -
character:
.PHONY: clean
clean:
# we don't care if rm fails
-rm -r ./build
Prefixing a recipe line with @
will disable echoing that line before
executing:
clean:
@# this recipe will just print 'About to clean everything!'
@# prefixing the shell comment lines '#' here also prevents them from
@# appearing during execution
@echo About to clean everything!
Make will expand variable/function expressions in the recipe context before
running them, but will otherwise not process it. If you want to access shell
variables, escape them with $
:
USER = linus
print-user:
# print out the shell variable $USER
echo $$USER
# print out the make variable USER
echo $(USER)
Advanced Topics
These features are less frequently encountered, but provide some powerful
functionality that can enable sophisticated behavior in your build.
Functions
Make functions are called with the syntax:
$(function-name arguments)
where arguments
is a comma-delimited list of arguments.
Built-in Functions
There are several functions provided by Make. The most common ones I use are for text
manipulation:
https://www.gnu.org/software/make/manual/html_node/Text-Functions.html
https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html
For example:
FILES=$(wildcard *.c)
# you can combine function calls; here we strip the suffix off of $(FILES) with
# the $(basename) function, then add the .o suffix
O_FILES=$(addsuffix .o,$(basename $(FILES)))
# note that the GNU Make Manual suggests an alternate form for this particular
# operation:
O_FILES=$(FILES:.c=.o)
User-Defined Functions
You can define your own functions as well:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
A more complicated but quite useful example:
# recursive wildcard (use it instead of $(shell find . -name '*.c'))
# taken from https://stackoverflow.com/a/18258352
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
C_FILES = $(call rwildcard,.,*.c)
Shell Function
You can have Make call a shell expression and capture the result:
TODAYS_DATE=$(shell date --iso-8601)
I’m cautious when using this feature, though; it adds a dependency on whatever
programs you use, so if you’re calling more exotic programs, make sure your
build environment is controlled (e.g. in a container or with
Conda).
Conditionals
Make has syntax for conditional expressions:
FOO=yolo
ifeq ($(FOO),yolo)
$(info foo is yolo!)
else
$(info foo is not yolo :( )
endif
# testing if a variable is set; unset variables are empty
ifneq ($(FOO),) # checking if FOO is blank
$(info FOO is unset)
endif
The “complex conditional” syntax is just the if-elseif-else
combination:
# "complex conditional"
ifeq ($(FOO),yolo)
$(info foo is yolo)
else ifeq ($(FOO), heyo)
$(info foo is heyo)
else
$(info foo is not yolo or heyo :( )
endif
include
Directive
You can import other Makefile contents using the include
directive:
sources.mk
:
SOURCE_FILES :=
bar.c
foo.c
Makefile
:
include sources.mk
OBJECT_FILES = $(SOURCE_FILES:.c=.o)
%.o: %.c
$(CC) -c $^ -o $@
Sub-make
Invoking Make from a Makefile should be done with the $(MAKE)
variable:
somelib.a:
$(MAKE) -C path/to/somelib/directory
This is often used when building external libraries. It’s also used heavily in Kconfig builds (e.g. when building the Linux kernel).
Note that this approach has some pitfalls:
- Recursive invocation can result in slow builds.
- Tracking prerequisites can be tricky; often you will see
.PHONY
used.
More details on the disadvantages here:
http://aegis.sourceforge.net/auug97.pdf
Metaprogramming with eval
Make
’s eval
directive allows us to generate Make syntax at runtime:
# generate rules for xml->json in some weird world
FILES = $(wildcard inputfile/*.xml)
# create a user-defined function that generates rules
define GENERATE_RULE =
$(eval
# prereq rule for creating output directory
$(1)_OUT_DIR = $(dir $(1))/$(1)_out
$(1)_OUT_DIR:
mkdir -p $@
# rule that calls a script on the input file and produces $@ target
$(1)_OUT_DIR/$(1).json: $(1) | $(1)_OUT_DIR
./convert-xml-to-json.sh $(1) $@
)
# add the target to the all rule
all: $(1)_OUT_DIR/$(1).json
endef
# produce the rules
.PHONY: all
all:
$(foreach file,$(FILES),$(call GENERATE_RULE,$(file)))
Note that approaches using this feature of Make can be quite confusing, adding
helpful comments explaining what the intent is can be useful for your future
self!
VPATH
VPATH
is a special Make variable that contains a list of directories Make
should search when looking for prerequisites and targets.
It can be used to emit object files or other derived files into a ./build
directory, instead of cluttering up the src
directory:
# This makefile should be invoked from the temporary build directory, eg:
# $ mkdir -p build && cd ./build && make -f ../Makefile
# Derive the directory containing this Makefile
MAKEFILE_DIR = $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
# now inform Make we should look for prerequisites from the root directory as
# well as the cwd
VPATH += $(MAKEFILE_DIR)
SRC_FILES = $(wildcard $(MAKEFILE_DIR)/src/*.c)
# Set the obj file paths to be relative to the cwd
OBJ_FILES = $(subst $(MAKEFILE_DIR)/,,$(SRC_FILES:.c=.o))
# now we can continue as if Make was running from the root directory, and not a
# subdirectory
# $(OBJ_FILES) will be built by the pattern rule below
foo.a: $(OBJ_FILES)
$(AR) rcs $@ $(OBJ_FILES)
# pattern rule; since we added ROOT_DIR to VPATH, Make can find prerequisites
# like `src/test.c` when running from the build directory!
%.o: %.c
# create the directory tree for the output file 👍
echo $@
mkdir -p $(dir $@)
# compile
$(CC) -c $^ -o $@
I recommend avoiding use of VPATH
. It’s usually simpler to achieve the same
out-of-tree behavior by outputting the generated files in a build directory
without needing VPATH
.
touch
You may see the touch
command used to track rules that seem difficult to
otherwise track; for example, when unpacking a toolchain:
# our tools are stored in tools.tar.gz, and downloaded from a server
TOOLS_ARCHIVE = tools.tar.gz
TOOLS_URL = https://httpbin.org/get
# the rule to download the tools using wget
$(TOOLS_ARCHIVE):
wget $(TOOLS_URL) -O $(TOOLS_ARCHIVE)
# rule to unpack them
tools-unpacked.dummy: $(TOOLS_ARCHIVE)
# running this command results in a directory.. but how do we know it
# completed, without a file to track?
tar xzvf $^
# use the touch command to record completion in a dummy file
touch $@
I recommend avoiding the use of touch
. However there are some cases where it
might be unavoidable.
Debugging Makefiles
I typically use the Make equivalent of printf
, the $(info/warning/error)
functions, for small problems, for example when checking conditional paths that
aren’t working:
ifeq ($(CC),clang)
$(error whoops, clang not supported!)
endif
For debugging why a rule is running when it shouldn’t (or vice versa), you can
use the --debug
options:
https://www.gnu.org/software/make/manual/html_node/Options-Summary.html
I recommend redirecting stdout to a file when using this option, it can produce
a lot of output.
Profiling
For profiling a make invocation (e.g. for attempting to improve compilation
times), this tool can be useful:
https://github.com/rocky/remake
Check out the tips here for compilation-related performance improvements:
https://interrupt.memfault.com/blog/improving-compilation-times-c-cpp-projects
Using a Verbose Flag
If your project includes a lot of compiler flags (search paths, lots of warning
flags, etc.), then you may want to simplify the output of Make rules. It can be
useful to have a toggle to easily see the full output, for example:
ifeq ($(V),1)
Q :=
else
Q := @
endif
%.o: %.c
# prefix the compilation command with the $(Q) variable
# use echo to print a simple "Compiling x.c" to show progress
@echo Compiling $(notdir @^)
$(Q) $(CC) -c $^ -o $@
To enable printing out the full compilation commands, set the V
environment
variable like so:
Full Example
Here’s an annotated example of a complete build process for an example C
project. You can see this example and the source tree
here.
# Makefile for building the 'example' binary from C sources
# Verbose flag
ifeq ($(V),1)
Q :=
else
Q := @
endif
# The build folder, for all generated output. This should normally be included
# in a .gitignore rule
BUILD_FOLDER := build
# Default all rule will build the 'example' target, which here is an executable
.PHONY:
all: $(BUILD_FOLDER)/example
# List of C source files. Putting this in a separate variable, with a file on
# each line, makes it easy to add files later (and makes it easier to see
# additions in pull requests). Larger projects might use a wildcard to locate
# source files automatically.
SRC_FILES =
src/example.c
src/main.c
# Generate a list of .o files from the .c files. Prefix them with the build
# folder to output the files there
OBJ_FILES = $(addprefix $(BUILD_FOLDER)/,$(SRC_FILES:.c=.o))
# Generate a list of depfiles, used to track includes. The file name is the same
# as the object files with the .d extension added
DEP_FILES = $(addsuffix .d,$(OBJ_FILES))
# Flags to generate the .d dependency-tracking files when we compile. It's
# named the same as the target file with the .d extension
DEPFLAGS = -MMD -MP -MF $@.d
# Include the dependency tracking files
-include $(DEP_FILES)
# List of include dirs. These are put into CFLAGS.
INCLUDE_DIRS =
src/
# Prefix the include dirs with '-I' when passing them to the compiler
CFLAGS += $(addprefix -I,$(INCLUDE_DIRS))
# Set some compiler flags we need. Note that we're appending to the CFLAGS
# variable
CFLAGS +=
-std=c11
-Wall
-Werror
-ffunction-sections -fdata-sections
-Og
-g3
# Our project requires some linker flags: garbage collect sections, output a
# .map file
LDFLAGS +=
-Wl,--gc-sections,-Map,$@.map
# Set LDLIBS to specify linking with libm, the math library
LDLIBS +=
-lm
# The rule for compiling the SRC_FILES into OBJ_FILES
$(BUILD_FOLDER)/%.o: %.c
@echo Compiling $(notdir $<)
@# Create the folder structure for the output file
@mkdir -p $(dir $@)
$(Q) $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
# The rule for building the executable "example", using OBJ_FILES as
# prerequisites. Since we're not relying on an implicit rule, we need to
# explicity list CFLAGS, LDFLAGS, LDLIBS
$(BUILD_FOLDER)/example: $(OBJ_FILES)
@echo Linking $(notdir $@)
$(Q) $(CC) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@
# Remove debug information for a smaller executable. An embedded project might
# instead using [arm-none-eabi-]objcopy to convert the ELF file to a raw binary
# suitable to be written to an embedded device
STRIPPED_OUTPUT = $(BUILD_FOLDER)/example-stripped
$(STRIPPED_OUTPUT): $(BUILD_FOLDER)/example
@echo Stripping $(notdir $@)
$(Q)objcopy --strip-debug $^ $@
# Since all our generated output is placed into the build folder, our clean rule
# is simple. Prefix the recipe line with '-' to not error if the build folder
# doesn't exist (the -f flag for rm also has this effect)
.PHONY: clean
clean:
- rm -rf $(BUILD_FOLDER)
Recommendations
A list of recommendations for getting the most of Make:
- Targets should usually be real files.
- Always use
$(MAKE)
when issuing sub-make commands. - Try to avoid using
.PHONY
targets. If the rule generates any file artifact,
consider using that as the target instead of a phony name! - Try to avoid using implicit rules.
- For C files, make sure to use
.d
automatic include tracking! - Use metaprogramming with caution.
- Use automatic variables in rules. Always try to use
$@
for a recipe
output path, so your rule and Make have the exact same path. - Use comments liberally in Makefiles, especially if there is complicated
behavior or subtle syntax used. Your co-workers (and future self) will thank
you. - Use the
-j
or-l
options to run Make in parallel! - Try to avoid using the
touch
command to track rule completion
Outro
I hope this article has provided a few useful pointers around GNU Make!
Make remains common in C language projects, most likely due to its usage in the
Linux kernel. Many recently developed statically compiled programming
languages, such as Rust or Go, provide their own build infrastructure. However, when
integrating Make-based software into those languages, for example when building
a C library to be called from Rust, it can be surprisingly helpful to understand
some Make concepts!
You may also encounter automake in
open source projects (look for a ./configure
script). This is a related tool
that generates Makefiles, and is worth a look (especially if you are writing
C software that needs to be very widely portable).
There are many competitors to GNU Make available today, I encourage everyone to
look into them. Some examples:
-
CMake is pretty popular (the Zephyr project uses this)
and worth a look. It makes out-of-tree builds pretty easy -
Bazel uses a
declarative syntax (vs. Make’s imperative approach) -
Meson is a meta-builder like
cmake
, but by
default uses Ninja as the backend, and can be very fast
References
-
Good detailed dive into less common topics (shout out on
remake
):
https://blog.jgc.org/2013/02/updated-list-of-my-gnu-make-articles.html -
Mix of very exotic and simpler material:
https://tech.davis-hansson.com/p/make/ -
Useful tutorial:
http://maemo.org/maemo_training_material/… -
Nice pictures:
https://www.jfranken.de/homepages/johannes/vortraege/make.en.html -
Very nice summary:
https://www.alexeyshmalko.com/2014/7-things-you-should-know-about-make/
Noah Pendleton is an embedded software engineer at Memfault. Noah previously worked on embedded software teams at Fitbit and Markforged