ci: run fresh-install workflows only in the upstream repo #473
Workflow file for this run
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: cross-build-test | |
| # mcpp cross-build test — the single source of truth for "which CROSS-build | |
| # target combinations mcpp supports", verified end-to-end. | |
| # | |
| # Cross = host arch ≠ target arch. Verification targets are mcpp ITSELF and | |
| # xlings (real, self-hosting C++23 module projects), cross-built from source for | |
| # each target triple, arch-checked, and smoke-run under qemu-user. | |
| # | |
| # ── Supported cross matrix (built + verified below) ──────────────────────── | |
| # target | toolchain | host→target | run | |
| # ----------------------|----------------------------------|---------------|----- | |
| # aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu | |
| # x86_64-w64-mingw32 | mingw-cross-gcc@16.1.0 (MSVCRT) | linux→windows | wine | |
| # x86_64-linux-musl | x86_64-linux-musl-gcc@16.1.0 | windows→linux | linux job | |
| # | |
| # The mingw row is OS-cross (same arch, different OS/ABI: ELF→PE), so it lives | |
| # in its own job below with wine verification instead of the qemu arch matrix. | |
| # See .agents/docs/2026-07-15-mingw-linux-cross-windows-design.md. | |
| # | |
| # The windows→linux row is the MIRROR of that one, and its verification has no | |
| # wine-equivalent: a Windows runner cannot execute the ELF it just produced. | |
| # So it is split across TWO jobs — build on windows-latest, upload the artefact, | |
| # then download and really run it on ubuntu. Static assertions alone would not | |
| # do: "it linked" has never implied "it runs" (see the elfpatch incident in | |
| # .agents/docs/, and 2026-08-03-windows-host-linux-cross-design.md §6.1). | |
| # The artefact is a fully static musl ELF (no PT_INTERP), so the consumer job | |
| # needs neither qemu nor a matching loader. | |
| # | |
| # mcpp resolves a cross `--target <triple>-musl` build to the triple-named cross | |
| # gcc musl toolchain from the xlings ecosystem (xim:<triple>-gcc, see | |
| # src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP), | |
| # which also makes the aarch64 artefact runnable natively in Termux/Android — | |
| # qemu-aarch64 is the CI proxy for "does this cross artefact actually execute". | |
| # | |
| # ── NOT here ─────────────────────────────────────────────────────────────── | |
| # * Same-arch builds (host arch == target arch) are NOT cross. The native musl | |
| # static build `--target x86_64-linux-musl` (x86_64 host) is exercised by | |
| # ci-linux.yml's "Toolchain: musl-gcc" step, and release.yml for the static | |
| # release artefact. Keep them there; this file is cross-arch only. | |
| # | |
| # ── Planned cross rows (documented; NOT yet wired in mcpp — keep as comments) ─ | |
| # * llvm/clang cross : clang is inherently a cross-compiler, but mcpp does not | |
| # yet inject `-target <triple>` + a cross sysroot for a | |
| # clang toolchain; cross `--target` resolves to gcc musl | |
| # only. Wire the clang cross path first, then add a row. | |
| # * riscv64-linux-musl: add once xim:riscv64-linux-musl-gcc ships to | |
| # xlings-res + xim-pkgindex. | |
| on: | |
| push: | |
| branches: [ main ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| cross-build: | |
| name: cross-build ${{ matrix.target }} (mcpp + xlings) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-linux-musl | |
| file_arch: "ARM aarch64" | |
| qemu_bin: qemu-aarch64-static | |
| env: | |
| MCPP_HOME: /home/runner/.mcpp | |
| # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). | |
| MCPP_VERBOSE: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache mcpp sandbox | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.mcpp | |
| key: mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} | |
| restore-keys: | | |
| mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}- | |
| - name: Cache xlings | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.xlings | |
| key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} | |
| restore-keys: | | |
| xlings-${{ runner.os }}-v2- | |
| - name: Install qemu-user-static | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y qemu-user-static | |
| ${{ matrix.qemu_bin }} --version | head -1 | |
| - name: Bootstrap mcpp via xlings | |
| env: | |
| XLINGS_NON_INTERACTIVE: '1' | |
| # Must equal `pinned::kXlingsVersion` (src/xlings.cppm) and the | |
| # xlings the release bundles — enforced by | |
| # .github/tools/check_version_pins.sh. | |
| # | |
| # Floors worth remembering. 0.4.67 carried the | |
| # multi-index_repo install fix (openxlings/xlings#374); 0.4.68 adds | |
| # per-repo index artifact sources (openxlings/xlings#377) so the | |
| # mcpplibs index syncs via artifact with git as fallback (mcpp#269); | |
| # 0.4.69 keys the index by (namespace, name) so two packages sharing | |
| # a short name in ONE index are both addressable (openxlings/xlings#381) | |
| # — the floor for SPEC-001 short-name descriptors. | |
| # A past 0.4.61 "download 404 | |
| # for mcpp@<pin>" was NOT a version bug — the xlings-res/mcpp GitHub | |
| # release assets were uploaded in a broken state (records present, | |
| # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX | |
| # half is handled by the marker-clear below. | |
| XLINGS_VERSION: '2026.8.4.1' | |
| run: | | |
| tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" | |
| curl -fsSL -o "/tmp/${tarball}" \ | |
| "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" | |
| tar -xzf "/tmp/${tarball}" -C /tmp | |
| "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install | |
| export PATH="$HOME/.xlings/subos/default/bin:$PATH" | |
| xlings --version | |
| # Force a real index re-sync even on a warm cache: drop the TTL refresh | |
| # markers so `xlings update` actually pulls the latest index (sees the | |
| # current bootstrap pin) while the toolchain payloads stay cached. | |
| find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true | |
| xlings config --mirror GLOBAL 2>/dev/null || true | |
| xlings update -y 2>/dev/null || xlings update 2>/dev/null || true | |
| # MCPP_BOOT is what actually runs the bootstrap build below, so it — | |
| # not just MCPP — has to be the pinned binary. It used to be the shim | |
| # in subos/default/bin, which resolves to whatever version xvm has | |
| # selected; pinning only MCPP would have looked right and changed | |
| # nothing. | |
| MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") | |
| echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" | |
| echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" | |
| echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" | |
| - name: Self-host build (bootstrap mcpp -> fresh host mcpp) | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true | |
| "$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true | |
| "$MCPP_BOOT" build | |
| MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") | |
| test -x "$MCPP" | |
| "$MCPP" self config --mirror GLOBAL | |
| echo "MCPP=$MCPP" >> "$GITHUB_ENV" | |
| - name: "Cross-build mcpp -> ${{ matrix.target }}" | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP" build --target ${{ matrix.target }} | |
| bin=$(find target/${{ matrix.target }} -type f -name mcpp | head -1) | |
| [ -n "$bin" ] || { echo "no mcpp artefact for ${{ matrix.target }}"; exit 1; } | |
| echo "== file =="; file "$bin" | |
| file "$bin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } | |
| file "$bin" | grep -q "statically linked" || { echo "expected static"; exit 1; } | |
| echo "MCPP_XBIN=$bin" >> "$GITHUB_ENV" | |
| - name: "Cross-build xlings -> ${{ matrix.target }}" | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src | |
| cd /tmp/xlings-src | |
| "$MCPP" self config --mirror GLOBAL 2>/dev/null || true | |
| "$MCPP" build --target ${{ matrix.target }} | |
| xbin=$(find target/${{ matrix.target }} -type f -name xlings | head -1) | |
| [ -n "$xbin" ] || { echo "no xlings artefact for ${{ matrix.target }}"; exit 1; } | |
| echo "== file =="; file "$xbin" | |
| file "$xbin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } | |
| file "$xbin" | grep -q "statically linked" || { echo "expected static"; exit 1; } | |
| echo "XLINGS_XBIN=$xbin" >> "$GITHUB_ENV" | |
| - name: "Smoke-run cross artefacts under qemu" | |
| run: | | |
| RUN="${{ matrix.qemu_bin }}" | |
| # mcpp is self-contained, so --version runs cleanly under bare qemu — | |
| # this is the hard execution proof for the cross artefact. | |
| echo "== mcpp --version ==" | |
| mver=$($RUN "$MCPP_XBIN" --version) | |
| echo "$mver"; echo "$mver" | grep -q "mcpp" || { echo "mcpp --version failed"; exit 1; } | |
| # xlings expects a real runtime environment (sandbox/config) and may | |
| # exit non-zero on a bare `--version` under qemu; its ELF arch + static | |
| # linkage were already asserted in the build step, so treat execution | |
| # here as best-effort rather than gating. | |
| echo "== xlings --version (best-effort under qemu) ==" | |
| xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true) | |
| echo "$xver" | |
| # ── Linux → Windows MinGW cross (OS-cross, same arch: ELF→PE) ───────────── | |
| # Builds a demo project for x86_64-w64-mingw32 with the from-source GCC-16 | |
| # MSVCRT cross toolchain, asserts the artefact is a fully-static PE, and runs | |
| # it under wine. Delegated to the e2e harness (tests/e2e/102_mingw_cross_wine.sh, | |
| # `# requires: mingw-cross wine`) so the run_all cap-gating stays the single | |
| # source of truth. See 2026-07-15-mingw-linux-cross-windows-design.md Part C. | |
| mingw-cross-wine: | |
| name: mingw-cross linux→windows (build + wine run) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 60 | |
| env: | |
| MCPP_HOME: /home/runner/.mcpp | |
| MCPP_VERBOSE: "1" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache mcpp sandbox | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.mcpp | |
| key: mcpp-sandbox-${{ runner.os }}-mingw-cross-${{ hashFiles('mcpp.toml', '.xlings.json') }} | |
| restore-keys: | | |
| mcpp-sandbox-${{ runner.os }}-mingw-cross- | |
| - name: Cache xlings | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.xlings | |
| key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} | |
| restore-keys: | | |
| xlings-${{ runner.os }}-v2- | |
| # wine 的包集固定不变 —— 缓存整个 .deb 依赖闭包,命中时跳过 apt update | |
| # 与下载(每轮省 ~1-2min)。镜像月度更新可能改变依赖缺口,dpkg -i 失败时 | |
| # 由 apt-get -f 兜底并重新回填缓存。 | |
| - name: Cache wine debs | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/wine-debs | |
| key: wine-debs-${{ runner.os }}-ubuntu24.04-v1 | |
| - name: Install wine | |
| run: | | |
| sudo dpkg --add-architecture i386 || true | |
| if ls ~/wine-debs/*.deb >/dev/null 2>&1; then | |
| sudo dpkg -i ~/wine-debs/*.deb 2>/dev/null \ | |
| || { sudo apt-get update -qq; sudo apt-get install -f -y; } | |
| else | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --download-only wine64 wine \ | |
| || sudo apt-get install -y --download-only wine | |
| mkdir -p ~/wine-debs | |
| cp /var/cache/apt/archives/*.deb ~/wine-debs/ 2>/dev/null || true | |
| sudo apt-get install -y wine64 wine || sudo apt-get install -y wine | |
| fi | |
| wine --version | |
| - name: Bootstrap mcpp via xlings | |
| env: | |
| XLINGS_NON_INTERACTIVE: '1' | |
| XLINGS_VERSION: '2026.8.4.1' | |
| run: | | |
| tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" | |
| curl -fsSL -o "/tmp/${tarball}" \ | |
| "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" | |
| tar -xzf "/tmp/${tarball}" -C /tmp | |
| "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install | |
| export PATH="$HOME/.xlings/subos/default/bin:$PATH" | |
| find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true | |
| xlings config --mirror GLOBAL 2>/dev/null || true | |
| xlings update -y 2>/dev/null || xlings update 2>/dev/null || true | |
| # MCPP_BOOT is what actually runs the bootstrap build below, so it — | |
| # not just MCPP — has to be the pinned binary. It used to be the shim | |
| # in subos/default/bin, which resolves to whatever version xvm has | |
| # selected; pinning only MCPP would have looked right and changed | |
| # nothing. | |
| MCPP_BOOT=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") | |
| echo "MCPP=$MCPP_BOOT" >> "$GITHUB_ENV" | |
| echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" | |
| echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" | |
| - name: Self-host build (fresh host mcpp) | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true | |
| "$MCPP_BOOT" build | |
| MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") | |
| test -x "$MCPP" | |
| "$MCPP" self config --mirror GLOBAL | |
| echo "MCPP=$MCPP" >> "$GITHUB_ENV" | |
| - name: Install mingw-cross toolchain | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP" toolchain install mingw-cross 16.1.0 | |
| - name: "e2e: cross-build + wine run" | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| bash tests/e2e/102_mingw_cross_wine.sh | |
| # ── windows → linux ─────────────────────────────────────────────────────── | |
| # The mirror of mingw-cross-wine. Two jobs because a Windows runner cannot | |
| # execute the ELF it produces; the artefact is handed to a Linux job and | |
| # really run there. | |
| windows-host-linux-cross: | |
| name: windows→linux cross-build (windows host) | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/bootstrap-mcpp | |
| with: | |
| # Do NOT restore target/ in a cross job. This job builds twice — once | |
| # for the host, then once for x86_64-linux-musl — and a restored BMI | |
| # tree makes the second build read `std` BMIs that no longer match | |
| # what the dependency BMIs were compiled against: | |
| # | |
| # mcpplibs.cmdline: error: import 'std' has CRC mismatch | |
| # | |
| # GCC bakes a CRC of each imported module's BMI into the importer, so | |
| # the two have to come from the same build round; a cache that | |
| # restores one without the other is not a partial speedup, it is an | |
| # unbuildable tree. It reproduced on rerun, and only in this job — | |
| # the other two cross jobs below cache ~/.mcpp and ~/.xlings but | |
| # deliberately never target/, which is the convention this now follows. | |
| # | |
| # It stayed hidden until a PR touched neither mcpp.toml nor | |
| # .xlings.json: those two files key the sandbox cache, so every | |
| # earlier run had been a cold miss. | |
| cache-target: 'false' | |
| - name: Build mcpp from source (self-host) | |
| shell: bash | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP" build | |
| # Newest, not first: target/ is cache-restored and keeps a directory | |
| # per build fingerprint, so `find | head -1` can hand back the | |
| # PREVIOUS release's binary (that is how a 0.0.106 build ran 0.0.105). | |
| MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ | |
| | sort -rn | head -1 | cut -d" " -f2-) | |
| test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; } | |
| MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") | |
| "$MCPP_SELF" --version | |
| echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" | |
| - name: Install the linux-musl cross toolchain (windows-hosted canadian) | |
| shell: bash | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP_SELF" toolchain install gcc 16.1.0 --target x86_64-linux-musl | |
| # The target row must now be visible on a Windows host — this is the | |
| # host gate from design §1.2 having been lifted, asserted rather than | |
| # eyeballed. | |
| "$MCPP_SELF" toolchain list | tee /tmp/tclist.txt | |
| grep -q "x86_64-linux-musl" /tmp/tclist.txt \ | |
| || { echo "FAIL: linux-musl target not listed on windows host"; exit 1; } | |
| - name: "Cross-build mcpp -> x86_64-linux-musl" | |
| shell: bash | |
| run: | | |
| export MCPP_VENDORED_XLINGS="$XLINGS_BIN" | |
| "$MCPP_SELF" build --target x86_64-linux-musl | |
| # Scope the search to the TARGET's output tree — target/ also holds the | |
| # host build from the previous step, and both are named "mcpp*". | |
| # | |
| # The artefact carries a `.exe` suffix even though it is an ELF: | |
| # plan.cppm's target_output() spells the suffix from | |
| # mcpp::platform::exe_suffix, a HOST constant. That is the same | |
| # host-decides-target confusion as B2, and it is symmetric — a | |
| # Linux→Windows cross produces a PE with no `.exe` today. Renaming the | |
| # output is a behaviour change that would touch the mingw e2e and any | |
| # user script, so it is filed as follow-up rather than folded in here; | |
| # match both spellings so this job is correct either way. | |
| OUT=$(find target/x86_64-linux-musl -type f -path "*/bin/*" \ | |
| \( -name "mcpp" -o -name "mcpp.exe" \) -printf "%T@ %p\n" \ | |
| | sort -rn | head -1 | cut -d" " -f2-) | |
| if [ -z "$OUT" ]; then | |
| echo "FAIL: no cross artefact produced; tree was:" | |
| find target/x86_64-linux-musl -type f -path "*/bin/*" | head -20 | |
| exit 1 | |
| fi | |
| cp "$OUT" mcpp-linux-musl | |
| ls -la mcpp-linux-musl | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: mcpp-x86_64-linux-musl-from-windows | |
| path: mcpp-linux-musl | |
| retention-days: 1 | |
| windows-host-linux-cross-run: | |
| name: windows→linux artefact really runs (linux) | |
| needs: windows-host-linux-cross | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: mcpp-x86_64-linux-musl-from-windows | |
| - name: Assert it is a static ELF, then run it | |
| run: | | |
| set -euo pipefail | |
| chmod +x mcpp-linux-musl | |
| file mcpp-linux-musl | |
| # B2 regression gate (design §1.3): before the fix, `-static` was | |
| # decided by a HOST constant (`supports_full_static = is_linux`), so | |
| # a Windows host emitted a NON-static binary here. Written as a | |
| # positive `grep -q` on purpose: `! cmd | grep` is exempt from | |
| # errexit and can never fail (see build-mcpp-helper-self-containment). | |
| file mcpp-linux-musl | grep -q "ELF 64-bit LSB" | |
| file mcpp-linux-musl | grep -q "x86-64" | |
| file mcpp-linux-musl | grep -q "statically linked" | |
| # A static musl ELF has no PT_INTERP at all — the stronger form of | |
| # the same claim, and independent of `file`'s wording. | |
| readelf -l mcpp-linux-musl > hdrs.txt | |
| if grep -q "INTERP" hdrs.txt; then | |
| echo "FAIL: artefact has a PT_INTERP segment — not statically linked" | |
| grep -A2 "INTERP" hdrs.txt | |
| exit 1 | |
| fi | |
| # Linked ≠ runs. This is the whole point of the second job. | |
| ./mcpp-linux-musl --version | |
| ./mcpp-linux-musl --help > /dev/null | |
| echo "OK: windows-built linux artefact executes natively" |