Nucleic-Session: 4FCF4F8B-A7C3-42F6-BE13-1979080F61C7 Co-authored-by: Nucleic <[email protected]>
144 lines
8.8 KiB
Bash
144 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# Build the golden macOS-guest base image the MacVMEngine clones per session.
|
|
#
|
|
# scripts/build-macos-base.sh [/path/to/UniversalMac.ipsw]
|
|
#
|
|
# This is the HOST-side orchestration for the one-time "install + provision locally" base flow.
|
|
# Nucleic boots each agent session in a macOS VM (Apple's Virtualization.framework) by cloning a
|
|
# pristine "golden base" bundle, then SSHing into it as `agent@<ip>`. This script installs a CLEAN
|
|
# macOS into that base and then hands you the manual/provisioning steps to make it golden.
|
|
#
|
|
# HEADS UP — this is a ~14 GB, multi-step, PARTLY MANUAL, one-time process.
|
|
# Apple provides NO unattended macOS-guest install outside MDM/DEP, so the operator MUST complete
|
|
# Setup Assistant once, by hand, in the VM's window (create the `agent` account, enable Remote
|
|
# Login), before the in-guest provisioning script can run. Don't expect this to be push-button.
|
|
#
|
|
# The flow, end to end:
|
|
# 1. Build + ad-hoc-codesign the `macvm-spike` helper (it needs the virtualization entitlement,
|
|
# exactly like the app — see signing/spike.entitlements). [automated, here]
|
|
# 2. Run the spike with NUCLEIC_MACVM_BUILD_BASE=1 to install a clean macOS into the base bundle
|
|
# at ~/Library/Application Support/Nucleic/macvms/base/ (HardwareModel, MachineIdentifier,
|
|
# AuxiliaryStorage, Disk.img). Downloads ~14 GB on first use unless you pass a local .ipsw.
|
|
# [automated, here]
|
|
# 3. Manually finish the guest: open the base VM in a window, complete Setup Assistant creating an
|
|
# account named `agent`, enable Remote Login, then run scripts/provision-macos-guest.sh INSIDE
|
|
# the guest to install the toolchain + authorize Nucleic's SSH key, and shut it down clean.
|
|
# [MANUAL — instructions only]
|
|
#
|
|
# After step 3 the base is "golden": MacVMEngine.ensureRunning() will clone it and SSH straight in.
|
|
# See docs/MACOS_VM.md for the full picture, screenshots, and the GUI helper for step 3.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
PRODUCT="macvm-spike"
|
|
ENTITLEMENTS="$ROOT/signing/spike.entitlements"
|
|
|
|
# The base bundle + host SSH key paths mirror MacVMEngine's on-disk layout exactly
|
|
# (defaultStorageRoot() → ~/Library/Application Support/Nucleic/macvms). Keep these in sync with
|
|
# Sources/NucleicCore/MacVM/MacVMEngine.swift if that ever moves.
|
|
STORAGE_ROOT="$HOME/Library/Application Support/Nucleic/macvms"
|
|
BASE_DIR="$STORAGE_ROOT/base"
|
|
PUBKEY="$STORAGE_ROOT/ssh/id_ed25519.pub"
|
|
|
|
# Optional local restore image: first positional arg wins, else $NUCLEIC_MACVM_IPSW, else the spike
|
|
# fetches the latest supported image over the network (~14 GB).
|
|
IPSW="${1:-${NUCLEIC_MACVM_IPSW:-}}"
|
|
|
|
# ── Preflight ────────────────────────────────────────────────────────────────────────────────────
|
|
# Virtualization of a macOS guest is Apple-silicon only; fail early rather than deep inside the spike.
|
|
if [ "$(uname -s)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then
|
|
echo "✗ macOS-guest VMs require an Apple-silicon Mac (arm64). This host is $(uname -s)/$(uname -m)." >&2
|
|
exit 1
|
|
fi
|
|
command -v swift >/dev/null || { echo "✗ swift not found (install Xcode / command line tools)." >&2; exit 1; }
|
|
command -v codesign >/dev/null || { echo "✗ codesign not found (install Xcode command line tools)." >&2; exit 1; }
|
|
[ -f "$ENTITLEMENTS" ] || { echo "✗ missing $ENTITLEMENTS" >&2; exit 1; }
|
|
if [ -n "$IPSW" ] && [ ! -f "$IPSW" ]; then
|
|
echo "✗ restore image not found: $IPSW" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── Step 1: build + ad-hoc-codesign the spike ────────────────────────────────────────────────────
|
|
# Ad-hoc signing (`--sign -`) is enough for com.apple.security.virtualization; a Developer ID is NOT
|
|
# required to run VMs locally. --force re-signs an existing binary so this stays idempotent.
|
|
echo "▸ [1/3] Building $PRODUCT …"
|
|
swift build --product "$PRODUCT"
|
|
BIN_DIR="$(swift build --product "$PRODUCT" --show-bin-path)"
|
|
BIN="$BIN_DIR/$PRODUCT"
|
|
[ -x "$BIN" ] || { echo "✗ built binary not found at $BIN" >&2; exit 1; }
|
|
|
|
echo "▸ Ad-hoc codesigning $PRODUCT with $(basename "$ENTITLEMENTS") …"
|
|
codesign --force --sign - --entitlements "$ENTITLEMENTS" "$BIN"
|
|
codesign --verify --verbose=1 "$BIN"
|
|
|
|
# ── Step 2: install a clean macOS into the base bundle ───────────────────────────────────────────
|
|
# Idempotency note: if a COMPLETE base bundle already exists we skip the multi-minute reinstall.
|
|
# (An interrupted install leaves base.building/, never a half-built base/ — see buildBaseImage.)
|
|
BASE_COMPLETE=1
|
|
for f in HardwareModel MachineIdentifier AuxiliaryStorage Disk.img; do
|
|
[ -e "$BASE_DIR/$f" ] || BASE_COMPLETE=0
|
|
done
|
|
if [ "$BASE_COMPLETE" -eq 1 ]; then
|
|
echo "▸ [2/3] Base bundle already installed at:"
|
|
echo " $BASE_DIR"
|
|
echo " (delete that directory to force a clean reinstall.) Skipping install."
|
|
else
|
|
echo "▸ [2/3] Installing clean macOS into the base bundle — this is the ~14 GB / multi-minute step."
|
|
if [ -n "$IPSW" ]; then
|
|
echo " using local restore image: $IPSW"
|
|
else
|
|
echo " no .ipsw supplied — the spike will fetch the latest supported restore image (~14 GB)."
|
|
echo " (pass a local UniversalMac.ipsw as \$1 or set NUCLEIC_MACVM_IPSW to skip the download.)"
|
|
fi
|
|
NUCLEIC_MACVM_BUILD_BASE=1 ${IPSW:+NUCLEIC_MACVM_IPSW="$IPSW"} "$BIN"
|
|
echo "✓ clean macOS installed into $BASE_DIR (guest now sits at Setup Assistant)."
|
|
fi
|
|
|
|
# ── Step 3: the manual + provisioning phase (instructions only) ──────────────────────────────────
|
|
# We CANNOT automate this: Apple has no unattended macOS-guest install/first-boot path outside
|
|
# MDM/DEP, so a human must click through Setup Assistant once, in a display session, to create the
|
|
# `agent` account. Everything after that (toolchain + SSH key) is provision-macos-guest.sh.
|
|
cat <<EOF
|
|
|
|
────────────────────────────────────────────────────────────────────────────────────────────────
|
|
✓ Base install done. NEXT STEPS ARE MANUAL — follow these to make the base "golden":
|
|
|
|
A. Boot the base VM in a WINDOW so you can drive Setup Assistant.
|
|
The installed guest sits at Setup Assistant, which needs a display — the headless spike/engine
|
|
path can't show it. Use the GUI helper (Apple's VZVirtualMachineView over the SAME base
|
|
bundle) described in docs/MACOS_VM.md. In the VM window:
|
|
• Complete Setup Assistant.
|
|
• Create an account whose SHORT NAME is exactly: agent
|
|
(this is MacVMSettings.defaultSSHUser — Nucleic logs in as agent@<ip>).
|
|
• Skip Apple ID / analytics; you just need a local admin `agent` account.
|
|
|
|
B. Get scripts/provision-macos-guest.sh + Nucleic's PUBLIC key into the guest and run it AS agent.
|
|
Easiest: temporarily enable Remote Login in the VM (System Settings ▸ General ▸ Sharing ▸
|
|
Remote Login), then from THIS host, scp the script + key in, or paste them via the window.
|
|
|
|
Nucleic's host PUBLIC key to authorize for \`agent\` (its private half never leaves the host):
|
|
$PUBKEY
|
|
|
|
Inside the guest, as the agent user:
|
|
chmod +x provision-macos-guest.sh
|
|
./provision-macos-guest.sh /path/to/id_ed25519.pub # the key you copied in
|
|
(It enables Remote Login, installs the toolchain, appends PATH to /etc/zshenv, authorizes the
|
|
key, and finally shuts the guest down cleanly.)
|
|
|
|
C. Once the guest has powered off, the base bundle at
|
|
$BASE_DIR
|
|
is golden and ready to clone. Verify with: swift build --product $PRODUCT && \\
|
|
codesign --force --sign - --entitlements signing/spike.entitlements "$BIN" && "$BIN"
|
|
(default spike mode: clone → boot → ssh \`sw_vers\` → teardown; expect "SPIKE PASS").
|
|
────────────────────────────────────────────────────────────────────────────────────────────────
|
|
EOF
|
|
|
|
if [ ! -f "$PUBKEY" ]; then
|
|
echo "⚠ Note: $PUBKEY does not exist yet — the spike generates the host keypair during step 2." >&2
|
|
echo " If step 2 was skipped, run the app once (or the spike) to create it, then re-read step B." >&2
|
|
fi
|