Кунг-фу стиля 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.c новее чем hello , то hello надо собрать. Так, а тут притормозим. Откуда программа знает о том, что ей делать? Обычно в Makefile включают инструкции по сборке целей ( hello в нашем случае) на основе зависимостей. Но существуют и правила, применяемые по умолчанию.
Если создать вышеописанный файл Makefile в директории, в которой уже есть файл hello.c, и выполнить в этой директории команду make , можно будет увидеть, что выполнится такая команда:
Это — совершенно нормально, так как в большинстве Linux-дистрибутивов cc указывает на компилятор C, используемый по умолчанию. Если снова запустить make , можно увидеть, что утилита ничего собирать не будет. Вместо этого она выдаст примерно такое сообщение:
Для того чтобы снова собрать проект, нужно либо внести в hello.c изменения, либо обработать hello.c командой touch , либо удалить файл hello . Между прочим, если запустить make с опцией -n , то программа сообщит о том, что собирается делать, но при этом ничего делать не будет. Это может пригодиться в тех случаях, когда нужно разобраться с тем, к выполнению каких команд приведёт обработка некоего Makefile .
Если хотите — вы можете изменить параметры, используемые по умолчанию, настроив значения различных переменных. Эти переменные могут задаваться средствами командной строки, они могут быть определены в окружении или могут быть установлены в самом Makefile . Вот пример:
CC=gcc CFLAGS=-g hello : hello.c
При использовании такого Makefile вызов make приведёт к выполнению такой команды:
В сложных файлах 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
На самом деле, для того чтобы сделать правила более гибкими, нужно использовать переменные — так же, как это делается в правилах, используемых по умолчанию. Ещё можно использовать символы-местозаполнители. Взгляните на следующий пример:
В 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 такую директиву:
К фиктивным целям можно прикреплять действия. Например, в Makefile часто можно увидеть примерно такую конструкцию:
Благодаря этому можно выполнить команду make clean для того чтобы удалить все объектные файлы. Возможно, подобную цель не будут делать первой, а такую команду не будут прикреплять к чему-то вроде цели all . Ещё одним распространённым приёмом является создание фиктивной цели, код, связанный с которой, прошивает программу на микроконтроллер. В результате, например, выполнив команду вроде make program можно записать программу на некое устройство.
Необычные способы использование make
Обычно make используют для сборки программ. Но, как и в случаях с любыми другими Unix/Linux-инструментами, находятся люди, которые используют make для решения самых разных задач. Например, совсем несложно написать Makefile , использующий pandoc для преобразования документа в разные форматы, выполняемого при изменении документа.
Главное тут — это понимать то, что make стремится к построению дерева зависимостей, выясняя то, какие его части нуждаются в обработке, и запуская скрипты, связанные с этими частями. А то, что делается в этих скриптах, не обязательно должно приводить к сборке программ. Они могут копировать файлы (даже на удалённые системы, например, используя scp ), они могут удалять файлы, или, в целом, выполнять любые действия, которые можно выполнить из командной строки.
Итоги
Утилиту make можно изучать ещё очень долго, можно почитать справку по ней, но даже того, что вы о ней сегодня узнали, будет достаточно для решения на удивление большого количества задач. Если вы посмотрите файлы Makefile крупных проектов, вроде тех, что создают для платформы Arduino, вы увидите в таких файлах много такого, о чём мы тут не говорили. Но вы сможете их прочитать и многое в них вам покажется знакомым. В целом же, владея основными приёмами работы с make , вы сможете сделать много всего полезного.
Используете ли вы make для решения каких-то особенных задач?