BuildStream Plugins Development Guide
BuildStream
BuildStream - инструмент для оркестрации сборки чего-либо - C++, python и т.д.
Не очень популярный (всего 114 звёзд на gh), но всё равно использующийся в Freedesktop, Gnome и других.
BuildStream is a powerful software integration tool that allows developers to automate the integration of software components including operating systems, and to streamline the software development and production process.
BuildStream поддерживает концепцию плагинов, которые определяют или элементы сборки, или исходники.
Примеры можно посмотреть или в официальной репе плагинов, или даже в самом проекте - основные элементы построены также на плагинах.
Плагины
Плагины описываются в виде двух файлов: .py-файл с кодом плагина и .yaml с параметрами по умолчанию. Файлы должны иметь одинаковое имя.
Хотя yml и .yaml обычно взаимозаменяемы, buildstream требует именно .yaml.
Плагин является классом, наследованным от одного из базовых классов, и реализующий/переписывающий необходимые методы.
Базовые классы:
- Элементы сборки:
- Исходники:
Общее
Каждый плагин определяет:
- Точку входа - функцию
setup, которая возвращает класс плагина. - Класс плагина.
- Минимально-совместимую версию BuildStream.
Элементы сборки
Интересные запчасти плагинов-элементов сборки.
- Метод
preflight- запускается при загрузке плагина, может использоваться для проверки его пререквизитов, по системным зависимостям или по самому элементу. - Метод
configure- позволяет прочитать, провалидировать и сохранить конфигурацию, переданную через полеconfigв.bst-файле. - Метод
get_unique_key- должна возвращать уникальный ключ для элемента, который используется при кешировании. Необходимо учитывать все входные данные. - Метод
stage- отвечает за процесс "прогрузки" зависимостей элемента. - Метод
assemble- отвечает непосредственно за процесс сборки элемента.
Исходники
Интересные запчасти плагинов для скачки исходников.
- Метод
preflight- запускается при загрузке плагина, может использоваться для проверки его пререквизитов. - Метод
configure- позволяет прочитать, провалидировать и сохранить конфигурацию, переданную через полеconfigв.bst-файле. - Метод
track- должен возвращатьrefв соответсвии с конфигурацией.refдолжен однозначно определять источник исходника, и учитывается как хеш для кеша. - Метод
fetch- отвечает за процесс загрузки исходников, то есть их перекладывания из источника в кеш BuildStream. - Метод
stage- перекладывает исходники из кеша в целевую директорию элемента. - Метод
collect_source_info- возвращает информацию об исходниках в специально обозначенном формате; может потребоваться для SBOM.
Полезные функции
translate_url- переводит URL из вида<alias>:<path>в канонический.timed_activity- декоратор, запускающий код с красивым логом с таймингом.blocking_activity- декоратор для блокирующей активности, для действий с блокирующими syscalls.call,check_output- обвязки дляsubprocessв контексте BuildStream; выполняют команды на хосте, вне sandbox.
Что плагин не может делать
Плагин не может легально материализовать новые зависимости: все depends в BuildStream должны быть прописаны статично и резолвятся до загрузки плагинов; плагин не может добавить новую зависимость.
Можно попытаться, но это нарушит контракты BuildStream.
Поставка плагинов
Плагины могут поставляться в нескольких разных видах.
Python Package
Плагин можно поставить в виде python-package, который в entry-points указывает доступные запчасти.
Проблема этого подхода - в контексте BuildStream никак нельзя указать зависимость на него; пользователи должны будут ставить его через pip и следить чтобы версии не разъехались.
Junction
Плагин может определяться элементом вида junction в проекте. Тогда он подчиняется всем контрактам BuildStream, как и остальные элементы. BuildStream сам менеджит его загрузку и жизнь.
Проблема этого подхода - плагин никак не может определить свои python или хост-зависимости; их должен обеспечить конечный пользователь.
Local
Локальные плагины поставляются как файлы прямо в проекте BuildStream. Это самый простой способ, похожий на Junction но без лишних сложностей. Страдает теми же проблемами.
Junction/Local & Imports
Плагины, загруженные через junction не могут полноценно пользоваться импортами.
Если проект, загруженный через junction определяет полноценный Python-проект, плагин не сможет пользоваться абсолютными импортами внутри.
Необходимо использовать относительные импорты, или пользоваться хаком для подкладывания в sys.path нужных путей:
# _loader.py
import sys
from pathlib import Path
_CURRENT_FILE = Path(__file__)
_ROOT = _CURRENT_FILE.parents[2]
sys.path.insert(0, str(_ROOT))
# plugin.py
import _loader
import my_cool_plugin.utils
Тестирование плагинов
Рекомендуется покрывать плагины unit-тестами: выносить основные функции в отдельные модули и тестировать их вне BuildStream.
Для интеграционных тестов можно использовать настоящий BuildStream с фикстурами, адаптированными из официальной репы:
import shutil
import random
from pathlib import Path
from unittest.mock import patch
import pytest
from buildstream._testing.runcli import CliIntegration
"""
Настройка кешей, сохраняющихся между запусками BuildStream.
Ускоряет последовательный запуск тестов.
"""
@pytest.fixture(scope="session", name="persistent_cache_dir")
def persistent_cache_dir_fixture(pytestconfig) -> Path:
d = Path(pytestconfig.cache.makedir("tool-caches"))
d.mkdir(parents=True, exist_ok=True)
return d
"""
Фикстура, предоставляющая функцию запуска команд BuildStream
из текущего процесса.
В отличие от запуска shell-команд позволяет мокать куски билдстрима по необходимости.
"""
@pytest.fixture(name="bst", scope="session")
def bst_fixture(persistent_cache_dir: Path, tmp_path_factory):
# Check for buildstream prerequisites and skip the test if they are not found.
if shutil.which("bwrap") is None:
pytest.skip("BuildStream prerequisites not found, skipping test")
# BST_TEST_SUITE env var is required for CliIntegration
with patch.dict("os.environ", {"BST_TEST_SUITE": "1"}, clear=False):
fixture = CliIntegration(str(tmp_path_factory.mktemp("bst-cli") / "bst-cli"))
# We want to cache sources for integration tests more permanently,
# to avoid downloading the huge base-sdk repeatedly
fixture.configure(
{
"cachedir": str(persistent_cache_dir / "bst-cachedir"),
"sourcedir": str(persistent_cache_dir / "bst-sourcedir"),
}
)
def run(project: str, args: list[str]):
res = fixture.run(project, args=args)
assert res.exit_code == 0, (res.output, res.stderr)
yield run
Для тестирования плагинов удобно иметь плагин в виде pip-пакета, установленного в editable-режим. Поддержка этого уже есть в мастере, но не доехала до релиза.
Можно накатить патч вручную на установленный билдстрим:
curl https://raw.githubusercontent.com/kotborealis/buildstream/172671ebef61af7fde7bd3cbdcc91b3d7ca1bd8d/src/buildstream/_pluginfactory/pluginoriginpip.py > $(python3 -c 'import buildstream._pluginfactory.pluginoriginpip; print(buildstream._pluginfactory.pluginoriginpip.__file__)')