Каков ваш опыт работы с нерекурсивными программами?
Несколько лет назад я прочитал статью " Рекурсивное вредное воздействие" и реализовал эту идею в своем собственном процессе сборки. Недавно я прочитал другую статью с идеями о том, как реализовать нерекурсивныйmake
, Итак, у меня есть несколько точек данных, которые не рекурсивные make
работает как минимум на несколько проектов.
Но мне интересно узнать об опыте других. Вы пробовали не рекурсивный make
? Это сделало вещи лучше или хуже? Это стоило времени?
8 ответов
Мы используем нерекурсивную систему GNU Make в компании, в которой я работаю. Он основан на статье Миллера и особенно на ссылке "Реализация нерекурсивного создания", которую вы дали. Нам удалось уточнить код Бергена в систему, в которой в подкаталоге make-файлов вообще нет котельной. По большому счету, он работает нормально и намного лучше, чем наша предыдущая система (рекурсивная работа с GNU Automake).
Мы поддерживаем все "основные" операционные системы (коммерчески): AIX, HP-UX, Linux, OS X, Solaris, Windows, даже мэйнфрейм AS/400. Мы компилируем один и тот же код для всех этих систем, а зависимые от платформы части выделяются в библиотеки.
В нашем дереве более двух миллионов строк кода на C в около 2000 подкаталогах и 20000 файлов. Мы серьезно подумали об использовании SCons, но просто не смогли заставить его работать достаточно быстро. В более медленных системах Python будет использовать пару десятков секунд, просто разбирая файлы SCons, где GNU Make делает то же самое примерно за одну секунду. Это было около трех лет назад, поэтому с тех пор все могло измениться. Обратите внимание, что мы обычно храним исходный код в общей папке NFS/CIFS и строим один и тот же код на нескольких платформах. Это означает, что инструмент сборки еще медленнее сканирует исходное дерево на наличие изменений.
Наша нерекурсивная система GNU Make не без проблем. Вот некоторые из самых больших препятствий, с которыми вы можете столкнуться:
- Сделать его переносимым, особенно для Windows, - большая работа.
- Хотя GNU Make является практически используемым функциональным языком программирования, он не подходит для программирования в целом. В частности, нет пространств имен, модулей или чего-либо подобного, чтобы помочь вам изолировать части друг от друга. Это может вызвать проблемы, хотя и не так много, как вы думаете.
Основные победы над нашей старой рекурсивной системой makefile:
- Это быстро Требуется около двух секунд, чтобы проверить все дерево (2k каталогов, 20k файлов) и либо решить, что оно обновлено, либо начать компиляцию. Старая рекурсивная вещь заняла бы больше минуты, чтобы ничего не делать.
- Правильно обрабатывает зависимости. Наша старая система основывалась на том, что подкаталоги были построены и т. Д. Как и следовало ожидать от чтения статьи Миллера, рассмотрение всего дерева как единой сущности действительно является правильным способом решения этой проблемы.
- Он переносим на все наши поддерживаемые системы, после всей тяжелой работы, которую мы вложили в него. Это довольно круто.
- Система абстракции позволяет нам писать очень сжатые make-файлы. Типичный подкаталог, который определяет только библиотеку, состоит из двух строк. В одной строке указано имя библиотеки, а в другой - библиотеки, от которых зависит эта библиотека.
Относительно последнего пункта в приведенном выше списке. В итоге мы внедрили своего рода средство расширения макросов в системе сборки. Makefiles подкаталога перечисляют программы, подкаталоги, библиотеки и другие общие вещи в переменных, таких как PROGRAMS, SUBDIRS, LIBS. Затем каждый из них превращается в "настоящие" правила GNU Make. Это позволяет нам избежать многих проблем с пространством имен. Например, в нашей системе нормально иметь несколько исходных файлов с одинаковыми именами, никаких проблем там нет.
В любом случае, это оказалось много работы. Если вы можете заставить SCons или подобное работать на ваш код, я бы посоветовал вам сначала взглянуть на это.
Прочитав статью RMCH, я решил написать правильный нерекурсивный Makefile для небольшого проекта, над которым я работал в то время. После того, как я закончил, я понял, что должна быть возможность создать универсальный "каркас" Makefile, который может быть использован для очень простого и краткого определения того, какие конечные цели вы хотите построить, какие это цели (например, библиотеки или исполняемые файлы).) и какие исходные файлы должны быть скомпилированы для их создания.
После нескольких итераций я в конечном итоге создал именно это: один шаблонный Makefile из примерно 150 строк синтаксиса GNU Make, который никогда не нуждается в каких-либо изменениях - он просто работает для любого проекта, на котором я хочу его использовать, и достаточно гибок для сборки несколько целей разных типов с достаточной степенью детализации, чтобы указать точные флаги компиляции для каждого исходного файла (если я хочу) и точные флаги компоновщика для каждого исполняемого файла. Для каждого проекта все, что мне нужно сделать, - это снабдить его небольшими отдельными Make-файлами, которые содержат биты, подобные этой:
TARGET := foo
TGT_LDLIBS := -lbar
SOURCES := foo.c baz.cpp
SRC_CFLAGS := -std=c99
SRC_CXXFLAGS := -fstrict-aliasing
SRC_INCDIRS := inc /usr/local/include/bar
Makefile проекта, такой как приведенный выше, будет делать именно то, что вы ожидаете: создать исполняемый файл с именем "foo", скомпилировать foo.c (с CFLAGS=-std=c99) и baz.cpp (с CXXFLAGS=-fstrict-aliasing) и добавив "./inc" и "/usr/local/include/bar" к #include
путь поиска, с окончательной ссылкой, включая библиотеку "libbar". Было бы также заметить, что существует исходный файл C++ и известно, что использовать компоновщик C++ вместо компоновщика C. Фреймворк позволяет мне указать гораздо больше, чем показано здесь в этом простом примере.
Шаблонный Makefile выполняет все построение правил и автоматическую генерацию зависимостей, необходимых для построения указанных целей. Все сгенерированные сборкой файлы помещаются в отдельную иерархию выходных каталогов, поэтому они не смешиваются с исходными файлами (и это делается без использования VPATH, поэтому нет проблем с наличием нескольких исходных файлов с одинаковым именем).
Я сейчас (повторно) использовал этот же Makefile как минимум для двух десятков различных проектов, над которыми я работал. Вот некоторые вещи, которые мне больше всего нравятся в этой системе (кроме того, насколько легко создать подходящий Makefile для любого нового проекта):
- Это быстро Это может фактически мгновенно сказать, устарело ли что-нибудь.
- 100% надежные зависимости. Вероятность того, что параллельные сборки будут таинственным образом разрушены, практически отсутствует, и всегда создается именно тот минимум, который необходим для того, чтобы все обновилось.
- Мне никогда не нужно будет переписывать полный make-файл снова:D
Наконец, я бы только упомянул, что с проблемами, присущими рекурсивному созданию, я не думаю, что я смог бы справиться с этим. Я, вероятно, был бы обречен переписывать ошибочные make-файлы снова и снова, тщетно пытаясь создать тот, который действительно работал бы должным образом.
Позвольте мне подчеркнуть один аргумент в статье Миллера: когда вы начинаете вручную разрешать отношения зависимости между различными модулями и испытываете трудности с обеспечением порядка сборки, вы эффективно реализуете логику, которую система сборки была создана для решения в первую очередь. Создание надежных рекурсивных сборочных систем очень сложно. Реальные проекты имеют много взаимозависимых частей, порядок сборки которых нетривиален, и поэтому эту задачу следует оставить за системой сборки. Тем не менее, он может решить эту проблему, только если он имеет глобальные знания о системе.
Кроме того, рекурсивные системы сборки make склонны разваливаться при сборке одновременно на нескольких процессорах / ядрах. Хотя кажется, что эти системы сборки работают надежно на одном процессоре, многие отсутствующие зависимости остаются незамеченными, пока вы не начнете строить свой проект параллельно. Я работал с рекурсивной системой сборки сборки, которая работала на четырех процессорах, но внезапно зависала на машине с двумя четырехъядерными процессорами. Затем я столкнулся с другой проблемой: эти проблемы параллелизма практически невозможно отладить, и я закончил тем, что нарисовал блок-схему всей системы, чтобы выяснить, что пошло не так.
Возвращаясь к вашему вопросу, мне трудно придумать веские причины, по которым кто-то хочет использовать рекурсивный make. Производительность нерекурсивных систем сборки GNU Make во время выполнения трудно превзойти, и, напротив, многие рекурсивные системы сборки имеют серьезные проблемы с производительностью (слабая поддержка параллельной сборки снова является частью проблемы). Есть статья, в которой я оценил конкретную (рекурсивную) систему сборки Make и сравнил ее с портом SCons. Результаты производительности не являются репрезентативными, потому что система сборки была очень нестандартной, но в данном конкретном случае порт SCons был на самом деле быстрее.
Итог: если вы действительно хотите использовать Make для управления вашими сборками программного обеспечения, выберите нерекурсивный Make, потому что в долгосрочной перспективе это сделает вашу жизнь намного проще. Лично я бы предпочел использовать SCons из соображений удобства использования (или Rake - в основном любую систему сборки, использующую современный язык сценариев и имеющую неявную поддержку зависимостей).
Я сделал нерешительную попытку на своей предыдущей работе сделать систему сборки (основанную на GNU make) полностью нерекурсивной, но столкнулся с рядом проблем:
- Источники артефактов (то есть созданные библиотеки и исполняемые файлы) были распределены по нескольким каталогам, полагаясь на vpath, чтобы найти их
- Несколько исходных файлов с одинаковым именем существовали в разных каталогах
- Несколько исходных файлов были разделены между артефактами, часто скомпилированы с разными флагами компилятора
- Разные артефакты часто имели разные флаги компилятора, настройки оптимизации и т. Д.
Одной из особенностей GNU make, которая упрощает нерекурсивное использование, являются значения переменных, специфичные для цели:
foo: FOO=banana
bar: FOO=orange
Это означает, что при построении целевого "foo", $(FOO) расширится до "банана", но при построении целевого "бара", $ (FOO) расширится до "оранжевого".
Одним из ограничений этого является то, что невозможно иметь специфичные для цели определения VPATH, то есть нет способа однозначно определить VPATH для каждой цели. Это было необходимо в нашем случае, чтобы найти правильные исходные файлы.
Основная недостающая особенность GNU make, необходимая для поддержки нерекурсивности, заключается в том, что в ней отсутствуют пространства имен. Целевые переменные могут ограниченным образом использоваться для "имитации" пространств имен, но на самом деле вам нужно иметь возможность включать Make-файл в подкаталог с использованием локальной области видимости.
РЕДАКТИРОВАТЬ: Другая очень полезная (и часто недостаточно используемая) функция GNU make в этом контексте - это средства макроразложения (см., Например, функцию eval). Это очень полезно, когда у вас есть несколько целей, которые имеют похожие правила / цели, но отличаются способами, которые нельзя выразить с помощью обычного синтаксиса GNU make.
Я согласен с утверждениями в упомянутой статье, но мне потребовалось много времени, чтобы найти хороший шаблон, который делает все это и все еще прост в использовании.
В настоящее время я работаю над небольшим исследовательским проектом, в котором я экспериментирую с непрерывной интеграцией; автоматически выполнить юнит-тест на ПК, а затем запустить системный тест для (встроенной) цели. Это не тривиально в make, и я искал хорошее решение. Обнаружение этого make все еще является хорошим выбором для портативных многоплатформенных сборок. Я наконец нашел хорошую отправную точку в http://code.google.com/p/nonrec-make
Это было настоящее облегчение. Теперь мои make-файлы
- очень просто изменить (даже с ограниченными знаниями)
- быстро скомпилировать
- полная проверка (.h) зависимостей без усилий
Я, конечно, также буду использовать его для следующего (большого) проекта (при условии, что C/C++)
Я разработал нерекурсивную систему make для одного C++ проекта среднего размера, который предназначен для использования в unix-подобных системах (включая macs). Код в этом проекте весь в дереве каталогов с корнем в каталоге src/. Я хотел написать нерекурсивную систему, в которой можно набрать "make all" из любого подкаталога каталога src/ верхнего уровня, чтобы скомпилировать все исходные файлы в дереве каталогов с корнем в рабочем каталоге, как в рекурсивной системе make. Поскольку мое решение, кажется, немного отличается от других, которые я видел, я хотел бы описать его здесь и посмотреть, получу ли я какие-либо реакции.
Основными элементами моего решения были следующие:
1) Каждый каталог в дереве src/ имеет файл с именем sources.mk. Каждый такой файл определяет переменную makefile, которая перечисляет все исходные файлы в дереве с корнем в каталоге. Имя этой переменной имеет вид [каталог]_SRCS, в котором [каталог] представляет канонизированную форму пути от каталога верхнего уровня src/ к этому каталогу с обратными слешами, замененными подчеркиванием. Например, файл src/util/param/sources.mk определяет переменную с именем util_param_SRCS, которая содержит список всех исходных файлов в src/ util / param и его подкаталогах, если таковые имеются. Каждый файл sources.mk также определяет переменную с именем [directory]_OBJS, которая содержит список соответствующих объектных файлов *.o. В каждом каталоге, который содержит подкаталоги, sources.mk включает файл sources.mk из каждого подкаталога и объединяет переменные [subdirectory]_SRCS для создания собственной переменной [directory]_SRCS.
2) Все пути выражаются в файлах sources.mk как абсолютные пути, в которых каталог src/ представлен переменной $(SRC_DIR). Например, в файле src/util/param/sources.mk файл src/util/param/Componenent.cpp будет указан как $(SRC_DIR)/util/param/Component.cpp. Значение $ (SRC_DIR) не задано ни в одном файле sources.mk.
3) Каждый каталог также содержит Makefile. Каждый Makefile включает в себя файл глобальной конфигурации, который устанавливает значение переменной $ (SRC_DIR) в абсолютный путь к корневому каталогу src/. Я решил использовать символическую форму абсолютных путей, потому что это оказалось самым простым способом создания нескольких make-файлов в нескольких каталогах, которые интерпретировали бы пути для зависимостей и целей одним и тем же способом, в то же время позволяя перемещать все дерево исходных текстов при желании., изменяя значение $ (SRC_DIR) в одном файле. Это значение устанавливается автоматически с помощью простого скрипта, который пользователь должен запустить при загрузке пакета или клонировании из репозитория git или при перемещении всего исходного дерева.
4) Makefile в каждом каталоге включает файл sources.mk для этого каталога. Цель "all" для каждого такого Makefile перечисляет файл [directory]_OBJS для этого каталога в качестве зависимости, поэтому требуется компиляция всех исходных файлов в этом каталоге и его подкаталогах.
5) Правило для компиляции файлов *.cpp создает файл зависимости для каждого исходного файла с суффиксом *.d, как побочный эффект компиляции, как описано здесь: http://mad-scientist.net/make/autodep.html. Я решил использовать компилятор gcc для генерации зависимостей, используя опцию -M. Я использую gcc для генерации зависимостей даже при использовании другого компилятора для компиляции исходных файлов, потому что gcc почти всегда доступен в unix-подобных системах, и потому что это помогает стандартизировать эту часть системы сборки. Другой компилятор может использоваться для фактической компиляции исходных файлов.
6) Использование абсолютных путей для всех файлов в переменных _OBJS и _SRCS потребовало, чтобы я написал скрипт для редактирования файлов зависимостей, сгенерированных gcc, который создает файлы с относительными путями. Для этого я написал скрипт на python, но другой человек мог использовать sed. Пути для зависимостей в результирующих файлах зависимостей являются буквальными абсолютными путями. Это нормально в этом контексте, потому что файлы зависимостей (в отличие от файлов sources.mk) создаются локально, а не распространяются как часть пакета.
7) Makefile в каждом директории включает файл sources.mk из того же каталога и содержит строку "-include $([directory]_OBJS:.o=.d)", которая пытается включить файлы зависимостей для каждого исходного файла. в каталоге и его подкаталогах, как описано в приведенном выше URL.
Основное различие между этой и другими схемами, которые я видел, которые позволяют вызывать "все" из любого каталога, заключается в использовании абсолютных путей, позволяющих последовательно интерпретировать одни и те же пути, когда Make вызывается из разных каталогов. Пока эти пути выражаются с использованием переменной для представления исходного каталога верхнего уровня, это не мешает перемещению исходного дерева и является более простым, чем некоторые альтернативные способы достижения той же цели.
В настоящее время моя система для этого проекта всегда выполняет сборку на месте: объектный файл, создаваемый путем компиляции каждого исходного файла, помещается в тот же каталог, что и исходный файл. Было бы просто включить сборку вне места, изменив скрипт, который редактирует файлы зависимостей gcc, чтобы заменить абсолютный путь к каталогу src/ dirct на переменную $(BUILD_DIR), которая представляет каталог сборки в выражении для цель объектного файла в правиле для каждого объектного файла.
До сих пор я нашел эту систему простой в использовании и обслуживании. Требуемые фрагменты make-файла короткие и сравнительно простые для понимания соавторами.
Проект, для которого я разработал эту систему, написан на полностью автономном ANSI C++ без внешних зависимостей. Я думаю, что такого рода самодельная нерекурсивная система make-файлов - разумный вариант для автономного, очень переносимого кода. Однако я хотел бы рассмотреть более мощную систему сборки, такую как CMake или gnu autotools, для любого проекта, который имеет нетривиальные зависимости от внешних программ или библиотек или от нестандартных функций операционной системы.
Мне известен по крайней мере один крупномасштабный проект ( ROOT), который рекламирует с использованием [powerpoint link] механизм, описанный в разделе Рекурсивное создание, считающееся вредным. Фреймворк превышает миллион строк кода и довольно умно компилируется.
И, конечно же, все большие проекты, с которыми я работаю, которые используют рекурсивный make, мучительно медленно компилируются.::вздох::
Я написал не очень хорошую нерекурсивную систему сборки make и с тех пор очень чистую модульную систему рекурсивной сборки сборки для проекта под названием Pd-extended. Это в основном своего рода язык сценариев с кучей библиотек. Сейчас я также работаю над нерекурсивной системой Android, так что это контекст моих мыслей по этой теме.
Я не могу особо сказать о различиях в производительности между ними, на самом деле я не обратил внимания, так как полные сборки действительно когда-либо делались только на сервере сборки. Я обычно работаю либо над базовым языком, либо над конкретной библиотекой, поэтому меня интересует только создание этого подмножества всего пакета. Техника рекурсивного создания имеет огромное преимущество, заключающееся в том, что система сборки будет как автономной, так и интегрированной в большее целое. Это важно для нас, поскольку мы хотим использовать одну систему сборки для всех библиотек, независимо от того, интегрированы они или написаны внешним автором.
Сейчас я работаю над созданием пользовательской версии внутренних компонентов Android, например, версии классов SQLite для Android, основанных на зашифрованном sqlite SQLCipher. Поэтому я должен написать нерекурсивные файлы Android.mk, которые обертывают все виды странных систем сборки, таких как sqlite. Я не могу понять, как заставить Android.mk выполнять произвольный скрипт, хотя в моем случае это было бы легко в традиционной рекурсивной системе make.