Программирование ARM Магия makefile на простых примерах Fri, December 06 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Магия makefile на простых примерах Печать
Добавил(а) microsin   

Система make + Makefiles используется для того, чтобы помочь с принятием решения, какие части большой программы следует перекомпилировать, а какие пока не нужно. Понятно почему - если тупо перекомпилировать все заново, то это может занять много времени (в больших проектах час или больше). В подавляющем большинстве случаев это используется для компиляции файлов кода на языках C или C++. Другие языки имеют собственные инструменты, которые делают примерно то же самое, что и make. Make может также использоваться и по завершению компиляции, когда требуется выполнить ряд инструкций в зависимости от того, какие файлы поменялись в процессе компиляции. В этом руководстве (перевод статьи [1]) основной акцент сделан на компиляции кода C/C++.

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

dependency graph example

Рис. 1. Пример зависимостей основного модуля main.cpp от других файлов.

Среди популярных систем сборки кода C/C++ можно вспомнить SCons [7], CMake [8], Bazel и Ninja. Некоторые редакторы кода наподобие Microsoft Visual Studio имеют свои встроенные инструменты сборки. Для Java это Ant, Maven и Gradle. Другие языки, такие как Go, Rust и TypeScript снабжены своими инструментами сборки.

Интерпретируемые языки, такие как Python, Ruby и чистый Javascript не требуют аналогов файлов Makefile. Цель Makefile в том, чтобы компилировать только те файлы, которые нужно перекомпилировать, основываясь на отслеживании изменения файлов. Однако когда поменялся интерпретируемый файл, то нет необходимости его перекомпилировать. Когда программа работает, всегда используется самая свежая версия интерпретируемого файла.

Существуют различные реализации make, отличающиеся своим поведением, однако это руководство вероятно будет корректным независимо от того, какую версию make вы используете. Однако следует иметь в виду, что руководство написано для GNU Make, которое считается стандартной реализацией make на Unix-системах (различные версии Linux, FreeBSD и MacOS). Все примеры будут работать для версий make 3 и 4, которые почти эквивалентны и отличаются только некоторой эзотерикой.

Версию make можно проверить командой make -v:

$ make -v
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.

Чтобы запускать примеры из этого руководства, то вам понадобится окно терминала и установленный инструментарий, где присутствует make. Для тех, у кого есть Linux, это условие по умолчанию выполнено. Для пользователей Windows это не всегда, но не составит большого труда установить себе такую систему как avr-gcc, MinGW/MSYS [9] или WSL [10]. Для каждого примера скопируйте его содержимое в файл Makefile, поместите его в каталог, где содержится компилируемый код, и в этой директории запустите команду make.

Давайте запустим самый простой пример Makefile:

hello:→echo "Hello, World"

Важное замечание: здесь слово с двоеточием "hello:" это маркер так называемой цели компиляции (compile target). Следующая за ним строка обязательно должна начинаться с символа табуляции TAB ('\t', ASCII-код 0x09). В этом руководстве символы табуляции будут показаны стрелочками →. Если вместо табуляции будут пробелы, то make выведет сообщение об ошибке [2].

Если запустить этот простой пример, то получится следующее:

$ make
echo "Hello, World"
Hello, World

[Синтаксис Makefile]

Makefile работает по определенному набору правил. Главное правило выглядит следующим образом:

targets: prerequisites
→command
→command
→command

targets. Здесь targets обозначает целевые объекты (prerequisites), которые являются именами файлов, отделенными друг от друга пробелами. Вы наверное уже встречались с такими маркерами, как all, clean, flash. На каждый маркер targets обычно приходится 1 файл.

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

prerequisites. Это также имена файлов, отделенные друг от друга пробелами. Эти файлы должны существовать до того, как запустится обработка команд targets. Это так называемые зависимости (dependencies).

Примечание: иногда маркер цели targets называют правилом, а зависимости prerequisites целями. Что вносит некоторую путаницу, особенно в русскоязычной документации.

Давайте разберем основу технологии make. Начнем с традиционного примера hello world:

hello:echo "Hello, World"echo "Эта строка напечатается, если файл hello не существует."

По этому примеру можно уже многое понять:

• У нас есть одна цель компиляции (target) hello.
• У этой цели 2 команды.
• У этой цели нет зависимостей (no prerequisites).

Пока файла hello нет, при каждом запуске make будут выполняться команды цели hello. Как только файл hello появится, никакие команды этой цели выполняться не будут.

Важно отметить, что о маркере hello говорят и как о цели компиляции (target), и как о файле. Причина в том, что они напрямую взаимосвязаны. Обычно, когда запускается target, т. е. когда её команды запускаются, эти команды создадут файл, у которого будет такое же имя, что и у target. В этом нашем последнем примере цель компиляции hello не создает файл hello.

Давайте создадим более традиционный файл Makefile - он будет компилировать один файл на языке C. Но перед этим создадим файл test.c со следующим содержимым:

int main()
{
   return 0;
}

Затем создадим файл Makefile. Он как всегда, носит имя Makefile, и содержит инструкции для make:

test:
→cc test.c -o test

Примечание: подразумевается, что у вас установлен компилятор gcc, он необходим для успешного запуска команды cc test.c -o test.

Запустите теперь команду make. Поскольку для команды make мы не предоставили никакой цели компиляции в командной строке, то в Makefile будет выполняться первая встретившаяся цель компиляции. В нашем случае, это единственная цель (test). При первом запуске будет создан файл test. При втором запуске будет выведено сообщение make: 'test' is up to date. Это потому, что файл test уже существует.

$ make
cc test.c -o test
$ make
make: 'test' is up to date.

Но здесь есть проблема: если мы изменим test.c и затем запустим make, то он не перекомпилируется. Это можно исправить путем добавления prerequisite:

test: test.c
→cc test.c -o test

Когда мы теперь снова запустим make, то произойдет следующая последовательность шагов:

• В Makefile будет выбрана первая цель (test). Первая цель по правилам make является целью по умолчанию (default target).
• Будет обнаружена зависимость этой цели test.c.
• Утилита make решает, должна ли она запустить на выполнение цель test. Она запустит команды цели test, если файл test не существует, или если файл test.c более новый, чем файл test.

Этот последний шаг самый важный, и он представляет сущность make. На этом шаге make должна определить, был ли изменен файл зависимости test.c (prerequisite, указанный после двоеточия цели) с момента последней компиляции. Если это так, и файл test.c был изменен (в простейшем случае изменение обнаруживается путем сравнения времени и даты последнего изменения файлов test и test.c), то запуск make должен перекомпилировать файл. И наоборот, если файл test.c не менялся, то его не нужно перекомпилировать.

Использование метки времени файлов для определения наличия изменения зависимости - разумная эвристика, потому что метка времени файла обычно меняется на более свежую только если файл был изменен. Но важно понимать, это так может быть не всегда. Вы могли бы, например, изменить файл зависимости test.c, и потом поменять его метку времени на какую-нибудь старую. Если вы это сделаете, то make некорректно решит, что файл зависимости не поменялся, и проигнорирует его изменение.

Демонстрация зависимостей на примере из трех целей. Следующий пример Makefile будет запускать при первом запуске все три свои цели.

test: test.o
→cc test.o -o test # запустится как третье действие
 
test.o: test.c
→cc -c test.c -o test.o # запустится как второе действие
 
# Если файл test.c существует, то эта цель не выполнится. Но этот пример
# в демонстрационных целях сделан более универсальным: если файл test.c
# будет создан этой целью.
test.c:
→echo "int main() { return 0; }" > test.c # запустится как первое действие

Когда вы запустите make в терминале, то скомпилируется программа test следующей последовательностью шагов:

• Команда make выберет цель test, потому что она первая в Makefile, и это таким образом default target.
• Цель test требует test.o, поэтому make ищет цель test.o.
• Цель test.o требует test.c, поэтому make ищет цель test.c.
• У цели test.c зависимостей нет, поэтом просто запустится команда echo, которая создаст файл test.c.
• Затем запустится команда cc -c цели test.o, потому что все её зависимости (test.c) удовлетворены.
• Затем запустится верхняя команда cc цели test, потому что все её зависимости (test.o) удовлетворены.
• В результате будет скомпилирована программа test.

Если вы удалите файл test.c, то все три цели перезапустятся друг за другом. Если вы его отредактируете и сохраните (и измените метку времени этого файла так, что она станет более поздней, чем у test.o), то запустятся первые две цели test: и test.o: (и тем самым поменяются метки времени у файлов test и test.o на более свежие). Если вы поменяете метку времени у файла test.o (например командой touch test.o), то запустится заново только первая цель test. Если вы ничего не поменяете, то ни одна из целей не запустится. Попробуйте!

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

some_file: other_fileecho "Эта команда запустится всегда, и запустится второй"
→touch some_file
 
other_file:echo "Эта команда запустится всегда, и запустится первой"

[make clean]

ель clean часто используется для удаления результатов работы других целей, но не следует думать, что это какое-то специальное слово для make. Создайте следующий Makefile, и запустите для него команды make и make clean. Команда make создаст файл some_file, а команда make clean удалит его.

some_file:
→touch some_file
 
clean:
→rm -f some_file

Обратите внимание здесь на две новые вещи:

• Цель clean здесь не первая (она не является целью по умолчанию), и у неё нет зависимостей. Это значит, что она не запустится, пока не будет явно указана в командной строке make clean.
• Имя цели clean не предназначено для имени файла. Если по какой-то причине вдруг появится файл с именем clean, то эта цель никогда не запустится, и это будет сосем не тем, что вы хотели бы получить. Далее мы рассмотрим .PHONY, что позволит решить эту проблему.

[Переменные Makefile]

Переменные могут быть только строками. Для них обычно используют присваивание :=, но также работает и присваивание =, см. далее "Переменные Makefile, часть 2".

Пример использования переменных:

files := file1 file2
some_file: $(files)
→echo "Look at this variable: " $(files)
→touch some_file
 
file1:
→touch file1
 
file2:
→touch file2
 
clean:
→rm -f file1 file2 some_file

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

a := one two # эта команда установит строку a в значение "one two"
b := 'one two' # Не рекомендуется. Переменная b установится в строку"'one two'".
all:printf '$a'printf $b

Обращаются к переменным с помощью либо ${}, либо $(). В следующем примере происходит обращение к значению переменной x:

x := dude
 
all:echo $(x)echo ${x}
 
→# Плохая практика, но это работает:echo $x

[Цели Makefile]

Цель all. Сделали несколько целей, и хотите их все запустить? Для этого создайте первой цель all. Поскольку это будет первым перечисленным правилом, то all запустится по умолчанию, когда make вызывается без указания цели.

all: one two three
 
one:
→touch one
 
two:
→touch two
 
three:
→touch three
 
clean:
→rm -f one two three

Несколько целей. Когда для правила (target) существует несколько целей, команды запустятся для каждой цели (и/или для каждой зависимости списка prerequisites). $@ это автоматическая переменная, которая содержит имя цели.

all: f1.o f2.o
 
f1.o f2.o:echo $@

Это эквивалентно следующему:

f1.o:echo f1.o
 
f2.o:echo f2.o

[Автоматические переменные и символы подстановки]

Символ подстановки *. Оба символа * и % называются в технологии Make символами подстановки (wildcards), однако они означают полностью не то, что они означают для имен файловой системы. В файловой системе * при поиске соответствует именам файлов. Рекомендуется всегда использовать символы макроподстановки, обозначающие несколько файлов, с помощью следующей конструкции на основе функции wildcard:

# Напечатает информацию о каждом файле .c:
print: $(wildcard *.c)
→ls -la  $?

Символ * можно использовать для цели (target), зависимостей (prerequisites), или для функции wildcard.

Внимание: * может косвенно использоваться в определениях переменных. Когда * не соответствует ни одном файлу, то он интерпретируется как есть, за исключением работы в функции wildcard. Поэтому обязательно используйте фукцию wildcard.

thing_wrong := *.o # Не делайте так! '*' не будет развернут.
thing_right := $(wildcard *.o)
 
all: one two three four
 
# Приведет к ошибке, потому что $(thing_wrong) это даст строку "*.o"
one: $(thing_wrong)
 
# Останется как *.o, если ни одного файла не попадет под этот шаблон :(
two: *.o
 
# Сработает, как и ожидалось! В этом примере не будет никаких действий.
three: $(thing_right)
 
# То же самое, что правило three.
four: $(wildcard *.o)

Символ подстановки %. % по-настоящему полезен, однако в связи с различными ситуациями использования иногда бывает недопонимание его назначения.

• Когда используется режим соответствия ("matching" mode), то % соответствует одному или большему количеству символов в строке. Это совпадение называется stem.
• Когда используется режим замены ("replacing" mode), берется совпавший stem и заменяется на строку.
• % чаще всего используется в определениях правил и в некоторых специальных функциях.

См. далее следующие примеры использования %:

Static Pattern Rules
Pattern Rules
Подстановка строки
Директива vpath

Автоматические переменные. Существует несколько автоматических переменных, но часто используются только несколько:

hey: one two
# Выведет "hey", поскольку $@ соответствует имени цели (target name):
→echo $@
 
# Выведет все зависимости (prerequisites), которые более свежие, чем target:
→echo $?
 
# Выведет все зависимости:
→echo $^
→touch hey
 
one:
→touch one
 
two:
→touch two
 
clean:
→rm -f hey one two

[Причудливые правила]

Implicit Rules. Make любят применять в компиляциях проектов языка C. И каждый раз выражения этой любви сбивают с толку. Возможно самая запутанная часть технологии Make - реализованные магические/автоматические правила. Make называет их "неявными" (implicit rules). Автор [1] персонально не согласен с такими решениями, и не рекомендует их использовать. И тем не менее о них следует знать. Вот список неявных правил:

• Компиляция программы C: объектный файл n.o генерируется автоматически из n.c командой вида:

$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@

• Компиляция программы C++: объектный файл n.o генерируется автоматически из n.cc или n.cpp командой вида:

$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@

• Линковка одного объектного файла: исполняемый файл n автоматически генерируется из n.o командой:

$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@

В этих неявных правилах используются важные переменные:

CC: компилятор для программ C; по умолчанию это cc.
CXX: компилятор для программ C++; по умолчанию это g++.
CFLAGS: дополнительные флаги для компилятора C.
CXXFLAGS: дополнительные флаги для компилятора C++.
CPPFLAGS: дополнительные флаги для препроцессора C.
LDFLAGS: дополнительные флаги для компиляторов, когда подразумевается, что они вовлекают в процесс компиляции запуск линкера.

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

CC = gcc # компилятор
CFLAGS = -g # флаг для неявных правил. Он включит отладочную информацию
            # в результат компиляции
 
# Неявное правило №1: test будет собран через неявное правило линкера C
# Неявное правило №2: test.o собирается через неявное правило компиляции C,
# потому что присутствует test.c
test: test.o
 
test.c:
→echo "int main() { return 0; }" > test.c
 
clean:
→rm -f test*

Static Pattern Rules. Правила статического шаблона (Static Pattern Rules) это другой способ сделать Makefile лаконичным, причем это вероятно более полезный способ, в котором меньше "магии". Вот его синтаксис:

targets...: target-pattern: prereq-patterns ...
→commands

Суть заключается в том, что указанный целевой объект (target) сопоставляется с целевым шаблоном target-pattern (через использование % wildcard). То, что совпало, называется stem. Этот stem заменяется на prereq-pattern для генерации зависимостей (prereqs) для target.

Типовой случай - компиляция файлов .c в файлы .o. Вот как это делается вручную:

objects = foo.o bar.o all.o
all: $(objects)
 
# Эти файлы компилируются через неявные правила:
foo.o: foo.
 
cbar.o: bar.
 
call.o: all.c
 
all.c:
→echo "int main() { return 0; }" > all.c
 
%.c:
→touch $@
 
clean:
→rm -f *.c *.o all

А вот более эффективный способ, с использованием static pattern rule:

objects = foo.o bar.o all.o
all: $(objects)
 
# Эти файлы компилируются через неявные правила.
# Синтаксис - targets ...: target-pattern: prereq-patterns ...
# В случае первой target, foo.o, получится совпадение target-pattern с foo.o,
# и "stem" установится в "foo". Тогда '%' заменится в prereq-patterns
# на этот stem.
$(objects): %.o: %.c
 
all.c:
→echo "int main() { return 0; }" > all.c
 
%.c:
→touch $@
 
clean:
→rm -f *.c *.o all

Static Pattern Rules и фильтр. Функция фильтра может использоваться в статическом шаблоне для совпадения с нужными файлами. В следующем примере фильтрация происходит по расширениям .raw и .result имени файла.

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
 
all: $(obj_files)
 
# Замечание: PHONY в этом месте важная вещь. Без него неявные правила попытаются
# собрать исполняемый файл "all", поскольку зависимости prereqs это файлы ".o".
.PHONY: all
 
# Пример 1: файлы .o зависят от файлов .c. Хотя на самом деле мы не создаем файл .o.
$(filter %.o,$(obj_files)): %.o: %.c
→echo "target: $@ prereq: $<"
 
# Пример 2: файлы .result зависят от фалов .raw. Хотя на самом деле мы не создаем
# файл .result.
$(filter %.result,$(obj_files)): %.result: %.raw
→echo "target: $@ prereq: $<" 
 
%.c %.raw:
→touch $@
 
clean:
→rm -f $(src_files)

Pattern Rules. Правила шаблона (pattern rules) часто используются, но они довольно запутанные. На них можно смотреть по-разному:

• Как на способ определить свои собственные неявные правила.
• Как на более простую форму static pattern rules.

Начнем со следующего примера:

# Определение правила шаблона, которое компилирует каждый файл .c в файл .o:
%.o : %.c$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

Правило шаблона содержит '%' в target. Это '%' совпадет с любой не пустой строкой, а другие символы совпадут буквально. '%' в prerequisite правила шаблона означает такой же stem, который совпал с '%' в target.

Вот другой пример:

# Определение правила шаблона, у которого в prerequisites отсутствует шаблон.
# Это просто создаст пустые файлы .c, когда это необходимо.
%.c:
→touch $@

Правила ::. Правила двойного двоеточия используются редко, однако они позволяют определить несколько правил для одной и той же target.

all: test
 
test::echo "hello"
 
test::echo "hello again"

Результат выполнения команды make:

$ make
echo "hello"
hello
echo "hello again"
hello again

Если бы это были одинарные двоеточия, печаталось бы предупреждение и выполнялся бы только второй набор команд:

$ make
Makefile:7: предупреждение: переопределение способа для цели «test»
Makefile:4: предупреждение: старый способ для цели «test» игнорируются
echo "hello again"
hello again

[Команды и их выполнение]

Подавленный вывод команды. Если добавить @ перед командой, то в выводе она не напечатается.

all:
→@echo "Команда echo для этой строчки не будет напечатана."echo "А здесь напечатается."

Также можно запустить make с опцией -s, чтобы @ добавился перед каждой строкой.

$ make
Команда echo для этой строчки не будет напечатана.
echo "А здесь напечатается."
А здесь напечатается.
$ make -s
Команда echo для этой строчки не будет напечатана.
А здесь напечатается.

Выполнение команды. Каждая команда запускается в новом shell (или как минимум эффект запуска такой).

all:
cd ..
# Команда cd выше не повлияет на следующую строку, потому что каждая команда
# выполняется в собственном изолированном окружении:
→echo `pwd`
 
# А здесь команда cd повлияет на следующую команду, потому что они находятся
# на одной строке:
→cd ..;echo `pwd`
 
# То же самое, что и выше:
→cd ..; \
→echo `pwd`

Shell по умолчанию. Средой окружения по умолчанию (default shell) является /bin/sh. Вы можете поменять это путем изменения переменной SHELL:

SHELL=/bin/bash
 
cool:echo "Hello from bash"

Двойной знак доллара. Если вы хотите, чтобы в строке появился знак доллара $, это можно сделать, указав $$. Это способ использования переменной оболочки в bash или sh.

Обратите внимание на различия между переменными Makefile и переменными shell в следующем примере.

make_var = Я переменная make
 
all:
# То же самое, что и запуск в шелл: "sh_var='I am a shell variable'; echo $sh_var"
→sh_var='I am a shell variable'; echo $$sh_var
 
# То же самое, что и запуск в шелл: "echo I am a make variable"
→echo $(make_var)

Обработка ошибок с помощью -k, -i и -. Добавьте опцию -k для команды make, чтобы она продолжала работать, даже когда встретилась с ошибками. Это поможет сразу увидеть все ошибки Makefile. Добавьте дефис '-' перед командой для подавления ошибки. Добавьте -i к опциям, чтобы это происходило с каждой командой.

one:
→# Эта ошибка отобразится, но make продолжит работу:
→-false
→touch one

Как оборвать работу make. Прервать обработку make можно комбинацией клавиш Ctrl+c, это удалит новые target-ы, которые только что созданы.

Рекурсивное использование make. Чтобы рекурсивно вызывать makefile, используйте специальную переменную $(MAKE) вместо make, потому что это передаст флаги make, и не будет затрагиваться ими.

new_contents = "hello:\n\ttouch inside_file"
 
all:
→mkdir -p subdir
→printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
→cd subdir && $(MAKE)
 
clean:
→rm -rf subdir

Export, рабочие окружения и рекурсия make. Когда запускается команда make, она автоматически создает переменные make из всех переменных окружения, в котором была запущена.

# Запустите следующее с "export shell_env_var='Я переменная окружения'; make"
all:
→# Печать всех переменных шеллаecho $$shell_env_var
 
→# Печать переменной Makeecho $(shell_env_var)

Директива export берет переменную, и установит её в рабочее окружение всех команд shell:

shell_env_var=Переменная окружения shell, созданная внутри make
export shell_env_var
 
all:echo $(shell_env_var)echo $$shell_env_var

Так что когда запустите команду make внутри make, вы можете использовать директиву export, чтобы сделать доступной переменную в командах sub-make. В этом примере переменная cooly экспортируется так, что makefile в subdir может её использовать.

new_contents = "hello:\n\techo \$$(cooly)"
 
all:
→mkdir -p subdirprintf $(new_contents) | sed -e 's/^ //' > subdir/makefile
→@echo "---MAKEFILE CONTENTS---"
→@cd subdir && cat makefile
→@echo "---END MAKEFILE CONTENTS---"cd subdir && $(MAKE)
 
# Обратите внимание на переменные и экспорт. Они устанавливаются глобально,
# и влияют на все.
cooly = "Подкаталог может меня видеть!"
export cooly
# Это обнулит строку выше: unexport cooly
 
clean:
→rm -rf subdir

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

one=это будет работать только локально
export two=с этой переменной мы можем запускать субкоманды
 
all: 
→@echo $(one)
→@echo $$one
→@echo $(two)
→@echo $$two

.EXPORT_ALL_VARIABLES экспортирует все переменные.

.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"
 
cooly = "Подкаталог может меня видеть!"
# Это обнулит строку выше: unexport cooly
 
all:
→mkdir -p subdirprintf $(new_contents) | sed -e 's/^ //' > subdir/makefile
→@echo "---MAKEFILE CONTENTS---"
→@cd subdir && cat makefile
→@echo "---END MAKEFILE CONTENTS---"cd subdir && $(MAKE)
 
clean:
→rm -rf subdir

Аргументы для make. Существует приятный список опций, с которыми можно запустить make. Попробуйте --dry-run, --touch, --old-file.

Вы можете задать в командной строке несколько target для make, например make clean run test сначала запустит clean, затем run, и затем test.

[Переменные Makefile, часть 2]

Виды переменных и модификация. Существует 2 вида переменных (flavors) [11]:

• Рекурсивная (устанавливается с использованием =) - ищет переменные только при использовании команды, но не при ее определении.
• Просто расширяемая (устанавливается с использованием :=) - как в обычном императивном программировании - расширяются только те, которые были определены до сих пор.

# Рекурсивная переменная. Она напечатает "later"
one = one ${later_variable}
# Просто расширяемая переменная. Она не напечатает "later"
two := two ${later_variable}
 
later_variable = later
 
all:echo $(one)echo $(two)

Просто расширяемая переменная (определенная через :=) позволяет добавление к переменной. Без этого рекурсивные определения приведут к ошибке зацикливания.

one = hello
# one становится определенной как просто расширяемая переменная (:=),
# и таким образом становится возможным добавление.
one := ${one} there
 
all:echo $(one)

?= установит переменные только если они не были еще установлены.

one = hello
one ?= Эта переменная не установится
two ?= Эта переменная установится
 
all:echo $(one)echo $(two)

Пробелы в конце строки не вырезаются, однако вырезаются в начале строки. Чтобы сделать переменную с одним пробелом, используйте $(nullstring).

with_spaces = hello   # with_spaces в конце получит несколько пробелов после "hello"
after = $(with_spaces)there
 
nullstring =
space = $(nullstring) # вот так делается переменная с одним пробелом.
 
all:echo "$(after)"echo start"$(space)"end

Не определенная переменная в действительности дает пустую строку!

all:
→# Не определенные переменные просто представляют собой пустые строки:echo $(nowhere)

Для добавления используйте += оператор:

foo := start
foo += more
 
all:echo $(foo)

Подстановка строк также является действительно распространенным и полезным способом изменения переменных. См. также описание функций для замены строк [3] и функции имен файлов [4].

Аргументы командной строки и override. Вы можете переназначить переменные, которые были введены через командную строку с помощью override. Попробуйте запустить следующий пример Makefile с командной строкой make option_one=hi.

# Переназначит аргументы командной строки:
override option_one = did_override
 
# Не переназначит аргументы командной строки.
option_two = not_override
 
all:echo $(option_one)echo $(option_two)

Список команд и define. Директива define это не функция, хотя может выглядеть как функция. Она нечасто используется, так что не будем вдаваться в детали. Но следует отметить, что она используется в основном для определения "фиксированных рецептов" [5], и также хорошо сочетается с функцией eval [6].

define/endef просто создает переменную, которая установит список команд. Обратите внимание, что это устроено несколько иначе, чем использование точки с запятой между командами, потому что каждая команда запускается в отдельном shell, как и ожидалось.

one = export blah="Я была установлена"; echo $$blah
 
define two
export blah="Я была установлена"
echo $$blah
endef
 
all:
→@echo "Это напечатает 'Я была установлена'"
→@$(one)
→@echo "Это не напечатает 'Я была установлена', потому что каждая команда\
→запускается в отдельном shell."
→@$(two)

Target-специфичные переменные. Переменные могут быть установлены для определенных target.

all: one = cool
 
all:echo one is defined: $(one)
 
other:echo one is nothing: $(one)

Pattern-специфичные переменные. Вы можете установить переменные для определенных шаблонов цели (target patterns).

%.c: one = cool
 
blah.c:echo one is defined: $(one)
 
other:echo one is nothing: $(one)

[Ветвления в Makefile]

Условное выполнение if/else:

foo = ok
 
all:
ifeq ($(foo), ok)echo "foo равно ok"
elseecho "nope"
endif

Проверка переменной на пустоту. Вот так можно проверить, есть ли что-нибудь в переменной:

nullstring =
foo = $(nullstring) # конец строки; сюда вставляется пробел
 
all:
ifeq ($(strip $(foo)),)echo "foo пустая после обрезки"
endif
ifeq ($(nullstring),)echo "nullstring даже не имеет пробелов"
endif

Проверка, была ли определена переменная. Директива ifdef не расширяет ссылки на переменные; она просто просматривает, существует ли определение вообще.

bar =foo = $(bar)
 
all:
ifdef fooecho "foo была определена"
endif
ifndef barecho "но bar не была определена"
endif

$(MAKEFLAGS). Этот пример покажет, как проверить флаги make с помощью функции findstring и переменной MAKEFLAGS. Запустите этот пример командой make -i, чтобы увидеть, как напечатается оператор echo.

all:
# Поиск флага "-i" flag. Переменная MAKEFLAGS просто содержит список одиночных символов,
# по одному на флаг командной строки. Здесь ищется "i".
ifneq (,$(findstring i, $(MAKEFLAGS)))echo "i was passed to MAKEFLAGS"
endif

[Функции]

Функции в Makefile используются главным образом для обработки текста. Вызывайте функции через $(fn, arguments) или ${fn, arguments}. У make есть приличное количество встроенных функций.

bar := ${subst не,настоящий, "Я не супермен"}
 
all:
→@echo $(bar)

Если вы хотите заменить пробелы или запятые, то используйте переменные:

comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))
 
all:
→@echo $(bar)

НЕ включайте пробелы в аргументы после первого. Это будет рассматриваться как часть строки.

comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))
 
all:
→# Выведется ", a , b , c". Обратите внимание, что вводятся пробелы.
→@echo $(bar)

Подстановка строки. Функция $(patsubst pattern,replacement,text) делает следующее, цитата из GNU docs:

"patsubst ищет слова, разделенные пробелами в text, которые совпадут с pattern, и заменит их на replacement. Здесь pattern может содержать '%', который действует как wildcard, совпадая с любым количеством любых символов в слове. Если replacement также содержит '%', то '%' заменяется на текст, который совпал с '%' в pattern. Таким способом обрабатывается только первый '%' в pattern и replacement; любые последующие '%' не изменяются."

Ссылка на замещение $(text:pattern=replacement) представляет сокращение этой конструкции.

Еще одно сокращение, которое заменит только суффиксы: $(text:suffix=replacement). Здесь не используется % wildcard.

Замечание: не добавляйте дополнительные пробелы для этого сокращения. Они будут рассматриваться как часть для поиска или замены.

foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# Это сокращение для конструкции выше:
two := $(foo:%.o=%.c)
# Это сокращение только для суффиксов, и также эквивалентно конструкции выше:
three := $(foo:.o=.c)
 
all:echo $(one)echo $(two)echo $(three)

Функция foreach. Функция foreach выглядит следующим образом: $(foreach var,list,text). Она преобразует один список слов (разделенных пробелами) в другой. Здесь var установится в каждое слово списка list, и text расширяется для каждого слова.

Следующее добавит восклицательный знак после каждого слова:

foo := Hello World
# Для каждого слова в foo выведет то же самое словo,
# добавив к нему восклицательный знак:
bar := $(foreach wrd,$(foo),$(wrd)!)
 
all: # Выведет "Hello! World!"
→@echo $(bar)

Функция if. Эта функция проверит, не пуст ли первый аргумент. Если первый аргумент не пустой, то запустится второй аргумент, иначе запустится третий аргумент.

foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)
 
all:
→@echo $(foo)
→@echo $(bar)

Функция call. Make поддерживает создание базовых функций. Вы "определяете" функцию просто создавая переменную, но используете параметры $(0), $(1) и так далее. Затем вы вызываете функцию с помощью специальной встроенной функции call. Её синтаксис $(call variable,param,param). $(0) это переменная, в то время как $(1), $(2) и т. д. это параметры.

sweet_new_fn = Имя переменной: $(0) 1 параметр: $(1) 2 параметр: $(2) Пустая переменная: $(3)
 
all:
→# Выведет "Имя переменной: sweet_new_fn 1 параметр: go 2 параметр: tigers Пустая переменная:"
→@echo $(call sweet_new_fn, go, tigers)

Функция shell. Эта функция вызовет shell, но заменит символы новой строки на пробелы!

all:
→@echo $(shell ls -la) # Очень странный шелл, потому что все CR исчезли...

[Другие возможности]

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

include именафайлов...

Эта возможность может быть в частности полезна, когда вы используете флаги компилятора наподобие -M, который создает файлы Makefile на основе некоторого исходного кода. Например, если некоторые C-файлы подключают заголовок, этот заголовок будет добавлен к Makefile, который записал gcc. Более подробно об этом см. далее во врезке "Makefile Cookbook".

Директива vpath. Используйте vpath, чтобы указать, где находятся некоторый набор зависимостей. Формат vpath < pattern> < directories, space/colon separated> < pattern> может использовать  %, что может совпадать с 0 или большим количеством символов. Вы также можете сделать это глобальным с помощью переменной VPATH.

vpath %.h ../headers ../other-directory
 
# Замечание: vpath позволяет найти blah.h, даже когда blah.h не находится
# в текущей директории.
some_binary: ../headers blah.h
→touch some_binary
 
../headers:
→mkdir ../headers
 
# Мы вызываем цель blah.h вместо ../headers/blah.h, потому что это
# то предварительное условие, которое ищет some_binary.
# Обычно blah.h уже существовал бы, и это могло не понадобиться.
blah.h:
→touch ../headers/blah.h
 
clean:
→rm -rf ../headers
→rm -f some_binary

Multiline. Символ обратного слеша ("\") дает возможность использовать команды, определенные несколькими строками, когда строка получается слишком длинной.

some_file:echo Эта строка слишком большая, так что \
→она была разбита на несколько строк.

.phony. Добавление .PHONY к target не даст make перепутать цель phony с именем файла. В этом примере, если создан файл clean, то make clean все еще запустится. Технически все примеры здесь, где используется цель clean, должны были бы использовать .PHONY, но для упрощения это не было сделано. Дополнительно цели "phony" обычно имеют имена, которые редко используются как имена файлов, и на практике многие не используют это.

some_file:
→touch some_file
→touch clean
 
.PHONY: clean
clean:
→rm -f some_file
→rm -f clean

.delete_on_error. Утилита make остановит работу правила (и будет продвигаться обратно к зависимостям prerequisites), если команда возвратит на выходе ненулевой статус. DELETE_ON_ERROR удалит target правила, если правило столкнулось с такой ошибкой. Это произойдет со всеми target, не только с такими, как были перед целями наподобие PHONY. Хорошая идея это использовать всегда, даже хотя make это не делает по историческим причинам.

.DELETE_ON_ERROR:
all: one two
 
one:
→touch one
→false
 
two:
→touch two
→false

[Как запустить компиляцию make из произвольного текущего каталога?]

Опция -C позволяет временно сменить текущую директорию для запуска компиляции. Например, проект и файл makefile находится у вас в каталоге ~/projects/blupdater. Тогда можно запустить компиляцию из любой текущей директории следующей командой:

$ make -C ~/projects/blupdater

После завершения компиляции произойдет автоматический возврат в текущую директорию.

Давайте рассмотрим по-настоящему полезный пример Makefile, который хорошо работает с проектами среднего размера. Приятная фича в том, что этот Makefile автоматически определит зависимости. Все, что вам надо сделать - поместить файлы C/C++ в папку src/.

# Автор Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program
 
BUILD_DIR := ./build
SRC_DIRS := ./src
 
# Для всех файлов C и C++, которые мы хотим скомпилировать.
# Обратите внимание на одинарные кавычки вокруг *-выражений. Иначе шелл некорректно
# развернет такие выражения, но мы хотим послать * напрямую в команду find.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')
 
# Подставит перед BUILD_DIR, и добавит .o к каждому файлу src:
# Например, ./your_dir/hello.cpp turns into ./build/./your_dir/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
 
# Замена строки (версия суффикса без %).
# Например, ./build/hello.cpp.o превратится в ./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)
 
# Каждая папка в ./src должна быть передана в GCC, чтобы можно было найти
# файлы заголовка.
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
 
# Добавит префикс к INC_DIRS. Так moduleA станет -ImoduleA. GCC понимает этот флаг -I
INC_FLAGS := $(addprefix -I,$(INC_DIRS))
 
# Флаги командной строки -MMD и -MP вместе генерируют для нас файлы Makefile!
# Эти файлы на выходе получат суффикс .d вместо .o
CPPFLAGS := $(INC_FLAGS) -MMD -MP
 
# Конечный шаг сборки.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
→$(CXX) $(OBJS) -o $@ $(LDFLAGS)
 
# Шаг для сборки файла C
$(BUILD_DIR)/%.c.o: %.c
→mkdir -p $(dir $@)
→$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
 
# Шаг для сборки файла C++$(BUILD_DIR)/%.cpp.o: %.cpp
→mkdir -p $(dir $@)
→$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@
 
.PHONY: clean
clean:
→rm -r $(BUILD_DIR)
 
# Подключение Makefile файлов .d. Символ - спереди подавляет ошибки не найденных
# файлов Makefiles. Изначально отсутствуют все файлы .d, и нам не надо по этой
# причине отображать ошибки.
-include $(DEPS)

[Ссылки]

1. Learn Makefiles With the tastiest examples site:makefiletutorial.com.
2. Как устроен Makefile и что это такое?
3. Functions for String Substitution and Analysis site:gnu.org.
4. Functions for File Names site:gnu.org.
5. Defining Canned Recipes site:gnu.org.
6. The eval Function site:gnu.org.
7SCons: руководство пользователя, быстрый старт.
8. Zephyr: пакет CMake.
9Установка MSYS2 на Windows.
10. Запуск Linux на Windows помощью WSL.
11. Makefile: установка переменных.
12Магия makefile на простых примерах.

 

Добавить комментарий


Защитный код
Обновить

Top of Page