Chunk 1 of the C3 refactor: replace the two-pass install model (archinstall CLI + custom shell dance + chroot bash installer) with a single Python orchestrator that owns phase ordering, using archinstall as a library subsystem. Splits the bash entry point into two: - install.sh: online entry point. Ensures omarchy runtime + base.packages are installed/up to date, then exec's finalize.sh. - finalize.sh: the in-target portion (preflight + packaging + config + login + post-install). Called by install.sh (online) AND by the orchestrator after arch-chroot -u $USER (offline). Adds the orchestrator skeleton under install/orchestrator/: - main.py: entry point, builds + runs the phase list - context.py: InstallContext (parsed configurator JSON + invocation paths) - phases.py: phase state machine (logging + state.json + error wrapping) - phases_impl.py: stubbed phase implementations (filled in by chunks 2-6) - archinstall_adapter.py: thin compat wall around archinstall lib imports (only this module imports from archinstall.*) - ui.py: gum subprocess wrappers so the orchestrator keeps the same styled-terminal UX as the bash installer Updates bin/omarchy-install to dispatch: - --config <json> in args → python -m orchestrator.main (ISO install) - anything else → bash install.sh (online rerun on installed system) Concrete phase logic lands in subsequent chunks. All phases currently raise NotImplementedError; the orchestrator imports cleanly and --help works as a smoke check.
58 lines
1.7 KiB
Python
58 lines
1.7 KiB
Python
"""Phase state machine. Each phase is a (name, callable) pair; callables take
|
||
the InstallContext and either return cleanly or raise to abort the install."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import time
|
||
import traceback
|
||
from collections.abc import Callable
|
||
from pathlib import Path
|
||
|
||
from .context import InstallContext
|
||
from .ui import error, info
|
||
|
||
|
||
PhaseFn = Callable[[InstallContext], None]
|
||
|
||
|
||
class PhaseError(Exception):
|
||
"""Raised when a phase fails. Wrapped with the phase name."""
|
||
|
||
|
||
def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
|
||
ctx.state_dir.mkdir(parents=True, exist_ok=True)
|
||
state_path = ctx.state_dir / "state.json"
|
||
state = {"started_at": time.time(), "phases": []}
|
||
_write_state(state_path, state)
|
||
|
||
for name, fn in phases:
|
||
info(f"› {name}")
|
||
started = time.time()
|
||
try:
|
||
fn(ctx)
|
||
except Exception as exc: # noqa: BLE001
|
||
elapsed = time.time() - started
|
||
state["phases"].append({
|
||
"name": name,
|
||
"status": "failed",
|
||
"elapsed": elapsed,
|
||
"error": str(exc),
|
||
})
|
||
_write_state(state_path, state)
|
||
|
||
error(f"Phase '{name}' failed after {elapsed:.1f}s: {exc}")
|
||
traceback.print_exc()
|
||
raise PhaseError(f"phase {name} failed: {exc}") from exc
|
||
|
||
elapsed = time.time() - started
|
||
state["phases"].append({"name": name, "status": "ok", "elapsed": elapsed})
|
||
_write_state(state_path, state)
|
||
|
||
state["finished_at"] = time.time()
|
||
_write_state(state_path, state)
|
||
|
||
|
||
def _write_state(path: Path, state: dict) -> None:
|
||
path.write_text(json.dumps(state, indent=2, default=str))
|