diff --git a/bin/omarchy-iso-test b/bin/omarchy-iso-test index fc3daa8..3607901 100755 --- a/bin/omarchy-iso-test +++ b/bin/omarchy-iso-test @@ -840,6 +840,11 @@ install_phase() { fi bootstrap_ssh + ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \ + >"$RUN_DIR/omarchy-install-timing.json" 2>/dev/null || true + ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/pacman.log 2>/dev/null" \ + >"$RUN_DIR/pacman.log" 2>/dev/null || true + log "Installed system is up. Saving base image." stop_vm } diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py index 79cae4b..31a6c76 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases.py @@ -65,6 +65,10 @@ def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None: state["finished_at"] = time.time() _write_state(state_path, state) + timing_path = ctx.target / "var" / "log" / "omarchy-install-timing.json" + timing_path.parent.mkdir(parents=True, exist_ok=True) + _write_state(timing_path, state) + def _write_state(path: Path, state: dict) -> None: # Dashboard polls this file while phases update it. Write atomically so the diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 2e90d88..dbfef75 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -162,11 +162,13 @@ def _early_packages() -> list[str]: # ───────────────────────────────────────────────────────────────────────────── def prepare_live(ctx: InstallContext) -> None: - info("› initializing pacman keyrings") - subprocess.run(["pacman-key", "--init"], check=True) - subprocess.run(["pacman-key", "--populate", "archlinux"], check=True) - subprocess.run(["pacman-key", "--populate", "omarchy"], check=True) - subprocess.run(["pacman", "-Sy", "--noconfirm"], check=True) + # archiso's pacman-init.service initializes and populates every installed + # keyring before the live environment reaches multi-user.target. Pacstrap + # then initializes the target's independent keyring, and archinstall does + # its own repository sync before the first package transaction. Repeating + # both operations here only rebuilds trust databases and syncs twice. + info("› verifying live pacman initialization") + subprocess.run(["systemctl", "start", "pacman-init.service"], check=True) if ctx.is_protected: info("› protected mode: skipping whole-disk cleanup") @@ -239,6 +241,7 @@ def arch_install_system(ctx: InstallContext) -> None: if config.mirror_config: installer.set_mirrors(mirror_handler, config.mirror_config, on_target=False) + _mount_offline_package_cache(ctx) _mask_mkinitcpio_pacman_hooks(ctx) try: info("› installing base system (mkinitcpio deferred to final Limine UKI build)") @@ -274,6 +277,7 @@ def arch_install_system(ctx: InstallContext) -> None: installer.add_additional_packages(_runtime_package_list(ctx)) finally: _unmask_mkinitcpio_pacman_hooks(ctx) + _unmount_offline_package_cache(ctx) # Standard arch finishers. if config.timezone: @@ -575,6 +579,35 @@ def _install_early_packages(installer) -> None: installer.add_additional_packages(user_seed_packages) +def _mount_offline_package_cache(ctx: InstallContext) -> None: + """Let pacstrap consume bundled packages without copying them first. + + Pacstrap always points pacman's CacheDir inside the target. Without this + bind mount, pacman copies every package from the ISO's file:// repository + into that cache and then extracts it, duplicating several GiB of I/O. + Mount the already-populated offline repository at the target cache for the + duration of package installation. It is unmounted before genfstab so the + live-only bind can never leak into the installed system's fstab. + """ + source = Path("/var/cache/omarchy/mirror/offline") + target = ctx.target / "var" / "cache" / "pacman" / "pkg" + if not source.is_dir(): + raise RuntimeError(f"offline package cache missing: {source}") + + target.mkdir(parents=True, exist_ok=True) + subprocess.run(["mount", "--bind", str(source), str(target)], check=True) + ctx.state.setdefault("bind_mounts", []).append(str(target)) + + +def _unmount_offline_package_cache(ctx: InstallContext) -> None: + target = str(ctx.target / "var" / "cache" / "pacman" / "pkg") + subprocess.run(["umount", target], check=True) + try: + ctx.state.get("bind_mounts", []).remove(target) + except ValueError: + pass + + def _is_devnull_symlink(path: Path) -> bool: try: return path.is_symlink() and path.readlink() == Path("/dev/null") diff --git a/configs/profiledef.sh b/configs/profiledef.sh index 949288f..c66d51a 100644 --- a/configs/profiledef.sh +++ b/configs/profiledef.sh @@ -12,7 +12,16 @@ bootmodes=('bios.syslinux' 'uefi.grub') arch="x86_64" pacman_conf="pacman-offline.conf" airootfs_image_type="squashfs" -airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M') +# Package archives in the offline mirror are already zstd-compressed. Storing +# them in an outer XZ stream saves little space but makes pacman decompress XZ +# while hashing and extracting every package during installation. +airootfs_image_tool_options=( + '-comp' 'xz' + '-Xbcj' 'x86' + '-b' '1M' + '-Xdict-size' '1M' + '-action' 'uncompressed@subpathname(var/cache/omarchy/mirror/offline)' +) bootstrap_tarball_compression=('zstd' '-c' '-T0' '--auto-threads=logical' '--long' '-19') file_permissions=( ["/etc/shadow"]="0:0:400"