Files
framebridge/source/expanded/framebridge_expanded/compatibility.py

146 lines
7.7 KiB
Python

"""Explicit candidate Linux profiles and installed-artifact ABI checks.
Eligibility is not evidence of a successful installation on a target system.
Never import vendored native code merely to diagnose an incompatible artifact.
"""
from __future__ import annotations
import json
import os
import platform
import re
import sys
from pathlib import Path
PROFILES = {
"debian13-amd64-py313": ("debian", "13", "3.13", "cp313"),
"ubuntu24.04-amd64-py312": ("ubuntu", "24.04", "3.12", "cp312"),
"ubuntu26.04-amd64-py314": ("ubuntu", "26.04", "3.14", "cp314"),
"mint22-amd64-py312": ("linuxmint", "22", "3.12", "cp312"),
"lmde7-amd64-py313": ("linuxmint", "7", "3.13", "cp313"),
}
SYSTEM_PYTHONS = frozenset({"/usr/bin/python3", "/usr/bin/python3.12",
"/usr/bin/python3.13", "/usr/bin/python3.14"})
INSTALLED_PREFIX = Path("/usr/lib/framebridge-expanded")
VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor")
def profile_for(os_release):
"""Match named releases only; ID_LIKE never opts other derivatives in."""
distro = os_release.get("ID", "")
version = os_release.get("VERSION_ID", "")
for name, (expected, release, _minor, _abi) in PROFILES.items():
matches = version == release
if name == "mint22-amd64-py312":
matches = bool(re.fullmatch(r"22(?:\.\d+)?", version))
if distro == expected and matches:
return name
return None
def _libc():
try:
value = os.confstr("CS_GNU_LIBC_VERSION") or ""
if value.startswith("glibc "):
return "glibc", value.split()[-1]
except (OSError, ValueError):
pass
return platform.libc_ver()
def _vendor_issues(prefix, abi):
vendor = prefix / VENDOR_RELATIVE
issues = []
expected = abi.removeprefix("cp")
if not vendor.is_dir():
return ["Bundled vendor directory is missing; use a complete artifact for this profile."]
required = {
"_cffi_backend": list(vendor.glob("_cffi_backend.cpython-*.so")),
"pydantic_core": list((vendor / "pydantic_core").glob("_pydantic_core.cpython-*.so")),
"rpds": list((vendor / "rpds").glob("rpds.cpython-*.so")),
}
for module, files in required.items():
if len(files) != 1:
issues.append("Expected exactly one bundled native extension for " + module + ".")
for path in vendor.rglob("*.so"):
match = re.search(r"\.cpython-([^-]+)-", path.name)
if match and (match.group(1) != expected or "x86_64-linux-gnu" not in path.name):
issues.append("Bundled native extension does not match " + abi + " amd64: " + str(path.relative_to(vendor)))
return issues
def assess(prefix, *, os_release=None, machine=None, python_version=None, libc=None, implementation=None, abi_flags=None):
"""Check profile and artifact identity without importing native modules."""
prefix = Path(prefix)
issues = []
warnings = []
if os_release is None:
try:
os_release = platform.freedesktop_os_release()
except OSError:
os_release = {}
machine = platform.machine() if machine is None else machine
version = sys.version_info[:2] if python_version is None else python_version[:2]
minor = ".".join(str(x) for x in version)
abi = "cp" + "".join(str(x) for x in version)
implementation = platform.python_implementation() if implementation is None else implementation
abi_flags = getattr(sys, "abiflags", "") if abi_flags is None else abi_flags
if implementation != "CPython" or abi_flags:
issues.append("This artifact requires standard CPython; alternate, debug or free-threaded interpreter ABIs need separate native artifacts.")
selected = profile_for(os_release)
if selected is None:
issues.append("No candidate profile for this release. Choose Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7.")
if machine not in {"x86_64", "amd64"}:
issues.append("This artifact supports amd64 only; ARM and other architectures need separate artifacts.")
if selected and minor != PROFILES[selected][2]:
issues.append("Profile " + selected + " requires Python " + PROFILES[selected][2] + "; running " + minor + ". Install its matching artifact without replacing the system Python.")
libc_name, libc_version = _libc() if libc is None else libc
match = re.match(r"(\d+)\.(\d+)", libc_version)
if libc_name != "glibc" or not match or tuple(map(int, match.groups())) < (2, 34):
issues.append("Bundled native libraries require glibc >= 2.34; this libc is not eligible.")
manifest = None
manifest_path = prefix / "release-profile.json"
if manifest_path.exists():
try:
if manifest_path.stat().st_size > 16384:
raise ValueError("manifest too large")
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if not isinstance(manifest, dict) or type(manifest.get("schema")) is not int or manifest["schema"] != 1:
raise ValueError("unsupported schema")
if manifest.get("profile_id") not in PROFILES:
raise ValueError("unknown profile")
expected = PROFILES[manifest["profile_id"]]
if (manifest.get("architecture"), manifest.get("python_minor"), manifest.get("python_abi")) != ("amd64", expected[2], expected[3]):
raise ValueError("inconsistent profile ABI")
if manifest.get("validation_status") not in {"candidate", "validated"}:
raise ValueError("missing validation status")
if manifest["profile_id"] != selected or manifest["python_minor"] != minor or manifest["python_abi"] != abi:
issues.append("Installed artifact profile/ABI does not match this operating system and interpreter.")
except (OSError, UnicodeError, ValueError, TypeError):
issues.append("Invalid release-profile.json; install a complete matching artifact.")
manifest = None
else:
# Source-tree fallback is intentionally the reviewed release's ABI only.
if prefix.resolve() == INSTALLED_PREFIX or selected != "debian13-amd64-py313" or minor != "3.13":
issues.append("Missing release-profile.json; this target requires a profile-specific artifact.")
else:
warnings.append("Source checkout has no artifact manifest; only the legacy Debian 13/cp313 profile is eligible.")
issues.extend(_vendor_issues(prefix, abi))
validation = manifest.get("validation_status") if manifest else "source-unvalidated"
if validation != "validated":
warnings.append("Candidate compatibility checks do not establish a tested installation on this target.")
return {"ok": not issues, "profile_id": selected, "artifact_profile_id": manifest.get("profile_id") if manifest else None,
"architecture": machine, "python_minor": minor, "python_abi": abi, "python_implementation": implementation, "python_abi_flags": abi_flags,
"libc": {"name": libc_name, "version": libc_version}, "validation_status": validation,
"issues": issues, "warnings": warnings}
def display_support(env, context):
wayland = env.get("XDG_SESSION_TYPE", "").lower() == "wayland" or bool(env.get("WAYLAND_DISPLAY"))
notes = []
if wayland:
notes.append("Native Wayland desktop capture/control is not implemented. XWayland does not establish access to the full Wayland desktop; text tools remain available, or explicitly select a private virtual X11 display.")
return {"session_type": "wayland" if wayland else env.get("XDG_SESSION_TYPE", "unknown"),
"physical_x11_verified": context.get("kind") == "physical-x11",
"private_virtual_x11": context.get("kind") == "virtual-x11",
"native_wayland_cua": False, "notes": notes}