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
+6
View File
@@ -295,6 +295,12 @@ def desktop_system() -> list[SystemConcern | UserConfigConcern]:
"config/system/desktop/network/NetworkManager.conf"
),
"/etc/sddm.conf.d/10-theme.conf": "config/system/desktop/sddm/10-theme.conf",
"/usr/share/sddm/themes/noctalia/Assets/ember-oled-background.svg": (
"config/system/desktop/sddm/ember-oled-background.svg"
),
"/usr/share/sddm/themes/noctalia/theme.conf.user": (
"config/system/desktop/sddm/theme.conf.user"
),
"/etc/udev/rules.d/50-zsa.rules": "config/system/desktop/input/50-zsa.rules",
},
system_manifests=("desktop/hardware-system.txt",),
+103
View File
@@ -0,0 +1,103 @@
"""Declarative global npm command packages for Decman profiles."""
from __future__ import annotations
import json
import shutil
import decman
import decman.core.error as errors
import decman.core.module as module
import decman.core.output as output
import decman.core.store as store_module
from decman import Plugin
from decman.plugins import run_methods_with_attribute
def packages(fn):
"""Annotate a method that returns globally installed npm package names."""
fn.__npm_packages__ = True
return fn
class Npm(Plugin):
"""Install declared global npm packages without pruning unrelated packages."""
NAME = "npm"
def __init__(self) -> None:
self.packages: set[str] = set()
def process_modules(
self, store: store_module.Store, modules: list[module.Module]
) -> None:
store.ensure("npm_packages_for_module", {})
for mod in modules:
store["npm_packages_for_module"].setdefault(mod.name, set())
declared = set().union(
*run_methods_with_attribute(mod, "__npm_packages__")
)
if store["npm_packages_for_module"][mod.name] != declared:
mod._changed = True
output.print_debug(
f"Module '{mod.name}' changed due to modified global npm packages."
)
self.packages |= declared
store["npm_packages_for_module"][mod.name] = declared
def apply(
self,
store: store_module.Store,
dry_run: bool = False,
params: list[str] | None = None,
) -> bool:
if not self.packages:
return True
if shutil.which("npm") is None:
if dry_run:
output.print_list(
"Installing global npm packages after npm becomes available:",
sorted(self.packages),
)
return True
output.print_error("npm is unavailable after the pacman step.")
return False
try:
installed = self._installed_packages()
to_install = self.packages - installed
output.print_list("Installing global npm packages:", sorted(to_install))
if to_install and not dry_run:
decman.prg(
[
"npm",
"install",
"--global",
"--no-audit",
"--no-fund",
*sorted(to_install),
]
)
except (errors.CommandFailedError, ValueError) as error:
output.print_error(f"Managing global npm packages failed: {error}")
return False
return True
@staticmethod
def _installed_packages() -> set[str]:
raw = decman.prg(
["npm", "list", "--global", "--depth=0", "--json"], pty=False
)
try:
result = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError("npm returned invalid JSON") from error
dependencies = result.get("dependencies", {})
if not isinstance(dependencies, dict):
raise ValueError("npm dependencies must be an object")
return set(dependencies)
+7
View File
@@ -7,6 +7,8 @@ from pathlib import Path
import decman
from decman.plugins import aur, pacman
from modules import npm_packages
ROOT = Path(__file__).resolve().parents[1]
PACKAGES = ROOT / "packages"
@@ -34,6 +36,11 @@ class PackageConcern(decman.Module):
def aur_packages(self) -> set[str]:
return _names(PACKAGES / f"{self.concern}-aur.txt")
@npm_packages.packages
def global_npm_packages(self) -> set[str]:
path = PACKAGES / f"{self.concern}-npm.txt"
return _names(path) if path.exists() else set()
def configure_no_removal_guard() -> None:
"""Keep Decman from pruning current dependency-orphans during adoption."""