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 с параметрами по умолчанию. Файлы должны иметь одинаковое имя.

Yaml vs yml

Хотя yml и .yaml обычно взаимозаменяемы, buildstream требует именно .yaml.

Плагин является классом, наследованным от одного из базовых классов, и реализующий/переписывающий необходимые методы.

Базовые классы:

Общее

Каждый плагин определяет:

Элементы сборки

Интересные запчасти плагинов-элементов сборки.

Исходники

Интересные запчасти плагинов для скачки исходников.

Полезные функции

Что плагин не может делать

Плагин не может легально материализовать новые зависимости: все depends в BuildStream должны быть прописаны статично и резолвятся до загрузки плагинов; плагин не может добавить новую зависимость.

Note

Можно попытаться, но это нарушит контракты 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__)')