Files
voculinux/snap/local/check_runtime.py
T
xavierk 180cd4d512
CI / security (push) Successful in 1m14s
CI / lint (push) Successful in 24s
CI / ai-review (push) Skipped
fix(spec16): close the review findings for the Void port (#34)
Model recommendation keyed to the installed build and free memory, fork-sourced Void templates, signed xbps release workflow on Gitea with the pinned repository key, niri injection guard and wtype-first order, tightened naming sweep, web copy fixes, and CI security and duplication fixes.
2026-11-06 14:13:49 +00:00

151 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Record locked Python versions at build time and audit the installed Snap.
Only recording uses packaging, from the locked build toolchain. The packaged
audit needs only the standard library and the runtime it is checking.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib
import importlib.metadata
import json
import re
import runpy
import sys
from pathlib import Path
MANIFEST = Path("usr/share/voculinux/python-inputs.json")
def normalize(name: str) -> str:
"""Normalize a Python distribution name."""
return re.sub(r"[-_.]+", "-", name).lower()
def locked_versions(export: Path) -> dict[str, str]:
"""Evaluate the complete export for the build interpreter, rejecting GI."""
from packaging.requirements import Requirement
expected = {}
blocks = re.split(r"(?=^[A-Za-z0-9_.-]+==)", export.read_text(), flags=re.MULTILINE)[1:]
if not blocks:
raise RuntimeError(f"No pinned requirements in {export}")
for block in blocks:
requirement = Requirement(re.split(r"\s+--hash=|\\", block, maxsplit=1)[0].strip())
if not re.search(r"--hash=sha256:[0-9a-f]{64}(?:\s|$)", block):
raise RuntimeError(f"Missing SHA-256 for {requirement.name}")
name = normalize(requirement.name)
if name == "pygobject":
raise RuntimeError("GI must come from the GNOME extension, not pip")
if requirement.marker and not requirement.marker.evaluate():
continue
pins = list(requirement.specifier)
if len(pins) != 1 or pins[0].operator != "==" or "*" in pins[0].version:
raise RuntimeError(f"Not an exact pin: {requirement}")
if name in expected:
raise RuntimeError(f"Multiple active pins for {name}")
expected[name] = pins[0].version
if not expected:
raise RuntimeError(f"No active requirements in {export}")
return expected
def audit(root: Path, expected: dict[str, str]) -> Path:
"""Check every bundled distribution, including missing/extra/build packages."""
site = root / f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages"
actual = {}
for distribution in importlib.metadata.distributions(path=[str(site)]):
name = normalize(distribution.metadata["Name"])
if name in actual:
raise RuntimeError(f"Duplicate installed distribution: {name}")
actual[name] = distribution.version
if actual != expected:
differences = {
name: {"expected": expected.get(name), "installed": actual.get(name)}
for name in sorted(expected.keys() | actual.keys())
if expected.get(name) != actual.get(name)
}
raise RuntimeError(f"Snap Python versions differ from the lock: {differences}")
return site
def record(root: Path, source: Path) -> None:
"""Retain the expected versions and original export digests in the payload."""
for name, expected_version in locked_versions(
source / "requirements/installer-build.txt"
).items():
if importlib.metadata.version(name) != expected_version:
raise RuntimeError(f"Build tool {name} differs from its locked version")
expected = locked_versions(source / "requirements/runtime.txt")
expected["voculinux"] = runpy.run_path(str(source / "src/vocalinux/version.py"))["__version__"]
audit(root, expected)
manifest = {
"python": list(sys.version_info[:2]),
"packages": expected,
"exports": {
name: hashlib.sha256((source / "requirements" / name).read_bytes()).hexdigest()
for name in ("runtime.txt", "installer-build.txt")
},
}
destination = root / MANIFEST
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def smoke(root: Path) -> None:
"""Audit a packaged runtime and import native/UI modules without a model."""
manifest = json.loads((root / MANIFEST).read_text())
if manifest["python"] != list(sys.version_info[:2]):
raise RuntimeError("The Snap is running with a different Python ABI")
site = audit(root, manifest["packages"])
sys.path.insert(0, str(site))
for name in ("numpy", "requests", "psutil", "pyaudio", "cairo", "evdev", "vocalinux"):
# nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import -- names come from the literal tuple above
module = importlib.import_module(name)
if not module.__file__ or not Path(module.__file__).resolve().is_relative_to(
site.resolve()
):
raise RuntimeError(f"{name} is shadowed by a platform/host package")
from vocalinux.utils.pywhispercpp_loader import preload_shared_libraries
preload_shared_libraries()
for name in (
"pywhispercpp.model",
"vocalinux.main",
"vocalinux.ui.tray_indicator",
"vocalinux.speech_recognition.recognition_manager",
"vocalinux.text_injection.text_injector",
):
# nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import -- names come from the literal tuple above
importlib.import_module(name)
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa: F401
if Path(gi.__file__).resolve().is_relative_to(site.resolve()):
raise RuntimeError("The Snap bundled GI instead of using the GNOME platform")
print("PASS: locked Snap Python versions, native modules and GNOME GTK import")
def main() -> None:
"""Record build inputs or check the installed, confined artifact."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
parser.add_argument("--record", type=Path)
args = parser.parse_args()
try:
if args.record:
record(args.root, args.record)
else:
smoke(args.root)
except (OSError, ValueError, KeyError, RuntimeError, ImportError) as error:
parser.exit(1, f"Snap runtime verification failed: {error}\n")
if __name__ == "__main__":
main()