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'а необходимо всем программистам, которым приходится использовать его в своей работе.
Автор: qehgt