GNU make — широко известная утилита для автоматической сборки проектов. В мире UNIX она является стандартом де-факто для этой задачи. Являясь не такой популярной среди Windows-разработчиков, тем не менее, привела к появлению таких аналогов, как nmake от Microsoft.
Однако, несмотря на свою популярность, make — во многом ущербный инструмент. Его надёжность вызывает сомнения; производительность низка, особенно для больших проектов; сам же язык файлов makefile выглядит заумно и при этом в нём отсутствуют многие базовые элементы, которые изначально присутствуют во многих других языках программирования.
Конечно, make — не единственная утилита для автоматизации сборки. Множество других средств были созданы для избавления от ограничений make’а. Некоторые из них однозначно лучше оригинального make’а, но на популярности make’а это сказалось мало. Цель этого документа, говоря простым языком, — рассказать о некоторых проблемах, связанных с make’ом — чтобы они не стали для вас неожиданностью.
Большинство аргументов в этой статье относятся к оригинальному UNIX make’у и GNU make’у. Так как GNU make сегодня, скорее всего, гораздо более распространён, то когда мы будем упоминать make или «makefiles», мы будем имеем в виду GNU make.
В статье также предполагается, что читатель уже знаком на базовом уровне с make’ом и понимает такие концепции как «правила», «цели» и «зависимости».
Дизайн языка
Каждый, кто хоть раз писал makefile, скорее всего уже натолкнулся на «особенность» его синтаксиса: в нём используются табуляции. Каждая строка, описывающая запуск команды, должна начинаться с символа табуляции. Пробелы не подходят — только табуляция. К сожалению, это только один из странных аспектов языка make’а.
Рекурсивный make
«Рекурсивный make» это распространённый паттерн при задании правил makefile’а когда правило создаёт другую сессию make’а. Так как каждая сессия make’а только один раз читает makefile верхнего уровня, то это — естественный способ для описания makefile’а для проекта, состоящего из нескольких под-проектов.
«Рекурсивный make» создаёт так много проблем, что даже была написана статья, показывающая, чем плохо это решение. В ней обозначены многие трудности (некоторые из них упомянуты ниже по тексту), но писать makefile’ы, которые не используют рекурсию — на самом деле сложная задача.
Парсер
Большинство парсеров языков программирования следуют одной и той же модели поведения. В начале, исходный текст разбивается на «лексемы» или «сканируется», выкидываются комментарии и пробелы и происходит перевод входного текста (заданного в достаточно свободной форме) в поток «лексем» таких как «символы», «идентификаторы» и «зарезервированные слова». Получившийся поток лексем далее «парсится» с использованием грамматики языка, которая определяет, какие комбинации и порядок лексем являются корректными. В конце, получившееся «грамматическое дерево» интерпретируется, компилируется и т.д.
Парсер make’а не следует этой стандартной модели. Вы не можете распарсить makefile без одновременного его выполнения. Замена переменных («variable substitution») может произойти в любом месте, и так как вы не знаете значения переменной, вы не можете продолжить синтаксический разбор. Как следствие, это очень нетривиальная задача — написать отдельную утилиту, которая может парсить makefile’ы, так как вам придётся написать реализацию всего языка.
Также отсутствует чёткое разделение на лексемы в языке. К примеру, посмотрим, как обрабатывается запятая.
Иногда запятая является частью строки и не имеет особого статуса:
X = y,z
Иногда запятая разделяет строки, которые сравниваются в операторе if:
ifeq ($(X),$(Y))
Иногда запятая разделяет аргументы функции:
$(filter %.c,$(SRC_FILES))
Но иногда, даже среди аргументов функций, запятая — всего лишь часть строки:
$(filter %.c,a.c b.c c.cpp d,e.c)
(так как <b>filter</b> принимает только два параметра, последняя запятая не добавляет нового параметра; она становится просто одним из символов второго аргумента)