Files
dotfiles/system-config/scripts/verify.py
T
2026-08-13 23:03:55 +02:00

93 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Read-only invariant checks for the active Decman configuration."""
from __future__ import annotations
import os
import runpy
import subprocess
import sys
from collections import Counter
from collections.abc import Callable
from pathlib import Path
from typing import cast
ROOT = Path(__file__).resolve().parents[1]
PACKAGES = ROOT / "packages"
def lines(path: Path) -> set[str]:
return {
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
}
def actual(command: str) -> set[str]:
return set(
subprocess.run(
["pacman", command], check=True, text=True, stdout=subprocess.PIPE
).stdout.split()
)
def main() -> None:
repo_files = list(PACKAGES.glob("**/*-repo.txt"))
aur_files = list(PACKAGES.glob("**/*-aur.txt"))
repo = [name for path in repo_files for name in lines(path)]
aur = [name for path in aur_files for name in lines(path)]
duplicate = {name for name, count in Counter(repo + aur).items() if count > 1}
if duplicate:
raise SystemExit(f"duplicate package ownership: {sorted(duplicate)}")
installed_repo = actual("-Qqen")
installed_aur = actual("-Qqem")
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))
import decman
runpy.run_path(str(ROOT / "source.py"))
loaded_repo: set[str] = set()
loaded_aur: set[str] = set()
for module in decman.modules:
native = getattr(module, "native_packages", None)
foreign = getattr(module, "aur_packages", None)
if callable(native):
loaded_repo |= cast(Callable[[], set[str]], native)()
if callable(foreign):
loaded_aur |= cast(Callable[[], set[str]], foreign)()
if loaded_repo != set(repo) or loaded_aur != set(aur):
raise SystemExit("active profile does not compose the full laptop package set")
expected_system = set().union(
*(lines(path) for path in (ROOT / "manifests").glob("**/*-system.txt"))
)
expected_user = set().union(
*(lines(path) for path in (ROOT / "manifests").glob("**/*-user.txt"))
)
loaded_system: set[str] = set()
loaded_user: set[str] = set()
for module in decman.modules:
system = getattr(module, "system_units", None)
user = getattr(module, "user_units", None)
if callable(system):
loaded_system |= cast(Callable[[], set[str]], system)()
if callable(user):
loaded_user |= cast(Callable[[], dict[str, set[str]]], user)().get(
"alex", set()
)
if loaded_system != expected_system or loaded_user != expected_user:
raise SystemExit("active profile does not compose the complete unit manifests")
pending_remove = sorted((installed_repo - set(repo)) | (installed_aur - set(aur)))
pending_install = sorted((set(repo) - installed_repo) | (set(aur) - installed_aur))
print(
f"OK: repo={len(repo)} aur={len(aur)} system={len(loaded_system)} "
f"user={len(loaded_user)} concerns={len(repo_files)} profile=laptop"
)
print(f"PENDING REMOVE: {' '.join(pending_remove) or 'none'}")
print(f"PENDING INSTALL: {' '.join(pending_install) or 'none'}")
if __name__ == "__main__":
main()