Чем плох GNU make?

    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)

    (так как filter принимает только два параметра, последняя запятая не добавляет нового параметра; она становится просто одним из символов второго аргумента)

    Пробелы следуют таким же малопонятным правилам. Иногда пробелы учитываются, иногда нет. Строки не заключаются в кавычки, из-за этого визуально не ясно, какие пробелы значимы. Из-за отсутствия такого типа данных как «список» (существуют только строки), пробелы должны быть использованы как разделитель элементов списка. Как следствие, это приводит к избыточному усложнению логики, например, если имя файла просто содержит пробел.

    Следующий пример иллюстрирует запутанную логику обработки пробелов. Требуется использовать малопонятный трюк, чтобы создать переменную, которая заканчивается пробелом. (Обычно пробелы на концах строк выкидываются парсером, но это происходит до, а не после замены переменных).
    NOTHING :=
    SPACE := $(NOTHING) $(NOTHING)
    CC_TARGET_PREFIX := -o$(SPACE)
    # вот теперь можно писать правила вида $(CC_TARGET_PREFIX)$@ 


    А мы только коснулись запятых и пробелов. Всего несколько человек понимают все хитросплетения парсера make’а.

    Неинициализированные переменные и переменные окружения.

    Если в makefile’е происходит обращение к неинициализированной переменной, make не сообщает об ошибке. Вместо этого он получает значение этой переменной из переменной окружения с таким же именем. Если же переменная окружения с таким именем не найдена, то просто считается, что значением будет пустая строка.

    Это приводит к двум типам проблем. Первая — опечатки не отлавливаются и не считаются ошибками (вы можете заставить make выдавать предупреждения для таких ситуаций, но такое поведение отключено по умолчанию, а иногда неинициализированные переменные используются умышленно). Вторая — переменные окружения могут неожиданно влиять на код вашего makefile’а. Вы не можете знать наверняка, какие переменные могли быть установлены пользователем, поэтому, для надёжности, вы должны инициализировать все переменные до ссылки на них или добавления через +=

    Также есть запутывающая разница между поведение make’а если его вызывать как "make FOO=1" с вызовом "export FOO=1 ; make". В первом случае строка в makefile’е FOO = 0 не имеет эффекта! Вместо этого, вы должны писать override FOO = 0.

    Синтаксис условных выражений

    Один из главных недостатков языка makefile’ов — это ограниченная поддержка для условных выражений (условные операторы, в частности, важны для написания кросс-платформенных makefile’ов). Новые версии make’а уже содержат поддержку для "else if" синтаксиса. Конечно, у оператора if существует только четыре базовых варианта: ifeq, ifneq, ifdef, и ifndef. Если ваше условие более сложное и требует проверки на «и/или/не», то приходится писать более громоздкий код.

    Допустим, нам надо определять Linux/x86 как целевую платформу. Следующий хак — обычный способ для замены условия «и» его суррогатом:
    ifeq ($(TARGET_OS)-$(TARGET_CPU),linux-x86)
        foo = bar
    endif 


    Условие «или» уже не будет таким простым. Допустим нам надо определять x86 или x86_64, а также вместо "foo = bar" у нас кода на 10+ строк и мы не хотим его дублировать. У нас есть несколько вариантов, каждых из которых плох:
    
    # Кратко, но непонятно
    ifneq (,$(filter x86 x86_64,$(TARGET_CPU))
      foo = bar
    endif
    # Многословно, но более понятно
    ifeq ($(TARGET_CPU),x86)
      TARGET_CPU_IS_X86 := 1
    else ifeq ($(TARGET_CPU),x86_64)
      TARGET_CPU_IS_X86 := 1
    else
      TARGET_CPU_IS_X86 := 0
    endif
    ifeq ($(TARGET_CPU_IS_X86),1)
      foo = bar
    endif


    Множество мест в makefile’ах могли бы быть упрощены, если бы язык поддерживал полноценный синтаксис.

    Два вида переменных

    Существуют два вида присваиваний переменных в make’е. ":=" вычисляет выражение справа сразу. Обычный "=" вычисляет выражение позже, когда переменная используется. Первый вариант используется в большинстве других языков программирования и, как правило, более эффективный, в частности, если выражение сложное для вычисления. Второй вариант, конечно же, используется в большинстве в makefile’ов.

    Есть объективные причины для использования "=" (с отложенным вычислением). Но часто от него можно избавиться, используя более аккуратную архитектуру makefile’ов. Даже не учитывая проблему с производительностью, отложенные вычисления делают код makefile’ов более сложным для чтения и понимания.

    Обычно, вы можете читать программу с начала к концу — в том же порядке, в котором она исполняется, и точно знать, в каком именно состоянии она находится в каждый момент времени. С отложенным вычислением же, вы не можете знать значение переменной без знания что произойдёт дальше в программе. Переменная может менять своё значение косвенно, без непосредственного её изменения. Если же вы попробуете искать ошибки в makefile’е используя «отладочный вывод», например так:
    $(warning VAR=$(VAR))
    …вы можете не получить то, что вам надо.

    Шаблонные подстановки и поиск файлов

    Некоторые правила используют знак % для обозначения основной части имени файла (без расширения) — для того, чтобы задать правило генерации одних файлов из других. Например, правило "%.o: %.c" для компиляции .c файлов в объектный файл с расширением .o.

    Допустим, нам нужно постоить объектный файл foo.o но исходный файл foo.c находится где-то не в текущей директории. У make’а есть директива vpath, которая сообщает ему, где искать такие файлы. К сожалению, если в директориях файл с именем foo.c встретится два раза, make может выбрать ошибочный файл.

    Следующий стандартный паттерн программирования makefile’ов даёт сбой, если два исходных файла имеют одинаковое имя (но разное расширение) и лежат рядом. Проблема в том, что преобразование «имя исходного файла => имя объектного файла» теряет часть информации, но дизайн make’а требует этого для выполнения обратного отображения.

    O_FILES := $(patsubst %.c,%.o,$(notdir $(C_FILES)))
    vpath %.c $(sort $(dir $(C_FILES)))
    $(LIB): $(O_FILES) 


    И другие отсутствующие возможности

    make не знает никаких типов данных — только строки. Нет булевского типа, списков, словарей.
    Нет понятия «область видимости». Все переменные — глобальные.
    Поддержка для циклов ограничена. $(foreach) будет вычислять выражение несколько раз и объединять результаты, но вы не сможете использовать $(foreach) для создания, к примеру, группы правил.
    Функции, определяемые пользователем, существуют, но имеют такие же ограничения, что и foreach. Они могут лишь заниматься подстановкой переменных и не могут использовать синтаксис языка полностью или создавать новые зависимости.

    Надёжность


    Надежность make’а низка, особенно на больших проектах или инкрементальной компиляции. Иногда сборка падает со странной ошибкой, и вам придётся использовать «магические заклинания» такие как make clean и надеяться, что всё починится. Иногда же (более опасная ситуация) всё выглядит благополучно, но что-то не было перекомпилировано и ваше приложение будет падать после запуска.

    Отсутствующие зависимости

    Вы должны рассказать make’у обо всех зависимостях каждой цели. Если вы этого не сделаете, он не перекомпилирует цель когда зависимый файл изменится. Для C/C++ многие компиляторы могут генерировать информацию о зависимостях в формате, понимаемом make’ом. Для других утилит, однако, ситуация существенно хуже. Допустим, у нас есть питоновский скрипт, который включает в себя другие модули. Изменения в скрипте приводят к изменению его результатов работы; это очевидно и легко внести в makefile. Но изменение в одном из модулей также могут изменить вывод скрипта. Полное описание же всех этих зависимостей и поддержка их в актуальном состоянии являются нетривиальной задачей.

    Использование метки «время последней модификации файла»

    make определяет, что цель требует пересборки сравнивая её «время последней модификации» с аналогичным временем у её зависимостей. Не происходит анализа содержимого файла, только сравнение их времён. Но использование этой информации файловой системы не всегда надёжно, особенно в сетевом окружении. Системные часы могут отставать, иногда другие программы могут принудительно выставлять нужное им время модификации у файлов, затирая «настоящее» значение. Когда такое происходит, make не перестраивает цели, которые должны быть перестроены. В результате получается только частичная перекомпиляция.

    Зависимость от параметров командной строки

    Когда строка параметров какой-либо программы изменяется, её результаты также могут измениться (например, изменение в -Doption, которое передаётся в препроцессор языка C). make не будет перекомпилировать в этом случае, что приведёт к некорректной промежуточной перекомпиляции.

    Вы можете попробовать защититься от этого, если внесёте в зависимость для каждой цели файл Makefile. Однако, этот подход ненадёжен, так как вы можете пропустить какую-нибудь цель. Более того, Makefile может включать другие Makefile’ы, которые тоже могут включать ещё Makefile’ы. Вы должны будете перечислить их все и поддерживать этот список в актуальном состоянии. Кроме того, многие изменения в makefile’ах являются незначительными. Вы, скорее всего, не хотите перекомпиляции всего проекта только из-за того, что вы изменили комментарий в makefile’е.

    Наследование переменных окружения и зависимость от них

    Не только каждая переменная окружения становится переменной make’а, но также эти переменные передаются в каждую программу, которую make запускает. Так как каждый пользователь имеет свой собственный набор переменных окружения, два пользователя, запускающих одну и туже сборку могут получать разные результаты.
    Изменение какой-нибудь переменной окружения, передаваемой в дочерний процесс, может изменить его вывод. То есть, такая ситуация должная инициировать пересборку, но make не будет этого делать.

    Множественные одновременные сессии

    Если вы запустите два экземпляра make’а в одной директории одновременно, они столкнутся между собой, когда попытаются компилировать одни и те же файлы. Скорее всего, один из них (или даже оба) аварийно завершат работу.

    Редактирование файлов во время пересборки.

    Если вы редактировали и сохранили файл во время работы make’а, то результат невозможно будет предсказать. Может быть, make корректно подхватит эти изменения, а может и нет — и вам надо будет запустить make снова. Или, если вам не повезло, сохранение может произойти в такой момент, что некоторые из целей потребуют пересборки, но последующие запуски make не обнаружат этого.

    Удаление ненужных файлов

    Предположим, ваш проект изначально использовал файл foo.c, но позже этот файл был удалён из проекта и из makefile’а. Временный объектный файл foo.o останется. Обычно, это допустимо, но такие файлы могут накапливаться с течением времени и иногда приводить к проблемам. Например, они могут быть ошибочно выбраны во время поиска по vpath. Другой пример: допустим один из файлов, ранее генерируемый make’ом во время сборки, теперь положен в систему версионного контроля. Правило, которое генерировало этот файл, также удалено из makefile’а. Однако, системы версионного контроля обычно не перезаписывают файлы, если видят, что не-версионный файл с таким же именем уже существует (из боязни удалить что-нибудь важное). Если вы не обратили внимание на сообщение о такой ошибке, не удалили этот файл вручную и не обновили повторно каталог с исходниками, то вы будете использовать устаревшую версию этого файла.

    Нормализация имён файлов

    К одному и тому же файлу можно обратиться, используя разные пути. Даже не беря во внимание жёсткие и символические ссылки, foo.c, ./foo.c, ../bar/foo.c, /home/user/bar/foo.c могут указывать на один и тот же файл. make’у следует обрабатывать их соответствующе, однако он этого не делает.
    Проблема ещё хуже под Windows, где файловая система не регистро-зависима.

    Последствия прерванной или сбойнувшей пересборки

    Если сборка упала в середине процесса, дальнейшие инкрементальные перекомпиляции могут быть ненадёжными. В частности, если команда вернула ошибку, make не удаляет промежуточный выходной файл! Если вы запустите make снова, он может посчитать, что файл уже не требует перекомпиляции и попытаться использовать его. У make’а есть специальная опция, заставляющая его удалять такие файлы, но она не включена по-умолчанию.
    Нажатие Ctrl-C во время пересборки также может привести ваше дерево исходников в непонятное состояние.
    Каждый раз, когда вы сталкиваетесь с проблемами во время инкрементальной пересборки, возникает сомнение — если один файл не перестроился корректно, кто знает, сколько ещё есть таких файлов? В такой ситуации, возможно, вам надо начать заново с make clean. Проблема в том, что make clean не даёт никакой гарантии (см. выше), возможно, вам придётся разворачивать дерево исходников заново в другой директории.

    Производительность


    Производительность make’а масштабируется плохо (нелинейно) с ростом размера проекта.

    Производительность инкрементальных сборок

    Вы можете надеяться, что пересборка проекта занимает время пропорциональное числу целей, которые требуется перестроить. К сожалению, это не так.
    Из-за того, что результат инкрементальных сборок не всегда внушает доверие, пользователи должны делать полную пересборку более-менее регулярно, иногда по необходимости (если что-то не собирается, попробуйте make clean; make), а иногда постоянно (из-за паранойи). Лучше быть уверенным и подождать полной пересборки, чем рисковать, что какая-то часть рассинхронизировалась с исходниками.
    «Время последнего изменения» файла может измениться без изменения содержимого файла. Это приводит к ненужным перекомриляциям.
    Плохо написанный makefile может содержать слишком много зависимостей, из-за этого цели могут перекомпилироваться даже если его (настоящие) зависимости не изменились. Неаккуратное использование «фальшивых» (phony) целей — это другой источник ошибок (такие цели всегда должны быть перестроены).
    Даже если ваши makefile’ы не собержат ошибок, а ваши инкрементальные сборки абсолютно надёжны, производительность не идеальна. Предположим, вы редактировали один из .c-файлов (не заголовочный файл) в большом проекте. Если вы наберёте make в корне проекта, make должен будет распарсить все makefile’ы, рекурсивно вызывая себя много раз, и пройти по всем зависимостям, выясняя, нужно ли им перестраиваться. Время запуска собственно компилятора может быть существенно меньше общего времени.

    Рекурсивный make и производительность

    Небрежное использование рекурсивного make’а может быть опасно, например, при таком сценарии. Допустим, ваш проект содержит исходники двух исполняемых файлов A и B, которые в свою очередь, зависят от библиотеки C. Makefile самого верхнего уровня должен рекурсивно входить в директории A и B, конечно же. Нам также хотелось бы иметь возможность вызывать make в директориях A и B, если мы хотим построить только один из исполняемых файлов. Соответственно, мы должны рекурсивно вызывать make и из директории ../C. А если вызвать make из корня проекта, мы попадём в C дважды!
    В данном примере это выглядит не страшно, но в больших проектах в некоторые директории make может заглядывать десятки раз. И каждый раз makefile должен быть прочитан, разобран и все его зависимости должны быть проверены. В make’е отсутствуют встроенные средства для предотвращения таких ситуаций.

    Параллельный Make

    «Параллельный запуск» make’а обещает большой прирост по скорости, особенно на современных процессорах с множеством ядер. К сожалению, реальность далека от обещаний.
    Текстовый вывод «параллельного make’а» тяжело читать. Трудно увидеть какое предупреждение/строка/и т.п. относится к какой команде, когда несколько процессов одновременно работают в одном окружении.
    Параллельный make особенно чувствителен к корректному указанию зависимостей. Если два правила не связаны через зависимости, make предполагает, что они могут быть вызваны в любом порядке. Когда вызывается одиночный make, его поведение предсказуемое: если A зависит от B и C, то сначала B будет построенно, затем C, потом A. Конечно, make имеет право построить C до В, но (в режиме последовательного make’а) порядок определён.
    В параллельном режиме, B и C могут (но не обязаны) быть построены параллельно. Если C (на самом деле) зависит от B, но эта зависимость не прописана в makefile’е, то построение C, скорее всего, провалится (но не обязательно, зависит от конкретных времён).
    Параллельный make выпячивает проблемы отсутствующих зависимостей в makefile’ах. Это само по себе хорошая вещь, т.к. они ведут к другим проблемам, и замечательно, что можно поймать их и исправить. Но на практике, на больших проектах, результат использования параллельного make’а разочаровывает.
    Взаимодействие параллельного make’а с рекурсивным make’ом затруднительно. Каждая сессия make’а — независима, то есть каждая пытается распараллелить свою работу независимо от других и не имеет общего представления о полном дереве зависимостей. Мы должны найти компромис между надёжностью и производительностью. С одной стороны, мы хотим распараллелить сборку не только одного-единственного makefile’а, но и всех остальных makefile’ов. Но, так как make не знает о меж-makefile’ных зависимостях, полное распараллеливание суб-make’ов не работает.
    Некоторые суб-make’и можно запускать параллельно, другие должны быть запущены в последовательном режиме. Указывать эти зависимости — неудобно, и очень легко пропустить несколько из них. Есть соблазн возврата к надёжному последовательному способу разбора дерева makefile’ов и распараллеливать только одиночные makefile’ы в каждый момент времени, но это сильно снижает итоговую производительность, в частности при инкрементальной сборке.

    Автоматическая генерация зависимостей для Microsoft Visual C++

    Многие компиляторы, как и GCC, могут выдавать информацию о зависимостях в формате, понимаемом make’ом. К сожалению, Microsoft Visual C++ не делает этого. У него есть специальный ключ /showIncludes, но требуется дополнительный скрипт, чтобы перевести эту информацию в формат make’а. Это требует запуска отдельного скрипта на каждый C-файл. Запуск, к примеру, интерпретатора Python’а для каждого файла — не мгновенная операция.

    Встроенные правила

    make содержит огромное количество встроенных правил. Они позволяют немного упростить код небольших makefile’ов, но средние и большие проекты обычно переопределяют их. Они влиют на производительность, так как make’у приходится пробираться через все эти дополнительные шаблоны пытаясь найти правила для компиляции файлов. Многие из них устарели — например, использование с RSC и SCCS системами ревизионного контроля. Их используют всего несколько человек, но эти правила будут замедлять все сборки всех остальных пользователей.
    Вы можете отключить их с командной строки через make -r, но это не поведение по-умолчанию. Вы можете отключить их, добавив специальную директиву в makefile, но это тоже не по-умолчанию — и многие забывают сделать это.

    Другое


    Есть также и другие замечания к make’у, которые не попадают в предыдущие категории.

    Молчание — золото

    Согласно Эрику Реймонду, «одно из самых старых и неизменных правил дизайна мира UNIX’а является то, что если программе нечего сказать интересного или неожиданного — она должна молчать. Хорошо ведущие себя программы делают свою работу ненавязчиво, с минимумом требуемого внимания и беспокойства. Молчание — золото». make не следует этому правилу.
    Когда вы запускаете make, его лог содержит все запускаемые им команды и всё, что выдают эти команды на stdout и stderr. Это слишком много. Важные предупреждения / ошибки тонут в этом потоке, а текст зачастую выводится так быстро, что становится нечитаемым.
    Вы можете сильно уменьшить эту выдачу запуская make -s, но это не поведение по-умолчанию. Также, нет промежуточного варианта, при котором make выдаёт, что он сейчас делает — без печатания командных строк.

    Многоцелевые правила

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

    Предупреждения, которые должны быть ошибками

    Make печатает предупреждения, но не прекращает работу, если он обнаруживает циклические зависимости. Это, скорее всего, свидетельствует о серьёзной ошибке в makefile’е, но make оценивает эту ситуацию как мелкую неприятность.
    Аналогично, make печатает предупреждение (и продолжает работать дальше), если обнаруживает, что есть два правила, описывающих как сделать одну цель. Он просто игнорирует одно из них. И снова — это серьёзный баг в makefile’е, но make так не считает.

    Создание директорий

    Весьма удобно класть выходные файлы для разных конфигураций в разные директории, и вам не нужно будет перестраивать весь проект когда вы смените конфигурацию. К примеру, вы можете положить «debug» бинарники в каталог «debug» и аналогично для «release» конфигурации. Но до того, как вы начнёте класть файлы в эти директории, вы должны будете создать их.
    Было бы здорово, если бы make делал это автоматически — очевидно же, что невозможно построить цель, если её директория пока не существует, — но make не делает этого.
    Не является очень практичным вызывать mkdir -p $(dir $@)) в каждом правиле. Это неэффективно, а кроме того, вы должны игнорировать ошибку, если директория уже существует.
    Вы можете попробовать решить проблему таким образом:
    debug/%.o: %.c debug
            $(CC) -c $< -o $@
    debug:
            mkdir $@


    Выглядит работоспособным — если «debug» не существует, то создать его до начала компиляции debug/foo.o. Но только выглядит. Создание нового файла в директории изменяет «время последней модификации» этой директории. Допустим, мы компилируем два файла — debug/foo.o и debug/bar.o. Создание debug/bar.o изменит время модификации директории «debug». Теперь оно станет более новым, чем время создания debug/foo.o, то есть, в следующий раз, когда мы вызовём make, файл debug/foo.o будет перекомпилирован без необходимости. А если перекомпиляция делается через удаление старого файла и создания нового (а не через перезапись существующего файла), вы получите нескончаемую череду ненужных перекомпиляций.
    Решением является создание зависимости от файла (например, debug/dummy.txt), а не от директории. Это требует дополнительных действий в makefile’е (touch debug/dummy.txt), и может конфликтовать с возможностью make’а по автоматическому удалению промежуточных файлов. А если же вы не будете аккуратны в указании этой дополнительной зависимости (от dummy.txt) для каждой цели, вы получите проблемы, когда запустите make в параллельном режиме.

    Выводы


    Make — популярная утилита с множеством изъянов. Она может упростить вам жизнь, а может и усложнить её. Если вы работаете над большим программным продуктом, вы должны рассмотреть и другие альтернативы make’у. Если же вы должны использовать только make, вы должны быть в курсе его недостатков.

    PS: всё вышесказанное — перевод вот этой статьи. Я давно собирался написать топик на такую же тему, но прочитав статью, понял, что лучше будет сделать перевод. Не все аргументы автора «make-специфичны» (а некоторые вообще подойдут к абсолютно всем утилитам подобного рода), но знание и понимание различных грабель make'а необходимо всем программистам, которым приходится использовать его в своей работе.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 84

    • UFO just landed and posted this here
        +5
        Мне понравилось, что автор всегда точно понимает, «как оно должно быть на самом деле». Он знает, как обязан выглядеть «идеальный make» и описывает разницу.

        Или, к примеру, почему удобный вариант использования надо дополнительно настраивать? Почему простые вещи делаются нетривиально?

        Меня в своё время поведение команды git log поразило — надо же, учитывает, что её человек запустить может.
          +1
          Зато если надо посмотреть так, чтобы в консоли что-то осталось, приходится подпрыгивать :)
            +1
            Ну, если очень хочется добиться поведения «как у остальных», то можно git log | cat использовать.
              +1
              Вот вы и подпрыгнули 8)
        +2
        Перевожу проект с make на scons (сейчас он переходит в фазу активного тестирования и внедрения), в ближайшем будующем напишу несколько статей на эту тему. Могу сказать одно — я убедился что make это устаревший анархаизм. Хотя scons тоже не идеален, но мне на его базе удалось сделать достаточно удобную систему сборки, а точнее целый фреймворк который я в данный момент дорабатываю и смогу использовать с любым другими проектами в будущем (а может даже кто-то еще захочет).
          +2
          Пользуюсь cmake + make. Вполне доволен.
            +2
            Плюсы, минусы? Что раздражает, что радует? Большой ли проект?
              +1
              Проекты у меня маленькие (вот этот, например, пока чуть больше восьми тысяч строк, в завершенном виде должен быть не больше ~30..40 тысяч строк; однако библиотек используется много — поэтому вручную писать Makefile неудобно).

              Поначалу я писал Makefile'ы вручную, но когда стало появляться все больше и больше зависимостей, попробовал на automake перейти. Эта зараза мало того, что медленно работает, так еще и не находила кое-какие библиотеки.

              А cmake'ом пользуюсь с удовольствием. По-моему, проще уже некуда. Разве только если какая-нибудь система сборки будет грепать мои исходники и самостоятельно каким-то чудным образом понимать, какие библиотеки надо подключить, всякие там msgfmt'ы и т.п. запустить, и куда чего при установке засунуть…
                0
                >Разве только если какая-нибудь система сборки будет грепать мои исходники и самостоятельно каким-то чудным образом понимать, какие библиотеки надо подключить, всякие там msgfmt'ы и т.п.
                А такого нигде нету? Было бы неплохо вообще не думать о системе сборки и зависимостях (ну или минимум телодвижений).
                  0
                  Ну откуда система сборки знает, что, например, вы используете функции из библиотеки A, а не аналогично обозванные функции из библиотеки Б? Да и даже если во всех библиотеках функции называются по-разному, такой системе пришлось бы сначала просканировать все /usr/include и подобные директории для составления БД функций, соответствующих им заголовочных файлов и соответствующих библиотек. Затем, когда система наткнется на функцию f1, если присутствует #include <h1.h>, подключить библиотеку lib1, если же присутствует #include <h2.h> — библиотеку l2.
                  Далее: как система сборки узнает, что за способ локализации вы используете?
                  А как она узнает, куда вы предполагаете скомпилированные файлы разложить?
                  А что делать с условной сборкой, когда у клиента нет библиотеки l1, но она необязательна — можно собрать без нее (просто приложение станет либо тормознутее, либо лишится дополнительной функциональностью)?
                  В общем, таких вопросов много.
                    +5
                    Я узнаю ТЗ на automake :-)
                      0
                      >Ну откуда система сборки знает, что, например, вы используете функции из библиотеки A,
                      >а не аналогично обозванные функции из библиотеки Б?

                      вы ей это скажете, конечно.
                      человек говорит компьютеру ЧТО делать, это его функция.

                      >Затем, когда система наткнется на функцию f1, если присутствует #include <h1.h>

                      сразу нет.
                      кто из нас делает проект — я или система сборки?
                      значит, я буду решать что, где и как подключать, а не какой-то магический автомат.

                      >А как она узнает, куда вы предполагаете скомпилированные файлы разложить?

                      Fine tuning of the installation directories:
                      --bindir=DIR user executables [EPREFIX/bin]
                      --sbindir=DIR system admin executables [EPREFIX/sbin]
                      --libexecdir=DIR program executables [EPREFIX/libexec]
                      --sysconfdir=DIR read-only single-machine data [PREFIX/etc]
                      --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com]
                      --localstatedir=DIR modifiable single-machine data [PREFIX/var]
                      --libdir=DIR object code libraries [EPREFIX/lib]
                      --includedir=DIR C header files [PREFIX/include]
                      --oldincludedir=DIR C header files for non-gcc [/usr/include]
                      --datarootdir=DIR read-only arch.-independent data root [PREFIX/share]
                      --datadir=DIR read-only architecture-independent data [DATAROOTDIR]
                      --infodir=DIR info documentation [DATAROOTDIR/info]
                      --localedir=DIR locale-dependent data [DATAROOTDIR/locale]
                      --mandir=DIR man documentation [DATAROOTDIR/man]
                      --docdir=DIR documentation root [DATAROOTDIR/doc/]
                      --htmldir=DIR html documentation [DOCDIR]
                      --dvidir=DIR dvi documentation [DOCDIR]
                      --pdfdir=DIR pdf documentation [DOCDIR]
                      --psdir=DIR ps documentation [DOCDIR]

                      вот вам куча всяких ручек, крутите на здоровье.

                      >А что делать с условной сборкой, когда у клиента нет библиотеки l1, но она необязательна

                      проверить наличие, сообщить о присутвии/отсутствии и продолжить сборку.

                      на все эти вопросы уже давно есть ответы, надо просто уметь их искать, а не изобретать свои системы сборки.
                        +2
                        Вот я и говорю, что «магической» системы сборки нет. А более-менее удобная — есть. Cmake.
                          –1
                          Я не считаю удобным использование -D<SOME_OBSCURE_DEFINE> вместо ./configure --with-option=.

                          Есть еще одна претензия — вместо шелла cmake изобретает свой собственный язык, который я вынужден использовать для написания вариации на тему configure.in:

                          почему
                          add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_DEPRECATE)
                          удобнее, чем
                          CFLAGS="$CFLAGS -D_CRT_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_DEPRECATE"
                          ?

                          Я с первого взгляда не скажу что делает add_definition, а в случае с CFLAGS очевидней уже некуда.
                        0
                        Тут зависит от, конечно. В случае с Си — всë очень сложно. А вот в случае с Go, например, они уже написали тулзу для сборки свою, которая понимает зависимости (и умеет их даже с гитхаба/битбакета/гкода качать) и собирать в библиотеки или бинарники всë.
                +10
                «Анархаизм» — интересное слово :) Может, «анахронизм»? Потому как «анархаизмов» никаких не существует, а «архаизмом» называют вышедшее из употребления слово :) Хотя, возможно, вы имели в виду, что make — это «анархизм»? :)
                  +2
                  Это был «неологизм» :)
                    0
                    Воинствующий анархизм!
                      0
                      Судя по всему, анархаизм — унаследованные хаки. Из серии, нет ничего постояннее временного.
                        +3
                        У меня есть железная отмазка — я писал это 6:33 утра и мой мозг после ночи бодрствования не смог оформить правильно поток моих мыслей. Я хотел сказать, что make существует уже очень давно и технологии постоянно развивается, а make как был так и остается на своем очень низком уровне. Но уровень технологий уже сильно вырос с его времен и соответственно требует новых подходов и решений к сборке и управлению ей.
                          0
                          анахренизм :)
                        +11
                        Позанудствую. Статья оформлена не по правилам. Для переводов есть специальный тип топика — «перевод».
                          0
                          А как правильно их делать?
                            +1
                            image
                              0
                              сменить тип уже опубликованного топика нельзя, только убирать текущий и делать новый.
                                +3
                                достаточно вверху статьи написать
                                Перевод статьи ...
                                  +3
                                  С точки зрения правил ресурса — недостаточно.
                                    +3
                                    зато с точки зрения пользователя вполне хватит, т.к. я буди читать уже готовым к тому что автору я ответить не смогу…
                                      0
                                      Автор в итоге не сделал вообще ничего. Показательное презрение к сообществу и администрации.
                                        0
                                        Нет, изменено форматирование и оформление. Удалять статью и выкладывать заново как перевод считаю ошибкой, так как она уже у 100+ людей в «избранном» — они её потеряют.
                                          0
                                          Не сделано даже упоминание о то, что это перевод (не сделано сверху, чтобы пользователь сразу понимал это, начав читать и не готовил по ходу чтения список вопросов и возражений «автору»). Это хотя бы минимум уважения к читающим.
                                          Кроме того, на момент, когда я сделал замечание, в избранное добавило всего пять человек и мой коммент был третьим по счету, была возможность спокойно сделать все по правилам. Ты же просто забил.
                                            0
                                            Список вопросов и возражений — это именно то, чего я добивался выкладывая этот перевод.

                                            Утверждение об уважении к читателям оставлю на вашей совести.
                                              0
                                              Как раз таки уважение к читателям осталось на твоей совести )
                                              Если не видишь проблемы — ок, дело твое.
                                                0
                                                А надуманная фраза — на вашей.
                                                  0
                                                  А надуманность надуманности — на твоей.
                                                  Я выиграл!
                                                    0
                                                    Ок, пусть будет так.
                            +6
                            Зато перевод на редкость хорош. Я уже ближе к середине текста всерьез заметил признаки перевода, хотя на Хабре очень часто даже оригинальные тексты читать невозможно.
                            +3
                            здесь должна быть шутка про обречённость любого языка, содержащего colon-equals assignment :)
                              +1
                              Colon-equals assignment приводит к появлению ошибок при опечатках в операторах «==», «!=», «+=», «-=», «*=», «/=», «.=», «%=», «&=», «|=», «^=», «=>», «>=», «<=», и тем экономит нервы программиста, не принуждая его прибегать к долгой, мучительной, нравственно тягостной отладке.

                              Поэтому обречённость этого оператора свидетельствует о том, что мы обречены существовать в мире техносадомазохистов, действующих по правилу «я мучительно потрахался — теперь и ты, и ты поди мучительно потрахайся».
                                +1
                                о каких конкретнее опечатках вы говорите?
                                  +1
                                  int a = 3;
                                  int b = 15;

                                  if (a = b)
                                  puts(«внезапно»);
                                    +1
                                    имхо, конечно, но ошибка с пропущенным равно в '==' довольно натянута. после 1-2 таких опечаток, они обычно начинают сами бросаться в глаза. а если такая даже случайно допущена, то в большинстве случаев мысль проверить сравнение приходит в голову одной из первых.

                                    если цепляться к опечаткам, то к примеру написать x := y вместо x >=y имеет такую же вероятность.
                                      +1
                                      Нормальный компилятор не пропустит такую опечатку.
                                      Поэтому приходится, если нужно в if для сокращения написать что-то вроде if(a=b), писать if((a=b)).
                                        0
                                        Это и спасает.
                                        А вообще пора бросать экономить строчки.
                                  0
                                  Эти ошибки возникают только в условиях. Поэтому проблема не в том, что присваивание записывается как "=", без двоеточия, а в том, что результат присваивания может неявно приводиться к типу boolean. К примеру, в Java и C# этого нет, и никто не ошибается в написании условий.
                                    +1
                                    Говоря еще точнее, эта ошибка прявляется там, где присваивание есть выражение (и возвращает значение). То, что оно кастуется к булеану — это даже хорошо :)
                                  0
                                  := в мейке значит — вычислить то, что присваивается переменной, прямо сейчас. Есть и просто '=' — тогда присвоенное (например, функция) будет вычисляться позже.
                                  +4
                                  Неожиданный вопрос: автор слышал про autoconf/automake?
                                  Писать руками Makefiles — это Свежая и Оригинальная Идея (tm).
                                    +12
                                    Я, возможно, слишком радикальных взглядов придерживаюсь, но autoconf/automake — это такой большой набор хрупких костылей, после которого make кажется образцом простоты и удобства. Спасибо, задействовать их, чтобы makefile'ы генерировать — это добавить к проекту ещё одну проблему.
                                      +1
                                      я думаю вы просто не разобрались с автотулзами, да это костыли, но вопервых вполне рабочие что подтверждается практикой многих открытых продуктов, во-вторых создание рукотворных мейкфайлов как правило означает, что на платформе отличной от той под которую писал автор, будут будут проблемы со сборой. Неоднократно убеждался в этом эмпирическом правиле…
                                      И да имхо порог вхождения в autotools + make достаточно высок.
                                        +2
                                        Рабочие они только там, где их тестируют. А вот стоит скомпилировать gdb на mingw64 и оказывается, что какая-то из автотулзов считает, что для компиляции маленькой вспомогательной программы можно использовать просто gcc, ведь он же есть на любой платформе. А то, что надо использовать x86_64-w64-mingw32-gcc, как указано в configure верхнего уровня, авторов волнует мало. В результате из-за какой-то ненужной документации к bfd не собирается gdb.
                                          0
                                          а если бы мэйкфайл gcc написали бы руками то конечно был бы рай, да?
                                            0
                                            Нет, просто автотулзы тоже не помогают в самом интересном для меня случае — cygwin/mingw.
                                              0
                                              я бы сказал что «автотулзы тоже не всегда помогают»…
                                          0
                                          Вот из последнего, к примеру — wiki.buici.com/xwiki/bin/view/Programing+C+and+C%2B%2B/Autoconf+and+RPL_MALLOC
                                          Берём проект, который использует autotools (который как бы декларирует, что будет компилироваться для любых платформ), компилируем для ARM'а — получаем ошибки. Особенно доставляет её описание.
                                          +8
                                          Ну да, а C — это такой хрупкий костыль вокруг ассемблера, который вообще похож на вывод из /dev/random (кстати, хорошая тема для статьи).
                                            +2
                                            Всё в этом мире — костыль!
                                              0
                                              А у костылей бывают костыли?
                                              0
                                              C — это такой хрупкий костыль вокруг ассемблера, который вообще похож на вывод из /dev/random
                                              — спасибо, супер!
                                                +2
                                                Вы ещё конфиг sendmail'a на m4 вспомните, который костыль для компиляции оригинального конфига.
                                              0
                                              autoconf/automake — ужасная штука. Ей только по историческим причинам и ради переносимости приходится пользоваться.

                                              Каждый раз, когда я какого-нибудь апача собираю, и эта простыня ./configure тянется по полчаса, и для каждой из библиотек Апача (apr, apr-utils) перезапускается заново, и если ошибся в настройках компилятора, и make упал, то надо ее снова запускать и ждать полчаса — полдня на это может уйти.
                                                0
                                                К счастью, постепенно народ с глючных тормозных autotools переходит на cmake.
                                                  +1
                                                  попробуйте кеширование в configure использовать, не будет так долго второй раз проверять библиотеки.
                                                    +1
                                                    он использует кеширование, но как-то безсистемно (часть проверок кешируется, но большАя часть — нет), и даже с кешированием долго работает…
                                                      0
                                                      … бОльшая…
                                                        0
                                                        «Бессистемно»…
                                                        0
                                                        да я тоже сижу бывает с лицом страдальца когда что то бисекчу… пересобирать приходится помногу…
                                                    +16
                                                    You have a problem and decide to use make.
                                                    Now you know how to make problems.

                                                    You have a problem and decide to use autotools.
                                                    Now you know how to make problems automatically.
                                                    +11
                                                    Статья очень спорная на самом деле.
                                                    Начиная с фактических ошибок (mkdir -p <existing-dir-name> завершается успешно).
                                                    Заканчивая полным непониманием «миссии» make.

                                                    Make решает одну единственную задачу — по спецификации цели/зависимости/правила генерации найти отсутствующие или устаревшие цели и собрать их. Просто и примитивно, воплощенный UNIX way.

                                                    Это не проблема make, что для сборки среднего проекта, его возможностей недостаточно.

                                                    Зато Makefile — замечательный промежуточный формат для генераторов вроде autotools/cmake. И все довольны — билд-инжинер, потому что огромную кучу вспомогательного кода в мэйкфайле за него написал генератор; автор генератора — потому что сканирование зависимостей, сортировку задач и распараллеливание он переложил на make.

                                                    Кстати возможно решение с таймстемпами на файловой системе и не лишено недостатков, но по-сути — оно единственное. Тот же git например при поиске измененных файлов сначала смотрит на таймстемпы, и если таймстеп не изменился, то внутрь файла он даже не заглядывает.
                                                      +8
                                                      Каждый, кто хоть раз писал makefile, скорее всего уже натолкнулся на “особенность” его синтаксиса: в нём используются табуляции. Каждая строка, описывающая запуск команды, должна начинаться с символа табуляции. Пробелы не подходят — только табуляция. К сожалению, это только один из странных аспектов языка make’а.


                                                      В книге Masterminds of programming кто-то (по-моему, Керниган) рассказывал историю: когда make только-только написали, он стал как-то использоваться и понемногу разошёлся по организации (Bell Labs).
                                                      И кто-то из «новых» пользователей обратил внимание на эту странную штуку с табами — why tabs? На что Feldman, автор этой новой утилиты, ответил, что пользователь конечно прав, и что с табами он плохо подумал, но менять это не будет, потому что это поломает обратную совместимость, а это делать нельзя, ведь у make уже 12 пользователей.

                                                      такой подход к обратной совместимости многое объясняет. фактически, он отвечает сразу по всем пунктам этой статьи.
                                                        –1
                                                        facepalm.tar.gz

                                                        Целых 12… Порой программисты слишком заботливы в отношении пользователей: то добавляют ненужные фичи, то (как здесь) не добавляют нужные. Всё больше убеждаюсь в том, что в каждом программисте должна быть частичка менеджера. Но только частичка, а не ядро U-239.
                                                          0
                                                          Интересно, спросил он у этих 12-ти пользователей их мнения? А то ведь могло быть и так, что им табы тоже не нужны.
                                                            0
                                                            Пробелы бы вроде не поломали бы совместимость, альтернатива табам как а питоне
                                                            • UFO just landed and posted this here
                                                              0
                                                              Пользуюсь make, ругаюсь, но пользуюсь. Удивился, когда узнал что он не умеет делать математические операции (напр., если версия меньше чем что-то, то сделать что-то особое).

                                                              Очень надеялся, что наберет популярность такая система сборки как ninja (с ее помощью хромиум собирается). martine.github.com/ninja/ Замысел хороший, нацелена на простоту и скорость, легкий синтаксис, минимум возможностей. Т.е. ninja только собирает, а всей конфигурацией должна заниматься отдельный генератор типа configure.

                                                              Жаль, кажется не взлетела.
                                                                0
                                                                С ниндзей лень возиться просто. Сделал бы там чувак хотя бы базовые фичи какие-то (ну там хотя бы паттерны для имëн переменных, подобные штуки) — тогда да, а так — оно имеет смысл только для больших проектов, когда мейк тормозит.
                                                                0
                                                                Кстати, по поводу странностей ситнтаксиса, парсинга и интерпретации мейкфайлов — все то же самое справедливо и для Bash. Недавно статью прочитал — долго удивлялся: www.aosabook.org/en/bash.html
                                                                  –1
                                                                  Кому не нравится, всегда может использовать аналоги, а также питон и прочие яп
                                                                    +2
                                                                    Последний пункт не совсем верен, тк в make есть order-only prerequisites, которые не будут вызывать перекомпиляцию
                                                                    debug/%.o: %.c | debug
                                                                    $(CC) -c $< -o $@
                                                                    debug:
                                                                    mkdir $@
                                                                      0
                                                                      Спасибо, интересно.
                                                                        0
                                                                        Хотел сказать то же самое. Странно что автор не знал этого
                                                                        +1
                                                                        Я в своём Leaflet сейчас использую jake — аналог rake под node.js. Очень просто, удобно, и к тому же приятно описывать билд-скрипты и зависимости для JS-проекта на JS же, без лишних сторонних языков.
                                                                          0
                                                                          Уже давно для своих проектов использую rake — весьма неплохая альтернатива make, лишенная перечисленных выше недостатков. К тому же, если возможностей Rake DSL не хватает, всегда можно локально перейти на обычный ruby — и все это бесшовно. Жаль только, что за пределами мира RoR rake малоизвестен. Фактически rake — это такой "'make' done right". Не больше, но и не меньше.

                                                                          Only users with full accounts can post comments. Log in, please.