from __future__ import annotations from dataclasses import dataclass, field import json import os from pathlib import Path from typing import Any, Mapping DEFAULT_RUNTIME_DIR = "/run/user/100000" DEFAULT_BUS_ADDRESS = "unix:path=/run/user/100000/dbus/user_bus_socket" BUNDLED_BUILD_HELPER = ( Path(__file__).resolve().parent / "vendor" / "build_sailfishos.py" ) @dataclass(frozen=True) class DeviceConfig: name: str ssh_target: str username: str = "defaultuser" architecture: str = "" release: str = "" user_bus_runtime_dir: str = DEFAULT_RUNTIME_DIR user_bus_address: str = DEFAULT_BUS_ADDRESS def public_dict(self) -> dict[str, object]: return { "name": self.name, "ssh_target": self.ssh_target, "username": self.username, "architecture": self.architecture, "release": self.release, "user_bus_runtime_dir": self.user_bus_runtime_dir, "user_bus_address": self.user_bus_address, } @dataclass(frozen=True) class PathConfig: git_root: Path = Path.home() / "git" ssh_config: Path = Path.home() / ".ssh" / "config" build_sailfishos: Path = BUNDLED_BUILD_HELPER local_sdk: Path | None = None osc_api_alias: str = "" def public_dict(self) -> dict[str, str | None]: return { "git_root": str(self.git_root), "ssh_config": str(self.ssh_config), "build_sailfishos": str(self.build_sailfishos), "local_sdk": str(self.local_sdk) if self.local_sdk else None, "osc_api_alias": self.osc_api_alias, } @dataclass(frozen=True) class Config: path: Path | None default_device: str devices: Mapping[str, DeviceConfig] = field(default_factory=dict) paths: PathConfig = field(default_factory=PathConfig) def device(self, name: str | None = None) -> DeviceConfig: key = name or self.default_device if key in self.devices: return self.devices[key] if name and "@" in name: return DeviceConfig(name=name, ssh_target=name) raise KeyError(f"unknown Sailfish device: {key}") def public_dict(self) -> dict[str, Any]: return { "path": str(self.path) if self.path else None, "default_device": self.default_device, "devices": { name: device.public_dict() for name, device in self.devices.items() }, "paths": self.paths.public_dict(), } def default_config_path() -> Path: if "SAILFISH_MCP_CONFIG" in os.environ: return Path(os.environ["SAILFISH_MCP_CONFIG"]).expanduser() return Path.home() / ".config" / "sailfish-devel-mcp" / "config.json" def load_config(path: str | os.PathLike[str] | None = None) -> Config: config_path = Path(path).expanduser() if path else default_config_path() raw: dict[str, Any] = {} if config_path.exists(): with config_path.open("r", encoding="utf-8") as handle: loaded = json.load(handle) if not isinstance(loaded, dict): raise ValueError(f"{config_path} must contain a JSON object") raw = loaded devices = _load_devices(raw.get("devices", {})) if not devices: default_name = os.environ.get("SAILFISH_MCP_DEFAULT_DEVICE", "device") devices = { default_name: DeviceConfig( name=default_name, ssh_target=os.environ.get("SAILFISH_MCP_SSH_TARGET", "root@device"), ) } default_device = str(raw.get("default_device") or next(iter(devices))) paths = _load_paths(raw.get("paths", {})) return Config( path=config_path if config_path.exists() else None, default_device=default_device, devices=devices, paths=paths, ) def _load_devices(raw_devices: Any) -> dict[str, DeviceConfig]: if not isinstance(raw_devices, dict): raise ValueError("devices must be a JSON object") devices: dict[str, DeviceConfig] = {} for name, value in raw_devices.items(): if isinstance(value, str): value = {"ssh_target": value} if not isinstance(value, dict): raise ValueError(f"device {name!r} must be a string or object") ssh_target = str(value.get("ssh_target") or name) devices[str(name)] = DeviceConfig( name=str(name), ssh_target=ssh_target, username=str(value.get("username") or "defaultuser"), architecture=str(value.get("architecture") or ""), release=str(value.get("release") or ""), user_bus_runtime_dir=str( value.get("user_bus_runtime_dir") or DEFAULT_RUNTIME_DIR ), user_bus_address=str(value.get("user_bus_address") or DEFAULT_BUS_ADDRESS), ) return devices def _load_paths(raw_paths: Any) -> PathConfig: if not isinstance(raw_paths, dict): raise ValueError("paths must be a JSON object") defaults = PathConfig() raw_local_sdk = raw_paths.get("local_sdk") or os.environ.get("SAILFISH_MCP_LOCAL_SDK") return PathConfig( git_root=Path( str( raw_paths.get("git_root") or os.environ.get("SAILFISH_MCP_GIT_ROOT") or defaults.git_root ) ).expanduser(), ssh_config=Path( str( raw_paths.get("ssh_config") or os.environ.get("SAILFISH_MCP_SSH_CONFIG") or defaults.ssh_config ) ).expanduser(), build_sailfishos=Path( str( raw_paths.get("build_sailfishos") or os.environ.get("SAILFISH_MCP_BUILD_HELPER") or defaults.build_sailfishos ) ).expanduser(), local_sdk=Path(str(raw_local_sdk)).expanduser() if raw_local_sdk else None, osc_api_alias=str( raw_paths.get("osc_api_alias") or os.environ.get("SAILFISH_MCP_OSC_API_ALIAS") or defaults.osc_api_alias ), )