import CryptoKit
import Foundation
#if arch(arm64)
import Virtualization
#endif

#if arch(arm64)
/// A one-shot resume gate: whichever of the VZ stop delegate or the timeout fires first delivers its
/// value to the awaiting continuation exactly once; the loser is a no-op. Thread-safe (both fire from
/// arbitrary queues). Used to await a self-powering-off base-build boot without blocking a thread.
final class OnceResumer: @unchecked Sendable {
    private let lock = NSLock()
    private var cont: CheckedContinuation<Bool, Never>?
    private var pending: Bool?

    /// Attach the awaiting continuation. If a value already arrived, resume immediately.
    func attach(_ c: CheckedContinuation<Bool, Never>) {
        lock.lock()
        if let v = pending {
            lock.unlock()
            c.resume(returning: v)
        } else {
            cont = c
            lock.unlock()
        }
    }

    /// Deliver `value` once. Resumes an already-attached continuation, else latches for `attach`.
    func fire(_ value: Bool) {
        lock.lock()
        if pending != nil { lock.unlock(); return }
        pending = value
        let c = cont
        cont = nil
        lock.unlock()
        c?.resume(returning: value)
    }
}

extension MacVMEngine {
    // MARK: - Linux base build: the two in-guest boots

    /// **Phase 1** — boot the kernel + bootstrap initramfs with the empty root disk and the provisioning
    /// payload shared in over virtiofs, and wait for its `/init` to format the disk, unpack the rootfs,
    /// bake in the agent, and power off. Headless: no graphics/vsock/network — just block + virtiofs +
    /// console (the console log lands next to the bundle for diagnosis).
    func runLinuxBootstrapBoot(bundle: MacVMBundle, artifacts: LinuxArtifacts) async throws {
        let initramfs = try ensureBootstrapInitramfs()
        let payloadDir = try await stageProvisioningPayload(
            rootfsTarball: artifacts.rootfsTarball, kernel: artifacts.kernel)
        defer { try? FileManager.default.removeItem(at: payloadDir) }
        // The payload rides in as a raw tar on a second virtio-blk disk (not virtiofs): modern Ubuntu
        // arm64 kernels ship virtiofs as a module absent from the busybox initramfs, but virtio_blk is
        // built-in. `/init` finds the disk by its `ustar` magic and untars it.
        let payloadDisk = try makePayloadDiskImage(from: payloadDir)
        defer { try? FileManager.default.removeItem(at: payloadDisk) }

        let config = try makeLinuxPhaseConfig(
            kernel: bundle.kernelURL, initrd: initramfs, cmdline: "console=hvc0",
            rootDisk: bundle.diskImageURL, consoleLog: bundle.root.appendingPathComponent("bootstrap.log"),
            withNetwork: false, withVsock: false, payloadDisk: payloadDisk)

        try await bootAndAwaitPowerOff(
            config: config, label: "linux-bootstrap", timeout: 600,
            onTimeout: "the bootstrap boot didn't finish assembling the root filesystem in time")
    }

    /// **Phase 2** — boot the installed rootfs; its first-boot unit installs the desktop + toolchain. We
    /// wait for the agent (vsock), poll the `provisioned` flag it writes (streaming the guest's own
    /// progress line into the base progress), then power off cleanly to seal the base.
    ///
    /// Failures here write a `debug-report.txt` into the bundle (``writeLinuxDebugReport``) and carry
    /// the guest's last console lines, because "the agent never answered" alone is undiagnosable —
    /// the real cause lives guest-side (see docs/LINUX_VM.md §Debugging a failed base build).
    func runLinuxProvisionBoot(bundle: MacVMBundle) async throws {
        let config = try makeLinuxPhaseConfig(
            kernel: bundle.kernelURL, initrd: bundle.initrdURL,
            cmdline: Self.linuxProvisionCommandLine(debug: MacVMSettings.linuxBaseBuildDebug),
            rootDisk: bundle.diskImageURL, consoleLog: bundle.root.appendingPathComponent("provision.log"),
            withNetwork: true, withVsock: true, payloadDisk: nil)

        let instance = MacVMInstance(configuration: config, label: "linux-provision")
        try await instance.start()
        defer { Task { await instance.stop() } }

        // Wait for the agent to answer (it starts at multi-user.target, well before apt finishes).
        baseProgress = MacVMBaseProgress(
            phase: .provisioning, fraction: nil, detail: "Waiting for the guest agent (vsock handshake)")
        do {
            try await awaitAgentReadyOnInstance(instance, timeout: 300)
        } catch {
            // Give the in-guest nucleic-bootdebug snapshot (multi-user + ~15s) a beat to reach the
            // console log before it's tailed into the report.
            try? await Task.sleep(nanoseconds: 2_000_000_000)
            throw linuxProvisionFailure(bundle: bundle, phase: "the agent handshake", underlying: error)
        }

        // Poll the readiness flag the provisioner writes when the desktop + toolchain are in.
        let deadline = Date().addingTimeInterval(3600)  // apt of a desktop can take a while
        while Date() < deadline {
            let flag = await runOnInstance(instance, "test -f /var/lib/nucleic/provisioned && echo READY")
            if flag.stdout.contains("READY") {
                await instance.stop()
                return
            }
            let step = await runOnInstance(instance, "cat /var/lib/nucleic/progress 2>/dev/null | tr -d '\\n'")
            let detail = step.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
            baseProgress = MacVMBaseProgress(
                phase: .provisioning, fraction: nil,
                detail: detail.isEmpty ? "Installing the narOS desktop + toolchain" : detail)
            try? await Task.sleep(nanoseconds: 5_000_000_000)
        }
        throw linuxProvisionFailure(
            bundle: bundle, phase: "first-boot provisioning",
            underlying: MacVMError.provisionFailed(
                "the guest never wrote /var/lib/nucleic/provisioned within the deadline"))
    }

    // MARK: - Provision-failure debug reporting

    /// Wrap a provision-boot failure: write ``writeLinuxDebugReport`` into the bundle and return an
    /// error that leads with the guest's own last console lines and points at the report file.
    func linuxProvisionFailure(bundle: MacVMBundle, phase: String, underlying: Error) -> MacVMError {
        let report = writeLinuxDebugReport(bundle: bundle, phase: phase, underlying: underlying)
        let tail = Self.consoleTail(
            of: bundle.root.appendingPathComponent("provision.log"), lines: 8)
        var message = "Linux base provisioning failed during \(phase): \(String(describing: underlying))"
        if !tail.isEmpty { message += "\nGuest console (last lines):\n\(tail)" }
        message += "\nDebug report: \((report ?? bundle.root.appendingPathComponent("provision.log")).path)"
        if !MacVMSettings.linuxBaseBuildDebug {
            message += "\nFor a verbose guest console, enable \(MacVMSettings.linuxBaseBuildDebugKey) "
                + "and rebuild (docs/LINUX_VM.md §Debugging a failed base build)."
        }
        return .provisionFailed(message)
    }

    /// Write a self-contained `debug-report.txt` into the bundle: the failure, the artifact recipe
    /// (URLs/refs + the kernel release actually booted), and sanitized tails of both boot logs.
    /// Written on **every** provision failure, debug switch or not — it is the difference between
    /// "rebuild and hope" and a diagnosable bug report. Best-effort: `nil` when it can't write.
    func writeLinuxDebugReport(bundle: MacVMBundle, phase: String, underlying: Error) -> URL? {
        var r = "Nucleic Linux base build — debug report\n"
        r += "generated: \(ISO8601DateFormatter().string(from: Date()))\n"
        r += "failed phase: \(phase)\n"
        r += "error: \(String(describing: underlying))\n\n"
        r += "[recipe]\n"
        r += "kernel: \(MacVMSettings.linuxKernelURL)\n"
        r += "initrd: \(MacVMSettings.linuxInitrdURL)\n"
        let rootfs = MacVMSettings.linuxRootfsURL
        r += "rootfs: \(rootfs.isEmpty ? MacVMSettings.linuxRootfsImage : rootfs)\n"
        r += "modules manifest: \(MacVMSettings.linuxModulesManifestURL)\n"
        r += "kernel release booted: \(Self.kernelRelease(of: bundle.kernelURL) ?? "unknown")\n"
        r += "debug switch (\(MacVMSettings.linuxBaseBuildDebugKey)): \(MacVMSettings.linuxBaseBuildDebug)\n\n"
        for log in ["bootstrap.log", "provision.log"] {
            let tail = Self.consoleTail(of: bundle.root.appendingPathComponent(log), lines: 80)
            r += "[\(log) tail]\n" + (tail.isEmpty ? "(missing or empty)" : tail) + "\n\n"
        }
        r += "Guest-side markers: '[nucleic-modsetup]' lines record kernel-module setup; the "
        r += "'[nucleic-bootdebug]' snapshot (~15s after multi-user) records agent/vsock state.\n"
        let url = bundle.root.appendingPathComponent("debug-report.txt")
        do {
            try r.write(to: url, atomically: true, encoding: .utf8)
            return url
        } catch {
            return nil
        }
    }

    /// The last `lines` printable lines of a serial console log (sanitized). Empty when unreadable.
    static func consoleTail(of url: URL, lines: Int) -> String {
        guard let data = try? Data(contentsOf: url) else { return "" }
        return sanitizeConsoleLog(String(decoding: data, as: UTF8.self))
            .split(separator: "\n").suffix(lines).joined(separator: "\n")
    }

    /// Strip terminal escape sequences (CSI/OSC/DCS/two-byte) and non-printing controls from raw
    /// serial-console output, then drop the blank lines the stripping leaves. systemd probes its
    /// "terminal" aggressively, so a raw provision.log buries the few real lines under DEC/OSC noise.
    /// Pure and unit-testable.
    static func sanitizeConsoleLog(_ raw: String) -> String {
        let scalars = Array(raw.unicodeScalars)
        var out = String.UnicodeScalarView()
        var i = 0
        while i < scalars.count {
            let c = scalars[i]
            if c == "\u{1B}" {
                i += 1
                guard i < scalars.count else { break }
                switch scalars[i] {
                case "[":  // CSI: skip params/intermediates through the final byte (0x40–0x7E)
                    i += 1
                    while i < scalars.count, !(0x40...0x7E).contains(scalars[i].value) { i += 1 }
                    if i < scalars.count { i += 1 }
                case "]", "P", "^", "_", "X":  // OSC/DCS/PM/APC/SOS: through BEL or ST (ESC \)
                    i += 1
                    while i < scalars.count {
                        if scalars[i] == "\u{07}" { i += 1; break }
                        if scalars[i] == "\u{1B}", i + 1 < scalars.count, scalars[i + 1] == "\\" {
                            i += 2
                            break
                        }
                        i += 1
                    }
                default:  // two-byte escape (ESC 7, ESC 8, ESC =, …)
                    i += 1
                }
                continue
            }
            if c == "\n" || c == "\t" || (c.value >= 0x20 && c.value != 0x7F) { out.append(c) }
            i += 1
        }
        return String(out)
            .split(separator: "\n", omittingEmptySubsequences: false)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
            .joined(separator: "\n")
    }

    /// Boot `config` on a background queue and resume once the guest powers itself off (its `/init`
    /// calls `poweroff`). Force-stops and throws on the timeout. The VZ stop delegate and the timeout
    /// race to a one-shot resumer, so no cooperative thread is ever blocked on a semaphore.
    private func bootAndAwaitPowerOff(
        config: VZVirtualMachineConfiguration, label: String, timeout: TimeInterval, onTimeout: String
    ) async throws {
        let instance = MacVMInstance(configuration: config, label: label)
        let resumer = OnceResumer()
        // Register the stop hook BEFORE start so a fast poweroff can't be missed.
        instance.setOnStop { resumer.fire(true) }
        try await instance.start()

        let timeoutTask = Task {
            try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
            resumer.fire(false)
        }
        let poweredOff = await withCheckedContinuation { (c: CheckedContinuation<Bool, Never>) in
            resumer.attach(c)
        }
        timeoutTask.cancel()
        if !poweredOff {
            await instance.stop()
            throw MacVMError.installFailed(onTimeout)
        }
    }

    // MARK: - Agent readiness / exec against an out-of-registry instance

    /// Wait until the agent on `instance` answers a vsock `ping` (used during the base build, where the
    /// instance isn't in the `live` registry).
    private func awaitAgentReadyOnInstance(_ instance: MacVMInstance, timeout: TimeInterval) async throws {
        let deadline = Date().addingTimeInterval(timeout)
        while Date() < deadline {
            if let box = try? await withAgentTimeout(seconds: 5, {
                try await instance.connectAgent(port: MacVMAgentWire.port)
            }) {
                let client = MacVMAgentClient(connection: box)
                if let pong = try? await client.request(op: "ping", timeout: 5),
                    pong["version"]?.intValue == MacVMAgentWire.version
                {
                    await client.close()
                    return
                }
                await client.close()
            }
            try? await Task.sleep(nanoseconds: 2_000_000_000)
        }
        throw MacVMError.agentUnavailable("the Linux guest agent never answered within the boot deadline")
    }

    /// Run one command in `instance` over the vsock exec channel and collect its stdout + exit code.
    private func runOnInstance(_ instance: MacVMInstance, _ command: String) async -> (exitCode: Int32, stdout: String) {
        do {
            let channel = try await openExecChannel(
                instance: instance, label: "linux-provision", workdir: nil, env: [:], remoteBody: command)
            var out = ""
            do {
                for try await line in channel.stdoutLines { out += String(decoding: line, as: UTF8.self) + "\n" }
            } catch {}
            let code = await channel.wait()
            return (code, out)
        } catch {
            return (-1, "")
        }
    }

    // MARK: - Provisioning payload + bootstrap initramfs

    /// Stage the phase-1 provisioning payload into a temp directory (packed into the payload disk by
    /// ``makePayloadDiskImage``): the narOS-vm rootfs tarball, the provisioning script + systemd units,
    /// and the **kernel modules** matching the external kernel (baked into the rootfs by the bootstrap —
    /// the minimal rootfs ships none, and modern kernels modularize vsock/gpu). The guest *agents* are
    /// not here: they come baked into the rootfs from the narOS apt pool.
    func stageProvisioningPayload(rootfsTarball: URL, kernel: URL) async throws -> URL {
        let fm = FileManager.default
        let dir = storageRoot.appendingPathComponent("linux-prov-payload", isDirectory: true)
        try? fm.removeItem(at: dir)
        try fm.createDirectory(at: dir, withIntermediateDirectories: true)

        // Name the rootfs so /init picks the right decompressor.
        let rootfsName = rootfsTarball.pathExtension == "gz" ? "rootfs.tar.gz" : "rootfs.tar.xz"
        try fm.copyItem(at: rootfsTarball, to: dir.appendingPathComponent(rootfsName))

        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/provision-linux-guest.sh"),
            to: dir.appendingPathComponent("provision-linux-guest.sh"))
        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/nucleic-firstboot.service"),
            to: dir.appendingPathComponent("nucleic-firstboot.service"))
        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/nucleic-modsetup.service"),
            to: dir.appendingPathComponent("nucleic-modsetup.service"))
        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/nucleic-modsetup.sh"),
            to: dir.appendingPathComponent("nucleic-modsetup.sh"))
        // The boot-health snapshot unit (docs/LINUX_VM.md §Debugging a failed base build): the guest's
        // own diagnosis pushed out through the serial console, for boots the host can't exec into.
        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/nucleic-bootdebug.service"),
            to: dir.appendingPathComponent("nucleic-bootdebug.service"))
        try fm.copyItem(
            at: linuxAsset("guest/linux-base/provision/nucleic-bootdebug.sh"),
            to: dir.appendingPathComponent("nucleic-bootdebug.sh"))
        // The debug switch rides in as a marker file the bootstrap bakes to /etc/nucleic-debug.
        if MacVMSettings.linuxBaseBuildDebug {
            fm.createFile(atPath: dir.appendingPathComponent("nucleic-debug").path, contents: Data())
        }
        // NOTE: no guest-agent binaries ride in this payload. The vsock control agent, the AT-SPI
        // semantic agent (docs/LINUX_VM_SEMANTIC_AGENT.md) and the Mutter geometry helper are all baked
        // into the rootfs from the narOS apt pool (os/mkimage/profiles/vm*.naros-pkgs). The host used to
        // additionally download them from GHCR and have the bootstrap overlay them, which gave the same
        // agent two independent sources that could silently diverge; apt is now the only one.
        try stageKernelModules(into: dir, kernel: kernel)
        return dir
    }

    /// Fetch the `linux-modules{,-extra}-<ver>-generic` matching the external kernel (version pinned by
    /// the cloud image manifest) into `dir/modules.tar.gz` via `scripts/fetch-kernel-modules.sh`. The
    /// base boots an external kernel against a modules-less cloud rootfs; without these the in-guest
    /// agent can't bind AF_VSOCK (module) and the desktop has no virtio_gpu.
    private func stageKernelModules(into dir: URL, kernel: URL) throws {
        // The modules must match the EXTERNAL kernel (Ubuntu), NOT the narOS rootfs userland — so the
        // manifest is its own pinned URL (NAROS.md §7.4), decoupled from the rootfs source (which is now
        // a narOS OCI artifact, not the Ubuntu cloud tarball this used to derive the manifest from).
        let manifest = MacVMSettings.linuxModulesManifestURL
        let script = linuxAsset("scripts/fetch-kernel-modules.sh")
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/bin/bash")
        proc.arguments = [script.path, manifest, dir.path]
        // macOS has no system zstd (Debian debs are data.tar.zst); make Homebrew/CI zstd discoverable.
        var env = ProcessInfo.processInfo.environment
        env["PATH"] = "/opt/homebrew/bin:/usr/local/bin:" + (env["PATH"] ?? "/usr/bin:/bin")
        proc.environment = env
        try proc.run()
        proc.waitUntilExit()
        guard proc.terminationStatus == 0,
            FileManager.default.fileExists(atPath: dir.appendingPathComponent("modules.tar.gz").path)
        else {
            throw MacVMError.provisionFailed(
                "could not fetch the kernel modules matching the external kernel "
                    + "(scripts/fetch-kernel-modules.sh exited \(proc.terminationStatus)). The base boots an "
                    + "external kernel against a modules-less cloud rootfs; without vsock/gpu modules the "
                    + "in-guest agent can't come up. Needs curl + a zstd decoder on PATH (brew install zstd).")
        }

        // The modules must be the ones for the kernel we actually boot. The script reads the live cloud-
        // image manifest while the kernel comes from the cache, so a respin upstream (7.0.0-27 → -28)
        // can hand back modules for a kernel we don't have — and then `/lib/modules/$(uname -r)` is
        // missing in the guest, `modprobe vmw_vsock_virtio_transport` fails, and the agent can never
        // bind AF_VSOCK. The artifact cache keeps them in step; this asserts it rather than trusting it,
        // because the failure it prevents surfaces 5+ minutes later as an opaque agent timeout.
        let modulesRelease = (try? String(
            contentsOf: dir.appendingPathComponent("kernel-release"), encoding: .utf8))?
            .trimmingCharacters(in: .whitespacesAndNewlines)
        if let modulesRelease, !modulesRelease.isEmpty,
            let kernelRelease = Self.kernelRelease(of: kernel), kernelRelease != modulesRelease
        {
            throw MacVMError.provisionFailed(
                "the downloaded kernel modules (\(modulesRelease)) don't match the kernel the base "
                    + "boots (\(kernelRelease)). Upstream respun the rolling cloud image; delete the "
                    + "Linux base image (which now also clears the artifact cache) and rebuild so both "
                    + "are fetched fresh.")
        }
    }

    /// Build (once per `/init` revision, cached) the bootstrap initramfs via
    /// `scripts/build-linux-bootstrap.sh` — portable (busybox download + `ar`/`tar`/`gzip`/`python3`,
    /// all present on macOS). The cache is **content-keyed on the `/init` script**
    /// (``bootstrapCacheName(initScript:)``): a name-keyed cache once kept serving an old init after
    /// the repo's changed, so freshly staged payload files were never installed into the rootfs and
    /// the baked units pointed at missing scripts — surfacing minutes later as an unreachable-agent
    /// timeout that looked like a guest bug. Superseded revisions are swept. Returns the path.
    func ensureBootstrapInitramfs() throws -> URL {
        let fm = FileManager.default
        let initData = try? Data(contentsOf: linuxAsset("guest/linux-base/bootstrap/init"))
        let out = linuxArtifactsDir.appendingPathComponent(Self.bootstrapCacheName(initScript: initData))
        if fm.fileExists(atPath: out.path) { return out }
        try fm.createDirectory(at: linuxArtifactsDir, withIntermediateDirectories: true)
        // One initramfs in the cache: drop superseded revisions (and the legacy unkeyed name).
        if let entries = try? fm.contentsOfDirectory(atPath: linuxArtifactsDir.path) {
            for e in entries where e.hasPrefix("nucleic-bootstrap") && e.hasSuffix(".cpio.gz") {
                try? fm.removeItem(at: linuxArtifactsDir.appendingPathComponent(e))
            }
        }
        let script = linuxAsset("scripts/build-linux-bootstrap.sh")
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/bin/bash")
        proc.arguments = [script.path]
        var env = ProcessInfo.processInfo.environment
        env["OUT"] = out.path
        proc.environment = env
        try proc.run()
        proc.waitUntilExit()
        guard proc.terminationStatus == 0, fm.fileExists(atPath: out.path) else {
            throw MacVMError.installFailed(
                "could not build the Linux bootstrap initramfs (scripts/build-linux-bootstrap.sh exited \(proc.terminationStatus))")
        }
        return out
    }

    /// The content-keyed cache filename for the bootstrap initramfs: the first 12 hex chars of the
    /// `/init` script's SHA-256, so editing the script invalidates the cache. Pure and unit-testable;
    /// `nil` (unreadable init) gets a fixed non-hash tag rather than colliding with a real digest.
    static func bootstrapCacheName(initScript: Data?) -> String {
        guard let initScript else { return "nucleic-bootstrap-unhashed.cpio.gz" }
        let digest = SHA256.hash(data: initScript).map { String(format: "%02x", $0) }.joined()
        return "nucleic-bootstrap-\(digest.prefix(12)).cpio.gz"
    }

    /// Resolve a repo-relative Linux asset. Order: `NUCLEIC_LINUX_ASSETS` override, a bundled
    /// `LinuxAssets` resource dir, else the repo root inferred from this source file (dev / spike /
    /// tests). Returns the path even if absent so callers surface a precise "missing X" error.
    func linuxAsset(_ relativePath: String) -> URL {
        let fm = FileManager.default
        if let override = ProcessInfo.processInfo.environment["NUCLEIC_LINUX_ASSETS"] {
            let u = URL(fileURLWithPath: override).appendingPathComponent(relativePath)
            if fm.fileExists(atPath: u.path) { return u }
        }
        if let res = Bundle.main.resourceURL {
            let u = res.appendingPathComponent("LinuxAssets").appendingPathComponent(relativePath)
            if fm.fileExists(atPath: u.path) { return u }
        }
        // Dev fallback: …/Sources/NucleicCore/MacVM/<thisfile> → repo root is four levels up.
        let repoRoot = URL(fileURLWithPath: #filePath)
            .deletingLastPathComponent().deletingLastPathComponent()
            .deletingLastPathComponent().deletingLastPathComponent()
        return repoRoot.appendingPathComponent(relativePath)
    }

    // MARK: - Phase config builder

    /// Assemble a VZ config for one base-build boot: an external kernel + initrd, a writable root disk,
    /// a serial console → `consoleLog`, and (optionally) NAT networking, the vsock agent device, and a
    /// read-only **payload disk** (a raw tar the bootstrap untars). Deliberately minimal — no graphics/HID
    /// (the base build is headless).
    func makeLinuxPhaseConfig(
        kernel: URL, initrd: URL, cmdline: String, rootDisk: URL, consoleLog: URL,
        withNetwork: Bool, withVsock: Bool, payloadDisk: URL?
    ) throws -> VZVirtualMachineConfiguration {
        let config = VZVirtualMachineConfiguration()
        config.platform = VZGenericPlatformConfiguration()
        let bootLoader = VZLinuxBootLoader(kernelURL: kernel)
        bootLoader.initialRamdiskURL = initrd
        bootLoader.commandLine = cmdline
        config.bootLoader = bootLoader

        config.cpuCount = Self.clampCPU(MacVMSettings.defaultVMCPUs)
        config.memorySize = Self.clampMemory(UInt64(MacVMSettings.defaultVMMemoryGiB) * 1024 * 1024 * 1024)

        // Root disk first (→ /dev/vda), then the read-only payload tar (→ /dev/vdb) when provided. The
        // bootstrap `/init` doesn't rely on that ordering — it detects each by content — but keeping root
        // first matches the phase-2 boot (which attaches only the root disk).
        let rootAttachment = try VZDiskImageStorageDeviceAttachment(url: rootDisk, readOnly: false)
        var storage: [VZStorageDeviceConfiguration] = [VZVirtioBlockDeviceConfiguration(attachment: rootAttachment)]
        if let payloadDisk {
            let payloadAttachment = try VZDiskImageStorageDeviceAttachment(url: payloadDisk, readOnly: true)
            storage.append(VZVirtioBlockDeviceConfiguration(attachment: payloadAttachment))
        }
        config.storageDevices = storage

        if withNetwork {
            let net = VZVirtioNetworkDeviceConfiguration()
            net.macAddress = VZMACAddress.randomLocallyAdministered()
            net.attachment = VZNATNetworkDeviceAttachment()
            config.networkDevices = [net]
        }
        if withVsock {
            config.socketDevices = [VZVirtioSocketDeviceConfiguration()]
        }

        let console = VZVirtioConsoleDeviceSerialPortConfiguration()
        console.attachment = try VZFileSerialPortAttachment(url: consoleLog, append: false)
        config.serialPorts = [console]

        config.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
        config.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()]

        try config.validate()
        return config
    }

    /// Pack the staged provisioning payload directory into a single **ustar** tar written as a raw disk
    /// image (its bytes ARE the disk). The bootstrap attaches it read-only as a second virtio-blk device
    /// and untars it — no filesystem driver, so it needs only the built-in `virtio_blk`. ustar puts a
    /// `ustar` magic at byte 257 of the first header, which `/init` uses to tell the payload disk from
    /// the (empty) root disk regardless of enumeration order. tar pads to 512-multiple records, which
    /// satisfies VZ's disk-size requirement.
    func makePayloadDiskImage(from dir: URL) throws -> URL {
        let image = storageRoot.appendingPathComponent("linux-prov-payload.img")
        try? FileManager.default.removeItem(at: image)
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: "/usr/bin/tar")
        // `-C dir .` archives the payload files at the archive root (./rootfs.tar.xz, ./nucleic-…).
        proc.arguments = ["--format", "ustar", "-cf", image.path, "-C", dir.path, "."]
        try proc.run()
        proc.waitUntilExit()
        guard proc.terminationStatus == 0, FileManager.default.fileExists(atPath: image.path) else {
            throw MacVMError.installFailed("could not pack the Linux provisioning payload into a tar disk image")
        }
        return image
    }
}
#endif
