Wire archinstall adapter + concrete phase implementations
Chunks 2-6 of the C3 refactor. With the adapter in place, the phase
list collapses from 12 stubs to 5 real phases:
prepare_live parse user_*.json into archinstall handlers
arch_install partition + base + bootloader + limine config +
early omarchy + useradd + runtime omarchy (all
inside a single Installer context manager)
run_chroot_finalizer arch-chroot -u $user → finalize.sh
validate_boot halt if /boot/limine.conf, /etc/kernel/cmdline,
or UKI are missing/malformed
finish reboot prompt
Key reordering vs upstream guided.py: write_limine_config runs BETWEEN
add_bootloader and the first add_additional_packages call. By the time
omarchy-limine pulls limine-mkinitcpio-hook (in the runtime package
install), /etc/default/limine and /etc/kernel/cmdline are in place, so
the UKI is built correctly on the first try — no stale-UKI purge,
no follow-up limine-update.
add_additional_packages is split into two calls so /etc/skel is
populated (via omarchy-settings in EARLY_PACKAGES) BEFORE useradd:
EARLY_PACKAGES = base-devel git omarchy-keyring omarchy-settings omarchy-installer
→ installer.create_users(users)
→ runtime: omarchy + omarchy-base.packages
archinstall_adapter.py is the ONLY module that imports from archinstall.
If archinstall's API churns, the blast radius is contained here. Tested
against archinstall 4.3 / Python 3.14.
Limine template lookup tries the new install/assets/limine/ path first
and falls back to the legacy default/limine/ location during the
template-ownership migration (lands in a follow-up chunk).
This commit is contained in:
@@ -1,37 +1,116 @@
|
||||
"""Thin compatibility wall around the archinstall Python library.
|
||||
|
||||
ONLY this module imports from archinstall. Everything else uses these functions.
|
||||
ONLY this module imports from archinstall. Everything else uses these helpers.
|
||||
If archinstall's API churns, the blast radius is contained here.
|
||||
|
||||
Tested against archinstall 4.3 (Python 3.14).
|
||||
|
||||
The canonical call sequence (mirrored from archinstall.scripts.guided.py) is:
|
||||
|
||||
FilesystemHandler(disk_config).perform_filesystem_operations()
|
||||
with Installer(mountpoint, disk_config, kernels=, silent=) as inst:
|
||||
inst.mount_ordered_layout()
|
||||
inst.sanity_check(offline=, skip_ntp=, skip_wkd=)
|
||||
inst.generate_key_files() # encrypted only
|
||||
inst.set_mirrors(handler, mirror_config, on_target=False)
|
||||
inst.minimal_installation(...) # base + linux pacstrap
|
||||
inst.set_mirrors(handler, mirror_config, on_target=True)
|
||||
inst.setup_swap(algo=...)
|
||||
inst.add_bootloader(bootloader, uki, removable)
|
||||
inst.create_users(users)
|
||||
inst.add_additional_packages(packages)
|
||||
inst.set_timezone(tz)
|
||||
inst.activate_time_synchronization()
|
||||
inst.set_user_password(root_user)
|
||||
inst.enable_service(services)
|
||||
inst.genfstab()
|
||||
|
||||
Our orchestrator interleaves `write_limine_config` between `add_bootloader`
|
||||
and the first `add_additional_packages` so the limine UKI hook fires once,
|
||||
correctly, on its first install.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Phase 2 will populate this module. For now we declare the contract so the
|
||||
# rest of the orchestrator can be wired up against the eventual surface area.
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
# Imports are top-level so a missing/incompatible archinstall surfaces at
|
||||
# orchestrator startup, not deep inside a phase.
|
||||
from archinstall.lib.args import ArchConfig, ArchConfigHandler
|
||||
from archinstall.lib.authentication.authentication_handler import AuthenticationHandler
|
||||
from archinstall.lib.configuration import ConfigurationOutput
|
||||
from archinstall.lib.disk.filesystem import FilesystemHandler
|
||||
from archinstall.lib.installer import Installer
|
||||
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
|
||||
from archinstall.lib.models import Bootloader
|
||||
from archinstall.lib.models.device import DiskLayoutType, EncryptionType
|
||||
from archinstall.lib.models.users import User
|
||||
|
||||
|
||||
def prepare_live() -> None:
|
||||
"""pacman-key init/populate, mount checks, etc. Currently a no-op stub."""
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
def load_arch_config(config_path: Path, creds_path: Path) -> ArchConfigHandler:
|
||||
"""Build an ArchConfigHandler from on-disk JSON. archinstall reads the
|
||||
paths via env vars, so we set them before instantiating the handler."""
|
||||
os.environ["ARCHINSTALL_CONFIG"] = str(config_path)
|
||||
os.environ["ARCHINSTALL_CREDS"] = str(creds_path)
|
||||
return ArchConfigHandler()
|
||||
|
||||
|
||||
def cleanup_disk() -> None:
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
def make_mirror_handler(offline: bool = True) -> MirrorListHandler:
|
||||
return MirrorListHandler(offline=offline, verbose=False)
|
||||
|
||||
|
||||
def create_partitions_and_mounts(install_ctx) -> None:
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
def perform_filesystem_operations(arch_config: ArchConfig) -> None:
|
||||
"""Partition, format, encrypt. archinstall's FilesystemHandler is its own
|
||||
object (separate from Installer) so we run it before opening the
|
||||
Installer context manager."""
|
||||
if not arch_config.disk_config:
|
||||
raise RuntimeError("disk_config missing from arch config")
|
||||
FilesystemHandler(arch_config.disk_config).perform_filesystem_operations()
|
||||
|
||||
|
||||
def install_base_system(install_ctx) -> None:
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
@contextmanager
|
||||
def open_installer(
|
||||
arch_config: ArchConfig,
|
||||
mountpoint: Path,
|
||||
silent: bool = True,
|
||||
) -> Iterator[Installer]:
|
||||
"""Yield an open Installer; ensures __exit__ runs even on exception so
|
||||
/mnt is left clean for a retry."""
|
||||
if not arch_config.disk_config:
|
||||
raise RuntimeError("disk_config missing from arch config")
|
||||
with Installer(
|
||||
str(mountpoint),
|
||||
arch_config.disk_config,
|
||||
kernels=arch_config.kernels,
|
||||
silent=silent,
|
||||
) as installer:
|
||||
yield installer
|
||||
|
||||
|
||||
def install_limine_bootloader(install_ctx) -> None:
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
def is_encrypted(arch_config: ArchConfig) -> bool:
|
||||
disk = arch_config.disk_config
|
||||
if not disk or not disk.disk_encryption:
|
||||
return False
|
||||
return disk.disk_encryption.encryption_type != EncryptionType.NO_ENCRYPTION
|
||||
|
||||
|
||||
def create_users(install_ctx) -> None:
|
||||
raise NotImplementedError("populated in Chunk 2")
|
||||
def is_pre_mount(arch_config: ArchConfig) -> bool:
|
||||
return bool(
|
||||
arch_config.disk_config
|
||||
and arch_config.disk_config.config_type == DiskLayoutType.Pre_mount
|
||||
)
|
||||
|
||||
|
||||
def is_limine(arch_config: ArchConfig) -> bool:
|
||||
bl = arch_config.bootloader_config
|
||||
return bool(bl and bl.bootloader == Bootloader.Limine)
|
||||
|
||||
|
||||
def root_user(arch_config: ArchConfig) -> User | None:
|
||||
auth = arch_config.auth_config
|
||||
if not auth or not auth.root_enc_password:
|
||||
return None
|
||||
return User("root", auth.root_enc_password, False)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"""Install context: parsed configurator output + invocation paths."""
|
||||
"""Install context: parsed configurator output, invocation paths, and a
|
||||
mutable `state` dict for objects that live across phases (e.g., the
|
||||
archinstall config handler and mirror list handler)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class InstallContext:
|
||||
config_path: Path
|
||||
creds_path: Path
|
||||
@@ -24,6 +27,10 @@ class InstallContext:
|
||||
log_path: Path = Path("/var/log/omarchy-install.log")
|
||||
target_log_path: Path = Path("/mnt/var/log/omarchy-install.log")
|
||||
|
||||
# Mutable per-run state shared across phases (e.g., 'arch_config_handler',
|
||||
# 'mirror_handler'). Phases populate as needed; later phases read.
|
||||
state: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args) -> "InstallContext":
|
||||
config_path = Path(args.config)
|
||||
|
||||
@@ -33,32 +33,18 @@ def build_phases():
|
||||
"""
|
||||
from .phases_impl import (
|
||||
prepare_live,
|
||||
cleanup_disk,
|
||||
partition_and_mount,
|
||||
install_base,
|
||||
install_bootloader,
|
||||
write_limine_config,
|
||||
install_early_omarchy_packages,
|
||||
create_user,
|
||||
install_omarchy_runtime,
|
||||
arch_install,
|
||||
run_chroot_finalizer,
|
||||
validate_boot,
|
||||
finish,
|
||||
)
|
||||
|
||||
return [
|
||||
("Preparing live environment", prepare_live),
|
||||
("Cleaning install disk", cleanup_disk),
|
||||
("Partitioning + mounting", partition_and_mount),
|
||||
("Installing base system", install_base),
|
||||
("Installing bootloader", install_bootloader),
|
||||
("Writing Limine config", write_limine_config),
|
||||
("Installing Omarchy keyring + settings", install_early_omarchy_packages),
|
||||
("Creating user", create_user),
|
||||
("Installing Omarchy runtime", install_omarchy_runtime),
|
||||
("Finalizing in chroot", run_chroot_finalizer),
|
||||
("Validating boot setup", validate_boot),
|
||||
("Finishing", finish),
|
||||
("Preparing live environment", prepare_live),
|
||||
("Installing Arch + Omarchy", arch_install),
|
||||
("Finalizing in chroot", run_chroot_finalizer),
|
||||
("Validating boot setup", validate_boot),
|
||||
("Finishing", finish),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,63 +1,261 @@
|
||||
"""Concrete phase implementations.
|
||||
|
||||
Each function takes the InstallContext and either returns or raises. Phases are
|
||||
small wrappers — heavy lifting lives in archinstall_adapter (for Arch substrate
|
||||
work) and helpers.* (for Omarchy-specific work).
|
||||
Phase ordering:
|
||||
|
||||
Most are stubbed until Chunks 2–6 land. Keeping them here so the phase wiring
|
||||
in main.py is testable end-to-end as a smoke check ('every phase imports
|
||||
cleanly') from Chunk 1 onwards.
|
||||
prepare_live → live ISO env + load arch config
|
||||
arch_install → archinstall-driven install (partition, base,
|
||||
bootloader, write limine config, early omarchy
|
||||
pkgs, useradd, runtime omarchy pkgs)
|
||||
run_chroot_finalizer → arch-chroot finalize.sh as the install user
|
||||
validate_boot → assert UKI / limine.conf / kernel cmdline are sane
|
||||
finish → reboot prompt
|
||||
|
||||
Heavy lifting in arch_install lives in archinstall_adapter (for the Installer
|
||||
context manager) and in this file (for our limine-config write + omarchy
|
||||
package selection). Other phases are kept small.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from . import archinstall_adapter as arch
|
||||
from .context import InstallContext
|
||||
from .ui import info
|
||||
|
||||
|
||||
# Packages installed BEFORE useradd. omarchy-settings populates /etc/skel so
|
||||
# the user's home gets seeded correctly; omarchy-installer ships the install/
|
||||
# tree + finalize.sh that the in-chroot finalizer needs.
|
||||
EARLY_PACKAGES = [
|
||||
"base-devel",
|
||||
"git",
|
||||
"omarchy-keyring",
|
||||
"omarchy-settings",
|
||||
"omarchy-installer",
|
||||
]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# prepare_live: parse user_configuration.json/user_credentials.json, build the
|
||||
# archinstall handlers. Cached on ctx.state for downstream phases.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def prepare_live(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
ctx.state["arch_config_handler"] = arch.load_arch_config(
|
||||
ctx.config_path, ctx.creds_path
|
||||
)
|
||||
ctx.state["mirror_handler"] = arch.make_mirror_handler(offline=True)
|
||||
|
||||
|
||||
def cleanup_disk(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# arch_install: everything inside a single Installer context manager. Mirrors
|
||||
# guided.py's perform_installation() but reorders so our limine config write
|
||||
# lands between add_bootloader and the first add_additional_packages call, and
|
||||
# user creation happens AFTER early omarchy packages populate /etc/skel.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def arch_install(ctx: InstallContext) -> None:
|
||||
handler = ctx.state["arch_config_handler"]
|
||||
mirror_handler = ctx.state["mirror_handler"]
|
||||
config = handler.config
|
||||
|
||||
info("› partitioning + formatting + encrypting")
|
||||
arch.perform_filesystem_operations(config)
|
||||
|
||||
info("› opening installer context")
|
||||
with arch.open_installer(config, ctx.target, silent=True) as installer:
|
||||
if not arch.is_pre_mount(config):
|
||||
installer.mount_ordered_layout()
|
||||
|
||||
installer.sanity_check(
|
||||
offline=True,
|
||||
skip_ntp=True,
|
||||
skip_wkd=True,
|
||||
)
|
||||
|
||||
if not arch.is_pre_mount(config) and arch.is_encrypted(config):
|
||||
installer.generate_key_files()
|
||||
|
||||
if config.mirror_config:
|
||||
installer.set_mirrors(mirror_handler, config.mirror_config, on_target=False)
|
||||
|
||||
info("› installing base system")
|
||||
installer.minimal_installation(
|
||||
optional_repositories=(
|
||||
config.mirror_config.optional_repositories
|
||||
if config.mirror_config else []
|
||||
),
|
||||
mkinitcpio=True,
|
||||
hostname=config.hostname,
|
||||
locale_config=config.locale_config,
|
||||
pacman_config=config.pacman_config,
|
||||
)
|
||||
|
||||
if config.mirror_config:
|
||||
installer.set_mirrors(mirror_handler, config.mirror_config, on_target=True)
|
||||
|
||||
if config.swap and config.swap.enabled:
|
||||
installer.setup_swap(algo=config.swap.algorithm)
|
||||
|
||||
info("› installing bootloader (Limine)")
|
||||
if config.bootloader_config:
|
||||
installer.add_bootloader(
|
||||
config.bootloader_config.bootloader,
|
||||
config.bootloader_config.uki,
|
||||
config.bootloader_config.removable,
|
||||
)
|
||||
|
||||
info("› writing Limine config (so limine-mkinitcpio-hook fires correctly)")
|
||||
_write_limine_defaults(ctx)
|
||||
|
||||
info(f"› installing early Omarchy packages: {', '.join(EARLY_PACKAGES)}")
|
||||
installer.add_additional_packages(EARLY_PACKAGES)
|
||||
|
||||
info("› creating user (with /etc/skel populated)")
|
||||
if config.auth_config and config.auth_config.users:
|
||||
installer.create_users(config.auth_config.users)
|
||||
|
||||
info("› installing Omarchy runtime + omarchy-base.packages")
|
||||
runtime_pkgs = _runtime_package_list(ctx)
|
||||
installer.add_additional_packages(runtime_pkgs)
|
||||
|
||||
# Standard arch finishers.
|
||||
if config.timezone:
|
||||
installer.set_timezone(config.timezone)
|
||||
if config.ntp:
|
||||
installer.activate_time_synchronization()
|
||||
if root := arch.root_user(config):
|
||||
installer.set_user_password(root)
|
||||
|
||||
installer.genfstab()
|
||||
|
||||
|
||||
def partition_and_mount(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Limine config write — extracted from install/login/limine-snapper.sh logic.
|
||||
# Reads cmdline from /mnt/boot/limine.conf (which add_bootloader wrote),
|
||||
# substitutes @@CMDLINE@@ into the template, writes /etc/default/limine +
|
||||
# /etc/kernel/cmdline.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _write_limine_defaults(ctx: InstallContext) -> None:
|
||||
if not arch.is_limine(ctx.state["arch_config_handler"].config):
|
||||
return
|
||||
|
||||
limine_conf = ctx.target / "boot" / "limine.conf"
|
||||
if not limine_conf.exists():
|
||||
raise RuntimeError(f"{limine_conf} not found after add_bootloader")
|
||||
|
||||
cmdline = _extract_cmdline(limine_conf)
|
||||
if not cmdline.strip():
|
||||
raise RuntimeError("Could not extract kernel cmdline from limine.conf")
|
||||
if "root=" not in cmdline:
|
||||
raise RuntimeError(f"Extracted cmdline has no root=: {cmdline!r}")
|
||||
|
||||
# The template lives in omarchy-installer (this package), so it's
|
||||
# available from /mnt/usr/share/omarchy/... as soon as the early
|
||||
# omarchy-installer pacstrap completes — but we want it BEFORE that.
|
||||
# Read from our own runtime tree on the live ISO instead.
|
||||
template = ctx.omarchy_path / "install" / "assets" / "limine" / "default.conf"
|
||||
if not template.exists():
|
||||
# Fallback to the legacy path while we migrate templates between packages.
|
||||
template = ctx.omarchy_path / "default" / "limine" / "default.conf"
|
||||
if not template.exists():
|
||||
raise RuntimeError(f"Limine template not found at {template}")
|
||||
|
||||
default_limine = ctx.target / "etc" / "default" / "limine"
|
||||
default_limine.parent.mkdir(parents=True, exist_ok=True)
|
||||
default_limine.write_text(template.read_text().replace("@@CMDLINE@@", cmdline))
|
||||
|
||||
kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline"
|
||||
kernel_cmdline.parent.mkdir(parents=True, exist_ok=True)
|
||||
kernel_cmdline.write_text(cmdline + "\n")
|
||||
|
||||
|
||||
def install_base(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
def _extract_cmdline(limine_conf: Path) -> str:
|
||||
for line in limine_conf.read_text().splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("cmdline:"):
|
||||
return stripped[len("cmdline:"):].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def install_bootloader(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
def _runtime_package_list(ctx: InstallContext) -> list[str]:
|
||||
"""omarchy + every package in install/omarchy-base.packages that isn't
|
||||
already in EARLY_PACKAGES."""
|
||||
base_pkgs_file = ctx.omarchy_path / "install" / "omarchy-base.packages"
|
||||
pkgs = ["omarchy"]
|
||||
early = set(EARLY_PACKAGES)
|
||||
for raw in base_pkgs_file.read_text().splitlines():
|
||||
s = raw.strip()
|
||||
if not s or s.startswith("#"):
|
||||
continue
|
||||
if s not in early and s not in pkgs:
|
||||
pkgs.append(s)
|
||||
return pkgs
|
||||
|
||||
|
||||
def write_limine_config(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 3")
|
||||
|
||||
|
||||
def install_early_omarchy_packages(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 4")
|
||||
|
||||
|
||||
def create_user(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 2")
|
||||
|
||||
|
||||
def install_omarchy_runtime(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 4")
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# run_chroot_finalizer: arch-chroot -u $user into /mnt and run finalize.sh.
|
||||
# Inherits stdout/stderr so the in-target output streams to our log capture.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run_chroot_finalizer(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 5")
|
||||
env_extras = [
|
||||
f"OMARCHY_INSTALL_MODE=offline",
|
||||
f"OMARCHY_USER_NAME={ctx.full_name}",
|
||||
f"OMARCHY_USER_EMAIL={ctx.email}",
|
||||
f"USER={ctx.username}",
|
||||
f"HOME=/home/{ctx.username}",
|
||||
]
|
||||
cmd = [
|
||||
"arch-chroot",
|
||||
"-u", ctx.username,
|
||||
str(ctx.target),
|
||||
"env", "--unset=XDG_RUNTIME_DIR",
|
||||
*env_extras,
|
||||
"/bin/bash", "-lc",
|
||||
f"bash {ctx.omarchy_path}/finalize.sh",
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# validate_boot: hard checks before reboot. If the install ran but produced
|
||||
# a UKI that can't actually boot, we want to halt here, not surprise the user.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def validate_boot(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 6")
|
||||
limine_conf = ctx.target / "boot" / "limine.conf"
|
||||
if not limine_conf.exists():
|
||||
raise RuntimeError(f"{limine_conf} missing")
|
||||
|
||||
content = limine_conf.read_text()
|
||||
if "^/+Omarchy" not in content and "Omarchy" not in content:
|
||||
raise RuntimeError("/boot/limine.conf has no Omarchy entry")
|
||||
|
||||
if ctx.encrypt and "cryptdevice=" not in content:
|
||||
raise RuntimeError("Encrypted install but /boot/limine.conf has no cryptdevice=")
|
||||
|
||||
kernel_cmdline = ctx.target / "etc" / "kernel" / "cmdline"
|
||||
if not kernel_cmdline.exists():
|
||||
raise RuntimeError(f"{kernel_cmdline} missing — UKI would have no cmdline")
|
||||
|
||||
uki_dir = ctx.target / "boot" / "EFI" / "Linux"
|
||||
if uki_dir.exists():
|
||||
ukis = list(uki_dir.glob("*_linux*.efi"))
|
||||
if not ukis:
|
||||
raise RuntimeError(f"No UKI found in {uki_dir}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# finish: show completion + offer reboot. No mutation.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def finish(ctx: InstallContext) -> None:
|
||||
raise NotImplementedError("Chunk 6")
|
||||
from .ui import confirm
|
||||
info("Installation finished. Reboot when ready.")
|
||||
if confirm("Reboot now?", default=True):
|
||||
os.system("reboot")
|
||||
|
||||
Reference in New Issue
Block a user