feat(decman): manage npm packages and deploy sddm theme assets

This commit is contained in:
2026-08-27 15:50:31 +02:00
parent b6ae6887a3
commit cd9b50feae
10 changed files with 212 additions and 14 deletions
+85 -9
View File
@@ -3,8 +3,10 @@
from __future__ import annotations
import json
import os
import runpy
import shutil
import subprocess
import sys
from collections import Counter
@@ -32,38 +34,100 @@ def actual(command: str) -> set[str]:
)
def npm_actual() -> set[str]:
if shutil.which("npm") is None:
return set()
raw = subprocess.run(
["npm", "list", "--global", "--depth=0", "--json"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout
try:
result = json.loads(raw)
except json.JSONDecodeError as error:
raise SystemExit("npm returned invalid JSON") from error
dependencies = result.get("dependencies", {})
if not isinstance(dependencies, dict):
raise SystemExit("npm dependencies must be an object")
return set(dependencies)
def optional_lines(path: Path) -> set[str]:
return lines(path) if path.exists() else set()
def main() -> None:
repo_files = list(PACKAGES.glob("**/*-repo.txt"))
aur_files = list(PACKAGES.glob("**/*-aur.txt"))
npm_files = list(PACKAGES.glob("**/*-npm.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)]
npm = [name for path in npm_files for name in lines(path)]
duplicate = {name for name, count in Counter(repo + aur).items() if count > 1}
duplicate_npm = {name for name, count in Counter(npm).items() if count > 1}
if duplicate:
raise SystemExit(f"duplicate package ownership: {sorted(duplicate)}")
if duplicate_npm:
raise SystemExit(f"duplicate npm package ownership: {sorted(duplicate_npm)}")
installed_repo = actual("-Qqen")
installed_aur = actual("-Qqem")
installed_npm = npm_actual()
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))
import decman
runpy.run_path(str(ROOT / "source.py"))
active_concerns = {
concern
for module in decman.modules
if isinstance((concern := getattr(module, "concern", None)), str)
}
expected_repo = set().union(
*(lines(PACKAGES / f"{concern}-repo.txt") for concern in active_concerns)
)
expected_aur = set().union(
*(lines(PACKAGES / f"{concern}-aur.txt") for concern in active_concerns)
)
expected_npm = set().union(
*(optional_lines(PACKAGES / f"{concern}-npm.txt") for concern in active_concerns)
)
loaded_repo: set[str] = set()
loaded_aur: set[str] = set()
loaded_npm: set[str] = set()
for module in decman.modules:
native = getattr(module, "native_packages", None)
foreign = getattr(module, "aur_packages", None)
global_npm = getattr(module, "global_npm_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")
if callable(global_npm):
loaded_npm |= cast(Callable[[], set[str]], global_npm)()
if (
loaded_repo != expected_repo
or loaded_aur != expected_aur
or loaded_npm != expected_npm
):
raise SystemExit("active profile does not compose its package concerns")
system_manifests = {
name
for module in decman.modules
for name in getattr(module, "_system_manifests", ())
}
user_manifests = {
name
for module in decman.modules
for name in getattr(module, "_user_manifests", ())
}
expected_system = set().union(
*(lines(path) for path in (ROOT / "manifests").glob("**/*-system.txt"))
*(lines(ROOT / "manifests" / name) for name in system_manifests)
)
expected_user = set().union(
*(lines(path) for path in (ROOT / "manifests").glob("**/*-user.txt"))
*(lines(ROOT / "manifests" / name) for name in user_manifests)
)
loaded_system: set[str] = set()
loaded_user: set[str] = set()
@@ -77,15 +141,27 @@ def main() -> None:
"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))
raise SystemExit("active profile does not compose its unit manifests")
host_scopes = active_concerns & {"laptop/hardware", "desktop/local"}
if len(host_scopes) != 1:
raise SystemExit(f"active profile has ambiguous host concerns: {host_scopes}")
profile = next(iter(host_scopes)).split("/", maxsplit=1)[0]
pending_remove = sorted(
(installed_repo - expected_repo) | (installed_aur - expected_aur)
)
pending_install = sorted(
(expected_repo - installed_repo) | (expected_aur - installed_aur)
)
pending_npm_install = sorted(expected_npm - installed_npm)
print(
f"OK: repo={len(repo)} aur={len(aur)} system={len(loaded_system)} "
f"user={len(loaded_user)} concerns={len(repo_files)} profile=laptop"
f"OK: repo={len(expected_repo)} aur={len(expected_aur)} "
f"npm={len(expected_npm)} system={len(loaded_system)} "
f"user={len(loaded_user)} concerns={len(active_concerns)} profile={profile}"
)
print(f"PENDING REMOVE: {' '.join(pending_remove) or 'none'}")
print(f"PENDING INSTALL: {' '.join(pending_install) or 'none'}")
print(f"PENDING NPM INSTALL: {' '.join(pending_npm_install) or 'none'}")
if __name__ == "__main__":