[Перевод] Кунг-фу стиля Linux: великая сила make
Со временем Linux (точнее — операционная система, которую обычно называют «Linux», представляющая собой ядро Linux и GNU-инструменты) стала гораздо сложнее, чем Unix — ОС, стоящая у истоков Linux. Это, конечно, неизбежно. Но это означает, что тем, кто пользуется Linux уже давно, нужно было постепенно расширять свои знания и навыки, осваивая новые возможности. А вот на тех, кто начинает работу в Linux в наши дни, сваливается необходимость освоить систему, так сказать, за один присест. Эту ситуацию хорошо иллюстрирует пример того, как в Linux обычно осуществляется сборка программ. Практически во всех проектах используется make
— утилита, которая, запуская процессы компиляции кода, пытается делать только то, что нужно. Это было особенно важно в те времена, когда компьютеру с процессором, работающим на частоте в 100 МГц, и с медленным жёстким диском, нужно было потратить целый день на то, чтобы собрать какой-нибудь серьёзный проект. Программа make
, судя по всему, устроена очень просто. Но сегодня у того, кто почитает типичный файл Makefile
, может закружиться голова. А во многих проектах используются дополнительные абстракции, которые ещё сильнее всё запутывают.
В этом материале я хочу продемонстрировать вам то, насколько простым может быть файл Makefile
. Если вы способны создать простой Makefile
, это значит, что вы сможете найти гораздо больше способов применения утилиты make
, чем может показаться на первый взгляд. Примеры, которые я будут тут показывать, основаны на языке C, но дело тут не в самом языке, а в его распространённости и широкой известности. С помощью make
можно, средствами командной строки Linux, собрать практически всё что угодно.
Если есть IDE, собирающая проекты, то Makefile не нужен?
Кто-то может усмехнуться и сказать, что Makefile
ему не нужен. Используемая им IDE (Integrated Development Environment, интегрированная среда разработки) сама собирает его проекты. Это, может, и так, но в недрах множества IDE, на самом деле, используются файлы Makefile
. Даже в тех, в которых не предусмотрены механизмы экспорта таких файлов. Умение обращаться с файлами Makefile
упрощает написание сборочных скриптов и работу с CI/CD-инструментами, которые обычно ожидают наличия таких файлов.
Единственным исключением является Java. В Java-разработке имеются собственные инструменты для сборки проектов, утилита make
в этой среде распространена не так сильно, как в других. Но make
можно использовать и тем, кто пишет на Java — вместе с инструментами командной строки вроде javac
или gcj
.
Простейший Makefile
Вот пример простейшего Makefile
:
hello: hello.c
Вот и всё. Тут имеется правило, в котором сказано, что имеется файл hello
, зависящий от файла hello.c
. Если hello.c
новее чем hello
, то hello
надо собрать. Так, а тут притормозим. Откуда программа знает о том, что ей делать? Обычно в Makefile
включают инструкции по сборке целей (hello
в нашем случае) на основе зависимостей. Но существуют и правила, применяемые по умолчанию.
Если создать вышеописанный файл Makefile
в директории, в которой уже есть файл hello.c, и выполнить в этой директории команду make
, можно будет увидеть, что выполнится такая команда:
cc hello.c -o hello
Это — совершенно нормально, так как в большинстве Linux-дистрибутивов cc
указывает на компилятор C, используемый по умолчанию. Если снова запустить make
, можно увидеть, что утилита ничего собирать не будет. Вместо этого она выдаст примерно такое сообщение:
make: 'hello' is up to date.
Для того чтобы снова собрать проект, нужно либо внести в hello.c
изменения, либо обработать hello.c
командой touch
, либо удалить файл hello
. Между прочим, если запустить make
с опцией -n
, то программа сообщит о том, что собирается делать, но при этом ничего делать не будет. Это может пригодиться в тех случаях, когда нужно разобраться с тем, к выполнению каких команд приведёт обработка некоего Makefile
.
Если хотите — вы можете изменить параметры, используемые по умолчанию, настроив значения различных переменных. Эти переменные могут задаваться средствами командной строки, они могут быть определены в окружении или могут быть установлены в самом Makefile
. Вот пример:
CC=gcc
CFLAGS=-g
hello : hello.c
При использовании такого Makefile
вызов make
приведёт к выполнению такой команды:
gcc -g hello.c -o hello
В сложных файлах Makefile
можно настраивать уже применяемые опции, можно добавлять комментарии (начинающиеся со знака #
):
CC=gcc
CFLAGS=-g
# Закомментируйте следующую строку для отключения оптимизации
CFLAGS+=-O
hello : hello.c
На самом деле, неявно используемое правило выглядит так:
$(CC) $(CFLAGS) $(CPPFLAGS) hello.c -o hello
Обратите внимание на то, что переменные принято оформлять с использованием конструкции вида $()
. Если имя переменной состоит всего из одного символа, то без скобок можно и обойтись (например — использовать конструкцию вроде $X
), но лучше так не делать, так как работоспособность подобных конструкций в разных системах не гарантируется.
Собственные правила
Возможно, правила, используемые по умолчанию, вас не устраивают. Это — не проблема. Утилита make
, правда, весьма внимательно относится к формату файлов Makefile
. Так, если начать строку с символа Tab
(не с нескольких пробелов, а именно с настоящего Tab
), тогда то, что находится в строке, будет восприниматься как команда запуска некоего скрипта, а не как правило. И хотя то, что показано ниже, возможно, выглядит не особенно аккуратно, это — пример вполне работоспособного Makefile:
hello : hello.c
gcc -g -O hello.c
На самом деле, для того чтобы сделать правила более гибкими, нужно использовать переменные — так же, как это делается в правилах, используемых по умолчанию. Ещё можно использовать символы-местозаполнители. Взгляните на следующий пример:
% : %.c
$(CC) $(CPPFLAGS) $(CFLAGS) $< -o $@
Тут представлено правило, которое, в целом, соответствует правилу, применяемому по умолчанию. Символ %
— это универсальный местозаполнитель, соответствующий любой последовательности символов. В переменную $<
попадает имя первого (и, в данном случае, единственного) реквизита, которым является hello.c
. Переменная $@
даёт нам имя цели (в данном примере — hello
). Есть и многие другие особые переменные, но для начала вам достаточно знать об этих двух.
В Makefile
могут быть многострочные скрипты (обрабатываемые системной командной оболочкой, используемой по умолчанию, хотя это можно и изменить). Главное требование — строки должны начинаться с Tab
. В файле можно описывать множество реквизитов. Например:
hello : hello.c hello.h mylocallib.h
Правда, подобный код довольно тяжело поддерживать. При использовании C и C++ большинство компиляторов (включая gcc
) могут создавать .d-файлы, которые способны автоматически сообщать make
о том, от каких именно файлов зависит объект. Это, правда, выходит за рамки данного материала. Если вас это заинтересовало — взгляните на справку по gcc
, и, в частности, на описание опции -MMD
.
Объектные файлы
В больших проектах обычно не занимаются только лишь компиляцией C-файлов (или каких-то других файлов с исходным кодом). Там исходные файлы компилируют в объектные файлы, которые, в итоге, компонуют. Правила, применяемые по умолчанию, это учитывают. Но, конечно, можно создавать и собственные правила:
hello : hello.o mylib.o
hello.o : hello.c hello.h mylib.h
mylib.o : mylib.c mylib.h
Благодаря правилам, применяемым по умолчанию, это всё, что вам нужно. Утилита make
достаточно интеллектуальна для того чтобы понять, что ей нужен файл hello.o
, поэтому она найдёт правило для него. Конечно, после каждого из этих правил можно добавить строку (или строки) со скриптом, сделав это в том случае, если нужно контролировать то, что происходит в процессе сборки проекта.
По умолчанию make
пытается собрать лишь первую обнаруженную цель. Иногда первой идёт фиктивная цель:
all : hello libtest config-editor
Предполагается, что где-то есть правила для трёх вышеупомянутых программ, и то, что make
соберёт их все. Этот приём будет работать до тех пор, пока в рабочей директории отсутствует файл all
. Для того чтобы предотвратить появление подобной проблемы, можно заранее указать то, что цель является фиктивной, используя в Makefile
такую директиву:
.PHONY: all
К фиктивным целям можно прикреплять действия. Например, в Makefile часто можно увидеть примерно такую конструкцию:
.PHONY: clean
clean:
rm *.o
Благодаря этому можно выполнить команду make clean
для того чтобы удалить все объектные файлы. Возможно, подобную цель не будут делать первой, а такую команду не будут прикреплять к чему-то вроде цели all
. Ещё одним распространённым приёмом является создание фиктивной цели, код, связанный с которой, прошивает программу на микроконтроллер. В результате, например, выполнив команду вроде make program
можно записать программу на некое устройство.
Необычные способы использование make
Обычно make
используют для сборки программ. Но, как и в случаях с любыми другими Unix/Linux-инструментами, находятся люди, которые используют make
для решения самых разных задач. Например, совсем несложно написать Makefile
, использующий pandoc для преобразования документа в разные форматы, выполняемого при изменении документа.
Главное тут — это понимать то, что make
стремится к построению дерева зависимостей, выясняя то, какие его части нуждаются в обработке, и запуская скрипты, связанные с этими частями. А то, что делается в этих скриптах, не обязательно должно приводить к сборке программ. Они могут копировать файлы (даже на удалённые системы, например, используя scp
), они могут удалять файлы, или, в целом, выполнять любые действия, которые можно выполнить из командной строки.
Итоги
Утилиту make
можно изучать ещё очень долго, можно почитать справку по ней, но даже того, что вы о ней сегодня узнали, будет достаточно для решения на удивление большого количества задач. Если вы посмотрите файлы Makefile
крупных проектов, вроде тех, что создают для платформы Arduino, вы увидите в таких файлах много такого, о чём мы тут не говорили. Но вы сможете их прочитать и многое в них вам покажется знакомым. В целом же, владея основными приёмами работы с make
, вы сможете сделать много всего полезного.
Используете ли вы make для решения каких-то особенных задач?