This commit is contained in:
2026-03-02 16:35:14 +01:00
parent d182532b34
commit 6c8e7ec373
17 changed files with 703 additions and 52 deletions

183
dist/run.sh vendored Executable file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# defaults
SSH_PORT=2222
MEMORY=8G
CPUS=4
PROJECTS=""
CLAUDE_DIR=""
SSH_KEYS=()
SEED_ISO=""
IMAGE=""
GUEST_ARCH=""
usage() {
cat <<EOF
Usage: run.sh <image.qcow2> [options]
Options:
--arch <arch> Guest architecture (auto-detected from image name)
--ssh-key <key.pub> SSH public key (repeatable, auto-generates seed ISO)
--seed-iso <iso> Pre-built seed ISO (alternative to --ssh-key)
--projects <path> Mount host directory as ~/projects in VM
--claude-dir <path> Mount host .claude directory for auth
--memory <size> VM memory (default: 8G)
--cpus <n> VM CPUs (default: 4)
--ssh-port <port> SSH port forward (default: 2222)
EOF
exit 1
}
[ "${1:-}" ] || usage
IMAGE="$1"
shift
while [ $# -gt 0 ]; do
case "$1" in
--arch) GUEST_ARCH="$2"; shift 2 ;;
--ssh-key) SSH_KEYS+=("$2"); shift 2 ;;
--seed-iso) SEED_ISO="$2"; shift 2 ;;
--projects) PROJECTS="$2"; shift 2 ;;
--claude-dir) CLAUDE_DIR="$2"; shift 2 ;;
--memory) MEMORY="$2"; shift 2 ;;
--cpus) CPUS="$2"; shift 2 ;;
--ssh-port) SSH_PORT="$2"; shift 2 ;;
*) echo "unknown option: $1"; usage ;;
esac
done
if [ ! -f "$IMAGE" ]; then
echo "error: image not found: $IMAGE"
exit 1
fi
# detect guest architecture from image filename if not specified
if [ -z "$GUEST_ARCH" ]; then
case "$IMAGE" in
*aarch64*|*arm64*) GUEST_ARCH="aarch64" ;;
*x86_64*|*amd64*) GUEST_ARCH="x86_64" ;;
*)
# fallback to host architecture
case "$(uname -m)" in
x86_64|amd64) GUEST_ARCH="x86_64" ;;
aarch64|arm64) GUEST_ARCH="aarch64" ;;
*) echo "error: cannot detect guest arch, use --arch"; exit 1 ;;
esac
;;
esac
fi
# normalize
case "$GUEST_ARCH" in
x86_64|amd64) GUEST_ARCH="x86_64" ;;
aarch64|arm64) GUEST_ARCH="aarch64" ;;
*) echo "error: unsupported architecture: $GUEST_ARCH"; exit 1 ;;
esac
# platform detection
HOST_ARCH=$(uname -m)
OS=$(uname -s)
ACCEL="tcg"
case "$OS" in
Linux)
[ -r /dev/kvm ] && ACCEL="kvm"
;;
Darwin)
# hvf only works when guest matches host
case "$HOST_ARCH" in
aarch64|arm64) [ "$GUEST_ARCH" = "aarch64" ] && ACCEL="hvf" ;;
x86_64|amd64) [ "$GUEST_ARCH" = "x86_64" ] && ACCEL="hvf" ;;
esac
;;
esac
case "$GUEST_ARCH" in
x86_64) QEMU_BIN="qemu-system-x86_64" ;;
aarch64) QEMU_BIN="qemu-system-aarch64" ;;
esac
# auto-generate seed ISO from SSH key
CLEANUP_SEED=""
if [ "${#SSH_KEYS[@]}" -gt 0 ] && [ -z "$SEED_ISO" ]; then
SEED_ISO="$(mktemp -d)/seed.iso"
CLEANUP_SEED="$SEED_ISO"
bash "$SCRIPT_DIR/make-seed.sh" "$SEED_ISO" "${SSH_KEYS[@]}"
fi
cleanup() {
[ -n "$CLEANUP_SEED" ] && rm -rf "$(dirname "$CLEANUP_SEED")"
}
trap cleanup EXIT
# build qemu command
QEMU_ARGS=(
"$QEMU_BIN"
-accel "$ACCEL"
-m "$MEMORY"
-smp "$CPUS"
-drive "file=$IMAGE,format=qcow2,snapshot=on"
-nic "user,hostfwd=tcp::${SSH_PORT}-:22"
-nographic
)
# aarch64 guest needs machine type and uefi firmware
if [ "$GUEST_ARCH" = "aarch64" ]; then
if [ "$ACCEL" = "hvf" ]; then
QEMU_ARGS+=(-machine virt -cpu host)
else
QEMU_ARGS+=(-machine virt -cpu max)
fi
EFI_CODE=""
for p in \
/opt/homebrew/share/qemu/edk2-aarch64-code.fd \
/usr/local/share/qemu/edk2-aarch64-code.fd \
/usr/share/qemu-efi-aarch64/QEMU_EFI.fd \
/usr/share/AAVMF/AAVMF_CODE.fd; do
[ -f "$p" ] && EFI_CODE="$p" && break
done
if [ -z "$EFI_CODE" ]; then
echo "error: aarch64 EFI firmware not found"
echo " macos: brew install qemu"
echo " linux: apt install qemu-efi-aarch64"
exit 1
fi
QEMU_ARGS+=(-bios "$EFI_CODE")
fi
# seed ISO
if [ -n "$SEED_ISO" ]; then
QEMU_ARGS+=(-drive "file=$SEED_ISO,format=raw,media=cdrom,readonly=on")
fi
# 9p mounts — none lets the guest control ownership via dfltuid/dfltgid
SECURITY_MODEL="none"
if [ -n "$PROJECTS" ]; then
QEMU_ARGS+=(
-virtfs "local,path=$PROJECTS,mount_tag=projects,security_model=$SECURITY_MODEL,id=fs0"
)
fi
if [ -n "$CLAUDE_DIR" ]; then
QEMU_ARGS+=(
-virtfs "local,path=$CLAUDE_DIR,mount_tag=hostclaude,security_model=$SECURITY_MODEL,id=fs1"
)
# also mount parent home for .claude.json
QEMU_ARGS+=(
-virtfs "local,path=$(dirname "$CLAUDE_DIR"),mount_tag=hosthome,security_model=$SECURITY_MODEL,id=fs2,readonly=on"
)
fi
echo "---"
echo "Guest: $GUEST_ARCH | Accel: $ACCEL"
echo "SSH: ssh -A -p $SSH_PORT sandbox@localhost"
echo "---"
exec "${QEMU_ARGS[@]}"