From 040e219c2125c87e43989368937ab882c2e00174 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Fri, 24 Apr 2026 00:41:06 +0200 Subject: [PATCH] refactor(setup): profile-based configuration --- setup.py | 258 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 164 insertions(+), 94 deletions(-) diff --git a/setup.py b/setup.py index 87cac80..29dff6c 100755 --- a/setup.py +++ b/setup.py @@ -64,9 +64,7 @@ class Apt(PackageManager): return ["sudo", "apt", "install"] def is_installed(self, name: str) -> bool: - r = subprocess.run( - [*self.check_cmd, name], capture_output=True, text=True - ) + r = subprocess.run([*self.check_cmd, name], capture_output=True, text=True) return r.returncode == 0 and r.stdout.strip() == "install ok installed" @@ -89,81 +87,138 @@ class Pkg: return (self.distro_names or {}).get(distro, self.default_name) -PKGS: list[Pkg] = [ - Pkg("foot"), - Pkg("jq"), - Pkg("lf"), - Pkg("tmux"), - Pkg("vim"), - Pkg("zsh"), -] +@dataclass(frozen=True) +class Profile: + pkgs: list[Pkg] | None = None + symlinks: list[str] | None = None + # repo-relative path -> $HOME-relative path + symlink_map: dict[str, str] | None = None + copies: list[str] | None = None + system_installs: list[str] | None = None -SYMLINKS: list[str] = [ - ".config/alacritty", - ".config/dolphinrc", - ".config/dunst", - ".config/fish", - ".config/foot", - ".config/fontconfig", - ".config/frogminer", - ".config/ghostty", - ".config/i3", - ".config/i3status", - ".config/kde-mimeapps.list", - ".config/kglobalshortcutsrc", - ".config/klipperrc", - ".config/kwinrc", - ".config/lf", - ".config/mimeapps.list", - ".config/opencode", - ".config/picom", - ".config/pipewire", - ".config/rofi", - ".config/strawberry", - ".config/tmux", - ".config/vlc", - ".config/wezterm", - ".config/wireplumber", - ".config/yay", - ".config/zed", - ".local/share/dbus-1", - ".local/share/easyeffects", - ".local/share/fonts", - ".local/share/konsole", - ".gdbinit", - ".vimrc", - ".xinit-scripts", - ".xinitrc", - ".Xresources", -] - - -COPIES: list[str] = [ - ".claude/settings.json", - ".codex/config.toml", - ".config/gtk-3.0/bookmarks", - ".config/gtk-3.0/settings.ini", - ".config/gtk-4.0/settings.ini", - ".gtkrc-2.0", -] - - -SYSTEM_INSTALLS: list[str] = [ - "sysctl.d/99-gaming-perf.conf", - "sysctl.d/99-network.conf", - "tmpfiles.d/99-gaming-perf.conf", - "udev/rules.d/99-perf.rules", - "ssh/sshd_config.d/sshd_harden.conf", -] - - -# repo-relative path -> $HOME-relative path -SYMLINK_MAP: dict[str, str] = { - "zsh/rc": ".zshrc", +PROFILES: dict[str, Profile] = { + "base": Profile( + pkgs=[ + Pkg("jq"), + Pkg("lf"), + Pkg("tmux"), + Pkg("vim"), + Pkg("zsh"), + ], + symlinks=[ + ".config/fish", + ".config/lf", + ".config/opencode", + ".config/tmux", + ".config/yay", + ".gdbinit", + ".vimrc", + ], + symlink_map={"zsh/rc": ".zshrc"}, + copies=[ + ".claude/settings.json", + ".codex/config.toml", + ], + system_installs=[ + "etc/sysctl.d/99-network.conf", + "etc/ssh/sshd_config.d/sshd_harden.conf", + ], + ), + "desktop": Profile( + pkgs=[ + Pkg("foot"), + Pkg("firefox"), + Pkg("mupdf"), + ], + symlinks=[ + ".config/alacritty", + ".config/dolphinrc", + ".config/dunst", + ".config/foot", + ".config/fontconfig", + ".config/frogminer", + ".config/ghostty", + ".config/i3", + ".config/i3status", + ".config/kde-mimeapps.list", + ".config/kglobalshortcutsrc", + ".config/klipperrc", + ".config/kwinrc", + ".config/mimeapps.list", + ".config/picom", + ".config/pipewire", + ".config/rofi", + ".config/strawberry", + ".config/vlc", + ".config/wezterm", + ".config/wireplumber", + ".config/zed", + ".local/share/dbus-1", + ".local/share/easyeffects", + ".local/share/fonts", + ".local/share/konsole", + ".xinit-scripts", + ".xinitrc", + ".Xresources", + ], + copies=[ + ".config/gtk-3.0/bookmarks", + ".config/gtk-3.0/settings.ini", + ".config/gtk-4.0/settings.ini", + ".gtkrc-2.0", + ], + ), + "gaming": Profile( + pkgs=[ + Pkg("steam"), + ], + system_installs=[ + "etc/sysctl.d/99-gaming-perf.conf", + "etc/tmpfiles.d/99-gaming-perf.conf", + "etc/udev/rules.d/99-perf.rules", + ], + ), } +@dataclass(frozen=True) +class Resolved: + pkgs: list[Pkg] + symlinks: list[str] + symlink_map: dict[str, str] + copies: list[str] + system_installs: list[str] + + +def resolve_profiles(ctx: Context, profile_names: list[str]) -> Resolved: + pkgs: list[Pkg] = [] + symlinks: list[str] = [] + symlink_map: dict[str, str] = {} + copies: list[str] = [] + system_installs: list[str] = [] + seen: set[str] = set() + for name in profile_names: + if name in seen: + continue + seen.add(name) + profile = PROFILES.get(name) + if profile is None: + ctx.error(f"unknown profile: {name!r}") + continue + if profile.pkgs: + pkgs.extend(profile.pkgs) + if profile.symlinks: + symlinks.extend(profile.symlinks) + if profile.symlink_map: + symlink_map.update(profile.symlink_map) + if profile.copies: + copies.extend(profile.copies) + if profile.system_installs: + system_installs.extend(profile.system_installs) + return Resolved(pkgs, symlinks, symlink_map, copies, system_installs) + + class Context: def __init__(self, args: argparse.Namespace) -> None: self.force: bool = args.force @@ -192,12 +247,15 @@ def prompt_yes_no(question: str, default: bool = False) -> bool: return default -def check_packages_installed(ctx: Context) -> None: +def check_packages_installed(ctx: Context, pkgs: list[Pkg]) -> None: + if not pkgs: + return + if PM is None: ctx.error(f"don't know how to manage packages on distro {DISTRO!r}") return - missing = [name for pkg in PKGS if not PM.is_installed(name := pkg.name(DISTRO))] + missing = [name for pkg in pkgs if not PM.is_installed(name := pkg.name(DISTRO))] if not missing: return @@ -239,10 +297,12 @@ def remove_symlink(ctx: Context, rel_dst: str) -> None: ) -def remove_all_symlinks(ctx: Context) -> None: - for rel in SYMLINKS: +def remove_all_symlinks( + ctx: Context, symlinks: list[str], symlink_map: dict[str, str] +) -> None: + for rel in symlinks: remove_symlink(ctx, rel) - for rel_dst in SYMLINK_MAP.values(): + for rel_dst in symlink_map.values(): remove_symlink(ctx, rel_dst) @@ -329,8 +389,8 @@ def _sudo_cmp(a: Path, b: Path) -> bool: def install_system_file(ctx: Context, rel: str) -> None: - src = SCRIPT_DIR / "etc" / rel - dst = Path("/etc") / rel + src = SCRIPT_DIR / rel + dst = Path("/") / rel if not src.exists(): ctx.error(f"the following source path does not exist: {src}") @@ -355,27 +415,29 @@ def install_system_file(ctx: Context, rel: str) -> None: ctx.error(f"system file already exists and differs: {dst}") return - print(f"Installing: etc/{rel} -> {dst}") + print(f"Installing: {rel} -> {dst}") subprocess.run(["sudo", "install", "-m", "644", str(src), str(dst)], check=True) -def create_all_symlinks(ctx: Context) -> None: - for rel in SYMLINKS: +def create_all_symlinks( + ctx: Context, symlinks: list[str], symlink_map: dict[str, str] +) -> None: + for rel in symlinks: create_symlink(ctx, rel, rel) - for src, dst in SYMLINK_MAP.items(): + for src, dst in symlink_map.items(): create_symlink(ctx, src, dst) -def copy_all_items(ctx: Context) -> None: - for rel in COPIES: +def copy_all_items(ctx: Context, rels: list[str]) -> None: + for rel in rels: copy_item(ctx, rel, rel) -def install_all_system_files(ctx: Context) -> None: - if not SYSTEM_INSTALLS: +def install_all_system_files(ctx: Context, rels: list[str]) -> None: + if not rels: return subprocess.run(["sudo", "-v"], check=True) - for rel in SYSTEM_INSTALLS: + for rel in rels: install_system_file(ctx, rel) @@ -407,21 +469,29 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Show unified diff when a managed file differs", ) + extras = sorted(PROFILES.keys() - {"base"}) + parser.add_argument( + "profiles", + nargs="*", + metavar="PROFILE", + help=f"Extra profiles to apply on top of base (available: {', '.join(extras)})", + ) return parser.parse_args() def main() -> int: args = parse_args() ctx = Context(args) + resolved = resolve_profiles(ctx, ["base", *args.profiles]) if args.remove_existing: - remove_all_symlinks(ctx) + remove_all_symlinks(ctx, resolved.symlinks, resolved.symlink_map) else: check_terminfo(ctx) - check_packages_installed(ctx) - create_all_symlinks(ctx) - copy_all_items(ctx) - install_all_system_files(ctx) + check_packages_installed(ctx, resolved.pkgs) + create_all_symlinks(ctx, resolved.symlinks, resolved.symlink_map) + copy_all_items(ctx, resolved.copies) + install_all_system_files(ctx, resolved.system_installs) return 1 if ctx.errors else 0