112 lines
3.2 KiB
Python
112 lines
3.2 KiB
Python
|
|
"""Portable Pfadregeln (PLAN.md §9, §9.1).
|
||
|
|
|
||
|
|
- Alle Pfade relativ zum Anwendungsroot; keine Laufwerksbuchstaben.
|
||
|
|
- Keine Abhängigkeit vom Working Directory.
|
||
|
|
- Temporäre Dateien in userdata/cache, nicht im OS-Profil.
|
||
|
|
- Schreibbarkeit wird beim Start geprüft (Read-only-Modus folgt Phase 1).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class AppPaths:
|
||
|
|
"""Alle portablen Pfade je Anwendungsroot (§9-Struktur)."""
|
||
|
|
|
||
|
|
root: Path
|
||
|
|
|
||
|
|
@property
|
||
|
|
def app(self) -> Path:
|
||
|
|
return self.root / "app"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def runtime(self) -> Path:
|
||
|
|
return self.root / "runtime"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def gstreamer_bin(self) -> Path:
|
||
|
|
return self.runtime / "gstreamer" / "bin"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def gstreamer_plugins(self) -> Path:
|
||
|
|
return self.runtime / "gstreamer" / "lib" / "gstreamer-1.0"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def web(self) -> Path:
|
||
|
|
return self.root / "web"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def projects(self) -> Path:
|
||
|
|
return self.root / "projects"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def media(self) -> Path:
|
||
|
|
return self.root / "media"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def userdata(self) -> Path:
|
||
|
|
return self.root / "userdata"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def database(self) -> Path:
|
||
|
|
return self.userdata / "database"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def cache(self) -> Path:
|
||
|
|
return self.userdata / "cache"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def identity(self) -> Path:
|
||
|
|
return self.userdata / "identity" / "node_id"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def config(self) -> Path:
|
||
|
|
return self.root / "config"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def logs(self) -> Path:
|
||
|
|
return self.root / "logs"
|
||
|
|
|
||
|
|
def ensure_writable(self) -> bool:
|
||
|
|
"""Prüft Schreibbarkeit des Roots (§9.1)."""
|
||
|
|
probe = self.root / ".write_probe"
|
||
|
|
try:
|
||
|
|
probe.write_text("ok", encoding="ascii")
|
||
|
|
probe.unlink()
|
||
|
|
return True
|
||
|
|
except OSError:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def portable_environment(self) -> dict[str, str]:
|
||
|
|
"""Umgebungsvariablen für gebündelte GStreamer-Runtime (§9.2, ADR-0002).
|
||
|
|
|
||
|
|
System-Plugins werden unterdrückt (leerer GST_PLUGIN_SYSTEM_PATH_1_0),
|
||
|
|
damit ausschließlich die gebündelte, manifestierte Untermenge lädt.
|
||
|
|
"""
|
||
|
|
env = dict(os.environ)
|
||
|
|
gs_bin = self.gstreamer_bin
|
||
|
|
if gs_bin.is_dir():
|
||
|
|
path_var = "PATH"
|
||
|
|
existing = env.get(path_var, "")
|
||
|
|
env[path_var] = f"{gs_bin}{os.pathsep}{existing}" if existing else str(gs_bin)
|
||
|
|
env["GST_PLUGIN_PATH_1_0"] = str(self.gstreamer_plugins)
|
||
|
|
env["GST_PLUGIN_SYSTEM_PATH_1_0"] = ""
|
||
|
|
return env
|
||
|
|
|
||
|
|
|
||
|
|
def resolve_app_root(start_from: Path | None = None) -> Path:
|
||
|
|
"""Bestimmt den Anwendungsroot anhand der PORTABLE_MODE-Markierung (§9).
|
||
|
|
|
||
|
|
Sucht vom gegebenen Pfad (Default: dieses Paket) aufwärts nach der
|
||
|
|
Datei PORTABLE_MODE; im Entwickungsbaum ist das Repo-Root gemeint.
|
||
|
|
"""
|
||
|
|
current = Path(start_from or __file__).resolve()
|
||
|
|
for candidate in [current, *current.parents]:
|
||
|
|
if (candidate / "PORTABLE_MODE").is_file() or (candidate / "pyproject.toml").is_file():
|
||
|
|
return candidate
|
||
|
|
raise RuntimeError("app root not found (PORTABLE_MODE or pyproject.toml missing)")
|