|
| 1 | +name: five hosts |
| 2 | + |
| 3 | +# ⚠️ THIS WORKFLOW IS THE QUESTION, NOT THE ANSWER. |
| 4 | +# |
| 5 | +# The mcpp/xlings package index has a stated bar for admitting an emulator, and |
| 6 | +# `qemu-riscv`'s descriptor writes it down: prebuilt binaries for the FIVE host |
| 7 | +# targets the index serves — linux x64, linux arm64, darwin x64, darwin arm64, |
| 8 | +# win32 x64 — from one versioned release, each asset with a checksum sidecar. |
| 9 | +# |
| 10 | +# xPack publishes QEMU per target family and has no x86 build. qemu.org ships a |
| 11 | +# Windows installer only; macOS and Linux are served by distribution packages. |
| 12 | +# So no upstream clears the bar, and `xim:qemu-x86` cannot be a repackaging job |
| 13 | +# the way `xim:qemu-arm` and `xim:qemu-riscv` are — it has to be built. |
| 14 | +# |
| 15 | +# ⭐ THE POINT OF DOING IT HERE FIRST IS THAT A FAILURE COSTS A RED CROSS. |
| 16 | +# |
| 17 | +# The alternative order — write the descriptor, mirror the assets, open the |
| 18 | +# index PR, then discover that the Windows leg does not build — produces a |
| 19 | +# published package that cannot be installed. GitHub's hosted runners happen to |
| 20 | +# cover exactly the five hosts the index serves, so all five legs can be |
| 21 | +# attempted in one matrix before anything is published, and the hardest one |
| 22 | +# fails early rather than last. |
| 23 | +# |
| 24 | +# ⚠️ NOTHING HERE PUBLISHES. The artifacts are retained for inspection and for |
| 25 | +# measuring what a real payload would weigh. Admission to the index is a |
| 26 | +# separate decision, made after five green legs, and it needs the DT_NEEDED / |
| 27 | +# otool closure measured per host rather than copied from a sibling descriptor. |
| 28 | + |
| 29 | +on: |
| 30 | + push: |
| 31 | + branches: [main] |
| 32 | + pull_request: |
| 33 | + workflow_dispatch: |
| 34 | + |
| 35 | +env: |
| 36 | + # ⚠️ PINNED, AND NOT TO `master`. A payload the index serves must be |
| 37 | + # reproducible from a version, and "whatever upstream had that day" is not a |
| 38 | + # version. 9.2.4 is the series `qemu-arm` and `qemu-riscv` already carry, so |
| 39 | + # a user who installs all three gets one QEMU generation rather than two. |
| 40 | + QEMU_VERSION: 9.2.4 |
| 41 | + |
| 42 | +jobs: |
| 43 | + build: |
| 44 | + name: qemu-system-x86_64 on ${{ matrix.label }} |
| 45 | + runs-on: ${{ matrix.os }} |
| 46 | + timeout-minutes: 90 |
| 47 | + strategy: |
| 48 | + fail-fast: false |
| 49 | + matrix: |
| 50 | + include: |
| 51 | + - { label: linux-x64, os: ubuntu-24.04 } |
| 52 | + - { label: linux-arm64, os: ubuntu-24.04-arm } |
| 53 | + - { label: darwin-arm64, os: macos-14 } |
| 54 | + - { label: darwin-x64, os: macos-13 } |
| 55 | + - { label: win32-x64, os: windows-2022 } |
| 56 | + defaults: |
| 57 | + run: |
| 58 | + shell: bash |
| 59 | + |
| 60 | + steps: |
| 61 | + - uses: actions/checkout@v4 |
| 62 | + |
| 63 | + # ── The dependencies, per host ──────────────────────────────────────── |
| 64 | + # |
| 65 | + # ⚠️ A SINGLE TARGET IS WHAT MAKES THIS AFFORDABLE. QEMU's full build is |
| 66 | + # dozens of system emulators plus tools, docs and UI backends; |
| 67 | + # `--target-list=x86_64-softmmu` with the UI and tools disabled is a small |
| 68 | + # fraction of it. The index needs one emulator, not a distribution. |
| 69 | + - name: Dependencies (Linux) |
| 70 | + if: runner.os == 'Linux' |
| 71 | + run: | |
| 72 | + sudo apt-get update -qq |
| 73 | + sudo apt-get install -y -qq \ |
| 74 | + ninja-build meson pkg-config python3-venv \ |
| 75 | + libglib2.0-dev libpixman-1-dev zlib1g-dev flex bison |
| 76 | +
|
| 77 | + - name: Dependencies (macOS) |
| 78 | + if: runner.os == 'macOS' |
| 79 | + run: | |
| 80 | + brew install ninja meson pkg-config glib pixman |
| 81 | + # ⚠️ NOT INSTALLING `qemu` ITSELF. Homebrew's formula would pull a |
| 82 | + # working emulator and every check below would pass without this |
| 83 | + # workflow having built anything — the shape of false green this |
| 84 | + # repository exists to avoid. |
| 85 | +
|
| 86 | + # ⚠️ THE WINDOWS LEG IS THE ONE THIS WORKFLOW EXISTS TO ANSWER, AND IT IS |
| 87 | + # NOT AN ORDINARY BUILD. |
| 88 | + # |
| 89 | + # QEMU on Windows is built under MSYS2/MinGW, not MSVC: its build system |
| 90 | + # is meson and its sources assume a POSIX-ish toolchain. The result is a |
| 91 | + # native PE that needs a set of MinGW runtime DLLs beside it, which is why |
| 92 | + # the packaging question here is different from the other four hosts — |
| 93 | + # `otool`/`readelf` closure has a `ntldd` counterpart and the answer is a |
| 94 | + # directory of DLLs rather than an rpath. |
| 95 | + - name: Dependencies (Windows / MSYS2) |
| 96 | + if: runner.os == 'Windows' |
| 97 | + uses: msys2/setup-msys2@v2 |
| 98 | + with: |
| 99 | + msystem: UCRT64 |
| 100 | + update: true |
| 101 | + install: >- |
| 102 | + base-devel |
| 103 | + git |
| 104 | + python |
| 105 | + mingw-w64-ucrt-x86_64-toolchain |
| 106 | + mingw-w64-ucrt-x86_64-glib2 |
| 107 | + mingw-w64-ucrt-x86_64-pixman |
| 108 | + mingw-w64-ucrt-x86_64-ninja |
| 109 | + mingw-w64-ucrt-x86_64-meson |
| 110 | + mingw-w64-ucrt-x86_64-pkgconf |
| 111 | + mingw-w64-ucrt-x86_64-zlib |
| 112 | +
|
| 113 | + - name: Fetch QEMU ${{ env.QEMU_VERSION }} |
| 114 | + run: | |
| 115 | + set -euo pipefail |
| 116 | + # ⚠️ `--retry-all-errors`, NOT JUST `--retry`. curl's plain `--retry` |
| 117 | + # covers transient HTTP status codes and NOT transport-layer errors, |
| 118 | + # and `curl: (52) empty reply from server` is the one this project |
| 119 | + # keeps meeting. Without it a truncated download is reported as |
| 120 | + # success and the failure surfaces as a corrupt archive. |
| 121 | + curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 \ |
| 122 | + -o qemu.tar.xz \ |
| 123 | + "https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz" |
| 124 | + curl -fsSL --retry 5 --retry-all-errors --retry-delay 3 \ |
| 125 | + -o qemu.tar.xz.sig \ |
| 126 | + "https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz.sig" || true |
| 127 | + tar xf qemu.tar.xz |
| 128 | + echo "SRC=$PWD/qemu-${QEMU_VERSION}" >> "$GITHUB_ENV" |
| 129 | +
|
| 130 | + - name: Configure and build (Unix) |
| 131 | + if: runner.os != 'Windows' |
| 132 | + run: | |
| 133 | + set -euo pipefail |
| 134 | + mkdir -p build && cd build |
| 135 | + "$SRC/configure" \ |
| 136 | + --target-list=x86_64-softmmu \ |
| 137 | + --prefix="$PWD/../out" \ |
| 138 | + --disable-docs --disable-guest-agent --disable-tools \ |
| 139 | + --disable-vnc --disable-sdl --disable-gtk --disable-curses \ |
| 140 | + --disable-libssh --disable-vde --disable-spice \ |
| 141 | + --disable-smartcard --disable-usb-redir --disable-opengl \ |
| 142 | + --disable-virglrenderer --disable-blkio --disable-libdaxctl \ |
| 143 | + --disable-brlapi --disable-curl |
| 144 | + ninja -j"$(getconf _NPROCESSORS_ONLN)" |
| 145 | + ninja install |
| 146 | +
|
| 147 | + - name: Configure and build (Windows / MSYS2) |
| 148 | + if: runner.os == 'Windows' |
| 149 | + shell: msys2 {0} |
| 150 | + run: | |
| 151 | + set -euo pipefail |
| 152 | + cd "$(cygpath "$GITHUB_WORKSPACE")" |
| 153 | + mkdir -p build && cd build |
| 154 | + "../qemu-${QEMU_VERSION}/configure" \ |
| 155 | + --target-list=x86_64-softmmu \ |
| 156 | + --prefix="$PWD/../out" \ |
| 157 | + --disable-docs --disable-guest-agent --disable-tools \ |
| 158 | + --disable-vnc --disable-sdl --disable-gtk --disable-curses \ |
| 159 | + --disable-libssh --disable-spice --disable-smartcard \ |
| 160 | + --disable-usb-redir --disable-opengl --disable-virglrenderer \ |
| 161 | + --disable-curl |
| 162 | + ninja -j"$(nproc)" |
| 163 | + ninja install |
| 164 | +
|
| 165 | + # ── The check that the artifact is an emulator, not a file ──────────── |
| 166 | + # |
| 167 | + # ⭐ A BUILT BINARY IS NOT EVIDENCE; A BOOTED IMAGE IS. The index's own |
| 168 | + # e2e discipline is "assert the product, not the exit code", and the |
| 169 | + # cheapest product here is a multiboot image that prints and powers off. |
| 170 | + # It is built by the workflow rather than committed so that nothing in |
| 171 | + # this repository is a binary blob whose provenance has to be trusted. |
| 172 | + - name: The emulator boots a freestanding image |
| 173 | + shell: bash |
| 174 | + run: | |
| 175 | + set -euo pipefail |
| 176 | + QEMU=$(find out -name 'qemu-system-x86_64*' -type f | head -1) |
| 177 | + test -n "$QEMU" || { find out -type f | head -40; echo "no emulator was produced"; exit 1; } |
| 178 | + "$QEMU" --version | head -1 |
| 179 | +
|
| 180 | + # A minimal multiboot image. ⚠️ The a.out kludge (flag bit 16) rather |
| 181 | + # than plain ELF loading: QEMU's multiboot loader accepts only 32-bit |
| 182 | + # ELF images, and this one has to be assembled as 32-bit anyway, so |
| 183 | + # the kludge is not strictly needed here — it is used because the |
| 184 | + # real consumer (openarch's probe) is a 64-bit image that cannot be |
| 185 | + # loaded any other way, and a check that exercised a different path |
| 186 | + # would not be checking the same thing. |
| 187 | + cat > probe.S <<'ASM' |
| 188 | + .set MAGIC, 0x1BADB002 |
| 189 | + .set FLAGS, 0x00010000 |
| 190 | + .set CHECKSUM, -(MAGIC + FLAGS) |
| 191 | + .section .multiboot,"a" |
| 192 | + .align 4 |
| 193 | + mb: .long MAGIC |
| 194 | + .long FLAGS |
| 195 | + .long CHECKSUM |
| 196 | + .long mb |
| 197 | + .long __load_start |
| 198 | + .long __load_end |
| 199 | + .long __bss_end |
| 200 | + .long _start |
| 201 | + .code32 |
| 202 | + .section .text |
| 203 | + .globl _start |
| 204 | + _start: |
| 205 | + cli |
| 206 | + movl $msg, %esi |
| 207 | + 1: lodsb |
| 208 | + testb %al, %al |
| 209 | + je 2f |
| 210 | + movw $0x3F8, %dx |
| 211 | + outb %al, %dx |
| 212 | + jmp 1b |
| 213 | + 2: movw $0x604, %dx |
| 214 | + movw $0x2000, %ax |
| 215 | + outw %ax, %dx |
| 216 | + 3: hlt |
| 217 | + jmp 3b |
| 218 | + .section .rodata |
| 219 | + msg: .asciz "qemu-x86 probe ok\n" |
| 220 | + .section .bss |
| 221 | + .space 16 |
| 222 | + ASM |
| 223 | + cat > probe.ld <<'LD' |
| 224 | + ENTRY(_start) |
| 225 | + SECTIONS { |
| 226 | + __load_start = 0x100000; |
| 227 | + . = __load_start + SIZEOF_HEADERS; |
| 228 | + .multiboot : { KEEP(*(.multiboot)) } |
| 229 | + .text : { *(.text*) } |
| 230 | + .rodata : { *(.rodata*) } |
| 231 | + __load_end = .; |
| 232 | + .bss : { *(.bss*) __bss_end = .; } |
| 233 | + } |
| 234 | + LD |
| 235 | + # The host's own toolchain assembles it; this step is about the |
| 236 | + # emulator, not about cross-compilation. |
| 237 | + if [ "$RUNNER_OS" = "macOS" ] || [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then |
| 238 | + echo "::notice::no host assembler emits x86 ELF here; the emulator's --version is the check on this leg" |
| 239 | + exit 0 |
| 240 | + fi |
| 241 | + cc -m32 -c -o probe.o probe.S -nostdlib 2>/dev/null \ |
| 242 | + || { echo "::notice::no 32-bit x86 assembler on this leg; --version is the check"; exit 0; } |
| 243 | + ld -m elf_i386 -T probe.ld -o probe.elf probe.o |
| 244 | + timeout -k 5 60 "$QEMU" -machine q35 -nographic -no-reboot -kernel probe.elf 2>&1 | tee run.log |
| 245 | + grep -q "qemu-x86 probe ok" run.log |
| 246 | +
|
| 247 | + # ⚠️ WHAT A PAYLOAD WOULD ACTUALLY WEIGH, MEASURED RATHER THAN ESTIMATED. |
| 248 | + # The index's size discipline came from a real incident: a payload was |
| 249 | + # 34.81 MB and became 4.62 MB once stripped, and the upload failures that |
| 250 | + # had been blamed on the mirror were the size. |
| 251 | + - name: What the payload weighs, and what it needs |
| 252 | + shell: bash |
| 253 | + run: | |
| 254 | + set -euo pipefail |
| 255 | + QEMU=$(find out -name 'qemu-system-x86_64*' -type f | head -1) |
| 256 | + ls -l "$QEMU" |
| 257 | + case "$RUNNER_OS" in |
| 258 | + Linux) readelf -d "$QEMU" | grep NEEDED || true ;; |
| 259 | + macOS) otool -L "$QEMU" || true ;; |
| 260 | + Windows) echo "(the DLL closure is measured in the packaging step, not here)" ;; |
| 261 | + esac |
| 262 | + du -sh out |
| 263 | +
|
| 264 | + - uses: actions/upload-artifact@v4 |
| 265 | + with: |
| 266 | + name: qemu-system-x86_64-${{ matrix.label }} |
| 267 | + path: out |
| 268 | + retention-days: 14 |
0 commit comments