Використання можливостей git-а в системі складання модульного проекту

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



Кілька слів про приклади
Спочатку передбачалося забезпечити цю публікацію фрагментами системи збирання в тому вигляді, як вона реалізована в ЛИНТЕРе, проте ми не використовуємо в цьому проекті нативні модулі git і застосовуємо утиліту make власної розробки. Тому, щоб практична цінність матеріалу для читачів не постраждала, всі приклади адаптовані для використання в зв'язці git submodules і gnu make, що призвело до певних складнощів, які будуть вказані нижче.

Опис демонстраційного прикладу
В цілях спрощення будемо розглядати інтеграцію системи збирання з git-му на прикладі умовного продукту з назвою project, який складається з наступних функціональних модулів:
applications — безпосередньо додаток;
demo — демонстраційні приклади;
libfoo і libbar — бібліотеки, від яких залежить applications.
Граф залежностей project буде наступним:

Ілюстрація 1: Граф залежностей проекту

Організація зберігання
З точки зору системи зберігання версій проект розбитий на п'ять окремих репозиторіїв — чотири для модулів і п'ятий — project.git, що виконує роль контейнера та містить систему складання. Такий спосіб організації має кілька переваг у порівнянні з монорепозиторием:
  • кожен підмодуль має окрему історію правок;
  • можливість клонування тільки частини проекту;
  • кожен репозиторій може мати індивідуальні правила і політики доступу;
  • можливість отримання проекту в довільну структуру робочої копії.


Подмодули і залежності
Незважаючи на те, що рекурсивний підхід до організації системи збирання виправдано критикується, все-таки він дозволяє істотно знизити витрати на супровід проекту, тому в нашому прикладі будемо застосовувати саме його. При цьому, кореневої makefile проекту повинен не тільки «знати» положення модулів всередині проекту, але і забезпечувати виклик дочірніх make-процесів в цілях в потрібній послідовності: від гілок дерева залежності до коріння. Для цього слід явно описати ці через-модульні залежності, в нашому прикладі це зроблено наступним чином:
MODS = project application libfoo libbar demo 

submodule.project.deps = application demo 
submodule.demo.deps = application 
submodule.application.deps = libfoo libbar 
submodule.libfoo.deps = 
submodule.libbar.deps =

Коректний обхід цього дерева можна забезпечити засобами make, створивши динамічні цілі з явним зазначенням залежностей, для чого оголосимо функцію gen-dep наступного виду:
define gen-dep 
$(1):$(foreach dep,$(submodule.$(1).deps),$(dep)) ;
endef 

Тепер, якщо в тілі кореневого Makefile викликати gen-dep для всіх модулів
$(foreach mod,$(MODS),$(eval $(call gen-dep,$(mod))))

це сформує наступні динамічні мети під час виконання (це можна перевірити запустивши make з ключем-p)
project: application demo 

demo: application 

application: libfoo libbar 

libbar: 

libfoo: 

що дозволяє при зверненні до них забезпечити виклик залежностей в потрібному порядку. При цьому, якщо ім'я цілі співпаде з існуючим файлом або директорією, то це може порушити виконання, оскільки make «не знає» що ці наші цілі — це дії, а не файли, щоб цього уникнути явно зазначимо:
$(eval .PHONY: $(foreach mod,$(MODS), $(mod)))

Припустимо, що перед розробником стоїть завдання внесення змін до application, для чого йому потрібно отримати тільки подмодули application, libbar, libfoo. Для цього система повинна складання на основі оголошених вище залежностей сформувати опис модулів і їх розміщення для подальшого використання git-ом, який, як відомо, описує зареєстровані подмодули у файлі з іменем .gitmodules, розташованому в корені клонованого репозиторію.

Внесемо такі зміни у наш приклад, щоб забезпечити генерацію .gitmodules мінімального необхідного складу:
...

MODURLPREFIX ?= [email protected]/
MODFILE ?= .gitmodules
...
define tmpl.module 
"[submodule \"$(1)\"]" 
endef 

define tmpl.path 
"\tpath = $(1)" 
endef 

define tmpl.url 
"\turl = $(1)" 
endef

...

define submodule-set 
submodule.$(1).name := $(2) 
submodule.$(1).path := $(3) 
submodule.$(1).url := $(4) 
endef 

define set-default 
$(call submodule-set,$(1),$(1),$(1),$(MODURLPREFIX)$(1).git) 
endef 

define gen-dep 
$(1):$(foreach dep,$(submodule.$(1).deps),$(dep)) 
@echo "Register module $(1)" 
@echo $(call tmpl.module,$(submodule.$(1).name)) >> $(MODFILE) 
@echo $(call tmpl.path,$(submodule.$(1).path)) >> $(MODFILE) 
@echo $(call tmpl.url$(submodule.$(1).url)) >> $(MODFILE) 
endef 

...

$(foreach mod,$(MODS),$(eval $(call set-default,$(mod))))

Тепер наш умовний розробник, викликавши make application зможе створити файл наступного змісту:
[submodule "libfoo"] 
path = libfoo 
url = [email protected]/libfoo.git 
[submodule "libbar"] 
path = libbar 
url = [email protected]/libbar.git 
[submodule "application"] 
path = application 
url = [email protected]/application.git

який вже може бути змінений і розібраний коштами git-a, наприклад таким чином:
git config-f .gitmodules --get submodule.application.path
application

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

Що ж стосується безпосередньо ініціалізації подмодулей — то тут проявляється перше серйозне незручність у реалізації нативних модулів в git-е: метадані про модулях ця система керування версіями зберігає і в індексі, і у файлі .gitmodules. Заглянувши в вихідні коди стає зрозумілим, що у нас є дві не найкращі альтернативи.
Перша — це внести інформацію про модулях в індекс наступним чином:
#!/bin/sh

git config-f .gitmodules --get-regexp '^submodule\..*\.path$' |
while read path_key path
do
url_key=$(echo $path_key | sed 's/\.path/.url/')
url=$(git config-f .gitmodules --get "$url_key")
git submodule add --force $url $path
done

у цьому випадку з'являється можливість працювати з подмодулями використовуючи штатний git-submodule (ітератори, групові операції тощо), однак переміщення/видалення модулів, а також їх розгалуження буде вимагати додаткових допоміжних операцій. Описана ситуація стала однією з причин, по якій ми відмовилися від використання git-submodules в репозиторії ЛИНТЕРа. Альтернативою submodule add може служити клонування модулів без реєстрації в індексі, що можна зробити так:
#!/bin/sh 

git config-f .gitmodules --get-regexp '^submodule\..*\.path$' | 
while read path_key path 
do 
url_key=$(echo $path_key | sed 's/\.path/.url/') 
url=$(git config-f .gitmodules --get "$url_key")
git clone $url $path 
done

у цьому випадку обов'язково потрібно явне вказівку усіх $path .gitignore, інакше git буде сприймати клоновані подмодули як звичайні директорії і обробляти їх і вміст як невідстежувані файли.

Так чи інакше, після клонування будь-яким із зазначених способів робоча копія буде відповідати ситуації вилучення виділеного фрагмента дерева


Ілюстрація 2: Граф залежностей проекту. Заливкою виділено извлекаемое дерево модулів.

і, за умови правильного оголошення межмодульних залежностей, містить все необхідне для компіляції application.

Визначення положення модулів
Ще одне завдання, яку вирішує система складання — це визначення поточного положення модулів. Для цього будемо використовувати сформований нами раніше файл-описувач. Як і у випадку з ініціалізацією — тут є кілька варіантів. Найпростіше — це скористатися можливостями git config:
define get-path 
$(shell git config-f .gitmodules --get "submodule.$(1).path") 
endef 

define get-url 
$(shell git config-f .gitmodules --get "submodule.$(1).url") 
endef 

Таке рішення не є ідеальним з точки зору переносимості, але інший варіант доступний тільки якщо використовувати GNU make версії 4 і вище — в цьому випадку парсинг файлу .gitmodules можна реалізувати з використанням розширень GNU make.

Висновок
Дозволимо собі ще раз нагадати, що приклад доступний на github є адаптацією наших рішень на базі зв'язки linmodules+linflow для gitmodules+GNU make, тому деякі недоліки сполучення цих інструментів не вирішені самим витонченим способом, а виклики дочірніх make файлів в модулях замінені на «пустушки».



Тим не менш, механізм зарекомендував себе досить добре при роботі з великим проектом і успішно «справляється» з репозиторієм в 102 подмодуля git, між якими існує 308 межмодульних зв'язків (як логічних, так і по збірці) з діаметром графа зв'язків 5 одиниць (див. малюнок вище).

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

0 коментарів

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