182 lines
4.4 KiB
Bash
Executable File
182 lines
4.4 KiB
Bash
Executable File
#!/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_KEY=""
|
|
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 (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_KEY="$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 [ -n "$SSH_KEY" ] && [ -z "$SEED_ISO" ]; then
|
|
SEED_ISO=$(mktemp /tmp/seed-XXXXXX.iso)
|
|
CLEANUP_SEED="$SEED_ISO"
|
|
bash "$SCRIPT_DIR/make-seed.sh" "$SSH_KEY" "$SEED_ISO"
|
|
fi
|
|
|
|
cleanup() {
|
|
[ -n "$CLEANUP_SEED" ] && rm -f "$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
|
|
if [ -n "$PROJECTS" ]; then
|
|
QEMU_ARGS+=(
|
|
-virtfs "local,path=$PROJECTS,mount_tag=projects,security_model=mapped-xattr,id=fs0"
|
|
)
|
|
fi
|
|
|
|
if [ -n "$CLAUDE_DIR" ]; then
|
|
QEMU_ARGS+=(
|
|
-virtfs "local,path=$CLAUDE_DIR,mount_tag=hostclaude,security_model=mapped-xattr,id=fs1"
|
|
)
|
|
# also mount parent home for .claude.json
|
|
QEMU_ARGS+=(
|
|
-virtfs "local,path=$(dirname "$CLAUDE_DIR"),mount_tag=hosthome,security_model=mapped-xattr,id=fs2,readonly=on"
|
|
)
|
|
fi
|
|
|
|
echo "---"
|
|
echo "Guest: $GUEST_ARCH | Accel: $ACCEL"
|
|
echo "SSH: ssh -A -p $SSH_PORT gordaina@localhost"
|
|
echo "---"
|
|
|
|
exec "${QEMU_ARGS[@]}"
|