feat(setup): per-distro package detection and install prompt

This commit is contained in:
2026-04-24 00:08:25 +02:00
parent 781e8c8686
commit 769fbc1181
+112 -20
View File
@@ -6,32 +6,94 @@ from __future__ import annotations
import argparse import argparse
import filecmp import filecmp
import platform import platform
import shlex
import shutil import shutil
import subprocess import subprocess
import sys import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
HOME = Path.home() HOME = Path.home()
DISTRO: str = platform.freedesktop_os_release().get("ID", "")
# Populated now so per-distro package tracking can key off it.
try:
DISTRO: str = platform.freedesktop_os_release().get("NAME", "")
except OSError:
DISTRO = ""
PKGS: list[str] = [ class PackageManager(ABC):
"alacritty", @property
"i3", @abstractmethod
"i3lock", def check_cmd(self) -> list[str]: ...
"i3status",
"jq", @property
"lf", @abstractmethod
"rofi", def install_cmd(self) -> list[str]: ...
"startx",
"tmux", def is_installed(self, name: str) -> bool:
"zsh", return (
subprocess.run(
[*self.check_cmd, name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
def install(self, names: list[str]) -> None:
subprocess.run([*self.install_cmd, *names], check=True)
class Pacman(PackageManager):
@property
def check_cmd(self) -> list[str]:
return ["pacman", "-Q"]
@property
def install_cmd(self) -> list[str]:
return ["sudo", "pacman", "-S", "--needed"]
class Apt(PackageManager):
@property
def check_cmd(self) -> list[str]:
return ["dpkg-query", "-W", "-f=${Status}"]
@property
def install_cmd(self) -> list[str]:
return ["sudo", "apt", "install"]
def is_installed(self, name: str) -> bool:
r = subprocess.run(
[*self.check_cmd, name], capture_output=True, text=True
)
return r.returncode == 0 and r.stdout.strip() == "install ok installed"
_apt = Apt()
PACKAGE_MANAGERS: dict[str, PackageManager] = {
"arch": Pacman(),
"ubuntu": _apt,
"debian": _apt,
}
PM: PackageManager | None = PACKAGE_MANAGERS.get(DISTRO)
@dataclass(frozen=True)
class Pkg:
default_name: str
distro_names: dict[str, str] | None = None
def name(self, distro: str) -> str:
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"),
] ]
@@ -114,10 +176,40 @@ class Context:
print(f"Error: {msg}", file=sys.stderr) print(f"Error: {msg}", file=sys.stderr)
def prompt_yes_no(question: str, default: bool = False) -> bool:
if not sys.stdin.isatty():
return default
suffix = " [Y/n] " if default else " [y/N] "
try:
answer = input(question + suffix).strip().lower()
except EOFError:
return default
if answer in ("y", "yes"):
return True
if answer in ("n", "no"):
return False
return default
def check_packages_installed(ctx: Context) -> None: def check_packages_installed(ctx: Context) -> None:
for pkg in PKGS: if PM is None:
if shutil.which(pkg) is None: ctx.error(f"don't know how to manage packages on distro {DISTRO!r}")
ctx.error(f"missing {pkg}") return
missing = [name for pkg in PKGS if not PM.is_installed(name := pkg.name(DISTRO))]
if not missing:
return
print(f"Missing packages: {', '.join(missing)}")
if not prompt_yes_no(f"Install with `{shlex.join([*PM.install_cmd, *missing])}`?"):
ctx.error(f"packages not installed: {', '.join(missing)}")
return
try:
PM.install(missing)
except subprocess.CalledProcessError as e:
ctx.error(f"package install failed (exit {e.returncode})")
def check_terminfo(ctx: Context) -> None: def check_terminfo(ctx: Context) -> None: