11name : Build QEMU Emulator Image (arm64 / macOS)
22
3- # arm64 emulator images are built on a macOS Apple Silicon runner so the
4- # snapshot is captured under HVF — the same accelerator developer Macs use.
5- # KVM snapshots (from Linux runners) are NOT resumable under HVF because
6- # `-cpu max` expands to different feature sets under each accelerator.
3+ # arm64 emulator images are built in two stages:
4+ # 1. docker-build (Linux): builds the Docker container image for arm64 and
5+ # exports a tarball — Docker is painful to run on macOS CI runners.
6+ # 2. qemu-snapshot (macOS): boots the image under HVF on Apple Silicon,
7+ # provisions it, and captures a snapshot. HVF snapshots are portable to
8+ # developer Macs; KVM snapshots are NOT (differing -cpu max features).
79
810on :
911 push :
@@ -22,14 +24,68 @@ concurrency:
2224
2325env :
2426 EMULATOR_IMAGE_NAME : stack-local-emulator
25- EMULATOR_IMAGE_DIR : ${{ github.workspace }}/docker/local-emulator/qemu/images
26- EMULATOR_RUN_DIR : ${{ github.workspace }}/docker/local-emulator/qemu/run
2727
2828jobs :
29- build :
30- name : Build QEMU Image (arm64)
29+ # ---------- Stage 1: build Docker image on Linux ----------
30+ docker-build :
31+ name : Build Docker Image (arm64)
32+ runs-on : ubicloud-standard-8
33+ timeout-minutes : 60
34+
35+ steps :
36+ - uses : actions/checkout@v6
37+
38+ - name : Set up QEMU user-mode emulation
39+ uses : docker/setup-qemu-action@v3
40+
41+ - name : Set up Docker Buildx
42+ uses : docker/setup-buildx-action@v3
43+
44+ - uses : pnpm/action-setup@v4
45+ with :
46+ version : 10.23.0
47+
48+ - uses : actions/setup-node@v4
49+ with :
50+ node-version : 22
51+ cache : pnpm
52+
53+ - name : Generate emulator env
54+ run : node docker/local-emulator/generate-env-development.mjs
55+
56+ - name : Build arm64 Docker image
57+ run : |
58+ docker buildx build \
59+ --platform linux/arm64 \
60+ --tag "$EMULATOR_IMAGE_NAME" \
61+ --load \
62+ -f docker/local-emulator/Dockerfile \
63+ .
64+
65+ - name : Export Docker image bundle
66+ run : |
67+ mkdir -p /tmp/bundle
68+ docker save "$EMULATOR_IMAGE_NAME" | gzip -c > /tmp/bundle/emulator-arm64-docker-images.tar.gz
69+ docker image inspect --format '{{.ID}}' "$EMULATOR_IMAGE_NAME" > /tmp/bundle/emulator-arm64-docker-images.tar.gz.image-ids
70+ ls -lh /tmp/bundle/
71+
72+ - name : Upload Docker bundle
73+ uses : actions/upload-artifact@v4
74+ with :
75+ name : arm64-docker-bundle
76+ path : /tmp/bundle/
77+ retention-days : 1
78+ compression-level : 0
79+
80+ # ---------- Stage 2: QEMU provision + snapshot on macOS (HVF) ----------
81+ qemu-snapshot :
82+ name : QEMU Snapshot (arm64 / HVF)
83+ needs : docker-build
3184 runs-on : macos-15
3285 timeout-minutes : 120
86+ env :
87+ EMULATOR_IMAGE_DIR : ${{ github.workspace }}/docker/local-emulator/qemu/images
88+ EMULATOR_RUN_DIR : ${{ github.workspace }}/docker/local-emulator/qemu/run
3389
3490 steps :
3591 - uses : actions/checkout@v6
@@ -46,17 +102,6 @@ jobs:
46102 - name : Install system dependencies
47103 run : brew install qemu socat zstd
48104
49- - name : Set up Docker via colima
50- run : |
51- brew install docker docker-buildx colima
52- # Wire up buildx as a CLI plugin
53- mkdir -p ~/.docker
54- echo '{"cliPluginsExtraDirs":["/opt/homebrew/lib/docker/cli-plugins"]}' > ~/.docker/config.json
55- # VZ driver doesn't work on GHA macOS runners — use QEMU backend
56- colima start --vm-type=qemu --cpu 4 --memory 6 --disk 60 --arch aarch64
57- docker info
58- docker buildx version
59-
60105 - name : Verify QEMU + HVF
61106 run : |
62107 qemu-system-aarch64 --version
@@ -67,17 +112,26 @@ jobs:
67112 exit 1
68113 fi
69114
70- - name : Build QEMU image
115+ - name : Download Docker bundle
116+ uses : actions/download-artifact@v4
117+ with :
118+ name : arm64-docker-bundle
119+ path : ${{ env.EMULATOR_IMAGE_DIR }}/
120+
121+ - name : Generate emulator env
122+ run : node docker/local-emulator/generate-env-development.mjs
123+
124+ - name : Build QEMU image (provision + snapshot)
71125 run : |
72126 chmod +x docker/local-emulator/qemu/build-image.sh
127+ # SKIP_DOCKER_BUILD=1 tells build-image.sh to skip the Docker
128+ # build + export steps — we already have the bundle from stage 1.
73129 EMULATOR_PROVISION_TIMEOUT=6000 \
130+ SKIP_DOCKER_BUILD=1 \
74131 docker/local-emulator/qemu/build-image.sh arm64
75132
76- - name : Generate emulator env
77- run : node docker/local-emulator/generate-env-development.mjs
78-
79- # HVF gives us native-speed arm64 — we can verify the image boots
80- # and services come up, unlike the old cross-arch TCG path.
133+ # HVF gives us native-speed arm64 — verify the image boots and
134+ # services come up (previously impossible under cross-arch TCG).
81135 - name : Build stack-cli
82136 run : |
83137 pnpm install --frozen-lockfile --filter '@stackframe/stack-cli...'
@@ -87,34 +141,27 @@ jobs:
87141 env :
88142 EMULATOR_ARCH : arm64
89143 EMULATOR_READY_TIMEOUT : 3200
90- EMULATOR_IMAGE_DIR : ${{ env.EMULATOR_IMAGE_DIR }}
91- EMULATOR_RUN_DIR : ${{ env.EMULATOR_RUN_DIR }}
92144 run : node packages/stack-cli/dist/index.js emulator start
93145
94146 - name : Verify services are healthy
95147 env :
96148 EMULATOR_ARCH : arm64
97- EMULATOR_IMAGE_DIR : ${{ env.EMULATOR_IMAGE_DIR }}
98- EMULATOR_RUN_DIR : ${{ env.EMULATOR_RUN_DIR }}
99149 run : node packages/stack-cli/dist/index.js emulator status
100150
101151 - name : Stop emulator
102152 if : always()
103153 env :
104154 EMULATOR_ARCH : arm64
105- EMULATOR_IMAGE_DIR : ${{ env.EMULATOR_IMAGE_DIR }}
106- EMULATOR_RUN_DIR : ${{ env.EMULATOR_RUN_DIR }}
107155 run : node packages/stack-cli/dist/index.js emulator stop
108156
109157 - name : Print serial log on failure
110158 if : failure()
111- run : |
112- tail -100 "$EMULATOR_RUN_DIR/vm/serial.log" 2>/dev/null || true
159+ run : tail -100 "$EMULATOR_RUN_DIR/vm/serial.log" 2>/dev/null || true
113160
114161 - name : Package image
115162 run : |
116- BASE_IMG="docker/local-emulator/qemu/images /stack-emulator-arm64.qcow2"
117- SAVEVM="docker/local-emulator/qemu/images /stack-emulator-arm64.savevm.zst"
163+ BASE_IMG="$EMULATOR_IMAGE_DIR /stack-emulator-arm64.qcow2"
164+ SAVEVM="$EMULATOR_IMAGE_DIR /stack-emulator-arm64.savevm.zst"
118165 cp "$BASE_IMG" "stack-emulator-arm64.qcow2"
119166 if [ -f "$SAVEVM" ]; then
120167 cp "$SAVEVM" "stack-emulator-arm64.savevm.zst"
0 commit comments