173 lines
5.4 KiB
Python
173 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import tomllib
|
|
from dataclasses import dataclass, field, replace
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
VALID_STRATEGIES = {"dll-to-plugins", "zip-to-pending", "bsipa-zip", "root-zip", "manual"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Dependency:
|
|
id: str
|
|
constraint: str | None = None
|
|
required: bool = True
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RegistryPlugin:
|
|
id: str
|
|
name: str
|
|
repo: str | None
|
|
asset_patterns: tuple[str, ...] = ()
|
|
install_strategy: str = "manual"
|
|
category: str | None = None
|
|
dependencies: tuple[Dependency, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Registry:
|
|
plugins: dict[str, RegistryPlugin] = field(default_factory=dict)
|
|
|
|
def get(self, plugin_id: str) -> RegistryPlugin | None:
|
|
return self.plugins.get(plugin_id)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LockedPlugin:
|
|
id: str
|
|
repo: str | None
|
|
tag: str | None
|
|
asset: str | None
|
|
sha256: str | None
|
|
download_url: str | None = None
|
|
install_strategy: str | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Lockfile:
|
|
beat_saber_version: str
|
|
instance: str
|
|
plugins: tuple[LockedPlugin, ...]
|
|
|
|
|
|
def _quote_toml_string(value: str) -> str:
|
|
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
|
|
|
|
def lockfile_to_toml(lockfile: Lockfile) -> str:
|
|
lines = [
|
|
f"beat_saber_version = {_quote_toml_string(lockfile.beat_saber_version)}",
|
|
f"instance = {_quote_toml_string(lockfile.instance)}",
|
|
"",
|
|
]
|
|
for plugin in lockfile.plugins:
|
|
lines.append("[[plugins]]")
|
|
lines.append(f"id = {_quote_toml_string(plugin.id)}")
|
|
if plugin.repo is not None:
|
|
lines.append(f"repo = {_quote_toml_string(plugin.repo)}")
|
|
if plugin.tag is not None:
|
|
lines.append(f"tag = {_quote_toml_string(plugin.tag)}")
|
|
if plugin.asset is not None:
|
|
lines.append(f"asset = {_quote_toml_string(plugin.asset)}")
|
|
if plugin.download_url is not None:
|
|
lines.append(f"download_url = {_quote_toml_string(plugin.download_url)}")
|
|
if plugin.sha256 is not None:
|
|
lines.append(f"sha256 = {_quote_toml_string(plugin.sha256)}")
|
|
if plugin.install_strategy is not None:
|
|
lines.append(f"install_strategy = {_quote_toml_string(plugin.install_strategy)}")
|
|
if plugin.reason is not None:
|
|
lines.append(f"reason = {_quote_toml_string(plugin.reason)}")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def write_lockfile(path: Path, lockfile: Lockfile) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(lockfile_to_toml(lockfile), encoding="utf-8")
|
|
|
|
|
|
def replace_locked_plugins(lockfile: Lockfile, replacements: dict[str, LockedPlugin]) -> Lockfile:
|
|
return replace(
|
|
lockfile,
|
|
plugins=tuple(replacements.get(plugin.id, plugin) for plugin in lockfile.plugins),
|
|
)
|
|
|
|
|
|
def _load_toml(path: Path) -> dict[str, Any]:
|
|
with path.open("rb") as handle:
|
|
return tomllib.load(handle)
|
|
|
|
|
|
def _registry_plugin_from_item(item: dict[str, Any], source: Path) -> RegistryPlugin:
|
|
dependencies = tuple(
|
|
Dependency(
|
|
id=dep["id"],
|
|
constraint=dep.get("constraint"),
|
|
required=dep.get("required", True),
|
|
)
|
|
for dep in item.get("dependencies", [])
|
|
)
|
|
strategy = item.get("install_strategy", "manual")
|
|
if strategy not in VALID_STRATEGIES:
|
|
raise ValueError(f"{source}: invalid install_strategy for {item['id']}: {strategy}")
|
|
return RegistryPlugin(
|
|
id=item["id"],
|
|
name=item.get("name", item["id"]),
|
|
repo=item.get("repo"),
|
|
asset_patterns=tuple(item.get("asset_patterns", [])),
|
|
install_strategy=strategy,
|
|
category=item.get("category"),
|
|
dependencies=dependencies,
|
|
)
|
|
|
|
|
|
def _add_registry_plugin(plugins: dict[str, RegistryPlugin], plugin: RegistryPlugin, source: Path) -> None:
|
|
if plugin.id in plugins:
|
|
raise ValueError(f"{source}: duplicate plugin id: {plugin.id}")
|
|
plugins[plugin.id] = plugin
|
|
|
|
|
|
def load_registry(path: Path) -> Registry:
|
|
if not path.exists():
|
|
legacy_path = path.with_suffix(".toml")
|
|
if path.name == "plugins" and legacy_path.exists():
|
|
return load_registry(legacy_path)
|
|
return Registry()
|
|
plugins: dict[str, RegistryPlugin] = {}
|
|
if path.is_dir():
|
|
for item_path in sorted(path.glob("*.toml")):
|
|
data = _load_toml(item_path)
|
|
_add_registry_plugin(plugins, _registry_plugin_from_item(data, item_path), item_path)
|
|
return Registry(plugins)
|
|
|
|
data = _load_toml(path)
|
|
for item in data.get("plugins", []):
|
|
_add_registry_plugin(plugins, _registry_plugin_from_item(item, path), path)
|
|
return Registry(plugins)
|
|
|
|
|
|
def load_lockfile(path: Path) -> Lockfile:
|
|
data = _load_toml(path)
|
|
plugins = tuple(
|
|
LockedPlugin(
|
|
id=item["id"],
|
|
repo=item.get("repo"),
|
|
tag=item.get("tag"),
|
|
asset=item.get("asset"),
|
|
sha256=item.get("sha256"),
|
|
download_url=item.get("download_url"),
|
|
install_strategy=item.get("install_strategy"),
|
|
reason=item.get("reason"),
|
|
)
|
|
for item in data.get("plugins", [])
|
|
)
|
|
return Lockfile(
|
|
beat_saber_version=data["beat_saber_version"],
|
|
instance=data.get("instance", data["beat_saber_version"]),
|
|
plugins=plugins,
|
|
)
|