feat: track Decman configuration

This commit is contained in:
2026-08-13 23:03:55 +02:00
parent 34acd648c9
commit f99cfeae79
96 changed files with 7120 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Modules
Only two adapters belong here:
- `packages.py` turns package concern lists into Decman package declarations.
- `host_system.py` deploys reviewed system policy, user configuration, and enabled units for a host.
Keep policy in `packages/`, `config/`, and `manifests/`; keep this directory
thin and reusable. User configuration belongs under `config/user/{common,laptop,desktop}/`
and is installed with owner `alex`.
View File
+238
View File
@@ -0,0 +1,238 @@
"""System files and concern-scoped unit enablement for machine profiles."""
from __future__ import annotations
from pathlib import Path
import decman
from decman.plugins import systemd
ROOT = Path(__file__).resolve().parents[1]
MANIFESTS = ROOT / "manifests"
def _units(*names: str) -> set[str]:
units: set[str] = set()
for name in names:
units |= {
line.strip()
for line in (MANIFESTS / name).read_text(encoding="utf-8").splitlines()
if line.strip()
}
return units
class UserConfigConcern(decman.Module):
"""Home-directory configuration owned by a named local user."""
def __init__(
self,
name: str,
files: dict[str, str] | None = None,
directories: dict[str, str] | None = None,
user: str = "alex",
) -> None:
super().__init__(f"user:{name}")
self._files = files or {}
self._directories = directories or {}
self.user = user
def files(self) -> dict[str, decman.File]:
return {
target: decman.File(
source_file=str(ROOT / source), owner=self.user, permissions=0o644
)
for target, source in self._files.items()
}
def directories(self) -> dict[str, decman.Directory]:
return {
target: decman.Directory(
source_directory=str(ROOT / source),
bin_files=True,
owner=self.user,
permissions=0o644,
)
for target, source in self._directories.items()
}
class SystemConcern(decman.Module):
"""One host/system concern with explicit files and unit manifests."""
def __init__(
self,
name: str,
files: dict[str, str | tuple[str, int]] | None = None,
system_manifests: tuple[str, ...] = (),
user_manifests: tuple[str, ...] = (),
user: str = "alex",
) -> None:
super().__init__(f"system:{name}")
self._files = files or {}
self._system_manifests = system_manifests
self._user_manifests = user_manifests
self.user = user
def files(self) -> dict[str, decman.File]:
return {
target: decman.File(
source_file=str(
ROOT / (source[0] if isinstance(source, tuple) else source)
),
permissions=source[1] if isinstance(source, tuple) else 0o644,
)
for target, source in self._files.items()
}
def on_change(self, store: decman.Store) -> None:
if self.name == "system:locale":
decman.prg(["locale-gen"])
if self.name == "system:boot":
decman.prg(["mkinitcpio", "-P"])
@systemd.units
def system_units(self) -> set[str]:
return _units(*self._system_manifests)
@systemd.user_units
def user_units(self) -> dict[str, set[str]]:
return {self.user: _units(*self._user_manifests)}
def common_user_config() -> list[UserConfigConcern]:
return [
UserConfigConcern(
"common-config",
{
"/home/alex/.config/environment.d/pi-status-bridge.conf": (
"config/user/common/environment.d/pi-status-bridge.conf"
),
"/home/alex/.config/fish/config.fish": "config/user/common/fish/config.fish",
"/home/alex/.config/hypr/hypridle.conf": "config/user/common/hypr/hypridle.conf",
"/home/alex/.config/hypr/hyprland.lua": "config/user/common/hypr/hyprland.lua",
"/home/alex/.config/hypr/hyprlock.conf": "config/user/common/hypr/hyprlock.conf",
"/home/alex/.config/kitty/kitty.conf": "config/user/common/kitty/kitty.conf",
"/home/alex/.config/noctalia/10-base.toml": "config/user/common/noctalia/10-base.toml",
"/home/alex/.config/noctalia/palettes/Ember OLED.json": (
"config/user/common/noctalia/palettes/Ember OLED.json"
),
"/home/alex/.config/systemd/user/idle-inhibitor-monitor.service": (
"config/user/common/systemd/user/idle-inhibitor-monitor.service"
),
"/home/alex/.config/systemd/user/pi-status-bridge.service": (
"config/user/common/systemd/user/pi-status-bridge.service"
),
"/home/alex/.config/systemd/user/ssh-agent.service": (
"config/user/common/systemd/user/ssh-agent.service"
),
},
{"/home/alex/.config/yazi": "config/user/common/yazi"},
)
]
def laptop_user_config() -> list[UserConfigConcern]:
return [
UserConfigConcern(
"laptop-config",
{
"/home/alex/.config/hypr/machine.lua": "config/user/laptop/hypr/machine.lua",
"/home/alex/.config/noctalia/90-machine.toml": (
"config/user/laptop/noctalia/90-machine.toml"
),
"/home/alex/.config/noctalia/99-extend.toml": (
"config/user/laptop/noctalia/99-extend.toml"
),
},
)
]
def desktop_user_config() -> list[UserConfigConcern]:
return [
UserConfigConcern(
"desktop-config",
{
"/home/alex/.config/hypr/machine.lua": "config/user/desktop/hypr/machine.lua",
"/home/alex/.config/noctalia/90-machine.toml": (
"config/user/desktop/noctalia/90-machine.toml"
),
},
)
]
def common_system() -> list[SystemConcern | UserConfigConcern]:
return [
SystemConcern(
"package-policy",
{
"/etc/pacman.conf": "config/system/common/pacman/pacman.conf",
"/etc/makepkg.conf": "config/system/common/makepkg/makepkg.conf",
},
),
SystemConcern(
"locale",
{
"/etc/locale.conf": "config/system/common/locale/locale.conf",
"/etc/locale.gen": "config/system/common/locale/locale.gen",
},
),
SystemConcern(
"keyboard",
{
"/etc/vconsole.conf": "config/system/common/keyboard/vconsole.conf",
"/etc/X11/xorg.conf.d/00-keyboard.conf": "config/system/common/keyboard/x11.conf",
},
),
SystemConcern("base", system_manifests=("common/base-system.txt",)),
SystemConcern("network", system_manifests=("common/network-system.txt",)),
SystemConcern("desktop", user_manifests=("common/desktop-user.txt",)),
SystemConcern("local", user_manifests=("common/local-user.txt",)),
SystemConcern(
"network-services", system_manifests=("common/network-services-system.txt",)
),
SystemConcern(
"development", system_manifests=("common/development-system.txt",)
),
SystemConcern("privacy", system_manifests=("common/privacy-system.txt",)),
*common_user_config(),
]
def laptop_system() -> list[SystemConcern | UserConfigConcern]:
return [
*common_system(),
SystemConcern(
"laptop",
{
"/etc/keyd/default.conf": "config/system/common/keyd.conf",
"/etc/NetworkManager/conf.d/wifi-powersave.conf": "config/system/laptop/wifi-powersave.conf",
"/etc/modprobe.d/iwlwifi.conf": "config/system/laptop/iwlwifi.conf",
"/etc/X11/xorg.conf.d/30-touchpad.conf": "config/system/laptop/touchpad.conf",
"/etc/tlp.conf": "config/system/laptop/power/tlp.conf",
"/etc/systemd/sleep.conf.d/90-laptop.conf": "config/system/laptop/sleep.conf.d/90-laptop.conf",
"/etc/systemd/logind.conf.d/90-noctalia-battery-idle.conf": "config/system/laptop/systemd/90-noctalia-battery-idle.conf",
"/boot/EFI/refind/refind.conf": (
"config/system/laptop/refind/refind.conf",
0o755,
),
"/boot/refind_linux.conf": (
"config/system/laptop/refind/refind_linux.conf",
0o755,
),
},
system_manifests=("laptop/hardware-system.txt",),
),
SystemConcern(
"boot",
{"/etc/mkinitcpio.conf": "config/system/laptop/boot/mkinitcpio.conf"},
),
*laptop_user_config(),
]
def desktop_system() -> list[SystemConcern | UserConfigConcern]:
"""Desktop: shared policy plus desktop-specific home configuration."""
return [*common_system(), *desktop_user_config()]
+43
View File
@@ -0,0 +1,43 @@
"""Package concern loading for Decman profiles."""
from __future__ import annotations
from pathlib import Path
import decman
from decman.plugins import aur, pacman
ROOT = Path(__file__).resolve().parents[1]
PACKAGES = ROOT / "packages"
def _names(path: Path) -> set[str]:
return {
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
}
class PackageConcern(decman.Module):
"""One named concern backed by native and AUR package lists."""
def __init__(self, concern: str) -> None:
super().__init__(f"packages:{concern}")
self.concern = concern
@pacman.packages
def native_packages(self) -> set[str]:
return _names(PACKAGES / f"{self.concern}-repo.txt")
@aur.packages
def aur_packages(self) -> set[str]:
return _names(PACKAGES / f"{self.concern}-aur.txt")
def configure_no_removal_guard() -> None:
"""Keep Decman from pruning current dependency-orphans during adoption."""
if decman.pacman is None or decman.aur is None:
raise decman.SourceError("pacman and AUR plugins must be available")
decman.pacman.ignored_packages |= _names(PACKAGES / "ignored/repo-dependencies.txt")
decman.aur.ignored_packages |= _names(PACKAGES / "ignored/aur-dependencies.txt")