Система сборки для больших модульных проектов

от автора

На страницах нашего блога мы уже писали о преимуществах организации репозитория крупного проекта способом, предполагающим возможность извлечения исходников в изменяемую структуру рабочей копии. Использование такого подхода вкупе с потребностями простого конфигурирования, фрагментарной сборки, поддержки несколько десятков ОС под широкий спектр аппаратных платформ стали причиной разработки нами собственной системы сборки. Эта статья рассказывает о найденных нами решениях, которые могут быть интересны разработчикам, сталкивающимися с трудностями поддержки инфраструктуры больших проектов.



Прежде чем перейти непосредственно к техническим деталям следует отметить два важных момента. Во-первых, система работает поверх разработанной нами make-утилиты linmake, об особенностях которой будет рассказано отдельно. И, во-вторых, разработка велась для решения задач производства СУБД ЛИНТЕР (www.linter.ru), что привнесло определенную специфику, но не настолько существенную, чтобы решение не могло быть адаптировано к любому проекту.

Зачем нужно было создавать новую систему сборки?

Как это часто бывает, развитие и усложнение проекта в какой-то момент привело к тому, что поддержка инфраструктуры сборки стала слишком накладной и этому поспособствовало несколько причин, полное перечисление которых заняло бы неприлично много места, поэтому позволим себе выделить только те, которые вызывали большее число нареканий от участников проекта:

  • из-за того, что в далеком 1999 году не было приемлемого кроссплатформенного инструмента мы были вынуждены долгое время поддерживать две параллельные системы сборки: на основе wmake для windows и make для *nix;
  • разнообразие поддерживаемых UNIX-like платформ привело к увеличению (а значит и усложнению) вариантов компиляции и компоновки в модулях проекта;
  • в свою очередь, сборка windows версии усложнялась необходимостью поддержки большого количества компиляторов;
  • не существовало простого механизма описания и разрешения как внешних и внутренних зависимостей проекта.

Конечно, помимо проблем были и пожелания по реализации новых «фич», поэтому, когда было принято решение о разработке новой унифицированной системы сборки, которую назвали unimake, мы вполне определенно представляли каких целей хотим достичь:

  • система должна однообразно работать на всех поддерживаемых платформах;
  • изменение положения модуля (здесь и далее под модулем мы будем понимать функционально самодостаточную часть проекта) в рабочем дереве не должно влиять на работоспособность;
  • необходим простой механизм по добавлению новых целевых платформ, архитектур, компиляторов и их версий;
  • следует хранить как типовые конфигурации для версий и редакций продукта, так и предоставлять возможность их настройки при необходимости;
  • нужен простой способ автоматического учета внешних и внутренних зависимостей в проекте, который бы автоматически определял порядок операций;
  • система должна предоставлять возможность простой сборки части проекта со всеми ее зависимостями.

Модель сборки, общие положения

Сборка производится в отличной от исходников (srcroot) директории — директории сборки (bldroot). Каждая сборка проекта целиком определяется набором множеств:

  • конфигураций/версий продуктов (CONFIGS);
  • целевых платформ (PLATFORMS);
  • целевых архитектур (ARCHS);
  • компиляторов (COMPILERS);
  • версиями компиляторов ($(CMPL)_VERS);
  • платформой сборки (HOST.PLT);
  • архитектурой платформы сборки $(HOST.ARCH).

Вариант конфигурации проекта

... CONFIGS = base60 full60  PLATFORMS = LINUX  ARCHS = AMD64 JAVA .NET  COMPILERS = GCC JAVAC MONO   JAVAC_VERS = 1.4 1.5 1.6  GCC_VERS = 4  MONO_VERS = 3 …  HOST.PLT = LINUX HOST.ARCH  = AMD64  DEBUG = RELEASE

Комбинация перечисленных параметров определяет все возможные варианты, которые предварительно фильтруется системой сборки с целью отсеять ненужные и не имеющие смысла комбинации.
В свою очередь, каждый модуль расширяет параметры «для себя» с помощью двух файлов-описателей: для модуля и для процесса сборки, которые написаны в декларативном стиле и не содержат правил (за редким исключением). Описатель модуля содержит общую информацию о модуле: наименование и версии, поддерживаемые платформы, компиляторы и архитектуры, модели потоков, цели. Все объявления (кроме имени) не являются обязательными и в случае их отсутствия используются значения по умолчанию.

Вариант описателя модуля

MODULE = example #наименование библиотеки  VERSIONS = #необходимы отдельные версии библиотеки для каждой версии проекта VERSIONS_REQ:= $(CFG.VER) #версия библиотеки совпадает с версией проекта  LINK_TYPES = static dynamic #будут созданы статическая и разделяемая/динамическая библиотеки THREAD_TYPES = mt #только многопоточная версия  DST_SRC = example.h #в целевую директорию помимо целей попадет и заголовочный файл   DONT_BUILD_WATCOM = # не выполнять сборку, если компилятор — watcom (любой версии) DONT_BUILD_WINCE = # не выполнять сборку если целевая платформа — WinCE

Описатель сборки объявляет цели, их состав, директивы, директории поиска, внешние и внутренние зависимости модуля.

Вариант описателя сборки

... TARGET = $(MODULE) #целевой файл библиотеки будет иметь имя, совпадающее с названием модуля + расширение, определяемое типом цели и платформой (.so, .a, .dll и т.д.)  DEFINES = _VER=$(CFG_VER) SOME_DEFINES #дефайны общие для всех платформ DEFINES_WINNT = EXAMPLE_WIN #директива только для Windows  DEFINES_UNIX = EXAMPLE_POSIX #директива для всех *nix  CDIR    = $(MODROOT);$(MODROOT)/utils; #директории с исходниками INCLDIR = $(MODROOT);$(ANOTHER_MOD); #директории поиска  OBJS = & 	example.obj # объектные файлы для всех платформ  OBJS_UNIX = &  	charset.obj # дополнительные объектные файлы для *nix платформ   SLIBS_WINNT = $(ANOTHER_LIB) oldnames #статические библиотеки для windows платформы... SLIBS_UNIX  = $(ANOTHER_LIB) #статическая библиотека для *nix  ...

В bldroot структура директорий повторяет srcroot до уровня корней каждого модуля (modsrc), но уже в них, содержатся все фактические варианты, задаваемые допустимыми комбинациями общепроектных и модульных конфигураций. Под каждый из таких вариантов создается директория вида $(MODULE)/$(PLT)_$(ARCH)_$(CMPL)$(CMPLV)_$(TYPE)_$(CFG) (например example/LINUX_AMD64_GCC4_MD_R_base60), будем именовать далее эти директории как modbld.

Вариант содержимого modsrc

<srcroot> └── example      ├── example.c      ├── example.h      ├── makefile.lmk      └── makelibs

Вариант содержимого modbld

<bldroot> └── example      ├── LINUX_AMD64_GCC4_MD_R_base60      │   ├── charset.obj      │   ├── example.cfl      │   ├── example.h      │   ├── example.lnk      │   ├── example.obj      │   ├── example.so      │   └── makefile      ├── LINUX_AMD64_GCC4_MD_R_full60      │   ├── charset.obj      │   ├── example.cfl      │   ├── example.h      │   ├── example.lnk      │   ├── example.obj      │   ├── example.so      │   └── makefile      ├── LINUX_AMD64_GCC4_MT_R_base60      │   ├── charset.obj      │   ├── example.a      │   ├── example.cfl      │   ├── example.h      │   ├── example.lnk      │   ├── example.obj      │   └── makefile      └── LINUX_AMD64_GCC4_MT_R_full60          ├── charset.obj          ├── example.a          ├── example.cfl          ├── example.h          ├── example.lnk          ├── example.obj          └── makefile 

В каждой допустимой modbld в процессе выполнения обхода директорий создается три файла: опций компилятора (*.cfl в нашем случае), опций компоновщика (*.lnk — в примере) и вспомогательный makefile, которые предназначены для проведения компиляции и компоновки целей в обход общей системы сборки, что бывает часто востребовано для задач отладки. Таким образом, существует два варианта использования системы:

  • сборка всего проекта/модуля впервые;
  • обновление модуля.

Схема вызовов для обоих случаев приведены на иллюстрациях ниже.


Иллюстрация 1: Сборка всего проекта (1) приводит к формированию последовательности вызовов корневого make-файла (3) для всех возможных комбинаций опций сборки (2). В результате фильтрации (3) отсеиваются заведомо непригодные варианты. Файлы описатели модулей, (4) исходя из зависимостей и дополнительных параметров корректируют варианты. Описатели сборки (5) выполняют правила (6) и формируют целевые директории с результатами исполнения (7).


Иллюстрация 2: Обновление существующих модулей (1) работает по упрощенной схеме: вспомогательные правила в modbld (3) обновляют (4) свои цели без использования описателя модуля и фильтров.

Как уже упоминалось выше, все правила вынесены в отдельный модуль (unimake) на уровне проекта, который, помимо заданий самих правил, отвечает за хранение дерева зависимостей между модулями. При этом, каждый модуль из объявленных порождает отдельную цель с генерируемыми зависимыми целями.

Хранение и использование зависимостей между модулями

… dep_example   = another  dep_another   =  … module-deps = $(foreach name,$(DEP_$(1)), $(MOD_$(name))) gen-module-deps = $(foreach name,$(DEP_$(1)), $(2)_$(MOD_$(name)))  !define gen-target  $(1): .SYMBOLIC  	@$(MAKE) MODULE=$(1) !endef  !define gen-targets  TARGETS_$(1) := $(foreach mod,$(ALL_MODULE_NAMES), $(1)_$(mod))  $(1): $$(TARGETS_$(1))  	@%null !endef   gen-targets-without-deps = $(foreach mod,$(ALL_MODULE_NAMES),$(gen-target ,$(mod)))  !eval $(gen-targets-without-deps)  !eval $(gen-targets dep)

Благодаря встроенному парсеру файлов размещения модулей linmodules имеется возможность отслеживает текущее положение модулей в дереве исходников и использовать простое определение пути.

Чтение и регистрация модулей и путей

#git modules  LINMODS=$(modlist $(SRCROOT)/.linmodule)  !define add-mod      MOD_$(1) = $$(modpath $(1))  !endef  !eval $(foreach i,$(LINMODS),$(add-mod $(i)))

Реализация

Описанный в предыдущем разделе подход был реализован нами для инфраструктуры проекта ЛИНТЕР. И, несмотря на то, что произошло это относительно недавно (около полугода назад) система уже положительно зарекомендовала себя с точки зрения простоты использования, масштабируемости и производительности.

Еще на ранних этапах реализации мы столкнулись с известными недостатками gnu make, поэтому решение базируется на make-утилите собственной разработки — linmake, в синтаксисе которой и приведены все листинги в этой статье. Вероятнее всего, в обозримом будущем мы на страницах блога вернемся к теме linmake и его особенностей, но пока этого не произошло публикация системы в том виде, как она используется в разработке не имеет смысла. Однако, было бы неправильно лишить читателя возможности апробировать предлагаемую модель, поэтому здесь (github.com) доступен рабочий прототип для gnu make.

ссылка на оригинал статьи http://habrahabr.ru/post/265001/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *