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

Время на прочтение
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 $&lt; &gt; $@

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.

Если какие-либо зависимости файла изменятся, то файл будет перекомпилирован:

Граф зависимостей для компиляции изображение с сайта www.andreyolegovich.ru

Граф зависимостей


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, and Makefile, 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”:

  1. 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))
    
  2. 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
goals 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:

  1. Targets should usually be real files.
  2. Always use $(MAKE) when issuing sub-make commands.
  3. Try to avoid using .PHONY targets. If the rule generates any file artifact,
    consider using that as the target instead of a phony name!
  4. Try to avoid using implicit rules.
  5. For C files, make sure to use .d automatic include tracking!
  6. Use metaprogramming with caution.
  7. Use automatic variables in rules. Always try to use $@ for a recipe
    output path, so your rule and Make have the exact same path.
  8. Use comments liberally in Makefiles, especially if there is complicated
    behavior or subtle syntax used. Your co-workers (and future self) will thank
    you.
  9. Use the -j or -l options to run Make in parallel!
  10. 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

Понравилась статья? Поделить с друзьями:

А вот и еще наши интересные статьи:

  • Ибп eaton 9130 инструкция на русском
  • Руководство пользования 1с розница
  • Руководство полиции перми
  • Часы smart watch user manual инструкция на русском языке
  • Potassium plus iodine now инструкция по применению

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии