refactor(setup): profile-based configuration
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user