refactor(setup): profile-based configuration
This commit is contained in:
@@ -64,9 +64,7 @@ class Apt(PackageManager):
|
|||||||
return ["sudo", "apt", "install"]
|
return ["sudo", "apt", "install"]
|
||||||
|
|
||||||
def is_installed(self, name: str) -> bool:
|
def is_installed(self, name: str) -> bool:
|
||||||
r = subprocess.run(
|
r = subprocess.run([*self.check_cmd, name], capture_output=True, text=True)
|
||||||
[*self.check_cmd, name], capture_output=True, text=True
|
|
||||||
)
|
|
||||||
return r.returncode == 0 and r.stdout.strip() == "install ok installed"
|
return r.returncode == 0 and r.stdout.strip() == "install ok installed"
|
||||||
|
|
||||||
|
|
||||||
@@ -89,21 +87,54 @@ class Pkg:
|
|||||||
return (self.distro_names or {}).get(distro, self.default_name)
|
return (self.distro_names or {}).get(distro, self.default_name)
|
||||||
|
|
||||||
|
|
||||||
PKGS: list[Pkg] = [
|
@dataclass(frozen=True)
|
||||||
Pkg("foot"),
|
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
|
||||||
|
|
||||||
|
|
||||||
|
PROFILES: dict[str, Profile] = {
|
||||||
|
"base": Profile(
|
||||||
|
pkgs=[
|
||||||
Pkg("jq"),
|
Pkg("jq"),
|
||||||
Pkg("lf"),
|
Pkg("lf"),
|
||||||
Pkg("tmux"),
|
Pkg("tmux"),
|
||||||
Pkg("vim"),
|
Pkg("vim"),
|
||||||
Pkg("zsh"),
|
Pkg("zsh"),
|
||||||
]
|
],
|
||||||
|
symlinks=[
|
||||||
|
".config/fish",
|
||||||
SYMLINKS: list[str] = [
|
".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/alacritty",
|
||||||
".config/dolphinrc",
|
".config/dolphinrc",
|
||||||
".config/dunst",
|
".config/dunst",
|
||||||
".config/fish",
|
|
||||||
".config/foot",
|
".config/foot",
|
||||||
".config/fontconfig",
|
".config/fontconfig",
|
||||||
".config/frogminer",
|
".config/frogminer",
|
||||||
@@ -114,56 +145,80 @@ SYMLINKS: list[str] = [
|
|||||||
".config/kglobalshortcutsrc",
|
".config/kglobalshortcutsrc",
|
||||||
".config/klipperrc",
|
".config/klipperrc",
|
||||||
".config/kwinrc",
|
".config/kwinrc",
|
||||||
".config/lf",
|
|
||||||
".config/mimeapps.list",
|
".config/mimeapps.list",
|
||||||
".config/opencode",
|
|
||||||
".config/picom",
|
".config/picom",
|
||||||
".config/pipewire",
|
".config/pipewire",
|
||||||
".config/rofi",
|
".config/rofi",
|
||||||
".config/strawberry",
|
".config/strawberry",
|
||||||
".config/tmux",
|
|
||||||
".config/vlc",
|
".config/vlc",
|
||||||
".config/wezterm",
|
".config/wezterm",
|
||||||
".config/wireplumber",
|
".config/wireplumber",
|
||||||
".config/yay",
|
|
||||||
".config/zed",
|
".config/zed",
|
||||||
".local/share/dbus-1",
|
".local/share/dbus-1",
|
||||||
".local/share/easyeffects",
|
".local/share/easyeffects",
|
||||||
".local/share/fonts",
|
".local/share/fonts",
|
||||||
".local/share/konsole",
|
".local/share/konsole",
|
||||||
".gdbinit",
|
|
||||||
".vimrc",
|
|
||||||
".xinit-scripts",
|
".xinit-scripts",
|
||||||
".xinitrc",
|
".xinitrc",
|
||||||
".Xresources",
|
".Xresources",
|
||||||
]
|
],
|
||||||
|
copies=[
|
||||||
|
|
||||||
COPIES: list[str] = [
|
|
||||||
".claude/settings.json",
|
|
||||||
".codex/config.toml",
|
|
||||||
".config/gtk-3.0/bookmarks",
|
".config/gtk-3.0/bookmarks",
|
||||||
".config/gtk-3.0/settings.ini",
|
".config/gtk-3.0/settings.ini",
|
||||||
".config/gtk-4.0/settings.ini",
|
".config/gtk-4.0/settings.ini",
|
||||||
".gtkrc-2.0",
|
".gtkrc-2.0",
|
||||||
]
|
],
|
||||||
|
),
|
||||||
|
"gaming": Profile(
|
||||||
SYSTEM_INSTALLS: list[str] = [
|
pkgs=[
|
||||||
"sysctl.d/99-gaming-perf.conf",
|
Pkg("steam"),
|
||||||
"sysctl.d/99-network.conf",
|
],
|
||||||
"tmpfiles.d/99-gaming-perf.conf",
|
system_installs=[
|
||||||
"udev/rules.d/99-perf.rules",
|
"etc/sysctl.d/99-gaming-perf.conf",
|
||||||
"ssh/sshd_config.d/sshd_harden.conf",
|
"etc/tmpfiles.d/99-gaming-perf.conf",
|
||||||
]
|
"etc/udev/rules.d/99-perf.rules",
|
||||||
|
],
|
||||||
|
),
|
||||||
# repo-relative path -> $HOME-relative path
|
|
||||||
SYMLINK_MAP: dict[str, str] = {
|
|
||||||
"zsh/rc": ".zshrc",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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:
|
class Context:
|
||||||
def __init__(self, args: argparse.Namespace) -> None:
|
def __init__(self, args: argparse.Namespace) -> None:
|
||||||
self.force: bool = args.force
|
self.force: bool = args.force
|
||||||
@@ -192,12 +247,15 @@ def prompt_yes_no(question: str, default: bool = False) -> bool:
|
|||||||
return default
|
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:
|
if PM is None:
|
||||||
ctx.error(f"don't know how to manage packages on distro {DISTRO!r}")
|
ctx.error(f"don't know how to manage packages on distro {DISTRO!r}")
|
||||||
return
|
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:
|
if not missing:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -239,10 +297,12 @@ def remove_symlink(ctx: Context, rel_dst: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_all_symlinks(ctx: Context) -> None:
|
def remove_all_symlinks(
|
||||||
for rel in SYMLINKS:
|
ctx: Context, symlinks: list[str], symlink_map: dict[str, str]
|
||||||
|
) -> None:
|
||||||
|
for rel in symlinks:
|
||||||
remove_symlink(ctx, rel)
|
remove_symlink(ctx, rel)
|
||||||
for rel_dst in SYMLINK_MAP.values():
|
for rel_dst in symlink_map.values():
|
||||||
remove_symlink(ctx, rel_dst)
|
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:
|
def install_system_file(ctx: Context, rel: str) -> None:
|
||||||
src = SCRIPT_DIR / "etc" / rel
|
src = SCRIPT_DIR / rel
|
||||||
dst = Path("/etc") / rel
|
dst = Path("/") / rel
|
||||||
|
|
||||||
if not src.exists():
|
if not src.exists():
|
||||||
ctx.error(f"the following source path does not exist: {src}")
|
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}")
|
ctx.error(f"system file already exists and differs: {dst}")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Installing: etc/{rel} -> {dst}")
|
print(f"Installing: {rel} -> {dst}")
|
||||||
subprocess.run(["sudo", "install", "-m", "644", str(src), str(dst)], check=True)
|
subprocess.run(["sudo", "install", "-m", "644", str(src), str(dst)], check=True)
|
||||||
|
|
||||||
|
|
||||||
def create_all_symlinks(ctx: Context) -> None:
|
def create_all_symlinks(
|
||||||
for rel in SYMLINKS:
|
ctx: Context, symlinks: list[str], symlink_map: dict[str, str]
|
||||||
|
) -> None:
|
||||||
|
for rel in symlinks:
|
||||||
create_symlink(ctx, rel, rel)
|
create_symlink(ctx, rel, rel)
|
||||||
for src, dst in SYMLINK_MAP.items():
|
for src, dst in symlink_map.items():
|
||||||
create_symlink(ctx, src, dst)
|
create_symlink(ctx, src, dst)
|
||||||
|
|
||||||
|
|
||||||
def copy_all_items(ctx: Context) -> None:
|
def copy_all_items(ctx: Context, rels: list[str]) -> None:
|
||||||
for rel in COPIES:
|
for rel in rels:
|
||||||
copy_item(ctx, rel, rel)
|
copy_item(ctx, rel, rel)
|
||||||
|
|
||||||
|
|
||||||
def install_all_system_files(ctx: Context) -> None:
|
def install_all_system_files(ctx: Context, rels: list[str]) -> None:
|
||||||
if not SYSTEM_INSTALLS:
|
if not rels:
|
||||||
return
|
return
|
||||||
subprocess.run(["sudo", "-v"], check=True)
|
subprocess.run(["sudo", "-v"], check=True)
|
||||||
for rel in SYSTEM_INSTALLS:
|
for rel in rels:
|
||||||
install_system_file(ctx, rel)
|
install_system_file(ctx, rel)
|
||||||
|
|
||||||
|
|
||||||
@@ -407,21 +469,29 @@ def parse_args() -> argparse.Namespace:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Show unified diff when a managed file differs",
|
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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
ctx = Context(args)
|
ctx = Context(args)
|
||||||
|
resolved = resolve_profiles(ctx, ["base", *args.profiles])
|
||||||
|
|
||||||
if args.remove_existing:
|
if args.remove_existing:
|
||||||
remove_all_symlinks(ctx)
|
remove_all_symlinks(ctx, resolved.symlinks, resolved.symlink_map)
|
||||||
else:
|
else:
|
||||||
check_terminfo(ctx)
|
check_terminfo(ctx)
|
||||||
check_packages_installed(ctx)
|
check_packages_installed(ctx, resolved.pkgs)
|
||||||
create_all_symlinks(ctx)
|
create_all_symlinks(ctx, resolved.symlinks, resolved.symlink_map)
|
||||||
copy_all_items(ctx)
|
copy_all_items(ctx, resolved.copies)
|
||||||
install_all_system_files(ctx)
|
install_all_system_files(ctx, resolved.system_installs)
|
||||||
|
|
||||||
return 1 if ctx.errors else 0
|
return 1 if ctx.errors else 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user