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



Прежде чем перейти непосредственно к техническим деталям следует отметить два важных момента. Во-первых, система работает поверх разработанной нами 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>
L-- example 
    +-- example.c 
    +-- example.h 
    +-- makefile.lmk 
    L-- makelibs


Вариант содержимого modbld
<bldroot>
L-- example 
    +-- LINUX_AMD64_GCC4_MD_R_base60 
    ¦   +-- charset.obj 
    ¦   +-- example.cfl 
    ¦   +-- example.h 
    ¦   +-- example.lnk 
    ¦   +-- example.obj 
    ¦   +-- example.so 
    ¦   L-- makefile 
    +-- LINUX_AMD64_GCC4_MD_R_full60 
    ¦   +-- charset.obj 
    ¦   +-- example.cfl 
    ¦   +-- example.h 
    ¦   +-- example.lnk 
    ¦   +-- example.obj 
    ¦   +-- example.so 
    ¦   L-- makefile 
    +-- LINUX_AMD64_GCC4_MT_R_base60 
    ¦   +-- charset.obj 
    ¦   +-- example.a 
    ¦   +-- example.cfl 
    ¦   +-- example.h 
    ¦   +-- example.lnk 
    ¦   +-- example.obj 
    ¦   L-- makefile 
    L-- LINUX_AMD64_GCC4_MT_R_full60 
        +-- charset.obj 
        +-- example.a 
        +-- example.cfl 
        +-- example.h 
        +-- example.lnk 
        +-- example.obj 
        L-- 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.

Комментарии (4)


  1. jreznot
    19.08.2015 18:00

    Вы пробовали Gradle? На первый взгляд кажется, что он может решить задачи описанные в статье.


    1. relexru
      19.08.2015 18:18

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


      1. dim_s
        19.08.2015 20:34

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


        1. npechenkin
          20.08.2015 00:11

          Согласен, но определенное значение играет и простота поддержки системы сборки. make-образный синтаксис, знакомый любому разработчику, позволяет свести к минимуму издержки на внесение правок в модуль.