chore(dotfiles): streamline workstation configuration workflows
This commit is contained in:
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the repository's common Decman workflows with safe, fixed commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
|
||||
|
||||
def format_command(command: Sequence[str] | str) -> str:
|
||||
"""Render a shell-safe command for diagnostic output."""
|
||||
return command if isinstance(command, str) else shlex.join(command)
|
||||
|
||||
|
||||
def run(command: Sequence[str]) -> None:
|
||||
"""Print and run a command from the repository root."""
|
||||
print(f"+ {format_command(command)}", file=sys.stderr)
|
||||
subprocess.run(command, check=True, cwd=ROOT)
|
||||
|
||||
|
||||
def run_script(name: str) -> None:
|
||||
run([sys.executable, str(SCRIPTS / name)])
|
||||
|
||||
|
||||
def verify() -> None:
|
||||
"""Check the active profile's package and unit invariants."""
|
||||
run_script("verify.py")
|
||||
|
||||
|
||||
def dry_run() -> None:
|
||||
"""Verify the configuration, then preview Decman's changes."""
|
||||
verify()
|
||||
run(["sudo", "decman", "--source", "source.py", "--dry-run", "--no-hooks"])
|
||||
|
||||
|
||||
def apply() -> None:
|
||||
"""Verify the configuration, then apply it without Decman hooks."""
|
||||
verify()
|
||||
run(["sudo", "decman", "--source", "source.py", "--no-hooks"])
|
||||
|
||||
|
||||
def split_packages() -> None:
|
||||
"""Regenerate concern package lists from explicit installed packages."""
|
||||
run_script("split_packages.py")
|
||||
|
||||
|
||||
def refresh_orphan_guards() -> None:
|
||||
"""Regenerate dependency-orphan guards from installed packages."""
|
||||
run_script("refresh_orphan_guards.py")
|
||||
|
||||
|
||||
def refresh_package_state() -> None:
|
||||
"""Regenerate package data and verify the resulting configuration."""
|
||||
split_packages()
|
||||
refresh_orphan_guards()
|
||||
verify()
|
||||
|
||||
|
||||
COMMANDS: dict[str, Callable[[], None]] = {
|
||||
"verify": verify,
|
||||
"dry-run": dry_run,
|
||||
"apply": apply,
|
||||
"split-packages": split_packages,
|
||||
"refresh-orphan-guards": refresh_orphan_guards,
|
||||
"refresh-package-state": refresh_package_state,
|
||||
}
|
||||
|
||||
|
||||
def failure_status(error: Exception) -> int | None:
|
||||
"""Report a launch failure and return its shell-compatible status."""
|
||||
if isinstance(error, subprocess.CalledProcessError):
|
||||
status = error.returncode if error.returncode > 0 else 128 - error.returncode
|
||||
print(
|
||||
f"error: command exited with status {status}: {format_command(error.cmd)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return status
|
||||
if isinstance(error, FileNotFoundError):
|
||||
print(f"error: executable not found: {error.filename}", file=sys.stderr)
|
||||
return 127
|
||||
if isinstance(error, PermissionError):
|
||||
print(f"error: executable is not permitted: {error.filename}", file=sys.stderr)
|
||||
return 126
|
||||
if isinstance(error, OSError):
|
||||
print(
|
||||
f"error: unable to execute {error.filename}: {error.strerror}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 126
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=COMMANDS,
|
||||
help="workflow to run; apply and package-state commands modify system or files",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
COMMANDS[args.command]()
|
||||
except KeyboardInterrupt:
|
||||
print("error: interrupted", file=sys.stderr)
|
||||
return 130
|
||||
except Exception as error:
|
||||
status = failure_status(error)
|
||||
if status is None:
|
||||
raise
|
||||
return status
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user