ci: 引导用的 mcpp 自带一份 xlings,而那份早于一次索引移动 #42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| mcpp_ref: | |
| description: "Branch of mcpp-community/mcpp to build and test against (empty = the released pin)" | |
| required: false | |
| default: "" | |
| env: | |
| MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} | |
| jobs: | |
| build: | |
| name: boots under OVMF as BOOTX64.EFI | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 40 | |
| env: | |
| MCPP_VERSION: 2026.8.26.2 | |
| XLINGS_VERSION: v2026.8.17.2 | |
| XLINGS_NON_INTERACTIVE: '1' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install xlings | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | |
| | bash -s "$XLINGS_VERSION" | |
| echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" | |
| - name: Install mcpp | |
| run: | | |
| # ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX | |
| # WITHOUT SAYING SO. | |
| # | |
| # The index is published as an artifact behind a pointer, and that | |
| # pointer propagates asynchronously after a version bump is merged. | |
| # Measured on release day: an update run four minutes after the merge | |
| # printed `index updated`, and the install then failed with | |
| # | |
| # package 'mcpp@<ver>' not found in the synced index | |
| # (xim@artifact:<an older sha>, ...), synced 0 seconds ago | |
| # | |
| # Nothing had gone wrong. The update fetched the PREVIOUS artifact, | |
| # and "synced 0 seconds ago" describes when it was fetched rather than | |
| # what it contains — which is why the message reads as freshness. | |
| # | |
| # So this is not a retry around flakiness; it is the wait that a | |
| # single update does not perform. A pin naming a version that was | |
| # never published still fails, after the last attempt, and says which | |
| # of the two situations it is. | |
| for attempt in 1 2 3 4 5 6; do | |
| xlings update > /dev/null 2>&1 || true | |
| if # ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does | |
| # not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap | |
| # from whatever the index has; the step below replaces it with the | |
| # build under review, and the pin is what an ordinary run tests. | |
| if [ -n "${MCPP_SOURCE_REF:-}" ]; then | |
| xlings install mcpp -y -g | |
| else | |
| xlings install "mcpp@$MCPP_VERSION" -y -g | |
| fi; then break; fi | |
| if [ "$attempt" = 6 ]; then | |
| echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will." | |
| exit 1 | |
| fi | |
| echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s" | |
| sleep 60 | |
| done | |
| mcpp --version | |
| mcpp self config --mirror GLOBAL | |
| # ⚠️⚠️ TRANSITION: THE BOOTSTRAP mcpp CARRIES ITS OWN xlings, AND | |
| # THAT COPY PREDATES AN INDEX MOVE. | |
| # | |
| # An mcpp release BUNDLES an xlings at `<install>/registry/bin/xlings` | |
| # (mcpp's `pinned::kXlingsVersion`), and that copy — not the one | |
| # installed above — is what resolves runtime bindings for everything | |
| # mcpp does. `xim:glibc`'s `latest` moved to 2.44.2, and only | |
| # xlings 2026.8.27.1 reads the default binding from the index instead | |
| # of a constant compiled into itself (openxlings/xlings#567). An | |
| # older bundled copy asks for `glibc@2.44`, a clean environment | |
| # installs `2.44.2`, and the toolchain post-install fixup stops: | |
| # | |
| # error: selected RuntimeBinding glibc@2.44 requires payload | |
| # '…/xpkgs/xim-x-glibc/2.44', but it is not installed | |
| # | |
| # on every NEW environment and on none that already existed. The | |
| # index records this failure verbatim in `pkgs/g/glibc.lua`. | |
| # | |
| # ⭐ REMOVE THIS ONCE THE PIN SHIPS. mcpp 2026.8.27.1 bundles | |
| # 2026.8.27.1, so a bootstrap from it needs nothing here. The step is | |
| # a no-op when the bundled copy is already new enough, and it says | |
| # which copy it replaced so a silent substitution is not possible. | |
| mcpp_home="${MCPP_HOME:-$HOME/.mcpp}" | |
| bundled="$mcpp_home/registry/bin/xlings" | |
| if [ -x "$bundled" ] && [ -n "$(command -v xlings)" ]; then | |
| have="$("$bundled" --version 2>/dev/null | head -1)" | |
| outer="$(xlings --version 2>/dev/null | head -1)" | |
| if [ "$have" != "$outer" ]; then | |
| cp "$(command -v xlings)" "$bundled" | |
| echo "bundled xlings: $have -> $("$bundled" --version | head -1) (was older than the installed one)" | |
| else | |
| echo "bundled xlings: $have (already the installed one)" | |
| fi | |
| fi | |
| # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. | |
| # | |
| # Empty in the ordinary run, so this job keeps testing the RELEASED | |
| # mcpp the pin above names. Set it — `workflow_dispatch` input, or the | |
| # repository variable — and the same job runs against that source. | |
| # | |
| # ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp | |
| # releases went out green and only then turned this ecosystem red: the | |
| # engine's own CI cannot see a defect that appears only in a real | |
| # dependency graph, and this repository could not see the engine until | |
| # it had been published. Validating before the release closes that gap. | |
| # | |
| # The released mcpp installed just above is the bootstrap that compiles | |
| # it; mcpp builds itself and there is no other compiler for it here. | |
| if [ -n "${MCPP_SOURCE_REF:-}" ]; then | |
| src="$RUNNER_TEMP/mcpp-src" | |
| [ -d "$src" ] || git clone --quiet --depth 1 \ | |
| --branch "$MCPP_SOURCE_REF" \ | |
| https://github.com/mcpp-community/mcpp.git "$src" | |
| # ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp | |
| # BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that | |
| # compiles mcpp, and that pin does not move when mcpp is released — | |
| # so a build inside the checkout obeys it and tries to install a | |
| # version the index may no longer carry: | |
| # | |
| # [error] xlings: version '2026.8.17.1' not found for 'mcpp' | |
| # available: 2026.8.25.1 | |
| # | |
| # What is wanted here is the source compiled by the mcpp installed | |
| # above, which is exactly what removing the file leaves. | |
| rm -f "$src/.xlings.json" | |
| ( cd "$src" && mcpp build --release ) | |
| # ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and | |
| # macOS runners too: on Windows the artefact is `mcpp.exe`, and | |
| # `-perm -u+x` is not a question that filesystem answers the way this | |
| # expects. Measured: `Finished release [optimized] in 173.44s` | |
| # followed by "mcpp did not build" — the build had succeeded and the | |
| # search was looking for the wrong name. | |
| # | |
| # `$src` is a FRESH clone each run, so `target/` holds exactly what | |
| # this step just built; `-printf` would be the safer form on a cached | |
| # tree and is a GNU extension this must not use. | |
| built=$(find "$src/target" -type f \ | |
| \( -name mcpp -o -name mcpp.exe \) | head -1) | |
| [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } | |
| echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" | |
| # ⚠️ Reported, because a PATH entry that does not win looks exactly | |
| # like one that does until something built with the wrong engine. | |
| echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" | |
| fi | |
| - name: Install OVMF and qemu | |
| run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86 | |
| - name: The example is a real EFI application | |
| run: | | |
| set -euo pipefail | |
| cd examples/hello | |
| mcpp build | |
| EXE=$(find target -name "hello.exe" | head -1) | |
| # ⚠️ THE TOOLCHAIN THAT BUILT THE IMAGE, NOT LLVM. | |
| # | |
| # This target resolves to mingw-cross-gcc, so on a machine that has | |
| # never built anything else there is no LLVM to borrow `llvm-readobj` | |
| # from — the previous version of this step globbed for one and died on | |
| # `ls: cannot access .../xim-x-llvm/*/bin/llvm-readobj`, AFTER the | |
| # build had succeeded. The verification tool has to come from the same | |
| # place as the compiler, or the check is only available on machines | |
| # that happen to have another one. | |
| OD=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-mingw-cross-gcc/*/bin/x86_64-w64-mingw32-objdump | head -1) | |
| test -x "$OD" | |
| # ⚠️ Subsystem 10 is what makes firmware load it. A Windows console | |
| # subsystem would build, link, and never be executed by UEFI. | |
| # Measured spelling from this objdump: `Subsystem 0000000a (EFI application)`. | |
| "$OD" -p "$EXE" | grep -qE 'Subsystem[[:space:]]+0000000a' | |
| # And no import table: a DLL dependency cannot be satisfied by firmware. | |
| if "$OD" -p "$EXE" | grep -qi "DLL Name"; then | |
| "$OD" -p "$EXE" | grep -i -A2 "DLL Name"; echo "unexpected DLL import"; exit 1 | |
| fi | |
| - name: It boots under OVMF and prints | |
| run: | | |
| set -euo pipefail | |
| cd examples/hello | |
| EXE=$(find target -name "hello.exe" | head -1) | |
| mkdir -p esp/EFI/BOOT && cp "$EXE" esp/EFI/BOOT/BOOTX64.EFI | |
| cp /usr/share/OVMF/OVMF_VARS_4M.fd vars.fd | |
| timeout 90 qemu-system-x86_64 -machine q35 -m 256 -display none \ | |
| -serial file:uefi.log \ | |
| -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \ | |
| -drive if=pflash,format=raw,file=vars.fd \ | |
| -drive format=raw,file=fat:rw:esp -net none || true | |
| cat uefi.log | tr -d "\\000" | head -40 | |
| grep -q "hello from openkal over UEFI" uefi.log | |
| grep -q "pool ok" uefi.log | |
| # --------------------------------------------------------------------------- | |
| # The cross-compilation is performed FROM three systems, not only from Linux. | |
| # | |
| # Every target in this repository is a cross target, which makes the host a | |
| # separate axis from the target: the compiler, the target C library and the | |
| # emulator are payloads mcpp resolves for whichever system it is running on. | |
| # A package that has only ever been built from Linux is a package whose | |
| # consumers must use Linux, and nothing in these sources says so. | |
| # | |
| # ⚠️ A TOOLCHAIN AXIS IS ABSENT HERE, AND THAT IS MEASURED RATHER THAN | |
| # ASSUMED. The row for a bare-metal triple names its compiler, and the | |
| # command-line override does not displace it: `--toolchain gcc@16.1.0` on a | |
| # `riscv64-none-elf` build resolves llvm@22.1.8 regardless. A matrix over | |
| # compiler families would therefore run the same compiler on every row and | |
| # report coverage it does not have. The toolchain axis belongs where the | |
| # choice is real — openkal, whose declarations are compiled by three families | |
| # on three systems. | |
| # | |
| # ⚠️ BUILD ONLY, AND DELIBERATELY. Behaviour is asserted once, above, under an | |
| # emulator. Booting the same image from three systems would be a statement | |
| # about the emulator rather than about this package, and "the image does what | |
| # the README says" does not become more true for having been observed from | |
| # macOS. | |
| portability: | |
| name: cross-builds from ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-14, windows-2022] | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| MCPP_VERSION: 2026.8.26.2 | |
| XLINGS_VERSION: v2026.8.17.2 | |
| XLINGS_NON_INTERACTIVE: '1' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install xlings (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ | |
| | bash -s "$XLINGS_VERSION" | |
| echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" | |
| - name: Install xlings (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| irm https://d2learn.org/xlings-install.ps1.txt | iex | |
| # The installer amends the user's environment; no later step in this | |
| # job reads it back, so the directory is named here. | |
| "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append | |
| - name: Install mcpp | |
| run: | | |
| # ⚠️ A LOOP, BECAUSE ONE `xlings update` CAN RETURN A STALE INDEX | |
| # WITHOUT SAYING SO. | |
| # | |
| # The index is published as an artifact behind a pointer, and that | |
| # pointer propagates asynchronously after a version bump is merged. | |
| # Measured on release day: an update run four minutes after the merge | |
| # printed `index updated`, and the install then failed with | |
| # | |
| # package 'mcpp@<ver>' not found in the synced index | |
| # (xim@artifact:<an older sha>, ...), synced 0 seconds ago | |
| # | |
| # Nothing had gone wrong. The update fetched the PREVIOUS artifact, | |
| # and "synced 0 seconds ago" describes when it was fetched rather than | |
| # what it contains — which is why the message reads as freshness. | |
| # | |
| # So this is not a retry around flakiness; it is the wait that a | |
| # single update does not perform. A pin naming a version that was | |
| # never published still fails, after the last attempt, and says which | |
| # of the two situations it is. | |
| for attempt in 1 2 3 4 5 6; do | |
| xlings update > /dev/null 2>&1 || true | |
| if # ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does | |
| # not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap | |
| # from whatever the index has; the step below replaces it with the | |
| # build under review, and the pin is what an ordinary run tests. | |
| if [ -n "${MCPP_SOURCE_REF:-}" ]; then | |
| xlings install mcpp -y -g | |
| else | |
| xlings install "mcpp@$MCPP_VERSION" -y -g | |
| fi; then break; fi | |
| if [ "$attempt" = 6 ]; then | |
| echo "::error::mcpp@$MCPP_VERSION never appeared in the index (6 attempts over 5 minutes). If it was just released, the pointer has not propagated; if the pin names a version that was never published, it never will." | |
| exit 1 | |
| fi | |
| echo "the index has not caught up yet (attempt $attempt of 6); waiting 60s" | |
| sleep 60 | |
| done | |
| mcpp --version | |
| mcpp self config --mirror GLOBAL | |
| # ⚠️⚠️ TRANSITION: THE BOOTSTRAP mcpp CARRIES ITS OWN xlings, AND | |
| # THAT COPY PREDATES AN INDEX MOVE. | |
| # | |
| # An mcpp release BUNDLES an xlings at `<install>/registry/bin/xlings` | |
| # (mcpp's `pinned::kXlingsVersion`), and that copy — not the one | |
| # installed above — is what resolves runtime bindings for everything | |
| # mcpp does. `xim:glibc`'s `latest` moved to 2.44.2, and only | |
| # xlings 2026.8.27.1 reads the default binding from the index instead | |
| # of a constant compiled into itself (openxlings/xlings#567). An | |
| # older bundled copy asks for `glibc@2.44`, a clean environment | |
| # installs `2.44.2`, and the toolchain post-install fixup stops: | |
| # | |
| # error: selected RuntimeBinding glibc@2.44 requires payload | |
| # '…/xpkgs/xim-x-glibc/2.44', but it is not installed | |
| # | |
| # on every NEW environment and on none that already existed. The | |
| # index records this failure verbatim in `pkgs/g/glibc.lua`. | |
| # | |
| # ⭐ REMOVE THIS ONCE THE PIN SHIPS. mcpp 2026.8.27.1 bundles | |
| # 2026.8.27.1, so a bootstrap from it needs nothing here. The step is | |
| # a no-op when the bundled copy is already new enough, and it says | |
| # which copy it replaced so a silent substitution is not possible. | |
| mcpp_home="${MCPP_HOME:-$HOME/.mcpp}" | |
| bundled="$mcpp_home/registry/bin/xlings" | |
| if [ -x "$bundled" ] && [ -n "$(command -v xlings)" ]; then | |
| have="$("$bundled" --version 2>/dev/null | head -1)" | |
| outer="$(xlings --version 2>/dev/null | head -1)" | |
| if [ "$have" != "$outer" ]; then | |
| cp "$(command -v xlings)" "$bundled" | |
| echo "bundled xlings: $have -> $("$bundled" --version | head -1) (was older than the installed one)" | |
| else | |
| echo "bundled xlings: $have (already the installed one)" | |
| fi | |
| fi | |
| # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. | |
| # | |
| # Empty in the ordinary run, so this job keeps testing the RELEASED | |
| # mcpp the pin above names. Set it — `workflow_dispatch` input, or the | |
| # repository variable — and the same job runs against that source. | |
| # | |
| # ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp | |
| # releases went out green and only then turned this ecosystem red: the | |
| # engine's own CI cannot see a defect that appears only in a real | |
| # dependency graph, and this repository could not see the engine until | |
| # it had been published. Validating before the release closes that gap. | |
| # | |
| # The released mcpp installed just above is the bootstrap that compiles | |
| # it; mcpp builds itself and there is no other compiler for it here. | |
| if [ -n "${MCPP_SOURCE_REF:-}" ]; then | |
| src="$RUNNER_TEMP/mcpp-src" | |
| [ -d "$src" ] || git clone --quiet --depth 1 \ | |
| --branch "$MCPP_SOURCE_REF" \ | |
| https://github.com/mcpp-community/mcpp.git "$src" | |
| # ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp | |
| # BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that | |
| # compiles mcpp, and that pin does not move when mcpp is released — | |
| # so a build inside the checkout obeys it and tries to install a | |
| # version the index may no longer carry: | |
| # | |
| # [error] xlings: version '2026.8.17.1' not found for 'mcpp' | |
| # available: 2026.8.25.1 | |
| # | |
| # What is wanted here is the source compiled by the mcpp installed | |
| # above, which is exactly what removing the file leaves. | |
| rm -f "$src/.xlings.json" | |
| ( cd "$src" && mcpp build --release ) | |
| # ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and | |
| # macOS runners too: on Windows the artefact is `mcpp.exe`, and | |
| # `-perm -u+x` is not a question that filesystem answers the way this | |
| # expects. Measured: `Finished release [optimized] in 173.44s` | |
| # followed by "mcpp did not build" — the build had succeeded and the | |
| # search was looking for the wrong name. | |
| # | |
| # `$src` is a FRESH clone each run, so `target/` holds exactly what | |
| # this step just built; `-printf` would be the safer form on a cached | |
| # tree and is a GNU extension this must not use. | |
| built=$(find "$src/target" -type f \ | |
| \( -name mcpp -o -name mcpp.exe \) | head -1) | |
| [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } | |
| echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" | |
| # ⚠️ Reported, because a PATH entry that does not win looks exactly | |
| # like one that does until something built with the wrong engine. | |
| echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" | |
| fi | |
| - name: The EFI application cross-builds | |
| run: | | |
| # ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this | |
| # matrix is a machine that has never targeted this triple, which is | |
| # precisely where mcpp's lazy install of the target C library shows. | |
| # The first build compiles sources needing its headers before they | |
| # exist; the second has them. A single build here reported a | |
| # portability failure that was really a cold-machine one. | |
| mcpp build || true | |
| mcpp build |