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

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



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

Навіщо потрібно було створювати нову систему складання?
Як це часто буває, розвиток і ускладнення проекту в якийсь момент призвело до того, що підтримка інфраструктури складання стала занадто накладної і цьому сприяло кілька причин, повний перелік яких зайняв би непристойно багато місця, тому дозволимо собі виділити тільки ті, які викликали більшу кількість нарікань від учасників проекту:
  • з-за того, що в далекому 1999 році не було прийнятного міжплатформового інструменту ми були змушені тривалий час підтримувати дві паралельні системи збирання: на основі wmake для windows і make для *nix;
  • різноманітність підтримуваних UNIX-like платформ призвело до збільшення (а значить і ускладнення) варіантів компіляції і компоновки в модулях проекту;
  • у свою чергу, збірка windows версії ускладнювалася необхідністю підтримки великої кількості компіляторів;
  • не існувало простого механізму опису та вирішення як зовнішніх і внутрішніх залежностей проекту.
Звичайно, крім проблем були і побажання щодо реалізації нових «фіч», тому, коли було прийнято рішення про розробку нової уніфікованої системи збирання, яку назвали unimake, ми цілком виразно уявляли яких цілей хочемо досягти:
  • система повинна одноманітно працювати на всіх підтримуваних платформах;
  • зміна стану модуля (тут і далі під модулем ми будемо розуміти функціонально самодостатню частина проекту) в робочому дереві не повинно впливати на працездатність;
  • необхідний простий механізм додавання нових цільових платформ, архітектур, компіляторів та їх версій;
  • слід зберігати як типові конфігурації для версій і редакцій продукту, так і надавати можливість їх установки при необхідності;
  • потрібен простий спосіб автоматичного обліку зовнішніх і внутрішніх залежностей в проекті, який би автоматично визначав порядок операцій;
  • система повинна надавати можливість простого складання частини проекту з усіма її залежностями.
Модель складання, загальні положення
Збірка проводиться в відмінній від исходников (srcroot) директорії — директорії складання(bldroot). Кожна складання проекту цілком визначається набором множин:
  • конфігурацій/версій продуктів (CONFIGS);
  • цільових платформ (PLATFORMS);
  • цільових архітектур (ARCHS);
  • компіляторів (КОМПІЛЯТОРИ);
  • версіями компіляторів($(CMPL)_VERS);
  • платформою збірки (HOST.PLT);
  • архітектурою платформи складання$(HOST.ARCH).
Варіант конфігурації проекту
...
CONFIGS = base60 full60 
PLATFORMS = LINUX 
ARCHS = AMD64 JAVA .NET 
КОМПІЛЯТОРИ = 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.