feat(setup): per-distro package detection and install prompt
This commit is contained in:
@@ -6,32 +6,94 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import filecmp
|
||||
import platform
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
HOME = Path.home()
|
||||
|
||||
# Populated now so per-distro package tracking can key off it.
|
||||
try:
|
||||
DISTRO: str = platform.freedesktop_os_release().get("NAME", "")
|
||||
except OSError:
|
||||
DISTRO = ""
|
||||
DISTRO: str = platform.freedesktop_os_release().get("ID", "")
|
||||
|
||||
|
||||
PKGS: list[str] = [
|
||||
"alacritty",
|
||||
"i3",
|
||||
"i3lock",
|
||||
"i3status",
|
||||
"jq",
|
||||
"lf",
|
||||
"rofi",
|
||||
"startx",
|
||||
"tmux",
|
||||
"zsh",
|
||||
class PackageManager(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def check_cmd(self) -> list[str]: ...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def install_cmd(self) -> list[str]: ...
|
||||
|
||||
def is_installed(self, name: str) -> bool:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
for pkg in PKGS:
|
||||
if shutil.which(pkg) is None:
|
||||
ctx.error(f"missing {pkg}")
|
||||
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))]
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user