#!/bin/bash set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" python3 - "$ROOT" <<'PY' import json import re import sys from pathlib import Path root = Path(sys.argv[1]) control = (root / "packaging/debian/control.template").read_text() postinst = (root / "packaging/debian/postinst").read_text() provision = (root / "scripts/classroom-provision").read_text() units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service")) canary = (root / "update_canary.sh").read_text() validation_script = (root / "tools/validate-trixie-release.sh").read_text() layout = (root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js").read_text() rules = (root / "config/plasma/xdg/kwinrulesrc").read_text() restrictions = (root / "config/plasma/xdg/kdeglobals").read_text() supervision = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text() build = (root / "tools/build-release.sh").read_text() policy = json.loads( (root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text() ) passed = failed = 0 def check(label, condition): global passed, failed if condition: print(f" ok {label}") passed += 1 else: print(f" FAIL {label}") failed += 1 depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ") print("1. Trixie package contract") for package in ( "network-manager", "cups", "cups-client", "cups-filters", "foomatic-db-compressed-ppds", ): check(f"depends on {package}", package in depends) print() print("2. networking service behavior") check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst) check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst) check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst) check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists()) print() print("3. printing service behavior") check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst) check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst) check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst) check("Avahi service and socket are disabled", "systemctl disable --now avahi-daemon.socket avahi-daemon.service || true" in postinst) check("Avahi service and socket are masked", "systemctl mask avahi-daemon.socket avahi-daemon.service || true" in postinst) check("printer setup is not a blocking boot dependency", "classroom-printer-setup" not in units) check("printer setup runs automatically in provisioning", "/usr/local/bin/classroom-printer-setup" in provision) check("printer provisioning is bounded", "--kill-after=5s 30s" in provision) check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True) print() print("4. secondary windows are the window manager's business") # Chrome gives its main window and every window a page opens the same # class, so a rule keyed on that class maximises the lesson's popup too. # It was measured doing exactly that. The browser is told to start # maximized instead, and no rule mentions it. check("no window rule claims the browser", "google-chrome" not in rules) check("the browser asks for its own first window", "--start-maximized" in (root / "scripts/classroom-browser").read_text()) check("Wonder Ink asks for the same", "--start-maximized" in (root / "scripts/classroom-wonderink").read_text()) check("the Chrome window arbiter is gone", not (root / "scripts/classroom-chrome-windows").exists()) check("and is not packaged", "classroom-chrome-windows" not in build) print() print("4b. the panel is never allowed to stay dead") # Plasma restarts a crashed panel three times a minute and then gives up. # Measured: dead on the third kill. These lines are what make it # unconditional, and losing them would lose the room its only way around. check("restart is unconditional", "Restart=always" in supervision) check("the burst limit is lifted", "StartLimitIntervalSec=0" in supervision) check("the drop-in is packaged", "plasma-plasmashell.service.d" in build) print() print("4b2. the session settings do not fight another package") # plasma-workspace owns /etc/xdg/kdeglobals and friends. dpkg refused the # package when the classroom tried to own them too, so the room's copies # live in a directory of their own, in front of /etc/xdg. check("settings install to the classroom's own directory", "$STAGE/etc/ekklesia-classroom/xdg/" in build) check("nothing is installed into /etc/xdg", "$STAGE/etc/xdg/" not in build) check("and that directory is searched first", "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in (root / "config/environment/90-ekklesia-classroom.conf").read_text()) check("the environment file is packaged", "/etc/environment.d/90-ekklesia-classroom.conf" in build) print() print("4b3. every icon the panel names is in the package") import configparser for entry in sorted((root / "config/plasma/applications").glob("*.desktop")): parser = configparser.ConfigParser(interpolation=None, strict=False) parser.read(entry) icon = parser["Desktop Entry"].get("icon", "") if not icon.startswith("ekklesia-"): continue check(f"{icon} is installed by the package", f'"$ROOT/config/icons/${{icon}}.svg"' in build or icon in build) check(f"{icon}.svg exists", (root / f"config/icons/{icon}.svg").is_file()) print() print("4c. the teacher cannot reach a command line") for restriction in ("run_command=false", "shell_access=false", "action/lock_screen=false", "plasma/plasmashell/unlockedDesktop=false"): check(f"{restriction}", restriction in restrictions) check("the restrictions are immutable", "[KDE Action Restrictions][$i]" in restrictions) print() print("4d. clicking a launcher never hides an application") check("minimise on click is off", 'minimizeActiveTaskOnClick", false' in layout) check("there is no minimise button", "ButtonsOnRight=FX" in (root / "config/plasma/xdg/kwinrc").read_text()) print() print("5. bounded Trixie validation") check("container has an outer deadline", "720s" in validation_script) check("package installs have deadlines", "candidate-install 360" in validation_script) check("container service startup is suppressed", "/usr/sbin/policy-rc.d" in validation_script and "exit 101" in validation_script) print() print("6. canary release gate") validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"') package_copy = canary.index('cp "$package_path"') first_rsync = canary.index("rsync -av") check("Trixie validation runs before the repository copy", validation < package_copy) check("Trixie validation runs before any upload", validation < first_rsync) print() print(f"passed: {passed} failed: {failed}") raise SystemExit(1 if failed else 0) PY