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 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:
|
||||||
|
|||||||
Reference in New Issue
Block a user