From ddeefc1da9a9c604f83eca7420c37045920977c1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:20:21 +0800 Subject: [PATCH 1/3] [DO NOT MERGE] probe: libc++ 22 std.cppm on macOS 27 --- .github/probe/m27.sh | 42 + .github/workflows/aur-publish.yml | 163 --- .github/workflows/bootstrap-macos.yml | 154 --- .../workflows/ci-aarch64-fresh-install.yml | 237 ----- .github/workflows/ci-fresh-install.yml | 718 -------------- .github/workflows/ci-linux-e2e.yml | 451 --------- .github/workflows/ci-linux.yml | 517 ---------- .github/workflows/ci-macos-e2e.yml | 98 -- .github/workflows/ci-macos-ios.yml | 388 -------- .github/workflows/ci-macos.yml | 431 -------- .github/workflows/ci-target-matrix.yml | 486 --------- .github/workflows/ci-windows-e2e.yml | 124 --- .github/workflows/ci-windows-msvc-xlings.yml | 108 -- .github/workflows/ci-windows.yml | 524 ---------- .github/workflows/cross-build-test.yml | 501 ---------- .github/workflows/homebrew-publish.yml | 83 -- .github/workflows/openkal-cross.yml | 478 --------- .github/workflows/probe.yml | 11 + .github/workflows/release.yml | 928 ------------------ 19 files changed, 53 insertions(+), 6389 deletions(-) create mode 100644 .github/probe/m27.sh delete mode 100644 .github/workflows/aur-publish.yml delete mode 100644 .github/workflows/bootstrap-macos.yml delete mode 100644 .github/workflows/ci-aarch64-fresh-install.yml delete mode 100644 .github/workflows/ci-fresh-install.yml delete mode 100644 .github/workflows/ci-linux-e2e.yml delete mode 100644 .github/workflows/ci-linux.yml delete mode 100644 .github/workflows/ci-macos-e2e.yml delete mode 100644 .github/workflows/ci-macos-ios.yml delete mode 100644 .github/workflows/ci-macos.yml delete mode 100644 .github/workflows/ci-target-matrix.yml delete mode 100644 .github/workflows/ci-windows-e2e.yml delete mode 100644 .github/workflows/ci-windows-msvc-xlings.yml delete mode 100644 .github/workflows/ci-windows.yml delete mode 100644 .github/workflows/cross-build-test.yml delete mode 100644 .github/workflows/homebrew-publish.yml delete mode 100644 .github/workflows/openkal-cross.yml create mode 100644 .github/workflows/probe.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/probe/m27.sh b/.github/probe/m27.sh new file mode 100644 index 000000000..4321f560d --- /dev/null +++ b/.github/probe/m27.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# macOS 27: why libc++ 22's std.cppm loses INFINITY/NAN. One READING per fact. +set +e +r() { echo "READING $*"; } +r "os $(sw_vers -productVersion) $(sw_vers -buildVersion) xcode=$(xcodebuild -version | tr '\n' ' ')" +r "xcrun sdk=$(xcrun --show-sdk-path) ver=$(xcrun --show-sdk-version)" +CLT=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk +r "clt sdk link=$(readlink $CLT) exists=$([ -d $CLT ] && echo y || echo n) settings=$(plutil -extract Version raw $CLT/SDKSettings.plist 2>/dev/null)" +ls -la /Library/Developer/CommandLineTools/SDKs/ 2>&1 | sed 's/^/READING clt-sdks /' +XSDK=$(xcrun --show-sdk-path) +for s in $CLT $XSDK; do + r "math.h in $s: INFINITY defs: $(grep -rn 'define[[:space:]]*INFINITY' $s/usr/include/math.h $s/usr/include/_math.h $s/usr/include/math 2>/dev/null | head -3 | tr '\n' ' ')" + r "math.h head in $s: $(grep -n 'include\|__MATH' $s/usr/include/math.h 2>/dev/null | head -12 | tr '\n' ' ')" +done +# xlings llvm +curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 >/dev/null 2>&1 +export PATH="$HOME/.xlings/subos/current/bin:$PATH" +xlings install llvm@22.1.8 -y >/dev/null 2>&1 +L=$(ls -d $HOME/.xlings/data/xpkgs/xim-x-llvm/22.1.8) +r "llvm cfg: $(cat $L/bin/clang++.cfg | tr '\n' ' ')" +S=$L/share/libc++/v1/std.cppm +W=$(mktemp -d); cd $W +try() { name=$1; shift; out=$("$@" 2>&1); rc=$?; r "$name rc=$rc $(printf '%s' "$out" | grep -m2 'error' | tr '\n' ' ')"; } +try cfg-clt $L/bin/clang++ -std=c++23 -Wno-reserved-module-identifier --precompile $S -o a.pcm +try isysroot-xc $L/bin/clang++ -std=c++23 -Wno-reserved-module-identifier --precompile $S -o b.pcm --sysroot=$XSDK +try no-cfg-xc $L/bin/clang++ --no-default-config -std=c++23 -nostdinc++ -isystem $L/include/c++/v1 -isysroot $XSDK -Wno-reserved-module-identifier --precompile $S -o c.pcm +printf '#include \n#ifndef INFINITY\n#error no INFINITY after cmath\n#endif\n#include \nint main(){}\n' > t.cpp +try hdr-clt $L/bin/clang++ -std=c++23 -fsyntax-only t.cpp +try hdr-xc $L/bin/clang++ -std=c++23 -fsyntax-only t.cpp --sysroot=$XSDK +printf '#include \n#ifndef INFINITY\n#error no INFINITY after math.h\n#endif\nint main(){}\n' > m.c +try c-math-clt $L/bin/clang -fsyntax-only m.c +try c-math-apple /usr/bin/clang -fsyntax-only m.c +try cxx-apple /usr/bin/clang++ -std=c++23 -fsyntax-only t.cpp +$L/bin/clang++ -std=c++23 -E -dD t.cpp 2>/dev/null | grep -n 'INFINITY\|# 1 ".*math' | head -10 | sed 's/^/READING pp-clt /' +$L/bin/clang++ -std=c++23 -H -fsyntax-only t.cpp 2>&1 | grep -i 'math' | head -12 | sed 's/^/READING includes-clt /' +# the released mcpp +xlings install mcpp@2026.9.16.2 -y >/dev/null 2>&1 +M=$(ls $HOME/.xlings/data/xpkgs/xim-x-mcpp/2026.9.16.2/bin/mcpp) +cd $(mktemp -d); "$M" new h >/dev/null 2>&1; cd h +out=$("$M" run 2>&1); rc=$? +r "mcpp-2026.9.16.2 new+run rc=$rc $(printf '%s' "$out" | grep -m3 -E 'error|Hello|sysroot' | tr '\n' ' ')" +exit 0 diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml deleted file mode 100644 index 63a1171ee..000000000 --- a/.github/workflows/aur-publish.yml +++ /dev/null @@ -1,163 +0,0 @@ -name: aur-publish - -# Reconcile only mcpp-bin. This workflow is downstream of `release`, so its -# failure is visible without changing the already-terminal release conclusion. -on: - workflow_run: - workflows: [release] - types: [completed] - schedule: - - cron: '17 */6 * * *' - workflow_dispatch: - inputs: - publish: - description: 'Publish the validated diff (false performs a dry-run only)' - type: boolean - required: true - default: false - tag: - description: 'Optional exact latest complete stable tag (no downgrade override)' - type: string - required: false - -concurrency: - group: aur-mcpp-bin-reconcile - cancel-in-progress: false - -permissions: - contents: read - -jobs: - reconcile: - name: reconcile mcpp-bin - if: >- - github.event_name != 'workflow_run' || - github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - GH_TOKEN: ${{ github.token }} - PYTHONDONTWRITEBYTECODE: '1' - REQUESTED_TAG: ${{ inputs.tag }} - steps: - - name: Checkout reconciler source - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha || github.ref }} - - - name: Reconciler contract tests - run: python3 tests/scripts/test_aur_reconcile.py - - # This phase has no SSH private key in its environment. It downloads the - # immutable manifest and both Linux payload/sidecars, recomputes hashes, - # runs makepkg as non-root in Arch, inspects RPC + HTTPS git, and emits the - # exact diff before any publishing secret is loaded. - - name: Inspect and validate desired state - id: plan - env: - TRIGGER: ${{ github.event_name }} - MANUAL_PUBLISH: ${{ inputs.publish }} - # Repository variable, absent until a human has watched one publish - # succeed. See "Arming the automatic triggers" in scripts/aur/README.md. - AUTOPUBLISH: ${{ vars.AUR_AUTOPUBLISH }} - run: | - args=( - --trigger "$TRIGGER" - --report-json "$RUNNER_TEMP/aur-plan.json" - --summary "$GITHUB_STEP_SUMMARY" - ) - [[ -z "$REQUESTED_TAG" ]] || args+=(--tag "$REQUESTED_TAG") - python3 scripts/aur/reconcile_mcpp_bin.py "${args[@]}" - - # An unattended push to a third-party service must be ARMED, not - # inherited from a merge. `schedule` fires every six hours off the - # default branch, so merging this workflow used to be enough to make - # mcpp start writing to the AUR on its own — before anyone had seen - # the reconciler complete a real push even once. Both automatic - # triggers therefore plan-and-report until AUR_AUTOPUBLISH is set; - # `workflow_dispatch` keeps its explicit per-run switch, which is how - # that first push is meant to happen. - case "$TRIGGER" in - workflow_run | schedule) - if [[ "${AUTOPUBLISH:-}" == "true" ]]; then - publish=true - else - publish=false - echo "::notice::AUR_AUTOPUBLISH is not set — reporting the desired state without publishing." - fi - ;; - *) - publish=${MANUAL_PUBLISH:-false} - ;; - esac - echo "needs_publish=$(jq -r '.needs_publish' "$RUNNER_TEMP/aur-plan.json")" >> "$GITHUB_OUTPUT" - echo "publish=$publish" >> "$GITHUB_OUTPUT" - - - name: Configure pinned AUR SSH identity - if: steps.plan.outputs.needs_publish == 'true' && steps.plan.outputs.publish == 'true' - env: - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - run: | - test -n "$AUR_SSH_PRIVATE_KEY" || { echo 'AUR_SSH_PRIVATE_KEY is empty'; exit 1; } - install -dm700 "$HOME/.ssh" - install -m600 /dev/null "$HOME/.ssh/aur" - printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$HOME/.ssh/aur" - install -m600 scripts/aur/aur.archlinux.org.known_hosts "$HOME/.ssh/known_hosts" - ssh-keygen -lf "$HOME/.ssh/known_hosts" -E sha256 \ - | grep -F 'SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4' - install -m600 /dev/null "$HOME/.ssh/config" - printf '%s\n' \ - 'Host aur.archlinux.org' \ - ' User aur' \ - ' IdentityFile ~/.ssh/aur' \ - ' IdentitiesOnly yes' \ - ' StrictHostKeyChecking yes' \ - ' UserKnownHostsFile ~/.ssh/known_hosts' \ - > "$HOME/.ssh/config" - - - name: Fast-forward publish and verify convergence - if: steps.plan.outputs.needs_publish == 'true' && steps.plan.outputs.publish == 'true' - env: - TRIGGER: ${{ github.event_name }} - run: | - args=( - --publish - --trigger "$TRIGGER" - --report-json "$RUNNER_TEMP/aur-final.json" - --summary "$GITHUB_STEP_SUMMARY" - ) - [[ -z "$REQUESTED_TAG" ]] || args+=(--tag "$REQUESTED_TAG") - - # EXIT 75 IS "THE PUSH LANDED, THE AUR'S INDEX HAS NOT CAUGHT UP", - # AND FAILING ON IT MAKES THIS REPOSITORY'S CI RED FOR SOMEBODY ELSE'S - # REFRESH SCHEDULE. - # - # The reconciler already classifies that case as TRANSIENT and exits - # 75 (the conventional EX_TEMPFAIL) rather than 2. It is reached only - # AFTER the git push has succeeded, so the AUR holds the new version - # at that point; what has not happened is the AUR's own RPC metadata - # refresh, which runs on a schedule measured in minutes. - # - # Measured twice (2026.8.21.2 and 2026.8.21.3): this step reported - # `AUR RPC did not converge to ` and the AUR RPC answered with - # that exact version when asked afterwards. - # - # EVERY OTHER NON-ZERO CODE STILL FAILS. A refused downgrade (3) and - # a permanent error (2) are this repository's problem and stay red. - set +e - python3 scripts/aur/reconcile_mcpp_bin.py "${args[@]}" - rc=$? - set -e - if [[ "$rc" == "75" ]]; then - echo "::warning::The push to the AUR succeeded; its RPC metadata had not refreshed within the poll window. Verify with: curl -s 'https://aur.archlinux.org/rpc/v5/info?arg[]=mcpp-bin'" - elif [[ "$rc" != "0" ]]; then - exit "$rc" - fi - - - name: Preserve reconciliation reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: aur-mcpp-bin-reconciliation - path: ${{ runner.temp }}/aur-*.json - if-no-files-found: error diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml deleted file mode 100644 index 4d65b1ebe..000000000 --- a/.github/workflows/bootstrap-macos.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: bootstrap-macos - -# One-shot workflow to produce the first macOS mcpp binary. -# Uses xmake + xlings LLVM to compile mcpp from source. -# Once a macOS binary exists, mcpp can self-host for future releases. - -on: - workflow_dispatch: - -jobs: - bootstrap: - name: Bootstrap mcpp (macOS ARM64) - runs-on: macos-15 - timeout-minutes: 30 - env: - XLINGS_NON_INTERACTIVE: '1' - # Dormant (workflow_dispatch only), but kept in step with the rest — - # check_version_pins.sh holds it there. Floor: 0.4.69, below which the - # index cannot resolve two packages that share a short name. - XLINGS_VERSION: '2026.9.16.1' - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - - - name: Install xlings - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "${WORK}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Install LLVM + xmake - run: | - xlings install llvm -y || xlings install llvm@20.1.7 -y - brew install xmake - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - "$LLVM_ROOT/bin/clang++" --version - xmake --version - - - name: Build mcpp with xmake - run: | - # Generate xmake.lua if not present - if [ ! -f xmake.lua ]; then - cat > xmake.lua << 'EOF' - add_rules("mode.release") - set_languages("c++23") - - package("cmdline") - set_homepage("https://github.com/mcpplibs/cmdline") - set_description("Modern C++ command-line parsing library") - set_license("Apache-2.0") - add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") - add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") - on_install(function (package) - import("package.tools.xmake").install(package) - end) - package_end() - - add_requires("cmdline 0.0.1") - - target("mcpp") - set_kind("binary") - add_files("src/main.cpp") - add_files("src/**.cppm") - -- `modules/` holds packages mcpp links into itself. This - -- bootstrap has no package manager, so it compiles their sources - -- directly. - add_files("modules/*/src/**.cppm") - add_packages("cmdline") - add_includedirs("modules/libs/src/json") - set_policy("build.c++.modules", true) - -- Static link libc++ for minimal runtime dependencies - add_ldflags("-static-libstdc++", {force = true}) - add_cxxflags("-stdlib=libc++", {force = true}) - add_ldflags("-stdlib=libc++", {force = true}) - EOF - fi - - # Configure with xlings LLVM - xmake f -y -m release --toolchain=llvm --sdk="$LLVM_ROOT" - # Build - xmake build -y mcpp - - - name: Verify built binary - run: | - MCPP=$(find build -name mcpp -type f -perm +111 | head -1) - test -x "$MCPP" - echo "=== file ===" - file "$MCPP" - echo "=== otool -L (dynamic deps) ===" - otool -L "$MCPP" - echo "=== version ===" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Package - id: package - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TARBALL="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - mkdir -p "dist/$WRAPPER/bin" - cp "$MCPP" "dist/$WRAPPER/bin/mcpp" - strip "dist/$WRAPPER/bin/mcpp" 2>/dev/null || true - cp LICENSE "dist/$WRAPPER/" 2>/dev/null || true - cp README.md "dist/$WRAPPER/" 2>/dev/null || true - - cat > "dist/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "dist/$WRAPPER/mcpp" - - # Bundle xlings - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "dist/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "dist/$WRAPPER/registry/bin/xlings" - fi - - (cd dist && tar -czf "$TARBALL" "$WRAPPER") - (cd dist && shasum -a 256 "$TARBALL" > "$TARBALL.sha256") - - echo "tarball=dist/$TARBALL" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke test - run: | - SMOKE=$(mktemp -d) - tar -xzf "${{ steps.package.outputs.tarball }}" -C "$SMOKE" - VERSION="${{ steps.package.outputs.version }}" - "$SMOKE/mcpp-${VERSION}-macosx-arm64/bin/mcpp" --version - "$SMOKE/mcpp-${VERSION}-macosx-arm64/mcpp" --version - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-macosx-arm64 - path: | - dist/mcpp-*-macosx-arm64.tar.gz - dist/mcpp-*-macosx-arm64.tar.gz.sha256 diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml deleted file mode 100644 index 4c6d2dd38..000000000 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ /dev/null @@ -1,237 +0,0 @@ -name: ci-aarch64-fresh-install - -# End-to-end "fresh install" of the whole ecosystem on a NATIVE aarch64 host, -# exactly as a new aarch64-Linux / Termux(-proot) user would: -# -# curl quick_install.sh | bash -> installs aarch64 xlings (static musl) -# xlings install mcpp -> installs aarch64 mcpp (static musl) -# mcpp new / build / run -> NATIVE aarch64 build (pulls the native -# musl-gcc toolchain from the ecosystem) -# -# Validates that every published aarch64 asset (xlings, mcpp, musl-gcc) lines -# up and that mcpp can build & run a real `import std` program natively on -# aarch64 — no cross, no qemu. Runs on GitHub's native ARM64 runner. - -on: - workflow_dispatch: - schedule: - - cron: '0 6 * * 1' # weekly Mon 06:00 UTC - pull_request: - branches: [ main ] - paths: - - 'src/build/build_program.cppm' - - 'modules/platform/src/process.cppm' - - 'tests/e2e/168_build_mcpp_musl_host_static.sh' - - '.github/workflows/ci-aarch64-fresh-install.yml' - push: - branches: [ main ] - paths: - - '.github/workflows/ci-aarch64-fresh-install.yml' - -permissions: - contents: read - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - fresh-install: - name: fresh install + native build (aarch64 / glibc) - runs-on: ubuntu-24.04-arm - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — cold bootstrap path (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - # NB: the checkout deliberately comes LAST, after every fresh-install - # step below — see the comment above it. - - name: System info - run: | - uname -a - echo "arch: $(uname -m)" # aarch64 on this runner - - - name: Fresh-install xlings (curl | bash) - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Verify xlings + GLOBAL mirror - run: | - xlings --version - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - - - name: Fresh-install mcpp via xlings - run: | - xlings install mcpp -y - mcpp --version - mcpp self config --mirror GLOBAL 2>/dev/null || true - - - name: Refresh mcpp package index (force latest xim-pkgindex) - run: | - # mcpp seeds a baseline index with a freshness TTL marker, so a plain - # `index update` can no-op within the window. Force the latest index - # so this run validates the native build against current packages. - mcpp index update || true - idx="$HOME/.mcpp/registry/data/xim-pkgindex" - rm -rf "$idx" - # A bare `git clone` here fails the whole job on a runner DNS - # hiccup. The policy lives in .github/tools/git_clone_retry.sh — - # but this job checks the repository out LAST ON PURPOSE (a - # `.xlings.json` in the workspace re-points where `xlings install` - # writes, so an early checkout silently changes what the - # fresh-install steps above are testing). The helper therefore does - # not exist on disk yet, and the retry is spelled inline. Same - # policy: retry every failure, bounded, git's own message survives. - clone_retry() { - local rc dest="${@: -1}" - for i in 1 2 3 4; do - rc=0; git clone "$@" || rc=$? - [ "$rc" = 0 ] && return 0 - [ "$i" = 4 ] && return "$rc" - [ -e "$dest" ] && rm -rf -- "$dest" - echo "clone_retry: attempt $i failed (exit $rc); retrying" >&2 - sleep $((i * 5)) - done - } - clone_retry --depth 1 https://github.com/openxlings/xim-pkgindex "$idx" - grep -n "skipping relocation\|os.isfile(path.join(bindir" "$idx/pkgs/m/musl-gcc.lua" | head -2 || true - - - name: Native build + run an `import std` program - run: | - work=$(mktemp -d); cd "$work" - mcpp new hello - cd hello - # default src uses import std (C++23) - mcpp build - out=$(mcpp run 2>/dev/null || true) - echo "program output: $out" - bin=$(find target -type f -path '*/bin/hello' | head -1) - file "$bin" - file "$bin" | grep -q "ARM aarch64" || { echo "expected aarch64 ELF"; exit 1; } - - - name: Self-host — build mcpp + xlings from source natively - run: | - # mcpp/xlings manifests pin a glibc default toolchain; on aarch64 the - # musl-static target is the published path, so build with --target. - # - # On a pull_request, self-host the code UNDER REVIEW rather than - # upstream main. Cloning main meant this gate never saw the PR at - # all: a change that breaks the from-source build passed here and - # only failed after merge, and — the way this surfaced — a fix to - # the repo's own bootstrap pin was untestable, because the fix was - # on the branch while the clone was of main. Outside a PR there is - # no head ref and main is exactly right. - ref='${{ github.event.pull_request.head.sha }}' - repo='${{ github.event.pull_request.head.repo.clone_url }}' - [ -n "$ref" ] || ref='${{ github.sha }}' - [ -n "$repo" ] || repo='https://github.com/mcpp-community/mcpp' - # fetch-by-sha rather than `clone --depth 1`, which cannot take one. - git init -q /tmp/mcpp-src - cd /tmp/mcpp-src - git remote add origin "$repo" - git fetch -q --depth 1 origin "$ref" - git checkout -q FETCH_HEAD - echo "self-hosting $repo @ $ref" - # The clone's .xlings.json declares `workspace.mcpp` — the BOOTSTRAP - # pin, hand-maintained and deliberately lagging the newest release. - # It is scoped to the working directory and beats anything installed, - # so every `mcpp` below resolved the bootstrap version, which the - # fresh-install steps above never installed: - # - # [error] xlings: version '2026.8.6.2' not found for 'mcpp' - # [error] available: 2026.8.8.2 - # - # "newest release != bootstrap pin" is the NORMAL state, so this step - # failed on every run from the moment the two diverged — and it is - # weekly, so nothing pointed at it. This step tests the freshly - # installed RELEASED binary against a source tree; the bootstrap pin - # has no standing in that question. install_released_mcpp.sh removes - # it for exactly this reason on the x86_64 legs (its point 1); this - # leg was written separately and never got it. - rm -f .xlings.json - mcpp self config --mirror GLOBAL 2>/dev/null || true - mcpp build --target aarch64-linux-musl - # Absolute: it is used again after `cd /tmp/xlings-src` below. - m=$(find "$PWD/target/aarch64-linux-musl" -type f -path '*/bin/mcpp' | head -1) - file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; } - "$m" --version - # MCPP_HOME must be carried over explicitly: mcpp derives it from the - # BINARY's location, so a binary sitting in /tmp/mcpp-src/target would - # otherwise adopt an empty home and re-bootstrap the whole ecosystem - # instead of reusing what the fresh-install steps above provisioned. - # - # Resolved HERE, before the cd below, and that placement is load- - # bearing: `mcpp` is the shim, so it obeys whatever workspace pin the - # CURRENT DIRECTORY carries — and the xlings checkout declares one too - # (`workspace.mcpp = 2026.8.6.1`, its own bootstrap). Run from there, - # this resolves a version nothing installed, MCPP_HOME comes back - # empty, and the step dies on the guard below instead of on the real - # cause. /tmp/mcpp-src has had its pin removed above, so ask from here. - export MCPP_HOME=$(mcpp self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}') - echo "reusing MCPP_HOME=$MCPP_HOME" - test -d "$MCPP_HOME" || { echo "could not determine MCPP_HOME"; exit 1; } - # Same inline retry as the index clone above, and for the same - # reason — the checkout that would provide the shared helper is - # deliberately the last step in this job. - clone_retry() { - local rc dest="${@: -1}" - for i in 1 2 3 4; do - rc=0; git clone "$@" || rc=$? - [ "$rc" = 0 ] && return 0 - [ "$i" = 4 ] && return "$rc" - [ -e "$dest" ] && rm -rf -- "$dest" - echo "clone_retry: attempt $i failed (exit $rc); retrying" >&2 - sleep $((i * 5)) - done - } - clone_retry --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - # "$m", not `mcpp`: the just-built binary is the code under review, - # and building xlings with the INSTALLED one meant this half of the - # gate never saw the PR — the same defect the clone-ref comment above - # records, one line further down. It surfaced the same way: a fix for - # an aarch64-only failure in exactly this build could not be - # validated here, because the binary running it predated the fix. - "$m" build --target aarch64-linux-musl - x=$(find target/aarch64-linux-musl -type f -path '*/bin/xlings' | head -1) - file "$x" | grep -q "ARM aarch64" || { echo "expected aarch64 xlings"; exit 1; } - "$x" --version - - # ── PR regression gate ──────────────────────────────────────────────── - # Everything above is the fresh-install charter: it must run exactly as a - # new user's machine does. Check out only NOW — this repo's .xlings.json - # declares an `mcpp` WORKSPACE pin, so with the checkout present in - # $GITHUB_WORKSPACE `xlings install mcpp` installs workspace-scoped - # instead of globally: the steps above would silently validate the pinned - # version rather than the freshly published one, and bare `mcpp` stops - # resolving anywhere outside the workspace. - - uses: actions/checkout@v4 - with: - persist-credentials: false - - # The PR's own source, then the musl host-helper regression against it. - # This is the only runner where a musl toolchain is the NATIVE one, so it - # is the only place #295 can actually be reproduced. - - name: Build current mcpp source for native regression tests - run: | - # Third site of the same pin, and the reason to remove it here too: - # this job installs `xlings install mcpp` (bare = latest) and NOTHING - # else, so the bootstrap version the checkout pins is never on this - # runner. Obeying the pin here does not select an older builder — it - # selects one that does not exist. Same removal as the self-host step - # above, same reason as install_released_mcpp.sh point 1. - rm -f .xlings.json - mcpp build --target aarch64-linux-musl - self=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1) - test -x "$self" - self=$(realpath "$self") - "$self" --version - echo "MCPP_SELF=$self" >> "$GITHUB_ENV" - - - name: "Regression: build.mcpp host helper is self-contained (#295)" - run: MCPP="$MCPP_SELF" bash tests/e2e/168_build_mcpp_musl_host_static.sh diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml deleted file mode 100644 index ea90dcd9d..000000000 --- a/.github/workflows/ci-fresh-install.yml +++ /dev/null @@ -1,718 +0,0 @@ -name: ci-fresh-install - -# Fresh install CI — validates the released mcpp binary via xlings. -# Simulates a real first-time user on a clean machine (no caches). -# -# For each platform, tests every supported toolchain: -# 1. mcpp new hello → mcpp run (basic project) -# 2. mcpp build (build mcpp itself from source) -# -# This workflow tests released mcpp, not PR code. -# It runs on release publish, manual trigger, and daily schedule. - -on: - # NOTE: `release: published` never fires from the release pipeline — the - # release is created by release.yml with GITHUB_TOKEN, and GitHub - # suppresses workflow triggers from GITHUB_TOKEN-generated events. Kept - # only for releases created manually outside the pipeline. The reliable - # post-release hook is `workflow_run` below: a platform-generated event, - # exempt from that suppression, and requiring no cross-repo PAT. - release: - types: [ published ] - workflow_run: - workflows: [ release ] - types: [ completed ] - workflow_dispatch: - schedule: - # Run daily at 06:00 UTC to catch issues from xlings/runner updates - - cron: '0 6 * * *' - -concurrency: - group: ci-fresh-install - cancel-in-progress: false # use false to test in PRs, true to only test released mcpp - -# The version under test is DERIVED, once, by the resolve-version job below, -# and every install job consumes that one value. -# -# Two things had to hold, and a hardcoded pin only bought the first: -# -# 1. It must be an explicit version string. Bare `xlings install mcpp` -# resolves "newest in the runner's index copy", so a runner whose copy -# lags silently tests an OLDER binary and reports green. Naming the -# version makes a lagging index fail loudly with `version not found`. -# -# 2. The version the index guard waits for must be the version the jobs -# install. On 2026-07-21 they disagreed: the guard reported "index tracks -# 0.0.102" and the jobs installed 0.0.100 ten seconds later, which then met -# an index whose floor was 0.0.101 (#265). The guard already derived the -# real answer from the releases API and threw it away. -# -# Deriving once and feeding both satisfies (1) and makes (2) structurally -# impossible, instead of relying on a human to keep two hand-edited numbers in -# step with a third that moves on its own. `xlings install mcpp@` is -# every bit as explicit as `mcpp@`. -# -# NOT to be confused with the .xlings.json workspace pin, which this used to be -# kept equal to. That one is the BOOTSTRAP compiler for the self-host builds and -# has a different requirement — it must be a released mcpp that can build the -# CURRENT source tree — so it stays hand-maintained. See docs/92-release.md. - -jobs: - # ────────────────────────────────────────────────────────────────── - # Linux: gcc@16.1.0, musl-gcc@16.1.0, llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - # A4: post-release runs race the xim-pkgindex bump (a PR a maintainer - # merges asynchronously) — the 0.0.85 fresh-install failed with - # "version not found: available 0.0.84" 55s after the bump merged. - # Convert the race into a bounded wait: poll the index's mcpp.lua until - # it carries the released version (<=15 min), then let every job run. - # Non-workflow_run triggers (manual, cron, PR) skip the wait. - wait-index: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 20 - outputs: - version: ${{ steps.resolve.outputs.version }} - steps: - # Derive on EVERY trigger, not just post-release. "The newest published - # release" is the version under test whether we got here from the release - # pipeline, from cron, or by hand — only the WAIT below is specific to a - # post-release run, because only then can the index legitimately lag. - - name: Resolve the version under test (newest published release) - id: resolve - run: | - VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" \ - | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") - # A blank version would silently degrade `mcpp@$VER` into bare `mcpp`, - # i.e. straight back to "newest in the runner's index copy" — the exact - # failure this job exists to prevent. Refuse instead. - case "$VER" in - ''|*[!0-9.]*) echo "::error::could not resolve a version from the releases API (got '$VER')"; exit 1 ;; - esac - echo "version=$VER" >> "$GITHUB_OUTPUT" - echo "version under test: $VER" - - name: Wait for the PUBLISHED index artifact to track the released mcpp - if: ${{ github.event_name == 'workflow_run' }} - env: - VER: ${{ steps.resolve.outputs.version }} - run: | - # Poll the ARTIFACT, not the git file. - # - # This used to curl raw.githubusercontent.com/openxlings/xim-pkgindex - # — the index's git source of truth, which updates the instant the - # bump PR merges. But the jobs install from the PUBLISHED ARTIFACT - # (xlings-res/xim-index → pointer → tarball), and that channel lags - # git by however long `Publish Index Artifact` plus release-CDN - # propagation takes. Measured on the 2026.8.3.5 release: this guard - # reported ready, and all 11 jobs then failed with - # [error] package 'mcpp@2026.8.3.5' not found - # A guard that measures a channel nobody installs from is not a guard. - # - # This narrows the window; it cannot close it, because the CDN - # propagates per edge and the runner that polls is not the runner - # that installs. install_released_mcpp.sh retries from the consumer - # side for exactly that residue — this step exists so the retry is - # rarely needed, not so it can be removed. - echo "released: $VER — waiting for the published index artifact..." - for i in $(seq 1 40); do - ptr=$(curl -fsSL "https://github.com/xlings-res/xim-index/releases/download/latest/xim-index-latest.json" 2>/dev/null || true) - # One line on purpose: an indented heredoc inside a YAML block - # scalar is a trap — unindented content silently ends the block. - name=$(printf '%s' "$ptr" | python3 -c 'import json,sys; d=json.load(sys.stdin); n=d.get("indexes",{}).get("xim",d); print(n.get("artifact",{}).get("name",""))' 2>/dev/null || true) - if [ -n "$name" ] && curl -fsSL \ - "https://github.com/xlings-res/xim-index/releases/download/latest/$name" \ - | tar -xzO --wildcards '*pkgs/m/mcpp.lua' 2>/dev/null | grep -q "\"$VER\""; then - echo "published index artifact ($name) tracks $VER (after $((i*30))s)"; exit 0 - fi - sleep 30 - done - echo "::error::the published index artifact never tracked $VER within 20min — check that the xim-pkgindex bump PR merged AND that Publish Index Artifact ran" - exit 1 - - name: No wait needed (manual/cron trigger) - if: ${{ github.event_name != 'workflow_run' }} - run: echo "not a post-release run; skipping index wait" - - linux-fresh: - needs: [wait-index] - name: Linux fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # The one derived value (see the header comment): every install job names - # the SAME version the index guard waited for, so the two cannot disagree. - MCPP_PIN: ${{ needs.wait-index.outputs.version }} - # Verbose every mcpp invocation — fresh-install is the cold index/sandbox - # bootstrap path, exactly where extra diagnostics matter (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - shell: bash - run: | - # ONE implementation for "make the released mcpp@X be what `mcpp` - # runs, and prove it" — see .github/tools/install_released_mcpp.sh - # for the three defects the inline version had (workspace pin, no - # activation, and waiting on the wrong index channel). - bash .github/tools/install_released_mcpp.sh "${MCPP_PIN}" "$(pwd)" - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "Default: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_gcc - cd hello_gcc - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new ... --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on hosts without a sha256sum binary). - - name: "Template: exact mcpplibs.imgui selector (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template mcpplibs.imgui 2>&1 | tee template.log - test -f abc1/mcpp.toml - grep -F 'namespace=mcpplibs name=imgui' template.log - - - name: "Default: build mcpp" - run: | - mcpp clean - mcpp run - - - name: "musl-gcc: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0-musl - mcpp toolchain default gcc@16.1.0-musl - cd "$(mktemp -d)" - mcpp new hello_musl - cd hello_musl - mcpp run - - - name: "musl-gcc: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0-musl - mcpp clean - mcpp run - - - name: "gcc 16: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0 - mcpp toolchain default gcc@16.1.0 - cd "$(mktemp -d)" - mcpp new hello_gcc16 - cd hello_gcc16 - mcpp run - - - name: "gcc 16: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0 - mcpp clean - mcpp run - - - name: "LLVM: mcpp new → run" - run: | - mcpp toolchain install llvm 20.1.7 - mcpp toolchain default llvm@20.1.7 - cd "$(mktemp -d)" - mcpp new hello_llvm - cd hello_llvm - mcpp run - - - name: "LLVM: build mcpp" - run: | - mcpp toolchain default llvm@20.1.7 - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Newer/rolling-glibc distros — reproduction surface for the - # bundled-glibc-vs-host-libtinfo `sh:` crash (host glibc > bundled). - # Plus older-glibc legs (the safe reverse direction) proving the - # musl-static mcpp + self-contained toolchain run end-to-end on old - # hosts. Runs the released mcpp inside distro containers. - # ────────────────────────────────────────────────────────────────── - linux-distro-matrix: - needs: [wait-index] - name: Linux distro (${{ matrix.distro }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - container: - image: ${{ matrix.image }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - # findutils (find) is mandatory on every leg: quick_install.sh - # locates the extracted xlings dir with `find`, so a missing find - # fails the install with exit 127 *before* mcpp ever runs. Minimal - # images (opensuse/tumbleweed) ship without it; arch's base merely - # bundles it by luck. List it explicitly everywhere — don't rely on - # the base image. - include: - - distro: fedora-latest - image: fedora:latest - setup: dnf -y install curl bash tar gzip xz git findutils binutils file glibc-langpack-en - - distro: arch - image: archlinux:latest - setup: pacman -Sy --noconfirm curl bash tar gzip xz git findutils binutils file - - distro: tumbleweed - image: opensuse/tumbleweed:latest - setup: zypper -n install curl bash tar gzip xz git findutils binutils file - - distro: debian-testing - image: debian:testing - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: ubuntu-2004 - image: ubuntu:20.04 - setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - # debian-12 AND NOT debian-11, AND THE REASON IS NOT THE FAILURE. - # - # The debian-11 leg started failing on 2026-09-11 with - # - # E: Release file for .../bullseye-security/InRelease is expired - # (invalid since 3d 5h 32min 52s) - # - # and `apt-get update` exits 100. Bullseye is end-of-life and its - # security suite's metadata has expired, which is a property of the - # distribution and not of this workflow -- `-o - # Acquire::Check-Valid-Until=false` would silence it and keep a leg - # that tests against metadata nobody maintains. - # - # What was measured while replacing it: debian 11 and ubuntu 20.04 - # both carry glibc 2.31, so the "older glibc" coverage this leg was - # here for was ALREADY DUPLICATED by the ubuntu-2004 leg above, and - # dropping bullseye loses nothing. Bookworm's 2.36 sits between that - # 2.31 and debian-testing's rolling version, so this leg now covers a - # point the matrix did not have. - - distro: debian-12 - image: debian:12 - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - env: - # The one derived value (see the header comment): every install job names - # the SAME version the index guard waited for, so the two cannot disagree. - MCPP_PIN: ${{ needs.wait-index.outputs.version }} - XLINGS_NON_INTERACTIVE: '1' - HOME: /root - steps: - - uses: actions/checkout@v4 - - - name: Install prerequisites (${{ matrix.distro }}) - run: ${{ matrix.setup }} - - - name: Install xlings + mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 - # Deliberately NOT writing to $GITHUB_PATH here. On container - # images that declare no PATH in their config (opensuse/ - # tumbleweed), appending a single dir to GITHUB_PATH makes the - # runner exec later steps' `sh` with only that dir on PATH — - # `sh` (in /usr/bin) vanishes and the next step dies with - # `exec: "sh": ... not found` / exit 127. Each step below already - # exports PATH itself, so the append is redundant anyway. - - - name: Configure mcpp - shell: bash - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - # ONE implementation for "make the released mcpp@X be what `mcpp` - # runs, and prove it" — see .github/tools/install_released_mcpp.sh - # for the three defects the inline version had (workspace pin, no - # activation, and waiting on the wrong index channel). - bash .github/tools/install_released_mcpp.sh "${MCPP_PIN}" "$(pwd)" - mcpp self config --mirror GLOBAL - - - name: "Regression: new → run (loader env must not crash /bin/sh)" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" - mcpp new hello_distro - cd hello_distro - mcpp run - - - name: "Self-containment: produced binary uses bundled loader" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" && mcpp new hc && cd hc && mcpp build - bin="$(find target -type f -name hc | head -1)" - interp="$(file "$bin" | grep -o 'interpreter [^,]*' | awk '{print $2}')" - echo "interp=$interp" - case "$interp" in - */.mcpp/*|*/registry/*|*xpkgs*) echo "OK bundled loader" ;; - *) echo "FAIL host loader: $interp"; exit 1 ;; - esac - - # ────────────────────────────────────────────────────────────────── - # macOS: llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - macos-fresh: - needs: [wait-index] - name: macOS fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # macos-14: the support floor (mcpp ≥0.0.50 / xlings ≥0.4.50 ship - # minos=14.0 static-libc++ binaries). A fresh install passing here - # is the continuous proof of the macOS 14 floor — and of host-tool - # independence (this image has no sha256sum; macos-15 does). - runs-on: macos-14 - timeout-minutes: 30 - env: - # The one derived value (see the header comment): every install job names - # the SAME version the index guard waited for, so the two cannot disagree. - MCPP_PIN: ${{ needs.wait-index.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - # Pinned to kXlingsVersion like every other bootstrap (see - # .github/tools/check_version_pins.sh). Two floors this image needs, - # both long satisfied — do not pin below them: - # v0.4.50+: first xlings whose macosx binary runs on macOS 14 - # (older ones carry minos=15 and refuse to start). - # v0.4.51+: in-process sha256 — this image has no sha256sum - # binary, so pinned fetches failed before it. - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - shell: bash - run: | - # ONE implementation for "make the released mcpp@X be what `mcpp` - # runs, and prove it" — see .github/tools/install_released_mcpp.sh - # for the three defects the inline version had (workspace pin, no - # activation, and waiting on the wrong index channel). - bash .github/tools/install_released_mcpp.sh "${MCPP_PIN}" "$(pwd)" - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "LLVM: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_mac - cd hello_mac - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path — this is what broke on hosts without a sha256sum binary - # (stock macOS / bare Windows) before xlings 0.4.51 hashed - # in-process. - - name: "Template: exact mcpplibs.imgui selector (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template mcpplibs.imgui 2>&1 | tee template.log - test -f abc1/mcpp.toml - grep -F 'namespace=mcpplibs name=imgui' template.log - - - name: "LLVM: build mcpp" - run: | - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # macOS via HOMEBREW — the other fresh-install channel - # - # A SEPARATE JOB, not extra steps in macos-fresh: that job already has an - # mcpp on PATH from xlings, and a second one from brew would make every - # assertion below ambiguous about which binary it measured. - # - # It also does NOT need `wait-index`. Homebrew installs the GitHub release - # tarball directly, and the tap's own bump workflow lags the release by - # minutes — asserting the just-released version here would be racy for a - # reason that has nothing to do with mcpp. The version assertion is instead - # made SELF-CONSISTENT: whatever version the tapped formula declares is the - # version the installed binary must report. - # - # WHAT THIS EXISTS TO CATCH (measured on macOS 14.8.7 / 15.7.7 / 26.5.2, - # Homebrew 6.0.5 / 6.0.12 / 6.0.13): - # - # Homebrew 6 refuses to load a formula from an untrusted third-party tap. - # `brew install //` is exempt — it reads as explicit - # intent — so the one-liner in README.md kept passing while EVERY other - # spelling a user reaches for was broken: - # - # brew install mcpp-m → exit 1, "untrusted tap" - # brew install mcpp-community/mcpp/mcpp → exit 1, "untrusted tap" - # - # A CI job that only ran the documented one-liner would have reported this - # channel healthy the entire time. So the trust gate itself is asserted - # from BOTH sides: refused before `brew trust`, accepted after. - macos-brew-fresh: - name: macOS fresh install (Homebrew) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # Same floor as macos-fresh: the formula declares `depends_on macos: - # :sonoma` + arm64, and macos-14 is the oldest image satisfying it. - runs-on: macos-14 - timeout-minutes: 30 - steps: - - name: Environment - run: | - echo "macOS $(sw_vers -productVersion) | $(uname -m) | $(brew --version | head -1)" - - # ① The command README.md documents, on a machine that has never tapped. - # Fully qualified, so Homebrew treats it as explicit intent. - - name: "Documented one-liner installs" - run: brew install mcpp-community/mcpp/mcpp-m - - # ② The launcher is the whole point of the formula: mcpp WRITES at - # runtime, so a bare symlink into the Cellar would make MCPP_HOME the - # versioned Cellar dir and `brew upgrade` would drop every installed - # toolchain. Assert the binary on PATH is brew's and reports the - # version the FORMULA declares — not a hardcoded one, which would go - # stale on every release. - - name: "Installed binary is brew's, and agrees with the formula" - run: | - set -euo pipefail - which mcpp - case "$(which mcpp)" in - "$(brew --prefix)"/bin/mcpp) ;; - *) echo "FAIL: mcpp on PATH is not the brew one"; exit 1 ;; - esac - formula_version="$(brew info --json=v2 mcpp-community/mcpp/mcpp-m \ - | python3 -c 'import json,sys; print(json.load(sys.stdin)["formulae"][0]["versions"]["stable"])')" - echo "formula declares: $formula_version" - mcpp --version - mcpp --version | grep -Fq "$formula_version" - - # ③ THE TRUST GATE, from the failing side first. - # - # Asserted before the fix is applied, because "short form works" alone - # cannot distinguish "the gate is handled" from "this Homebrew has no - # gate" — and a test that cannot fail is indistinguishable from one - # that is not running. If a future Homebrew drops the gate, this step - # turns red and says so, rather than silently protecting nothing. - - name: "Untrusted tap: short form is refused (states its own premise)" - run: | - set +e - brew uninstall --force mcpp-m > /dev/null 2>&1 - out="$(brew install mcpp-m 2>&1)"; rc=$? - echo "$out" | tail -5 - if [ $rc -eq 0 ]; then - echo "NOTE: this Homebrew did not gate the tap — the trust step below" - echo " is now a no-op and README's warning can be revisited." - exit 0 - fi - echo "$out" | grep -Fq 'untrusted tap' || { - echo "FAIL: short-form install failed for some OTHER reason than the" - echo " trust gate — that is a new bug, not the known one." - exit 1 - } - - # ④ …and from the working side. `brew trust` is what README tells users - # to run, so it is what CI runs. - - name: "brew trust unlocks the short form, the alias, and upgrade" - run: | - set -euo pipefail - brew trust mcpp-community/mcpp - brew uninstall --force mcpp-m - brew install mcpp-m # short form - brew uninstall --force mcpp-m - brew install mcpp-community/mcpp/mcpp # the `mcpp` alias - brew upgrade mcpp-m || true # no-op when current; must not be refused - - # ⑤ The only assertion a user actually cares about: it builds and runs. - # Exercises the launcher's MCPP_HOME/MCPP_VENDORED_XLINGS pinning, - # the bundled xlings, and a real toolchain bootstrap. - - name: "Real use: mcpp new → run (toolchain bootstrap)" - run: | - set -euo pipefail - cd "$(mktemp -d)" - mcpp new brewhello - cd brewhello - mcpp run | tee run.log - grep -Fq 'Hello from brewhello' run.log - - # ⑥ Per-user state must live outside the Cellar, or `brew upgrade` takes - # the user's toolchains with it. §⑤ just created it — prove where. - - name: "State lives in ~/.mcpp, not the Cellar" - run: | - set -euo pipefail - test -d "$HOME/.mcpp" - if find "$(brew --prefix)/Cellar/mcpp-m" -name 'toolchain*' -o -name 'xpkgs' 2>/dev/null | grep -q .; then - echo "FAIL: toolchain state landed inside the Cellar"; exit 1 - fi - - # ────────────────────────────────────────────────────────────────── - # Windows WITH Visual Studio: llvm@20.1.7 + MSVC STL - # - # Two images, because the OS version is a real variable for a tool that - # touches the UCRT, the Windows SDK and long paths. GitHub publishes no - # Windows 10/11 CLIENT image, so these Server builds are the closest - # available stand-ins: windows-2022 is the Win10 21H2 kernel generation, - # windows-2025 the Win11 24H2 one. What they cannot cover is genuinely - # client-only behaviour — UAC prompts, Defender real-time scanning, the - # long-path policy default — which needs a self-hosted runner. - # ────────────────────────────────────────────────────────────────── - windows-fresh: - needs: [wait-index] - name: Windows fresh install (${{ matrix.image }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ${{ matrix.image }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - image: [windows-2022, windows-2025] - env: - # The one derived value (see the header comment): every install job names - # the SAME version the index guard waited for, so the two cannot disagree. - MCPP_PIN: ${{ needs.wait-index.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex - - $xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin" - $env:PATH = "$xlingsbin;$env:PATH" - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: Install mcpp and config mirror - shell: bash - run: | - # ONE implementation for "make the released mcpp@X be what `mcpp` - # runs, and prove it" — see .github/tools/install_released_mcpp.sh - # for the three defects the inline version had (workspace pin, no - # activation, and waiting on the wrong index channel). - bash .github/tools/install_released_mcpp.sh "${MCPP_PIN}" "$(pwd)" - mcpp self config --mirror GLOBAL - - cat "$USERPROFILE/.xlings/.xlings.json" || true - - - name: "LLVM: mcpp new → run" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new hello_win - Set-Location hello_win - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new abc1 --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on bare Windows — no sha256sum binary - # outside git-bash; fixed by xlings 0.4.51 in-process hashing). - - name: "Template: exact mcpplibs.imgui selector (fetch path)" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new abc1 --template mcpplibs.imgui 2>&1 | Tee-Object -Variable templateOutput - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - if (!(Test-Path abc1/mcpp.toml)) { exit 1 } - if (-not (($templateOutput -join "`n") -match 'namespace=mcpplibs name=imgui')) { exit 1 } - - - name: "LLVM: build mcpp" - shell: pwsh - run: | - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Windows WITHOUT Visual Studio — the shape of an ordinary user's machine - # - # A stock Windows install ships the UCRT runtime DLLs and nothing else: the - # MSVC STL and the Windows SDK arrive only with Visual Studio's "Desktop - # development with C++" workload. mcpp's Windows default targeted the MSVC - # ABI, so `mcpp new && mcpp build` failed on every such box — and no CI job - # could see it, because every GitHub Windows image ships Visual Studio. - # - # There is no VS-free runner, so the image is masked instead. The risk with - # masking is a false green: miss one of msvc.cppm's three discovery - # strategies (vswhere, environment, well-known paths) and mcpp still finds - # MSVC, takes the ordinary path, and the job passes while proving nothing. - # e2e 182 opens by asserting `mcpp toolchain default msvc` FAILS, which - # turns exactly that into a hard failure. - # ────────────────────────────────────────────────────────────────── - windows-nomsvc-fresh: - needs: [wait-index] - name: Windows fresh install (no Visual Studio) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: windows-2025 - timeout-minutes: 30 - env: - MCPP_PIN: ${{ needs.wait-index.outputs.version }} - steps: - - uses: actions/checkout@v4 - - - name: Mask Visual Studio - shell: pwsh - run: | - $ErrorActionPreference = 'Continue' - - # All three of msvc.cppm's discovery strategies converge on - # \VC\Tools\MSVC, so mask the VC directory rather than the - # Visual Studio root: the root is held open on the runner and - # renaming it is denied, while VC one level down renames fine. - # The runner is disposable, so this is both safe and closer to - # "absent" than any env-only trick would be. - $vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" } - - Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC" ` - -Directory -ErrorAction SilentlyContinue | ForEach-Object { - Rename-Item $_.FullName "$($_.Name).masked" -ErrorAction SilentlyContinue - } - - foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir', - 'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) { - "$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 - } - - # Check the mask's own postcondition here, where the cause is - # obvious, rather than letting it surface later as a confusing pass. - $left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" ` - -Directory -ErrorAction SilentlyContinue - if ($left) { - Write-Host "FAIL: VC tools still present after masking:" - $left | ForEach-Object { Write-Host " $($_.FullName)" } - exit 1 - } - Write-Host "Visual Studio masked: no VC\Tools\MSVC remains." - - - name: Install xlings - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex - - $xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin" - $env:PATH = "$xlingsbin;$env:PATH" - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: Install mcpp and config mirror - shell: bash - run: | - # ONE implementation for "make the released mcpp@X be what `mcpp` - # runs, and prove it" — see .github/tools/install_released_mcpp.sh - # for the three defects the inline version had (workspace pin, no - # activation, and waiting on the wrong index channel). - bash .github/tools/install_released_mcpp.sh "${MCPP_PIN}" "$(pwd)" - mcpp self config --mirror GLOBAL - - # The self-check, the fallback, persistence, a self-contained exe, and - # the refusal to overrule an explicit [toolchain] — all in e2e 182, so - # the assertions live with the tests rather than in workflow YAML. - - name: "No Visual Studio: fallback to winlibs GCC (e2e 182)" - shell: bash - run: | - MCPP="$(command -v mcpp)" bash tests/e2e/182_windows_no_msvc_fallback.sh diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml deleted file mode 100644 index 20befd35a..000000000 --- a/.github/workflows/ci-linux-e2e.yml +++ /dev/null @@ -1,451 +0,0 @@ -name: ci-linux-e2e - -# The e2e suite (tests/e2e/run_all.sh) split out of ci-linux.yml so it runs in -# PARALLEL with the build/unit/toolchain jobs instead of tacked on after them, -# and SHARDED across two runners on top of that. Both workflows share the same -# cache lineage (mcpp sandbox + xlings + target/), so each shard restores a warm -# build and the only added wall-clock vs. the inline version is one extra warm -# `mcpp build` per runner. -# -# Paired workflows: ci-linux.yml (build + unit + toolchain legs + integration), -# ci-macos.yml / ci-macos-e2e.yml, ci-windows.yml / ci-windows-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e ${{ matrix.shard }}/2 (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - shard: [1, 2] - env: - # Round-robin slice of tests/e2e (see run_all.sh). Two shards halve the - # suite's wall-clock; the split is computed on the full file list, so a - # test's shard does not move when host capabilities differ. - E2E_SHARD: ${{ matrix.shard }}/2 - MCPP_HOME: /home/runner/.mcpp - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. Verbose is - # set only in the fresh-install workflows (cold bootstrap, no such asserts). - # A specific test that needs verbose passes `--verbose` itself. - steps: - # `submodules: recursive` so tests/e2e/233_bench_matrix.sh can check that - # each `hub`/`body` in bench/matrix.json exists in the tree it names. - # Without the trees that check reads "submodule not initialised" and - # reports nothing, which is how a hub path written for the CURRENT - # layout stayed in matrix.json while the workload it named is a - # HISTORICAL mcpp -- three cells reporting `skipped` on every bench run - # and the job still green (#599). - # - # It is not free and it is not expensive: the three pinned workloads are - # 725 + 701 + 806 tracked files, under 10 MB of source in total, and - # nothing here builds them. Both shards carry it because run_all.sh - # slices the file list round-robin, so which shard holds 233 moves when - # a test is added. - - uses: actions/checkout@v4 - with: - submodules: recursive - - # Same cache lineage as ci-linux.yml so this job lands on a warm - # toolchain/sandbox instead of re-installing it. - - uses: ./.github/actions/bootstrap-mcpp - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: E2E suite - # Per-test 600s timeout lives in tests/e2e/run_all.sh and identifies - # WHICH test hung; this caps the whole suite so a hang fails fast. - timeout-minutes: 25 - run: | - # Point the e2e runner at the freshly-built binary, not the - # bootstrap one. Tests cd into mktemp -d, so $MCPP must be - # absolute or the relative path breaks under the temp cwd. - # The RELEASED mcpp this job bootstrapped from, kept for e2e 252: the - # claim that an older client can still build against a package the PR - # produces is only worth making if something checks it against a real - # old binary. Captured before $MCPP is repointed at the fresh build. - export MCPP_BOOT="$MCPP" - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - # Tests that set MCPP_HOME to a fresh tmpdir need an xlings to - # bootstrap from; surface the xlings binary installed above. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - # GitHub-hosted runners are outside CN; keep CI toolchain downloads on - # the global mirror while mcpp's default remains CN for fresh local - # sandboxes. E2E tests with their own MCPP_HOME read this variable. - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # Pin the global default so test 28 (default-toolchain path) gets a - # deterministic GNU answer instead of an auto-install pick. - # - # Installed explicitly, not assumed. `toolchain default` requires the - # toolchain to be present, and nothing in this job puts it there: - # building mcpp above uses whatever first-run picks (llvm). It worked - # only because the sandbox cache happened to carry a gcc some earlier - # run had installed -- so the moment the cache key changed (an xlings - # pin bump moves the whole prefix), every e2e shard failed with - # `gcc@16.1.0 is not installed` before running a single test. - # Warm runs re-install nothing; this costs a lookup. - "$MCPP" toolchain install gcc 16.1.0 - "$MCPP" toolchain default gcc@16.1.0 - # Warm musl once so fresh-home e2e tests inherit the payload. - "$MCPP" toolchain install gcc 16.1.0-musl - bash tests/e2e/run_all.sh - - # ────────────────────────────────────────────────────────────────── - # Bare metal: the one chain the sharded suite above cannot be trusted - # to exercise. - # - # tests/e2e/130_freestanding_riscv_build_and_run.sh declares - # `# requires: qemu-riscv`, which is legitimately absent on the macOS and - # Windows runners — so it must be a SOFT token, and a soft token means the - # test skips in silence on a Linux runner that lost qemu too. That is the - # exact shape this repository has been burned by twice (65_* never ran at - # all; ten pack e2e skipped on two platforms), and no token can tell the two - # cases apart. - # - # So the guard lives here, where it can be exact: install qemu, run the one - # test, and assert its PASS line appeared. A skip fails this job. - # ────────────────────────────────────────────────────────────────── - baremetal: - name: bare-metal e2e (riscv64-none-elf + cortex-m, qemu) - runs-on: ubuntu-24.04 - timeout-minutes: 40 - env: - MCPP_HOME: /home/runner/.mcpp - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Install the emulator (xim:qemu-riscv) - run: | - # BOTH homes. The shim on PATH dispatches against whichever home - # owns it, and `mcpp run` runs the runner through that shim — so an - # emulator installed only in the ambient xlings home answers - # "xlings: 'qemu-system-riscv64' is not installed" when mcpp asks. - # Measured: the job installed it once, the shim resolved, and the - # run still failed. - "$XLINGS_BIN" install xim:qemu-riscv -y - XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ - "$XLINGS_BIN" install xim:qemu-riscv -y - # Assert it is reachable AND runnable BEFORE the tests. Without this - # the capability probe simply would not add `qemu-riscv` and the - # tests would skip — which is what this job exists to prevent. - command -v qemu-system-riscv64 - qemu-system-riscv64 --version | head -1 - # The target sysroot, into the home MCPP uses. Test 131's BSP - # declares it as an `[xlings] deps` entry and finds it through - # `xpkg_dir`; installed into the ambient xlings home instead, the - # test would SKIP and the seam would go unexercised. - XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ - "$XLINGS_BIN" install xim:picolibc-riscv -y - test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv" - # The M-profile emulator, in BOTH homes for the reason above. - # `xim:qemu-arm` carries `qemu-system-arm` as well as - # `qemu-system-aarch64`; test 332 addresses it by absolute path out of - # the payload, so what matters is that the payload EXISTS in the home - # mcpp uses rather than that a shim resolves. - "$XLINGS_BIN" install xim:qemu-arm -y - XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ - "$XLINGS_BIN" install xim:qemu-arm -y - # Reachable AND runnable before the tests, so that a missing emulator - # fails this step rather than silently skipping test 332. - ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \ - | sort -V | tail -1 | xargs -I{} {} --version | head -1 - - - name: Bare-metal e2e - timeout-minutes: 25 - run: | - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - # llvm is the toolchain a freestanding target pins; install it - # explicitly rather than relying on whatever the sandbox cache holds. - "$MCPP" toolchain install llvm 22.1.8 - # Run the two scripts DIRECTLY rather than through run_all.sh. - # They are standalone (they take $MCPP and nothing else), run_all.sh - # accepts no filter — it would run the whole 250-test suite here for - # two tests — and, more to the point, run_all.sh exits 0 on a skip. - # Invoked directly, a skip is visible: the script either prints its - # PASS line or it does not. - for t in tests/e2e/130_freestanding_riscv_build_and_run.sh \ - tests/e2e/131_freestanding_bsp_supplies_everything.sh \ - tests/e2e/132_freestanding_test_and_artifacts.sh \ - tests/e2e/133_freestanding_std_subset.sh \ - tests/e2e/332_cortex_m_builds_and_boots.sh \ - tests/e2e/336_armv7a_builds_and_boots.sh \ - tests/e2e/338_cortex_m_picolibc_sysroot.sh; do - echo "=== $t ===" - bash "$t" 2>&1 | tee "$(basename "$t").log" - rc=${PIPESTATUS[0]} - [ "$rc" = "0" ] || { echo "$t failed (exit $rc)"; exit 1; } - done - # The assertion this job exists for: both tests RAN. Each has an - # early `exit 0` for a missing capability, so a zero exit code alone - # does not distinguish "passed" from "skipped". - grep -q 'PASS: freestanding riscv64 build + run' \ - 130_freestanding_riscv_build_and_run.sh.log || { - echo "130 (engine chain) skipped on the runner that must run it"; exit 1; } - grep -q 'PASS: BSP supplies the sysroot' \ - 131_freestanding_bsp_supplies_everything.sh.log || { - echo "131 (ecosystem chain) skipped on the runner that must run it"; exit 1; } - grep -q 'PASS: bare-metal mcpp test names its failure' \ - 132_freestanding_test_and_artifacts.sh.log || { - echo "132 (test + artifacts) skipped on the runner that must run it"; exit 1; } - grep -q 'PASS: the freestanding std subset' \ - 133_freestanding_std_subset.sh.log || { - echo "133 (std subset) skipped on the runner that must run it"; exit 1; } - # 332 declares `# requires: qemu-arm`, which no sharded runner has - # — so on the shards it exits 0 without running. This job is the only - # place its PASS line can be demanded. - grep -q 'PASS: cortex-m rows build, boot' \ - 332_cortex_m_builds_and_boots.sh.log || { - echo "332 (cortex-m) skipped on the runner that must run it"; exit 1; } - # And a count, because four `grep -q` calls that each matched say - # nothing about how many rows the script actually booted: a fixture - # that stopped iterating would still print its PASS line. - booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true) - [ "$booted" = "4" ] || { - echo "332 booted $booted rows, expected 4"; exit 1; } - # 336 IS HERE FOR THE SAME REASON, AND ADDING IT TO THE LOOP ABOVE - # WITHOUT ADDING IT HERE WOULD HAVE BEEN THE SAME DEFECT: it declares - # `# requires: qemu-arm`, so on a shard it exits 0 having run nothing. - grep -q 'PASS: armv7-a rows build, boot' \ - 336_armv7a_builds_and_boots.sh.log || { - echo "336 (armv7-a) skipped on the runner that must run it"; exit 1; } - a32=$(grep -c 'booted on virt' 336_armv7a_builds_and_boots.sh.log || true) - [ "$a32" = "2" ] || { - echo "336 booted $a32 rows, expected 2"; exit 1; } - # 338 SKIPS UNTIL `xim:picolibc-arm` IS PUBLISHED, and a skip here - # is legitimate rather than a defect — the payload is a separate - # release. So its PASS line is NOT demanded; what IS demanded is that - # the script either passed or said why, which is what distinguishes a - # skip from a silent zero-exit. - grep -qE 'PASS: a Cortex-M project opts into picolibc|SKIP: picolibc-arm is not installed' \ - 338_cortex_m_picolibc_sysroot.sh.log || { - cat 338_cortex_m_picolibc_sysroot.sh.log - echo "338 neither passed nor reported why it did not run"; exit 1; } - - # ────────────────────────────────────────────────────────────────── - # Android: the rows no sharded runner can reach. - # - # `run_all.sh` adds the `android-ndk` capability only when the NDK payload - # is already installed, and no other job installs it, so every - # `# requires: android-ndk` test skipped on every CI runner while reporting - # green. This job installs the payload, runs those tests directly and demands - # each PASS line, then runs `mcpp test` on an API 34 emulator through - # `adb-run` (the configuration measured on mcpp#635), which is the only - # place a test program's loading on the device is observed. - # ────────────────────────────────────────────────────────────────── - android: - name: android e2e (NDK rows + x86_64 emulator) - runs-on: ubuntu-24.04 - timeout-minutes: 90 - env: - MCPP_HOME: /home/runner/.mcpp - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Install the NDK (xim:android-ndk) - timeout-minutes: 20 - run: | - XLINGS_HOME="$MCPP_HOME/registry" "$XLINGS_BIN" install xim:android-ndk@30.0.16248370 -y - # Present before the tests, so a missing payload fails this step - # rather than an assertion deep inside a test. - ls "$MCPP_HOME"/registry/data/xpkgs/xim-x-android-ndk/*/toolchains/llvm/prebuilt/*/bin/clang++ - - - name: Android e2e (no device) - timeout-minutes: 40 - run: | - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" toolchain install gcc 16.1.0 - "$MCPP" toolchain default gcc@16.1.0 - # Run directly, not through run_all.sh, so a skip cannot pass as a - # green: each script either prints its PASS line or this step fails. - declare -A pass=( - [652b_an_application_on_android_is_a_shared_library]='652b: kind = "app" on Android is a shared library OK' - [664_a_universal_apk_is_two_legs_in_one_tree]='664: the universal APK is the library route applied to an app OK' - [667_an_android_pack_stages_its_closure]='667: an Android pack stages its closure OK' - [675_android_test_programs_carry_their_cxx_runtime]='PASS: 675_android_test_programs_carry_their_cxx_runtime' - [680_a_dependency_floor_on_the_android_api_level]='PASS: 680_a_dependency_floor_on_the_android_api_level' - ) - for t in "${!pass[@]}"; do - echo "=== $t ===" - bash "tests/e2e/$t.sh" 2>&1 | tee "$t.log" - rc=${PIPESTATUS[0]} - [ "$rc" = "0" ] || { echo "$t failed (exit $rc)"; exit 1; } - grep -qF "${pass[$t]}" "$t.log" || { echo "$t did not print its PASS line"; exit 1; } - done - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Compile the device tests before the emulator starts - timeout-minutes: 20 - run: bash .github/tools/android_emulator_test.sh prebuild "$RUNNER_TEMP/droidtest" - - - name: mcpp test on an API 34 emulator - timeout-minutes: 30 - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 34 - arch: x86_64 - target: google_apis - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: bash .github/tools/android_emulator_test.sh run "$RUNNER_TEMP/droidtest" - - # ────────────────────────────────────────────────────────────────── - # Hermetic (no host toolchain): the ONLY environment class that - # faithfully reproduces issue #195. Standard runners ship gcc + - # libc6-dev, so a sandbox toolchain that leaks to the host's CRT - # still links "green" there; this container has no compiler and no - # host Scrt1.o, so any leak fails loudly. Builds PR code with the - # bootstrap mcpp, then runs the llvm flow end-to-end. - # ────────────────────────────────────────────────────────────────── - hermetic: - name: hermetic e2e (no host toolchain, container) - runs-on: ubuntu-24.04 - container: debian:stable-slim - timeout-minutes: 60 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - name: Install base utilities (NO compiler) - run: | - apt-get update -qq - apt-get install -y -qq curl ca-certificates git xz-utils unzip - # The whole point of this job: no host toolchain, no host CRT. - ! command -v gcc - ! command -v cc - test ! -e /usr/lib/x86_64-linux-gnu/Scrt1.o - test ! -e /usr/lib/gcc - - - uses: actions/checkout@v4 - - # Payload cache (downloads only — the container still has no host - # toolchain, which is the property under test). - - name: Cache mcpp sandbox payloads - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-hermetic-${{ hashFiles('mcpp.toml') }} - restore-keys: | - mcpp-hermetic- - - - name: Bootstrap xlings + released mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - MCPP_BOOT="$HOME/.xlings/subos/current/bin/mcpp" - "$MCPP_BOOT" --version - "$MCPP_BOOT" self config --mirror GLOBAL - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Build PR mcpp from source (sandbox gcc only) - run: | - "$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" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "issue #195 reproduction: manifest llvm toolchain, fresh" - run: | - cd "$(mktemp -d)" - "$MCPP" new hello195 - cd hello195 - printf '\n[toolchain]\nlinux = "llvm@22.1.8"\n' >> mcpp.toml - printf 'import std;\nint main() { std::println("hello {}", 195); return 0; }\n' > src/main.cpp - "$MCPP" run - - - name: Hermetic llvm e2e subset - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - export MCPP - bash tests/e2e/86_llvm_hermetic_link.sh - bash tests/e2e/37_llvm_import_std.sh - - # The scripts whose C++ runtime is a package (`llvm.libcxx`) declare - # `# requires: llvm`, which no shard has, and `run_all.sh` exits 0 when it - # skips. They run here, where llvm is installed, and each is held to the - # line it prints only when it ran to the end, and to the line of the step - # that runs a program (#641). - - name: "C++ runtime e2e that needs llvm (663, 690, 696, 700)" - run: | - set -o pipefail - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - export MCPP - run_and_assert() { # $1 = script; the remaining arguments are lines it must print - local script="$1"; shift - local log="$RUNNER_TEMP/$(basename "$script").log" - bash "$script" 2>&1 | tee "$log" - for line in "$@"; do - grep -qxF "$line" "$log" || { echo "::error::$script did not print: $line"; exit 1; } - done - } - run_and_assert tests/e2e/663_a_graph_libcxx_over_the_payloads_c_library.sh \ - "ok: llvm.libcxx supplies the C++ layer over the payload's C library, and the program runs" - run_and_assert tests/e2e/690_a_shared_library_over_a_graph_cxx_runtime.sh \ - 'ok: linkage = "static" builds and runs' \ - 'ok: cxx_runtime = { shared = "self-contained" } links a private copy and runs' \ - "PASS: 690 a shared library over a graph C++ runtime is refused or carries a stated private copy" - run_and_assert tests/e2e/696_a_cxx_layer_provider_keeps_its_own_standard.sh \ - "PASS: a C++-layer provider compiles its implementation units at its own standard" - # #646 F3a: the default llvm shape of a program over a C++ shared library - # aborted with std::bad_cast before its programs took the library's contract. - run_and_assert tests/e2e/700_a_program_over_a_cxx_shared_library_has_one_cxx_runtime.sh \ - "ok: a program over a C++ shared library runs on one C++ runtime" \ - "ok: a stated self-contained program over a coupled C++ shared library is refused" \ - "PASS: 700 a program over a C++ shared library has one C++ runtime" diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml deleted file mode 100644 index 05fb6e466..000000000 --- a/.github/workflows/ci-linux.yml +++ /dev/null @@ -1,517 +0,0 @@ -name: ci-linux - -# Self-host CI on Linux: mcpp builds mcpp. The bootstrap mcpp comes from -# `xlings install mcpp` (xim:mcpp in the xlings package index), so this -# workflow no longer depends on a previous-release tarball — the -# chicken-and-egg now lives upstream in the xlings index. -# -# SHAPE: four INDEPENDENT jobs, no `needs:` between them. Each restores the -# same cache lineage (see .github/actions/bootstrap-mcpp) and pays one warm -# `mcpp build` (~2.5 min) to get the PR's own binary, then does its own leg. -# That warm rebuild is far cheaper than serialising the legs behind a shared -# artifact would be: -# -# before: build → unit → gcc → musl → llvm → xlings ≈ 18 min (one job) -# after: max(build+unit, gcc, musl+llvm, xlings) ≈ 7-8 min -# -# The ~18 min e2e suite is a SEPARATE workflow (ci-linux-e2e.yml, sharded) -# that runs in parallel on the same caches. -# -# Paired workflows: ci-linux-e2e.yml, ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # MCPP_HOME pinned so the cache keys below restore into the same path - # mcpp resolves at runtime. - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - # Safe here: this workflow no longer runs the e2e suite, which is what - # asserts mcpp's default quiet output (tests 48/53). - MCPP_VERBOSE: "1" - -jobs: - build-test: - name: build + unit tests (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - # Before the bootstrap, not after: this needs no toolchain and no mcpp, - # takes under a second, and the drift it catches (a stale xlings pin) - # would otherwise surface minutes later as an unrelated-looking - # dependency-resolution failure. Pure text extraction on purpose — it - # has to work when the build is broken. - - name: Check version / xlings pin consistency - run: bash .github/tools/check_version_pins.sh - - # Same placement and the same reason as the pin check above: adding a - # package under `modules/` means editing four files, and three of those - # omissions fail later and elsewhere — the worst of them only on a macOS - # machine that has no mcpp to rebuild with. - - name: Check modules/ wiring - run: bash .github/tools/check_modules_wiring.sh - - # Same placement, same reason: pure text, no toolchain, under a second. - # - # This one is a HARD gate (unlike lint-ci-assertions.sh below) because it - # has no false positives left — its scope was cut to the two directories - # that walk trees mcpp does not control, and the one legitimate site - # carries a NARROW-OK rationale. See the script's header for why the - # scope is that narrow, and mcpp#516 for what it costs when it is missed. - # - # It runs on LINUX on purpose even though the bug it guards is - # Windows-only: it is text analysis, and putting it where the fast leg is - # means a violation is reported in seconds rather than after a Windows - # bootstrap. - - name: Check no walk-derived path is narrowed directly - run: bash .github/tools/check_narrow_conversions.sh - - # Same placement, same reason: pure text, no toolchain. - # - # IT PRINTS AND DOES NOT FAIL, DELIBERATELY. The three rules it carries - # each have real false positives — a control step legitimately pins one - # target, a `grep -q .` legitimately expects output — and the whole value - # of the thing is that somebody READS it. A hard gate would be routed - # around with a suppression within a month, and the suppression would then - # be the only record that a rule ever existed. - - name: Where the CI assertions live - run: bash tools/lint-ci-assertions.sh - - # Text-only, like the two steps around it, and it belongs here rather - # than in the target-matrix workflow: that workflow runs the matrix, and - # this asserts a property of the TABLE, which is readable without a - # single toolchain. - - name: The target matrix names every refusal - run: bash .github/tools/check_matrix_reasons.sh - - # Same placement and the same reason: pure text, no toolchain, and the - # drift it catches — a 简体中文 page that has silently fallen behind its - # English original — is invisible to every other job. - - name: Check documentation style and bilingual parity - run: bash .github/tools/check_docs_style.sh - - # The architecture half: which tree a document lives in, whether every - # path it names resolves, and whether each specification is reachable - # from all three indexes. Same placement and the same reason -- it needs - # no toolchain, and what it catches is invisible to every other job. - - name: Check documentation structure - run: bash .github/tools/check_docs_structure.sh - - # Every documented tier agrees with kKnownTargets. - # - # Four documents restate a row's tier and nothing compared them to - # the table. When wasm32-emscripten became `verified` and the - # Android rows gained tiers, docs/21 was updated and both READMEs - # were not -- so the front page said three targets were `planned` - # while the engine had built and run two of them. On its first run - # this check also found two Cortex-A rows missing from docs/21 - # entirely, which predates that work. - - name: Documented target tiers agree with the table - run: python3 .github/tools/check_target_tiers.py - - - uses: ./.github/actions/bootstrap-mcpp - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Set GLOBAL mirror via xlings directly (bootstrap mcpp may lack --mirror flag) - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp for test (it has --mirror support) - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - echo "MCPP_FRESH=$MCPP_FRESH" >> "$GITHUB_ENV" - "$MCPP_FRESH" self config --mirror GLOBAL - "$MCPP_FRESH" test - - # Each package under `modules/` carries its own tests, and they are built - # as that package ALONE -- a configuration the root suite never produces, - # since there every module is linked together. A subsystem that has - # quietly come to depend on something it does not declare compiles in the - # root build and fails here, which is the whole reason to run both. - # - # Every member is run, and the loop is derived from the manifest rather - # than written out: a list maintained by hand is a list that stops - # matching, and `check_modules_wiring.sh` cannot see this file. - - name: Per-subsystem tests (`mcpp test -p `) - run: | - set -euo pipefail - # `$MCPP_FRESH` comes from the step above, which takes the NEWEST - # mcpp anywhere under target/. That is correct in this job because - # nothing between the two steps writes another one — but `mcpp pack` - # leaves a copy under `target/dist/`, and a pack step inserted above - # would silently redirect both this loop and the suite before it to - # the packaged binary. Observed locally; kept as a note because the - # expression is shared with three other jobs and this is not the - # place to change it. - case "$MCPP_FRESH" in - */target/dist/*) echo "MCPP_FRESH points into target/dist — a pack step now runs before the tests"; exit 1 ;; - esac - members=$(sed -n '/^\[workspace\]/,/^\[/p' mcpp.toml \ - | grep -oE '"modules/[a-z0-9-]+"' | tr -d '"' | sed 's|modules/||') - [ -n "$members" ] || { echo "no workspace members found in mcpp.toml"; exit 1; } - echo "members: $(echo $members | tr '\n' ' ')" - for m in $members; do - echo "::group::mcpp test -p $m" - "$MCPP_FRESH" test -p "$m" - echo "::endgroup::" - done - - # A cold, from-scratch self-host build with the manifest-pinned GCC: the - # property `build-test` cannot cover, because it builds incrementally on a - # restored target/. `mcpp test` is deliberately NOT repeated here — it would - # be the same suite, same toolchain, same driver binary as `build-test`, - # differing only in incremental state. - toolchain-gcc: - name: "toolchain: gcc (cold self-host)" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Toolchain: GCC — cold rebuild with the PR binary" - run: | - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0" build.log - - # The two cheap legs share one runner: each is ~1 min after the warm build, - # so a runner apiece would cost more in setup than it saves in wall-clock. - toolchain-cross: - name: "toolchain: musl + llvm" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - # Auto-installs gcc@16.1.0-musl on demand (cached across runs). - - name: "Toolchain: musl-gcc — build mcpp (--target)" - run: | - "$MCPP" clean - "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0 → x86_64-linux-musl" build.log - - - name: "Toolchain: LLVM — build mcpp" - run: | - "$MCPP" toolchain install llvm 20.1.7 - # Override project toolchain to use LLVM for this build - sed -i 's/^default = "gcc@16.1.0"/default = "llvm@20.1.7"/' mcpp.toml - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log - # Restore - sed -i 's/^default = "llvm@20.1.7"/default = "gcc@16.1.0"/' mcpp.toml - - # Integration: the mcpp built from THIS PR's source builds & runs a real - # external C++ project — xlings (openxlings/xlings ships its own mcpp.toml). - # MCPP_VENDORED_XLINGS only supplies the xlings package backend that mcpp - # resolves deps through. - integration-xlings: - name: "integration: mcpp builds & runs xlings" - runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \ - --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run - - # THE CURRICULUM WAS NEVER BUILT BY ANY JOB. e2e 616 checks that the examples - # and the table that documents them agree, and says so explicitly: it builds - # nothing. Every example in this repository could therefore stop compiling and - # the only signal would be a user reporting it. - # - # The list is DERIVED FROM THE TREE, not written here. An example that is in - # neither the build list nor the skip table fails this job, so adding one - # forces a decision about whether CI can build it rather than silently - # leaving it uncovered. - examples: - name: "examples: the curriculum builds" - runs-on: ubuntu-24.04 - # Generous rather than tight: six cold example builds plus the Vulkan one, - # which provisions the shader compiler and a software driver as payloads. - timeout-minutes: 90 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP_FRESH" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: Build every example the runner can build - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - bash .github/tools/build_examples.sh - - # TWO CRITERIA A BUILD CANNOT SHOW, from the examples whose READMEs state - # them. Both are the shape this repository has paid for before: a green - # build over a result nobody compared. - - name: "examples: the feature criterion and the device language" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - set -e - - # 11-features. NOT "the default build works" -- that passes while the - # optional package is resolved and merely unused. The criterion is - # that the default build's RESOLUTION does not name it. - cd examples/11-features/greeter - "$MCPP" build 2>&1 | tee /tmp/f-default.log - "$MCPP" build --features metrics 2>&1 | tee /tmp/f-metrics.log - grep -qi counters /tmp/f-default.log && { - echo "FAIL: the default build resolved 'counters'"; exit 1; } - grep -qi counters /tmp/f-metrics.log || { - echo "FAIL: --features metrics did not resolve 'counters'"; exit 1; } - echo "ok: the optional package is absent without its feature" - cd - >/dev/null - - # 12-a-new-device-language. Three criteria, and a probe run exits - # non-zero on purpose -- the program returns 1 when the answer is not - # 42 -- so those runs are guarded with `|| true` and judged by their - # output rather than their status. - cd examples/12-a-new-device-language/app - "$MCPP" run | tee /tmp/toy.log - grep -q 'answer() = 42' /tmp/toy.log || { - echo "FAIL: the .toy did not reach the link"; exit 1; } - # THE LANGUAGE IS EXECUTED, NOT PATTERN-MATCHED: 21 is what the - # emitted `while` loop computes, and no constant in the tree holds it. - grep -q 'gcd(1071, 462) = 21' /tmp/toy.log || { - echo "FAIL: the emitted loop did not run"; exit 1; } - - # Editing the .toy reaches the artifact. - sed -i 's/scale(gcd(1071, 462), 2)/scale(gcd(1071, 462), 3)/' src/kernels/answer.toy - "$MCPP" run > /tmp/toy2.log 2>&1 || true - sed -i 's/scale(gcd(1071, 462), 3)/scale(gcd(1071, 462), 2)/' src/kernels/answer.toy - grep -q 'answer() = 63' /tmp/toy2.log || { - cat /tmp/toy2.log; echo "FAIL: editing the .toy did not reach the artifact"; exit 1; } - - # THE COMPILER IS A DECLARED INPUT OF THE ACTION -- the half that - # fails silently, and the one criterion here that has to be built to - # isolate it. Bumping the tool's version is NOT that criterion: the - # tool's path is on the action's command line, so a new path re-runs - # the edge whether or not it is also declared as an input. Measured: - # with `a.input(compiler)` removed, a version bump still reached the - # artifact, and this step passed. - # - # The isolating change is different BYTES AT THE SAME PATH. Build a - # compiler that behaves differently, overwrite the cached binary in - # place, and the command line is byte-identical. - # - # BOTH DIRECTIONS ARE THE CRITERION, not either one. Falsified by - # removing `a.input(compiler)`: the artifact followed the first - # overwrite anyway and stopped following the restore, so a check that - # asserted only the first direction would have passed on a rule that - # tracks nothing. - # - # `n.value * 2` keeps `0` at `0`: a probe that changed every literal - # would turn `while (b != 0)` into a loop that divides by zero, and - # this step would report a crash rather than an answer. - # THE PROBE COMPILER IS BUILT FROM A COPY OF THE TREE. The store's - # key carries a stamp of the tool's tree (#630, item 6), so touching - # `../toyc` would move the key and the next run would rebuild the - # tool from the restored source -- the isolating change here is - # different bytes at the SAME path, which needs the tree untouched. - probe_tree=/tmp/toyc-probe - rm -rf "$probe_tree"; cp -r ../toyc "$probe_tree"; rm -rf "$probe_tree/target" - probe_on() { sed -i 's/return std::format("{}", n.value);/return std::format("{}", n.value * 2);/' "$1/src/compile.cppm"; } - probe_off() { sed -i 's/return std::format("{}", n.value \* 2);/return std::format("{}", n.value);/' "$1/src/compile.cppm"; } - install_toyc() { # $1 = the tree to build, $2 = the store path to overwrite - ( cd "$1" && "$MCPP" build >/dev/null ) - cp "$(find "$1/target" -name toyc -type f -perm -u+x | head -1)" "$2" - } - - # `mcpp cache dir` prints a legacy-directory note on a second line. - # The newest entry, since a tree edited earlier in this job may have - # left another one. - store="$("$MCPP" cache dir | head -1)/tool" - cached="$(ls -t $(find "$store" -path '*toyc@0.1.0*/bin/toyc') 2>/dev/null | head -1)" - [ -n "$cached" ] || { - echo "FAIL: no toyc in the tool store under $store"; exit 1; } - - probe_on "$probe_tree"; install_toyc "$probe_tree" "$cached"; probe_off "$probe_tree" - "$MCPP" run > /tmp/toy3.log 2>&1 || true - grep -q 'answer() = 168' /tmp/toy3.log || { - cat /tmp/toy3.log - echo "FAIL: a changed compiler binary did not reach the artifact." - echo " rules-toy must declare the compiler among the action's inputs." - exit 1; } - install_toyc "$probe_tree" "$cached" - "$MCPP" run > /tmp/toy4.log 2>&1 || true - grep -q 'answer() = 42' /tmp/toy4.log || { - cat /tmp/toy4.log - echo "FAIL: restoring the compiler binary did not reach the artifact --" - echo " the action is not tracking the compiler's bytes." - echo " rules-toy must declare the compiler among the action's inputs." - exit 1; } - - # THE STORE'S KEY HOLDS THE SOURCE (#630, item 6), which the - # example's README and docs/30 both state: editing the compiler's - # sources at the same version rebuilds the tool, and so does the - # reversal. Both directions, because a key that only ever grew - # would pass the first and fail the second. - sleep 1; probe_on ../toyc - "$MCPP" run > /tmp/toy5.log 2>&1 || true - grep -q 'answer() = 168' /tmp/toy5.log || { - cat /tmp/toy5.log - echo "FAIL: editing the compiler's sources at the same version did not reach the artifact." - echo " The tool store's key must carry the path package's tree stamp." - exit 1; } - sleep 1; probe_off ../toyc - "$MCPP" run > /tmp/toy5b.log 2>&1 || true - grep -q 'answer() = 42' /tmp/toy5b.log || { - cat /tmp/toy5b.log - echo "FAIL: reverting the compiler's sources did not reach the artifact." - exit 1; } - - # ... and a version bump still rebuilds it, as it always did. - sleep 1; probe_on ../toyc - sed -i 's/^version = "0.1.0"/version = "0.1.1"/' ../toyc/mcpp.toml - "$MCPP" run > /tmp/toy6.log 2>&1 || true - sed -i 's/^version = "0.1.1"/version = "0.1.0"/' ../toyc/mcpp.toml - probe_off ../toyc - grep -q 'answer() = 168' /tmp/toy6.log || { - cat /tmp/toy6.log; echo "FAIL: bumping the tool version did not rebuild it"; exit 1; } - echo "ok: the compiler is a declared input, and the store is keyed on the tool's source" - - - name: "Graphics example: render offscreen on lavapipe and assert the pixels" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cd examples/10-graphics/offscreen - "$MCPP" build - # THE SHADERS ARE THE FIRST ASSERTION, and both of them: a rule that - # compiled only the first source would leave the second header absent - # and everything after this would still pass. - for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" - test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } - # Either file: which of the two carries the words is a property of - # the shader compiler the rule chose, not of the shader. See the - # cross-platform jobs, where that choice differs. - # ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b` - # does not exist -- even on a match in `a`, and even with `-s`, - # which suppresses the message and not the status. Written as one - # grep over both names, this criterion fails whenever the route - # that produces only a header is taken, which is a failure about - # the criterion and not about the shader. - found="" - for g in "$d/$f.h" "$d/$f.inc"; do - [ -f "$g" ] && grep -q '0x07230203' "$g" && found=1 - done - [ -n "$found" ] \ - || { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; } - done - icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mesa-lavapipe" \ - "$HOME/.xlings/data/xpkgs/xim-x-mesa-lavapipe" \ - -name 'lvp_icd.x86_64.json' -print -quit 2>/dev/null || true) - [ -n "$icd" ] || { echo "no lavapipe ICD in either store"; exit 1; } - out=$(VK_DRIVER_FILES="$icd" "$MCPP" run 2>&1) || { echo "$out"; exit 1; } - echo "$out" - # The program asserts the corners and the centre itself and exits - # non-zero on either. What CI adds is that the run reached the DEVICE: - # the software rasteriser produces the same pixels by construction, so - # the image cannot distinguish them and the device name is what does. - echo "$out" | grep -q 'llvmpipe' \ - || { echo "the run did not reach the lavapipe device"; exit 1; } - echo "$out" | grep -qE 'centre pixel: \([0-9]+, [0-9]+, [0-9]+, 255\)' \ - || { echo "no centre pixel was reported"; exit 1; } - # THE REVERSE LEG, AND IT COMPARES THE PIXELS RATHER THAN JUST - # RUNNING. The claim this example makes is that the image is a - # contract two independent rasterisers satisfy, not a property of one - # device -- so the criterion is that the two legs report the SAME - # centre pixel while reporting different devices. Measured: both give - # (124, 70, 62, 255), byte for byte. - gpu_px=$(echo "$out" | grep -m1 '^centre pixel:') - "$MCPP" build --no-accel - "$MCPP" run --no-accel | tee cpu.log - grep -q 'cpu rasteriser' cpu.log || { echo "the CPU leg did not run"; exit 1; } - cpu_px=$(grep -m1 '^centre pixel:' cpu.log) - [ -n "$gpu_px" ] && [ "$gpu_px" = "$cpu_px" ] || { - echo "the two legs disagree about the image:" - echo " device: $gpu_px" - echo " cpu: $cpu_px" - exit 1; } - echo "ok: both legs agree on $cpu_px" - - - name: "Vulkan example: build the device half and run it on lavapipe" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cd examples/09-heterogeneous/vulkan/app - "$MCPP" build - # BOTH STORES, because which one holds the payload is decided by how - # this mcpp resolved its home -- and a `find` over only one of them - # is the defect this repository has already paid for twice (e2e 614, - # llama.cpp-m's Vulkan job). Ask for the file, not for a layout. - icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mesa-lavapipe" \ - "$HOME/.xlings/data/xpkgs/xim-x-mesa-lavapipe" \ - -name 'lvp_icd.x86_64.json' -print -quit 2>/dev/null || true) - [ -n "$icd" ] || { echo "no lavapipe ICD in either store"; exit 1; } - echo "using ICD $icd" - out=$(VK_DRIVER_FILES="$icd" "$MCPP" run 2>&1) || { echo "$out"; exit 1; } - echo "$out" - # The device leg is the point: a run that silently fell back to the - # CPU variant would print the same numbers, so the assertion is that - # the program says which device answered AND that it is the payload's. - echo "$out" | grep -q 'llvmpipe' \ - || { echo "the run did not reach the lavapipe device"; exit 1; } - echo "$out" | grep -q '12 24 36 48' \ - || { echo "the device produced the wrong result"; exit 1; } diff --git a/.github/workflows/ci-macos-e2e.yml b/.github/workflows/ci-macos-e2e.yml deleted file mode 100644 index 80a0e0a4d..000000000 --- a/.github/workflows/ci-macos-e2e.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: ci-macos-e2e - -# The e2e suite on macOS ARM64, split out of ci-macos.yml (where it was 3.4 of -# the job's 8.4 min) so it runs in parallel with the toolchain/integration -# work — same shape as ci-linux-e2e.yml / ci-windows-e2e.yml. -# -# Not sharded: the macOS runners finish the suite in ~3.5 min, which is under -# this workflow's setup+build floor, so a second runner would buy nothing. -# -# Paired workflows: ci-macos.yml, ci-linux-e2e.yml, ci-windows-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e suite (macOS ARM64, self-host) - runs-on: macos-15 - timeout-minutes: 60 - # NOTE: no MCPP_VERBOSE — the e2e suite asserts mcpp's default quiet - # output (tests 48/53). - steps: - # `submodules: recursive` so tests/e2e/233_bench_matrix.sh can check that - # each `hub`/`body` in bench/matrix.json exists in the tree it names -- - # the check reads "submodule not initialised" without them and reports - # nothing, which is how a stale hub path survived (#599). Under 10 MB of - # source across the three pins, and nothing here builds them. - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: ./.github/actions/setup-macos-llvm - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - - - name: E2E suite - # Fail-fast on hung tests instead of burning the whole job budget. - # Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - # The RELEASED mcpp this job bootstrapped from, kept for e2e 252: the - # claim that an older client can still build against a package the PR - # produces is only worth making if something checks it against a real - # old binary. Captured before $MCPP is repointed at the fresh build. - export MCPP_BOOT="$MCPP" - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # macOS default toolchain is LLVM - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - set -o pipefail - bash tests/e2e/run_all.sh 2>&1 | tee "$RUNNER_TEMP/e2e-suite.log" - - # Measurement legs print READING lines and pass whatever they read; the - # readings are what a decision is taken from (#646 F2: whether a C++ - # exception thrown in a dylib is caught by its class under the payload's - # default runtime), so they are collected where a reader finds them. - - name: Measurement readings - if: always() - shell: bash - run: | - { - echo "### Measurement readings (macOS)" - echo '```' - grep -h '^READING' "$RUNNER_TEMP/e2e-suite.log" 2>/dev/null || echo "(none)" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - - # #647 E3: an Android row links on a macOS host. Its own step, because the - # NDK is a large download the suite's per-test bound does not allow for, - # and the shard does not carry it (the script declares android-ndk). - - name: "Android row on a macOS host (721)" - timeout-minutes: 30 - shell: bash - run: | - set -o pipefail - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" toolchain install android-ndk@30.0.16248370 - bash tests/e2e/721_*.sh 2>&1 | tee "$RUNNER_TEMP/721.log" - grep -q '^PASS: 721' "$RUNNER_TEMP/721.log" diff --git a/.github/workflows/ci-macos-ios.yml b/.github/workflows/ci-macos-ios.yml deleted file mode 100644 index 5d32e7d18..000000000 --- a/.github/workflows/ci-macos-ios.yml +++ /dev/null @@ -1,388 +0,0 @@ -name: ci-macos-ios - -# The iOS rows, measured on the only machine that can answer for them. -# -# iOS needs an ecosystem compiler and a LOCATED SDK: `xim:llvm` emits arm64 -# Mach-O for an iOS deployment target, and only the machine's Xcode can supply -# the iPhoneOS / iPhoneSimulator headers and stub libraries, which are not -# redistributable. The simulator runtime is the same category. So every claim -# about these three rows is a claim about a macOS runner, and this job is where -# they are made. -# -# Kept out of ci-macos.yml deliberately: that job asserts mcpp's default quiet -# output shape and tacking a differently-shaped leg onto it has broken that -# assertion before. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-macos-ios-${{ github.ref }} - cancel-in-progress: true - -jobs: - # THE PREMISES, AS A PROBE THAT RUNS ON REQUEST. - # - # This job measured what the iOS rows were scheduled on: that a GitHub macOS - # runner ships both SDKs and a bootable simulator, that `simctl spawn` takes a - # bare Mach-O, that the payload's `clang++.cfg` names the macOS SDK, and - # which C++ runtime an iOS link can use. Every answer is now encoded -- in - # `simctl-run`, in `--no-default-config` on the Apple cross path, and in the - # MachO contract cell -- and asserted by `ios-engine` below. - # - # Every step continues on error, because a probe's value is the complete set - # of answers. That is also why it does not run on every change: a job that - # cannot fail shown as a green check beside the gate reads as a second gate. - # It stays available for the day a runner image changes one of the premises. - ios-host-surface: - name: iOS - what this runner actually provides - if: github.event_name == 'workflow_dispatch' - runs-on: macos-15 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-macos-llvm - - - name: "Host surface: the two located SDKs and the simulator runtime" - continue-on-error: true - run: | - set -x - xcode-select -p - xcrun --sdk iphoneos --show-sdk-path - xcrun --sdk iphoneos --show-sdk-version - xcrun --sdk iphonesimulator --show-sdk-path - xcrun --sdk iphonesimulator --show-sdk-version - set +x - echo "--- runtimes ---" - xcrun simctl list runtimes - echo "--- devices available ---" - xcrun simctl list devices available - - # THE PAYLOAD'S CLANG READS A CONFIG FILE, AND THAT CONFIG NAMES A - # DIFFERENT SDK. Measured with `-isysroot ` on - # the command line and the cfg not suppressed: - # - # ld64.lld: error: /Library/Developer/CommandLineTools/SDKs/ - # MacOSX.sdk/usr/lib/libc++.tbd(/usr/lib/libc++.1.dylib) is - # incompatible with arm64 (iOS Simulator18.0.0) - # - # which is why mcpp's Apple cross path carries `--no-default-config`. - - name: "The payload's default config, which is why --no-default-config" - continue-on-error: true - run: | - set -x - ls -la "$LLVM_ROOT/bin/"*.cfg || true - for f in "$LLVM_ROOT/bin/"*.cfg; do echo "=== $f"; cat "$f"; done || true - - # WHETHER A BARE MACH-O CAN BE RUN AT ALL, which decided how much the - # runner program has to do: `simctl launch` needs an installed .app, - # `simctl spawn` takes an executable. - - name: "Device: is one bootable, and does spawn take a bare executable" - continue-on-error: true - run: | - set -uo pipefail - UDID=$(xcrun simctl list devices available \ - | grep -A50 -- '-- iOS' \ - | grep -m1 -oE '[0-9A-F]{8}-[0-9A-F-]{27}' || true) - echo "udid=[$UDID]" - if [ -z "$UDID" ]; then - echo "NO-IOS-SIMULATOR-DEVICE" - exit 0 - fi - xcrun simctl boot "$UDID" || true - xcrun simctl bootstatus "$UDID" -b 2>&1 | tail -3 || true - cat > /tmp/hello.cpp << 'CPP' - #include - int main() { std::puts("1-2-3"); return 0; } - CPP - SDK=$(xcrun --sdk iphonesimulator --show-sdk-path) - if ! "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ - -target arm64-apple-ios18.0-simulator \ - -isysroot "$SDK" -o /tmp/hello /tmp/hello.cpp; then - echo "SIM-COMPILE-FAILED" - exit 0 - fi - file /tmp/hello - otool -l /tmp/hello | grep -A5 LC_BUILD_VERSION || true - echo "--- simctl spawn on a bare Mach-O ---" - if xcrun simctl spawn "$UDID" /tmp/hello; then - echo "SPAWN-OK" - else - echo "SPAWN-FAILED exit=$?" - fi - - # THE C++ RUNTIME QUESTION. macOS links the PAYLOAD's static libc++ so - # that mcpp's deployment floor is real; that archive is built for macOS, - # and ld64 refuses an object built for one platform in a link for - # another. Both routes are tried because "which one works" is the fact - # the contract table needed. - - name: "C++ runtime: SDK libc++ versus the payload static archive" - continue-on-error: true - run: | - set -x - SDK=$(xcrun --sdk iphoneos --show-sdk-path) - cat > /tmp/cxx.cpp << 'CPP' - #include - #include - int main() { std::string s = "1-2-3"; std::puts(s.c_str()); return 0; } - CPP - echo "--- (a) SDK libc++, dynamic ---" - if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ - -target arm64-apple-ios18.0 \ - -isysroot "$SDK" -o /tmp/cxx-sdk /tmp/cxx.cpp; then - otool -L /tmp/cxx-sdk - echo "SDK-LIBCXX-OK" - else - echo "SDK-LIBCXX-FAILED" - fi - echo "--- (b) payload static libc++ ---" - if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ - -target arm64-apple-ios18.0 \ - -isysroot "$SDK" -nostdlib++ \ - "$LLVM_ROOT/lib/libc++.a" "$LLVM_ROOT/lib/libc++abi.a" \ - -o /tmp/cxx-static /tmp/cxx.cpp; then - echo "PAYLOAD-STATIC-OK" - else - echo "PAYLOAD-STATIC-FAILED" - fi - set +x - - # THE GATE. Every step here fails the job when its claim does not hold. - # - # The first version of this job was a probe too -- every step continued on - # error -- and it stayed that way after the rows it measured moved to - # `verified` and `preview`. That left a verified tier with no check that - # could turn red: a regression in the Apple cross path would have printed - # `RUN-THROUGH-RUNNER-FAILED` inside a green job. - ios-engine: - name: iOS - mcpp builds and the simulator runs it - runs-on: macos-15 - timeout-minutes: 40 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: ./.github/actions/setup-macos-llvm - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - echo "MCPP_DEV=$(ls -td "$PWD"/target/*/*/bin/mcpp | head -1)" >> "$GITHUB_ENV" - - # NO TOOLCHAIN IS DECLARED, AND THAT IS WHAT MAKES THIS THE USER'S PATH. - # - # While the rows were `planned` the fixture had to write - # `[target.] toolchain = "llvm@22.1.8"` to get past the tier gate, - # which meant it measured an override and never the row's own pin. The - # rows are now `verified` and `preview` and resolve `llvm@22.1.8` by - # themselves, so the fixture says nothing and the default is what is - # measured. - - name: "Fixture: a project that imports std and prints 1-2-3" - run: | - set -euo pipefail - mkdir -p /tmp/iostest/src - cat > /tmp/iostest/mcpp.toml << 'TOML' - [package] - name = "iostest" - version = "0.1.0" - - [build] - ios_deployment_target = "18.0" - - # THE C++ STANDARD LIBRARY AND THE COMPILER RUNTIME ARE PACKAGES ON - # THESE ROWS (#630). The payload's static libc++ is a macOS object - # and its resource directory carries no iOS builtins archive, so the - # engine used to link the SDK's libc++ under the payload's newer - # headers -- which fails at link on the first inline path the older - # dylib does not export. `llvm.libcxx` brings headers, module and - # objects as one release; `llvm.compiler-rt-builtins` brings - # `__isPlatformVersionAtLeast`. Declared by git until the index - # carries them. - [target.'cfg(os = "ios")'.dependencies] - llvm.libcxx = { git = "https://github.com/mcpplibs/libcxx.git", tag = "22.1.8.1" } - llvm.compiler-rt-builtins = { git = "https://github.com/mcpplibs/compiler-rt-builtins.git", tag = "22.1.8.5" } - - # THE RUNNER IS AN ARGV PREFIX AND THE SESSION BELONGS TO A - # PACKAGE. `simctl-run` comes from `xim:apple-simulator-tools`; it - # chooses a device, boots it if it is not booted, waits, spawns, and - # returns the program's own exit status. The device row keeps - # `runner` unset: an artefact cannot be run off an iOS device without - # a signature the developer owns. - [target.aarch64-ios-sim] - runner = ["simctl-run"] - - # Declared on the row that runs it, as examples/13 does. A target - # section's `xlings.workspace` is installed only when that target is - # built, so the step below that runs the simulator artefact through - # `simctl-run` is also the measurement that this declaration works. - [target.aarch64-ios-sim.xlings.workspace] - "xim:apple-simulator-tools" = "" - TOML - cat > /tmp/iostest/src/main.cpp << 'CPP' - import std; - // The two inline paths that failed at link under the payload's - // headers over the SDK's libc++ (`__hash_memory`, - // `__atomic_notify_all_global_table`), and an availability check, - // which is `__isPlatformVersionAtLeast` from the builtins package. - int main() { - std::unordered_map m; - m["three"] = 3; - std::atomic a{1}; - a.notify_all(); - int two = 2; - if (__builtin_available(iOS 17, *)) two = 2; - std::vector v{m["three"], a.load(), two}; - std::ranges::sort(v); - std::print("{}-{}-{}\n", v[0], v[1], v[2]); - } - CPP - cat /tmp/iostest/mcpp.toml - - # ONE ASSERTION FOR THE THREE ARTEFACTS. A build that succeeds cannot - # tell an iOS binary from a macOS one; LC_BUILD_VERSION can. Each - # value is read from the load command and compared whole, so an empty - # reading is a failure and not a pass. - cat > /tmp/assert-artefact.sh << 'SH' - #!/usr/bin/env bash - set -uo pipefail - target=$1 arch=$2 platform=$3 minos=$4 - arts=(/tmp/iostest/target/"$target"/*/bin/iostest) - art=${arts[0]} - if [ ! -f "$art" ]; then - echo "FAIL: $target produced no artefact under /tmp/iostest/target/$target" - exit 1 - fi - desc=$(file "$art") - lc=$(otool -l "$art") - echo "$desc" - grep -A5 LC_BUILD_VERSION <<<"$lc" || true - got_platform=$(awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="platform"{print $2; exit}' <<<"$lc") - got_minos=$(awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="minos"{print $2; exit}' <<<"$lc") - fail=0 - if ! grep -qE "Mach-O 64-bit executable ${arch}\$" <<<"$desc"; then - echo "FAIL: $target is not a Mach-O $arch executable"; fail=1 - fi - if [ "$got_platform" != "$platform" ]; then - echo "FAIL: $target LC_BUILD_VERSION platform is '$got_platform', expected $platform"; fail=1 - fi - if [ "$got_minos" != "$minos" ]; then - echo "FAIL: $target minos is '$got_minos', expected $minos from ios_deployment_target"; fail=1 - fi - [ "$fail" = 0 ] && echo "ok: $target is Mach-O $arch, platform $platform, minos $minos" - exit "$fail" - SH - chmod +x /tmp/assert-artefact.sh - - # THE DEVICE ROW. Nothing runs it -- that needs a signature the developer - # owns -- so the claim is the artefact: `platform 2` is IOS. The refusal - # for a machine WITHOUT the SDK is asserted by tests/e2e/641 on every - # non-Apple host, which is the only place the SDK is genuinely absent: - # pointing `DEVELOPER_DIR` at nothing here measured nothing, because - # `xcrun` falls back to the recorded developer directory. - - name: "aarch64-ios: the artefact names the iOS platform" - run: | - set -euo pipefail - cd /tmp/iostest - "$MCPP_DEV" build --target aarch64-ios - /tmp/assert-artefact.sh aarch64-ios arm64 2 18.0 - - # `platform 7` is IOSSIMULATOR. The number is what separates this row from - # the device row; the architecture does not. - - name: "aarch64-ios-sim: the artefact names the simulator platform" - run: | - set -euo pipefail - cd /tmp/iostest - "$MCPP_DEV" build --target aarch64-ios-sim 2>&1 | tee build-sim.log - /tmp/assert-artefact.sh aarch64-ios-sim arm64 7 18.0 - - # THE TWO LAYERS THE PACKAGES SUPPLY, READ FROM THE REPORT AND FROM THE - # ARTEFACT. A build that succeeds cannot tell a self-contained libc++ - # from the SDK's; the load commands can, and the report says which - # package answered for each layer. - - name: "aarch64-ios-sim: the C++ runtime and the builtins are the graph's" - run: | - set -euo pipefail - cd /tmp/iostest - grep -E 'c\+\+-abi +libc\+\+ +\(libcxx@22\.1\.8\.1, graph\)' build-sim.log \ - || { echo "FAIL: the report does not name llvm.libcxx as the C++ layer"; exit 1; } - grep -E 'compiler-runtime +compiler-rt +\(compiler-rt-builtins@22\.1\.8\.5, graph\)' build-sim.log \ - || { echo "FAIL: the report does not name llvm.compiler-rt-builtins as the compiler runtime"; exit 1; } - art=$(ls /tmp/iostest/target/aarch64-ios-sim/*/bin/iostest | head -1) - if otool -L "$art" | grep -q 'libc++'; then - echo "FAIL: the artefact links a libc++ dylib"; otool -L "$art"; exit 1 - fi - echo "ok: no libc++ dylib in the load commands; both layers are the graph's" - - # THE NEGATIVE DIRECTION: without the packages a program that imports - # std still builds, as it did before this release, and the degradation - # names the two lines; one that does not import std takes the SDK's - # headers under the SDK's libc++. Without this step the change could be - # read as "every iOS build now needs a package". - - name: "aarch64-ios-sim: without the packages, import std builds with the hazard named and plain C++ takes the SDK's headers" - run: | - set -euo pipefail - rm -rf /tmp/iosplain && mkdir -p /tmp/iosplain/src && cd /tmp/iosplain - # NO FLOOR STATED: the row takes the located SDK's version, read - # through xcrun, and the artefact's LC_BUILD_VERSION minos says so. - cat > mcpp.toml << 'TOML' - [package] - name = "iosplain" - version = "0.1.0" - TOML - printf 'import std;\nint main() { std::print("x\\n"); }\n' > src/main.cpp - "$MCPP_DEV" build --target aarch64-ios-sim > mixed.log 2>&1 || { echo "FAIL: import std without llvm.libcxx no longer builds"; cat mixed.log; exit 1; } - # A degradation renders its `what` text, not its domain, so the - # sentence is what a log can be asked for. - grep -q "links the SDK's libc++ under the toolchain payload's libc++ headers" mixed.log || { echo "FAIL: no degradation named the mixed libc++"; cat mixed.log; exit 1; } - grep -q 'llvm.libcxx' mixed.log || { echo "FAIL: the degradation does not name llvm.libcxx"; cat mixed.log; exit 1; } - printf '#include \n#include \nint main() { std::string s = "1-2-3"; std::puts(s.c_str()); }\n' > src/main.cpp - rm -rf target - "$MCPP_DEV" build --target aarch64-ios-sim 2>&1 | tee plain.log - grep -q 'carries no compiler runtime for aarch64-ios-sim' plain.log || { echo "FAIL: no degradation named the missing compiler runtime"; exit 1; } - grep -q "links the SDK's libc++ under" plain.log && { echo "FAIL: a program without import std was reported as mixing libc++"; exit 1; } - ninja=$(ls target/aarch64-ios-sim/*/build.ninja | head -1) - grep -q -- '-isystem[^ ]*iPhoneSimulator[^ ]*/usr/include/c++/v1' "$ninja" || { echo "FAIL: the plain program does not take the SDK's C++ headers"; grep -o -- '-isystem[^ ]*c++/v1' "$ninja" | sort -u; exit 1; } - grep -q -- '-isystem[^ ]*xim-x-llvm[^ ]*/c++/v1' "$ninja" && { echo "FAIL: the plain program still takes the payload's C++ headers"; exit 1; } - art=$(ls target/aarch64-ios-sim/*/bin/iosplain | head -1) - otool -L "$art" | grep -q '/usr/lib/libc++.1.dylib' || { echo "FAIL: the plain program does not link the SDK's libc++"; otool -L "$art"; exit 1; } - sdkver=$(xcrun --sdk iphonesimulator --show-sdk-version) - minos=$(otool -l "$art" | awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="minos"{print $2; exit}') - [ "$minos" = "$sdkver" ] || { echo "FAIL: with no floor stated, minos is '$minos' and the located SDK is $sdkver"; exit 1; } - echo "ok: an unset floor is the located SDK's version ($sdkver)" - echo "ok: import std builds with the hazard named; plain C++ takes the SDK's headers and libc++; the builtins degradation is printed" - - # THE SUPPORTED PATH, which is what the `verified` tier claims: a runner - # the manifest declares and a program a package provides. The program's - # own line is compared whole. An iOS-simulator Mach-O does not execute on - # the macOS host directly, so the line appearing at all means the - # simulator ran it. - - name: "aarch64-ios-sim: mcpp run prints 1-2-3 through the runner" - run: | - set -uo pipefail - cd /tmp/iostest - out=$("$MCPP_DEV" run --target aarch64-ios-sim 2>&1) && rc=0 || rc=$? - printf '%s\n' "$out" | tail -20 - if [ "$rc" -ne 0 ]; then - echo "FAIL: mcpp run --target aarch64-ios-sim exited $rc" - exit 1 - fi - if ! grep -qx '1-2-3' <<<"$out"; then - echo "FAIL: the program's output line '1-2-3' is absent" - exit 1 - fi - echo "ok: mcpp run --target aarch64-ios-sim printed 1-2-3 and exited 0" - - # THE THIRD ROW, ON A HOST THAT CANNOT RUN IT. A simulator runs the host's - # architecture and this runner is Apple silicon, so the claim is the - # artefact alone -- which is exactly the `preview` tier the row carries. - - name: "x86_64-ios-sim: the artefact, on a host that cannot run it" - run: | - set -euo pipefail - cd /tmp/iostest - "$MCPP_DEV" build --target x86_64-ios-sim - /tmp/assert-artefact.sh x86_64-ios-sim x86_64 7 18.0 diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml deleted file mode 100644 index 4f3f9a7ad..000000000 --- a/.github/workflows/ci-macos.yml +++ /dev/null @@ -1,431 +0,0 @@ -name: ci-macos - -# macOS CI for mcpp — validates LLVM/Clang as the default macOS toolchain. -# Tests the full xlings → LLVM → C++23 import std pipeline on macOS ARM64. -# -# The e2e suite runs in PARALLEL in ci-macos-e2e.yml (same setup via -# .github/actions/setup-macos-llvm), not tacked onto this job. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-macos-${{ github.ref }} - cancel-in-progress: true - -jobs: - macos-xlings-llvm: - name: macOS ARM64 — xlings LLVM end-to-end - runs-on: macos-15 - timeout-minutes: 30 - # NOTE: no MCPP_VERBOSE here — keep this job's output shape identical to - # ci-macos-e2e.yml, which asserts mcpp's default quiet output (48/53). - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - echo "SDK: $(xcrun --show-sdk-version)" - - - uses: ./.github/actions/setup-macos-llvm - - - name: Inspect LLVM package structure - run: | - echo "=== bin/ ===" - ls "$LLVM_ROOT/bin/" | grep -E "^(clang|llvm|lld|ld)" | head -20 - echo "=== lib/ ===" - ls "$LLVM_ROOT/lib/" 2>/dev/null | head -10 - echo "=== share/libc++/ ===" - find "$LLVM_ROOT" -name "std.cppm" -o -name "std.compat.cppm" 2>/dev/null - echo "=== clang++.cfg ===" - cat "$LLVM_ROOT/bin/clang++.cfg" 2>/dev/null || echo "(no cfg file)" - echo "=== Target triple ===" - "$CXX" -dumpmachine - echo "=== Module manifest ===" - "$CXX" -print-library-module-manifest-path 2>/dev/null || echo "(not available)" - - - name: Test — non-module C++23 compilation - run: | - WORK=$(mktemp -d) - cd "$WORK" - cat > main.cpp << 'EOF' - #include - #include - int main() { - std::cout << std::format("Hello from LLVM on macOS! clang {}", __clang_version__) << std::endl; - return 0; - } - EOF - "$CXX" -std=c++23 -o hello main.cpp - ./hello - - - name: Test — import std (two-stage module compilation) - run: | - WORK=$(mktemp -d) - cd "$WORK" - - # Find std.cppm - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - if [ -z "$STD_CPPM" ]; then - echo "::error::std.cppm not found in LLVM package" - find "$LLVM_ROOT" -name "*.cppm" 2>/dev/null - exit 1 - fi - echo "std.cppm at: $STD_CPPM" - - echo "=== Step 1: Precompile std module ===" - mkdir -p pcm.cache - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - - echo "=== Step 2: Compile std.pcm → std.o ===" - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - echo "=== Step 3: Compile main.cpp with import std ===" - cat > main.cpp << 'EOF' - import std; - int main() { - std::println("C++23 import std works on macOS via xlings LLVM!"); - return 0; - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm -c main.cpp -o main.o - - echo "=== Step 4: Link ===" - "$CXX" main.o std.o -o hello_modules - echo "=== Step 5: Run ===" - ./hello_modules - - - name: Test — import std.compat - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - STD_COMPAT_CPPM=$(find "$LLVM_ROOT" -name "std.compat.cppm" -path "*/libc++/*" | head -1) - - if [ -z "$STD_COMPAT_CPPM" ]; then - echo "::warning::std.compat.cppm not found, skipping" - exit 0 - fi - echo "std.compat.cppm at: $STD_COMPAT_CPPM" - - mkdir -p pcm.cache - # Build std first - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # Build std.compat (depends on std) - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - --precompile "$STD_COMPAT_CPPM" -o pcm.cache/std.compat.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/std.compat.pcm -c -o std.compat.o - - cat > main.cpp << 'EOF' - import std.compat; - #include - int main() { - printf("std.compat works on macOS! %s\n", "success"); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=std.compat=pcm.cache/std.compat.pcm \ - -c main.cpp -o main.o - "$CXX" main.o std.o std.compat.o -o compat_test - ./compat_test - - - name: Test — multi-module project - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - mkdir -p pcm.cache - - # Build std - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # User module: greeter - cat > greeter.cppm << 'EOF' - export module greeter; - import std; - export namespace greeter { - std::string hello(std::string_view name) { - return std::format("Hello, {}! (from macOS module)", name); - } - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - --precompile greeter.cppm -o pcm.cache/greeter.pcm - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/greeter.pcm -c -o greeter.o - - # Main - cat > main.cpp << 'EOF' - import std; - import greeter; - int main() { - std::println("{}", greeter::hello("mcpp")); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=greeter=pcm.cache/greeter.pcm \ - -c main.cpp -o main.o - "$CXX" main.o greeter.o std.o -o multimod - ./multimod - - - name: Validate mcpp probe logic expectations - run: | - echo "=== Verifying mcpp's assumptions ===" - echo "1. -print-sysroot returns empty (mcpp falls back to xcrun):" - result=$("$CXX" -print-sysroot 2>/dev/null || true) - if [ -z "$result" ]; then - echo " PASS: empty (xcrun fallback needed)" - else - echo " INFO: $result" - fi - - echo "2. xcrun --show-sdk-path works:" - xcrun --show-sdk-path && echo " PASS" - - echo "3. -dumpmachine returns darwin triple:" - triple=$("$CXX" -dumpmachine) - echo " $triple" - echo "$triple" | grep -q "darwin" && echo " PASS: contains 'darwin'" - - echo "4. libc++ module manifest discoverable:" - manifest=$("$CXX" -print-library-module-manifest-path 2>/dev/null || true) - if [ -n "$manifest" ] && [ -f "$manifest" ]; then - echo " PASS: $manifest" - echo " Content:" - cat "$manifest" | head -20 - else - echo " INFO: manifest not via flag, using fallback path" - find "$LLVM_ROOT/share/libc++" -name "*.cppm" 2>/dev/null && echo " PASS: fallback exists" - fi - - echo "5. llvm-ar available:" - ls "$LLVM_ROOT/bin/llvm-ar" && echo " PASS" - - echo "6. clang-scan-deps available:" - ls "$LLVM_ROOT/bin/clang-scan-deps" && echo " PASS" || echo " WARN: not found" - - - name: Validate install.sh platform detection - run: | - uname_s=$(uname -s) - uname_m=$(uname -m) - echo "Platform: ${uname_s}-${uname_m}" - case "${uname_s}-${uname_m}" in - Darwin-arm64) echo "PASS: would select darwin-arm64" ;; - Darwin-x86_64) echo "PASS: would select darwin-x86_64" ;; - *) echo "FAIL: unexpected platform"; exit 1 ;; - esac - - - name: Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp (has --mirror support) - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - "$MCPP" self config --mirror GLOBAL - "$MCPP" test - - - name: Forensics — test-binary link + load state (on failure) - if: failure() - run: | - BIN=$(find target -path "*/bin/test_manifest" | head -1) - echo "binary: $BIN" - [ -n "$BIN" ] || exit 0 - echo "--- otool -L ---"; otool -L "$BIN" || true - echo "--- rpaths ---"; otool -l "$BIN" | grep -A2 LC_RPATH || true - echo "--- statically embedded libc++? ---" - # `nm ... 2>/dev/null | grep -c` USED TO PRINT "0 (good)" WHEN `nm` - # ITSELF FAILED. A count of zero and a tool that never ran produce the - # same number, and the word "good" is then a claim nothing supports. - # This is diagnostic output rather than a gate, which makes it worse - # rather than better: a human reads it and believes it. - if nm "$BIN" > /tmp/nm.out 2> /tmp/nm.err; then - echo "$(grep -cE 'T __ZNSt3__1' /tmp/nm.out) libc++ symbols (0 = none embedded)" - else - echo "nm failed, so this measurement says nothing: $(head -1 /tmp/nm.err)" - fi - echo "--- direct run ---" - set +e - "$BIN" > run.out 2>&1 - echo "exit=$?" - head -20 run.out - sleep 5 - echo "--- newest crash report (termination) ---" - CR=$(ls -t "$HOME/Library/Logs/DiagnosticReports"/*.ips 2>/dev/null | head -1) - if [ -n "$CR" ]; then - python3 - "$CR" <<'PY' - import json, sys - lines = open(sys.argv[1]).read().splitlines() - meta = json.loads(lines[0]); body = json.loads("\n".join(lines[1:])) - print("proc:", meta.get("app_name"), "| exc:", body.get("exception", {})) - print("termination:", body.get("termination", {})) - t = [th for th in body.get("threads", []) if th.get("triggered")] - for fr in (t[0].get("frames", [])[:12] if t else []): - print(" ", fr.get("imageIndex"), fr.get("symbol", fr.get("imageOffset"))) - imgs = body.get("usedImages", []) - for i, im in enumerate(imgs[:12]): - print("img", i, im.get("path")) - PY - else - echo "none" - fi - - - name: "Toolchain: LLVM — build mcpp (self-host)" - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - cp "$MCPP" /tmp/mcpp-fresh - MCPP=/tmp/mcpp-fresh - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - "$MCPP" clean - "$MCPP" build - "$MCPP" --version - - - # GRAPHICS ON THIS HOST, BUILD ONLY, AND THAT IS THE WHOLE CLAIM. - # - # This runner has no Vulkan device, so what is asserted is what this - # platform decides: that the shader compiler THIS platform uses -- the - # rule declares `xim:shaderc` here and `xim:glslang` on Linux -- produces - # both SPIR-V headers, and that the Vulkan half compiles and links - # against the loader package. Running it is the Linux job's criterion, - # where a software device (`xim:mesa-lavapipe`) is published and the two - # legs' pixels are compared. - # - # The example is otherwise built only on Linux (`build_examples.sh` runs - # there), which is exactly the shape this change exists to remove: the - # half of a lane written for a host is the half that host never - # exercises. - - name: "Graphics: the offscreen example builds on this host" - shell: bash - run: | - set -e - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cd "$GITHUB_WORKSPACE/examples/10-graphics/offscreen" - # The toolchain is NAMED rather than inherited: leaving it to whatever - # a neighbouring step happened to select makes this step's subject - # depend on step order, which is not a property anybody reads. - "/tmp/mcpp-fresh" build --toolchain "llvm@${MCPP_LLVM_VER}" - for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" - test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } - # THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF - # THIS JOB. The rule chooses the shader compiler this platform - # publishes -- glslang on Linux, glslc here -- and the two split - # the declaration differently: glslang writes a complete `const - # uint32_t ...[] = {...}`, glslc an initialiser list the rule - # declares around, so the words land in `.inc`. An assertion - # naming only the header is an assertion about ONE compiler, which - # is exactly the shape this step exists to catch. - # ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b` - # does not exist -- even on a match in `a`, and even with `-s`, - # which suppresses the message and not the status. Written as one - # grep over both names, this criterion fails whenever the route - # that produces only a header is taken, which is a failure about - # the criterion and not about the shader. - found="" - for g in "$d/$f.h" "$d/$f.inc"; do - [ -f "$g" ] && grep -q '0x07230203' "$g" && found=1 - done - [ -n "$found" ] \ - || { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; } - done - echo "ok: both shader stages compiled and the Vulkan half linked" - - # AND NOW IT RUNS, WHICH IS A DIFFERENT CLAIM FROM THE ONE ABOVE. - # - # A build asserts that the shader compiler this platform publishes works - # and that the Vulkan half links. It cannot assert that the loader hands - # this program a device, and on macOS that is the interesting half: - # MoltenVK is a PORTABILITY driver, which the loader does not give to - # `vkEnumeratePhysicalDevices` unless the instance asked for portability - # enumeration. A program written against native drivers therefore finds no - # device here and reports it as "this machine has no GPU". - # - # This step is what turns that into a red build rather than a plausible - # message. It depends on the portability enumeration in this PR: without - # it the run reaches zero devices even though everything installed - # correctly. - # - # `VK_DRIVER_FILES` rather than an ICD search directory: the package is in - # the xlings store, not in `/usr/local/share/vulkan/icd.d`, and naming the - # file is the one form that does not depend on where the loader looks. - - name: "Graphics: the offscreen example RUNS on MoltenVK" - shell: bash - run: | - set -e - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # NOT installed from here. The example declares `xim:moltenvk` under - # `cfg(macos)`, so the build above already provisioned it, and looking - # for the ICD without installing anything is what asserts that the - # declaration works. An `xlings install` here would make this step - # pass whether the manifest named the driver or not. - icd=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-moltenvk" \ - "$HOME/.xlings/data/xpkgs/xim-x-moltenvk" \ - -name 'MoltenVK_icd.json' -print -quit 2>/dev/null || true) - [ -n "$icd" ] || { echo "no MoltenVK ICD in either store"; exit 1; } - echo "ICD: $icd" - cd "$GITHUB_WORKSPACE/examples/10-graphics/offscreen" - out=$(VK_DRIVER_FILES="$icd" "/tmp/mcpp-fresh" run 2>&1) || { echo "$out"; exit 1; } - echo "$out" - # The program asserts the corners and the centre itself and exits - # non-zero on either. What CI adds is that the run reached a DEVICE: - # a portability driver that the loader declined to show would leave - # the program reporting no device, which is the failure this step - # exists for. The name is not compared to a fixed string because it is - # the host's GPU and differs by runner; that it is non-empty and the - # centre pixel is opaque is what distinguishes reaching a device from - # not. - echo "$out" | grep -qE 'centre pixel: \([0-9]+, [0-9]+, [0-9]+, 255\)' \ - || { echo "no centre pixel was reported: the run reached no device"; exit 1; } - echo "ok: MoltenVK enumerated and the image was rendered on it" - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - MCPP=/tmp/mcpp-fresh # the freshly self-hosted binary built from this PR - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # THIS IS THE STEP THE RETRY WAS MEASURED ON: a macOS runner - # answered `Could not resolve host: github.com` after 30s of - # resolver timeout, failing a PR check before a single line of - # mcpp had run. See .github/tools/git_clone_retry.sh. - "$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \ - --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-target-matrix.yml b/.github/workflows/ci-target-matrix.yml deleted file mode 100644 index 62f22b9b4..000000000 --- a/.github/workflows/ci-target-matrix.yml +++ /dev/null @@ -1,486 +0,0 @@ -name: target matrix - -# 让支持矩阵成为一次测量的输出,而不是一份会悄悄过期的文档。 -# -# 这套东西存在的理由,是本仓库反复付出的一类代价:一格因为「今天这台机器恰好 -# 装了某个载荷」而通过,或因为没装而跳过,而两者在退出码上与「全部正确」没有区别。 -# 三个宿主各扫一遍,把结果与仓库里的期望表比对,差异即失败。 -on: - pull_request: - push: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - XLINGS_NON_INTERACTIVE: '1' - -jobs: - invariants: - # 第一层:四条恒等式,不需要期望表,也不依赖机器上装了什么。 - # 它们是结构约束 —— 任何一格只要跑起来了就该满足。 - name: invariants (${{ matrix.host }}) - runs-on: ${{ matrix.runner }} - timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - include: - # THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT - # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, - # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is - # distributed for and never scanned is a host whose target table is - # a claim nobody checked. - # - # `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which - # rows they serve — `x86_64-linux-gnu` needs the host-native glibc - # payload, so it is reachable on one and not the other — and a single - # `linux` key would have them overwrite each other in expected.tsv. - - { host: linux-x86_64, runner: ubuntu-24.04 } - - { host: linux-aarch64, runner: ubuntu-24.04-arm } - - { host: macos-arm64, runner: macos-14 } - - { host: windows-x86_64, runner: windows-2022 } - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build the mcpp in this pull request - run: | - set -euo pipefail - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --dev - # 两种拼写,且按 mtime 取最新 —— target/ 是缓存恢复的,`head -1` - # 会挑到上一次推送留下的二进制,版本号一样而代码是旧的。 - BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) \ - -newer mcpp.toml | head -1) - [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } - BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT") - echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV" - "$BUILT" --version - - - name: The invariants - run: | - set -euo pipefail - export MCPP="$MCPP_UNDER_TEST" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # These four read mcpp's MACHINE interface, so jq is not optional - # here. Without it each one takes its own "nothing to compare" exit — - # four honest-looking skips, and the next step would then report that - # the invariants did not run. Failing on the cause beats failing on - # the symptom four steps later. - command -v jq >/dev/null || { echo "::error::jq is missing on ${{ matrix.host }}"; exit 1; } - fail=0 - for t in tests/e2e/295_*.sh tests/e2e/296_*.sh \ - tests/e2e/297_*.sh tests/e2e/298_*.sh \ - tests/e2e/299_*.sh tests/e2e/300_*.sh \ - tests/e2e/301_*.sh tests/e2e/302_*.sh \ - tests/e2e/303_*.sh; do - echo "=== $t ===" - bash "$t" 2>&1 | tee "$(basename "$t").log" || true - rc=${PIPESTATUS[0]} - [ "$rc" = "0" ] || { echo "::error::$t failed (exit $rc)"; fail=1; } - done - [ "$fail" = 0 ] || exit 1 - - # A DEFERRAL NOBODY RECHECKS IS INDISTINGUISHABLE FROM A DEFECT. This - # step fails when its reason STOPS holding — the day an aarch64 llvm is - # published — which is the opposite of what a check usually does. - - name: The aarch64 llvm deferral still has its reason - if: matrix.host == 'linux-aarch64' - run: bash .github/tools/check_aarch64_llvm_deferral.sh - - - name: Each invariant RAN - run: | - set -euo pipefail - # 这一步存在的全部理由:退出码分不清「通过」与「跳过」。两条 e2e 都 - # 有为「这台机器没有可比的东西」准备的早退,而 CI 要的是它们真的比 - # 过了。 - check() { - grep -qF "$2" "$1".log || { - echo "::error::$1 did not reach its conclusion on ${{ matrix.host }}" - tail -6 "$1".log 2>/dev/null | sed 's/^/ /' - return 1 - } - echo " ok $1" - } - # A SKIP IS ACCEPTED FOR ONE NAMED REASON, NOT ON ONE NAMED HOST. - # - # 297 declares a non-llvm compiler, so it needs one to exist. Every - # toolchain mcpp installs on macOS is llvm; on windows-2022 it depends - # on what the restored cache holds — measured, one run had - # `gcc@16.1.0` and the next had only `llvm@20.1.7`. - # - # THE FIRST VERSION EXEMPTED macOS BY NAME, and the very next - # Windows run skipped for the same reason and went red. Naming the - # host encodes where the fact happened to hold; naming the FACT holds - # wherever it does. A skip for any other reason is still a failure. - # - # AND THE DENOMINATOR IS ASSERTED SEPARATELY: linux always has a gcc - # payload (it backs the host row), so that job uses `check` and the - # test is guaranteed to be exercised somewhere on every run. Without - # that, a reason accepted everywhere is a test that runs nowhere. - check_or_declared_skip() { # log ok-line acceptable-skip-substring - grep -qF "$2" "$1".log && { echo " ok $1"; return 0; } - if grep -q '^SKIP:' "$1".log && grep -qF "$3" "$1".log; then - echo " ok $1 (declared skip: $(grep -m1 '^SKIP:' "$1".log))" - return 0 - fi - echo "::error::$1 neither concluded nor declared the expected skip on ${{ matrix.host }}" - echo " expected skip to mention: $3" - tail -6 "$1".log 2>/dev/null | sed 's/^/ /' - return 1 - } - fail=0 - check 295_naming_the_host_target_changes_nothing.sh \ - "OK: naming the host's own target changes nothing" || fail=1 - check 296_what_the_report_names_is_what_the_link_line_uses.sh \ - "OK: what the report names is what the link line uses" || fail=1 - if [ "${{ matrix.host }}" = linux-x86_64 ]; then - # The denominator: gcc is always installed here, so this host must - # actually run the test. - check 297_a_capability_pin_is_not_a_preference.sh \ - "OK: a capability pin is not a preference" || fail=1 - else - check_or_declared_skip 297_a_capability_pin_is_not_a_preference.sh \ - "OK: a capability pin is not a preference" \ - "gcc is not installed here" || fail=1 - fi - if [ "${{ matrix.host }}" = linux-x86_64 ]; then - check 298_overriding_a_convention_requires_replacing_it.sh \ - "OK: a convention may be overridden, but not merely removed" || fail=1 - else - # THE MIRROR OF 297's EXEMPTION, AND FOR THE OTHER FAMILY. - # - # 298 declares llvm, and there is no llvm payload for aarch64 Linux — - # upstream stopped publishing linux-aarch64 after 19.x and the index - # has none. Measured on `ubuntu-24.04-arm`: - # - # SKIP: llvm is not installed here, and this test is about - # declaring it - # - # Granting it by REASON rather than by host is what makes it - # retire itself: the day - # `.agents/docs/2026-08-26-aarch64-linux-ecosystem-closure.md` - # lands an aarch64 llvm, this stops being a skip and starts being - # the assertion, with nothing here to change. - check_or_declared_skip 298_overriding_a_convention_requires_replacing_it.sh \ - "OK: a convention may be overridden, but not merely removed" \ - "llvm is not installed here" || fail=1 - fi - - # ── 2026.8.26.2: an answer mcpp already had, now used ───────────── - # - # 299/300/303 CARRY NO SKIP AT ALL, so they use `check` on every - # host. They read the vocabulary and the query's own document — - # neither depends on which payloads this machine happens to hold, and - # a version of them that skipped anywhere would be a version that - # could skip everywhere. - check 299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh \ - "OK: a request that named no C library resolves to a row that exists" || fail=1 - check 300_a_registered_family_is_not_reported_unknown.sh \ - "OK: a registered family is not reported unknown" || fail=1 - # 303's third half stacks a musl c-abi over this host's own target, - # and not every host stacks that — an MSVC-ABI host answers the - # layering question first, correctly, and there is then no - # two-answer document to check. Granted by reason; linux-x86_64 below - # is the denominator that must run the whole file. - if [ "${{ matrix.host }}" = linux-x86_64 ]; then - check 303_the_query_gives_one_answer_for_the_c_library.sh \ - "OK: the query gives one answer for the C library" || fail=1 - else - check_or_declared_skip 303_the_query_gives_one_answer_for_the_c_library.sh \ - "OK: the query gives one answer for the C library" \ - "refuses a musl c-abi over its own target" || fail=1 - fi - - # 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE - # MACHINE RATHER THAN OF THE CLAIM. "A requirement that DIFFERS from - # mcpp's own answer is applied" cannot be stated where only one family - # exists — macOS installs llvm only, and aarch64 Linux has no llvm - # payload at all (see 298's note). - # - # AND THE DENOMINATOR IS linux-x86_64, WHICH HAS BOTH. Without a - # host required to actually run these, a reason accepted everywhere is - # a test that runs nowhere. - if [ "${{ matrix.host }}" = linux-x86_64 ]; then - check 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \ - "OK: the graph's compiler is taken and nothing is written" || fail=1 - check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \ - "OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1 - else - check_or_declared_skip 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \ - "OK: the graph's compiler is taken and nothing is written" \ - "no other family is" || fail=1 - # 302's half two needs no second family and always runs; only half - # one is skipped, so the file still reaches its conclusion. - check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \ - "OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1 - fi - [ "$fail" = 0 ] || exit 1 - - scan: - # 第二层:全表扫描,与仓库里的期望表比对。 - name: scan (${{ matrix.host }}) - needs: invariants - runs-on: ${{ matrix.runner }} - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - # THE BUILD-HOST AXIS IS THE SET mcpp SHIPS FOR, NOT THE SET THAT - # WAS CONVENIENT. `release.yml` publishes four: linux-x86_64, - # linux-aarch64, macosx-arm64, windows-x86_64. A host mcpp is - # distributed for and never scanned is a host whose target table is - # a claim nobody checked. - # - # `host` IS (os, arch) AND NOT os. Two Linux hosts differ in which - # rows they serve — `x86_64-linux-gnu` needs the host-native glibc - # payload, so it is reachable on one and not the other — and a single - # `linux` key would have them overwrite each other in expected.tsv. - - { host: linux-x86_64, runner: ubuntu-24.04 } - - { host: linux-aarch64, runner: ubuntu-24.04-arm } - - { host: macos-arm64, runner: macos-14 } - - { host: windows-x86_64, runner: windows-2022 } - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build the mcpp in this pull request - run: | - set -euo pipefail - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --dev - BUILT=$(find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) \ - -newer mcpp.toml | head -1) - [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } - echo "MCPP_UNDER_TEST=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT")" >> "$GITHUB_ENV" - - # 格数不能是缓存状态的函数。 - # - # 实测:同一台 ubuntu-24.04,一轮装了 gcc+llvm(扫 40 格),下一轮只有 gcc - # (扫 20 格)。`expected.tsv` 声明的是前者,于是后者会把所有 llvm 行报成 - # 「期望表说有而扫描没跑到」—— 而那句报错是对的,问题在于覆盖面**漂移**了。 - # - # 矩阵要声明它扫哪些工具链,并把它们装上。装不上就红在这里,而不是 - # 变成一屏「没跑到」。 - - name: Install the toolchains this matrix declares - run: | - set -uo pipefail - export MCPP="$MCPP_UNDER_TEST" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - want="$(awk -F'\t' -v h='${{ matrix.host }}' \ - 'NF>=11 && $2==h {print $4}' tests/matrix/expected.tsv \ - | sort -u)" - if [ -z "$want" ]; then - echo " ? ${{ matrix.host }} 尚无期望行 —— 扫描它现有的工具链" - exit 0 - fi - fail=0 - for spec in $want; do - fam="${spec%@*}"; ver="${spec#*@}" - # `msvc@system` 是在机器上被找到的,不是装出来的。 - [ "$ver" = system ] && { echo " ok $spec (system)"; continue; } - if "$MCPP" toolchain install "$fam" "$ver" >/dev/null 2>&1; then - echo " ok $spec" - else - echo "::error::$spec 装不上,而期望表声明了它" - fail=1 - fi - done - [ "$fail" = 0 ] || exit 1 - - - name: Scan both systems - run: | - set -euo pipefail - export MCPP="$MCPP_UNDER_TEST" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # 编译器轴跟着**声明**走,不跟着「这台机器上装了什么」走。 - # - # 实测 2026-08-26,同一个提交:PR 上这个 job 绿,合入 main 后红 —— 那 - # 次 windows-2022 恢复出来的缓存里多了一个 `gcc@16.1.0`,扫描产出 24 - # 格而期望表声明 16 格,八格全部报成「表里没有这一格」。缓存里有什么 - # 不是这个仓库声明了什么,而判据必须是后者。 - # - # 与上一步取自同一列,所以「装它」和「扫它」不可能各说各话。 - MATRIX_COMPILERS="$(awk -F'\t' -v h='${{ matrix.host }}' \ - 'NF>=11 && $2==h {print $4}' tests/matrix/expected.tsv \ - | sort -u | tr '\n' ' ')" - export MATRIX_COMPILERS - echo "declared compiler axis: ${MATRIX_COMPILERS:-}" - # 两种体系各自成表。scan 把 mode 写进第一列,而比对必须按 mode 分开 - # 做 —— 拿一种体系的测量去比整张表,另一种的每一行都会被报成「没跑到」。 - bash tests/matrix/scan.sh payload > measured-payload.tsv - bash tests/matrix/scan.sh graph > measured-graph.tsv - cat measured-payload.tsv measured-graph.tsv > measured.tsv - echo "--- measured ---"; cat measured.tsv - - # 上传排在比对之前,而这是刻意的次序。宿主的第一次运行本就没有期望行, - # 比对会红 —— 而回填要用的正是这份产物。`if: always()` 也保留:一步失败不 - # 该把证据一起带走。 - - uses: actions/upload-artifact@v4 - if: always() - with: - name: matrix-${{ matrix.host }} - path: measured.tsv - - - name: Compare with the expected table - run: | - set -euo pipefail - fail=0 - bash tests/matrix/compare.sh measured-payload.tsv \ - tests/matrix/expected.tsv ${{ matrix.host }} payload || fail=1 - bash tests/matrix/compare.sh measured-graph.tsv \ - tests/matrix/expected.tsv ${{ matrix.host }} graph || fail=1 - [ "$fail" = 0 ] || exit 1 - - # A WEB ARTEFACT RUNS THROUGH THE INTERPRETER ITS PAYLOAD NAMES, NOT - # THROUGH PATH. That is what `wasm32-emscripten`'s `verified` tier - # claims, and no step measured it until a sandbox with no `node` on PATH - # showed that the run depended on the host: - # - # /usr/bin/env: 'node': No such file or directory - # - # A FRESH MCPP_HOME, BECAUSE THE CACHE IS NOT THE PUBLISHED FORM. - # `~/.mcpp` is restored by key prefix, so the emsdk payload in it may - # predate the recipe that writes `.mcpp-toolchain.json`, and that older - # payload would be the object measured. A fresh home installs what the - # index publishes today. - # - # AND A DECOY `node` FIRST ON PATH, rather than `node` removed from PATH. - # Removing it would also remove whatever else those directories hold, - # and an image that ships a real `node` would pass for the wrong reason. - # A decoy that exits 97 with a marker separates the two outcomes on any - # image: the payload's runner never reaches it. - - name: "wasm32-emscripten runs through its payload's runner, not PATH" - if: ${{ !cancelled() && matrix.host == 'linux-x86_64' }} - run: | - set -uo pipefail - home=$(mktemp -d); work=$(mktemp -d); decoy=$(mktemp -d) - printf '#!/bin/sh\necho "DECOY-NODE-WAS-RUN" >&2\nexit 97\n' > "$decoy/node" - chmod +x "$decoy/node" - export MCPP_HOME="$home" MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_UNDER_TEST" self config --mirror GLOBAL >/dev/null 2>&1 || true - mkdir -p "$work/src" - printf '[package]\nname = "w"\nversion = "0.1.0"\n' > "$work/mcpp.toml" - cat > "$work/src/main.cpp" <<'CPP' - import std; - int main() { - std::vector v{3, 1, 2}; - std::ranges::sort(v); - std::print("{}-{}-{}\n", v[0], v[1], v[2]); - } - CPP - out=$(cd "$work" && PATH="$decoy:$PATH" "$MCPP_UNDER_TEST" run --target wasm32-emscripten 2>&1) && rc=0 || rc=$? - printf '%s\n' "$out" | tail -12 - desc="$home/registry/data/xpkgs/xim-x-emsdk/6.0.9/.mcpp-toolchain.json" - if [ ! -f "$desc" ]; then - echo "::error::the freshly installed emsdk payload has no .mcpp-toolchain.json; the published recipe does not name its runner" - exit 1 - fi - if grep -q 'DECOY-NODE-WAS-RUN' <<<"$out"; then - echo "::error::the artefact ran through the node on PATH, not through the payload's runner" - exit 1 - fi - if [ "$rc" -ne 0 ]; then - echo "::error::mcpp run --target wasm32-emscripten exited $rc" - exit 1 - fi - if ! grep -qx '1-2-3' <<<"$out"; then - echo "::error::the program's output line 1-2-3 is absent" - exit 1 - fi - echo "ok: wasm32-emscripten ran through the payload's runner; the decoy node was never run" - - coverage: - # THE DENOMINATOR. Every check above is per host, and no per-host check - # can notice a host that never ran. - # - # Each `scan` job compares the rows for ITS OWN host, so deleting a host - # from the matrix above silently retires every expectation the table holds - # for it: nothing measures those rows, nothing compares them, and the - # workflow is green. This job is the one place that reads the expected table - # as a whole and requires the run to have covered it. - # - # It runs even when a scan failed (`always()`), because "which hosts were - # covered" is a different question from "did they pass" — and a run where a - # host is missing entirely should say so in those words rather than leaving - # a reader to infer it from a job list. - name: coverage (every host the table names was scanned) - needs: scan - if: always() - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - pattern: matrix-* - path: measured - - name: Every host in expected.tsv produced rows - run: | - set -euo pipefail - want=$(awk -F'\t' 'NF>=11 {print $2}' tests/matrix/expected.tsv | sort -u) - [ -n "$want" ] || { echo "::error::expected.tsv names no host at all"; exit 1; } - got=$(cat measured/*/measured.tsv 2>/dev/null \ - | awk -F'\t' 'NF>=11 {print $2}' | sort -u) - echo "expected hosts: $(echo $want)" - echo "scanned hosts: $(echo ${got:-})" - fail=0 - for h in $want; do - printf '%s\n' "$got" | grep -qx "$h" || { - echo "::error::expected.tsv holds rows for '$h', and no scan produced any" - fail=1 - } - done - # AND THE OTHER DIRECTION. A host that scanned but has no rows in the - # table is a new build host nobody declared expectations for — the - # per-host compare already reds on it, but saying it here names the - # cause rather than listing 40 unexplained cells. - for h in $got; do - printf '%s\n' "$want" | grep -qx "$h" || { - echo "::error::'$h' was scanned and the expected table does not mention it" - echo " add its rows to tests/matrix/expected.tsv from this run's artifact" - fail=1 - } - done - [ "$fail" = 0 ] || exit 1 - echo "OK: every build host the table names was scanned, and no other" - - - name: The build hosts mcpp ships for are the ones scanned - run: | - set -euo pipefail - # THE TABLE AND THE RELEASE MUST NAME THE SAME SET. A host that - # gets a published binary and no scan is a host whose target table is - # a claim nobody checked; a host that is scanned and never shipped is - # coverage spent on a machine no user has. - # - # Derived from release.yml's asset names rather than restated here, so - # adding a fifth host to the release fails this step until the matrix - # covers it. - ship=$(grep -oE 'mcpp-\$\{?[A-Za-z_{}. ]*\}?-(linux|macosx|windows)-(x86_64|aarch64|arm64)' \ - .github/workflows/release.yml \ - | sed -E 's/.*-(linux|macosx|windows)-/\1-/' \ - | sed 's/^macosx-/macos-/' | sort -u) - scanned=$(awk -F'\t' 'NF>=11 {print $2}' tests/matrix/expected.tsv | sort -u) - echo "release publishes: $(echo $ship)" - echo "matrix declares: $(echo $scanned)" - if [ "$ship" != "$scanned" ]; then - echo "::error::the set of build hosts mcpp publishes and the set the target matrix declares differ" - diff <(printf '%s\n' "$ship") <(printf '%s\n' "$scanned") | sed 's/^/ /' || true - exit 1 - fi - echo "OK: $(printf '%s\n' "$ship" | wc -l) build hosts, published and scanned" diff --git a/.github/workflows/ci-windows-e2e.yml b/.github/workflows/ci-windows-e2e.yml deleted file mode 100644 index da100042e..000000000 --- a/.github/workflows/ci-windows-e2e.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: ci-windows-e2e - -# The e2e suite on Windows, split out of ci-windows.yml (where it was 9.7 of -# the job's 20.4 min) and sharded across two runners — same shape as -# ci-linux-e2e.yml. Shares the cache lineage in .github/actions/bootstrap-mcpp, -# so each shard restores a warm sandbox and pays one incremental `mcpp build`. -# -# Paired workflows: ci-windows.yml (build + unit + package, toolchains + -# regressions), ci-linux-e2e.yml, ci-macos-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e ${{ matrix.shard }}/2 (windows x64, self-host) - runs-on: windows-latest - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - shard: [1, 2] - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - # Round-robin slice of tests/e2e (see run_all.sh). - E2E_SHARD: ${{ matrix.shard }}/2 - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. - steps: - # `submodules: recursive` so tests/e2e/233_bench_matrix.sh can check that - # each `hub`/`body` in bench/matrix.json exists in the tree it names -- - # the check reads "submodule not initialised" without them and reports - # nothing, which is how a stale hub path survived (#599). Under 10 MB of - # source across the three pins, and nothing here builds them. - - uses: actions/checkout@v4 - with: - submodules: recursive - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - 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" - - # MinGW-w64 GCC (xim:mingw-gcc). Installed here so the `mingw` capability - # is GRANTED rather than left to whatever the shared sandbox cache happens - # to carry: e2e 256 packs an MSVC leg and a MinGW leg into one package, - # which is the only place `lib/` keyed by triple is proven with two - # DIFFERENT artifact names (mathkit.lib beside libmathkit.a). Without this - # step that test skips, and a skipped test in a green suite reads exactly - # like a passing one. - # - # Not `|| true`: if the payload cannot be installed the capability quietly - # disappears and the coverage goes with it, which is the failure mode this - # step exists to prevent. - - name: "Toolchain: MinGW payload for the fat-package e2e" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP_SELF" toolchain install mingw 16.1.0 - # Verified through the SAME two locations run_all.sh probes — checking - # only one of them would let the step pass while the capability stays - # ungranted, which is the shape of a green run that tested nothing. - found="" - for c in "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-mingw-gcc/*/bin/g++.exe \ - "$HOME"/.xlings/data/xpkgs/xim-x-mingw-gcc/*/bin/g++.exe; do - [[ -x "$c" ]] && { found="$c"; break; } - done - test -n "$found" || { echo "FAIL: mingw installed but not where run_all.sh looks"; exit 1; } - echo "mingw payload: $found" - - - name: E2E suite - shell: bash - # Fail-fast on hung tests instead of burning the whole job budget. - # Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - # The RELEASED mcpp this job bootstrapped from, kept for e2e 252: the - # claim that an older client can still build against a package the PR - # produces is only worth making if something checks it against a real - # old binary. Captured before $MCPP is repointed at the fresh build. - export MCPP_BOOT="${MCPP:-$MCPP_BOOT}" - export MCPP="$MCPP_SELF" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP_SELF" self config --mirror GLOBAL - "$MCPP_SELF" toolchain default llvm@20.1.7 - set -o pipefail - bash tests/e2e/run_all.sh 2>&1 | tee "$RUNNER_TEMP/e2e-suite.log" - - # Measurement legs print READING lines (#646 F2 across PE images, #649 E10 - # the llvm row's recorded CRT); collected where a reader finds them. - - name: Measurement readings - if: always() - shell: bash - run: | - { - echo "### Measurement readings (Windows, shard ${{ matrix.shard }})" - echo '```' - grep -h '^READING' "$RUNNER_TEMP/e2e-suite.log" 2>/dev/null || echo "(none)" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci-windows-msvc-xlings.yml b/.github/workflows/ci-windows-msvc-xlings.yml deleted file mode 100644 index 1ed1ff1f3..000000000 --- a/.github/workflows/ci-windows-msvc-xlings.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: ci-windows-msvc-xlings - -# mcpp driving the xlings-MANAGED MSVC toolset (`msvc@`) — the whole -# chain, end to end: index → payload → unpack → resolve → build → run → -# remove. -# -# WHY THIS IS NOT IN ci-windows-e2e.yml, which also runs MSVC tests: -# -# Different subject. Everything MSVC in the main suite (95_msvc_system, -# 99_msvc_native_build, 177, 180, 182) tests mcpp against the machine's own -# Visual Studio. That is mcpp's code and nothing else's. This job tests -# mcpp against the xlings ECOSYSTEM — a package index, a mirror, a payload -# set, an unpack recipe — most of which lives in another repository and -# moves on its own schedule. -# -# Different failure meaning. When this job goes red it usually means the -# index moved, not that the pull request broke something. Mixed into the -# main suite that reads as "your change broke Windows", and the honest -# signal (100+ fast tests, all about mcpp) gets buried under one slow test -# about somebody else's package. Keeping them apart keeps both readable. -# -# Different cost. ~380 MB of downloads (xim:msvc + xim:windows-sdk) and a -# real toolchain install, against a suite whose other tests are seconds -# each. -# -# The split is enforced by a capability, not by a file list: the tests carry -# `# requires: xlings-msvc`, granted only by MCPP_E2E_XLINGS_MSVC=1 below. So -# the main suite skips them by construction, and a new test joins this job by -# declaring the capability — there is no second list to keep in sync. -# -# Paired workflows: ci-windows.yml, ci-windows-e2e.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - xlings-msvc: - name: xlings-managed msvc toolset (windows x64, self-host) - runs-on: windows-latest - timeout-minutes: 45 - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Newest mcpp.exe, not an arbitrary one — `target/` is restored from - # cache and keeps a directory per build fingerprint. Same reasoning - # as ci-windows-e2e.yml, where picking wrong ran the previous - # release's binary. - 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: xlings-managed msvc e2e - shell: bash - timeout-minutes: 30 - env: - # Grants the `xlings-msvc` capability. Without it these tests skip - # everywhere, which is exactly what the main suite wants. - MCPP_E2E_XLINGS_MSVC: '1' - # Name what this job runs, so the job title and its contents cannot - # drift apart. Widen the glob when a second test joins. - E2E_ONLY: '239_*.sh' - # THE DEFAULT PER-TEST CAP IS TOO CLOSE TO WHAT THIS TEST COSTS. - # 239 fetches ~376 MB, and its measured durations on this runner are - # 244 / 292 / 313 / 335 / 515s against run_all.sh's 600s default — a - # download-bound test whose spread nearly reaches its own deadline, - # so a slow mirror reports a red build for a correct one. - # - # Raised only here, where the job runs that one test and the step's - # own `timeout-minutes: 30` is the real backstop — which is what - # run_all.sh's comment says the per-test value is meant to sit under. - E2E_TEST_TIMEOUT: '1500' - run: | - export MCPP="$MCPP_SELF" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP_SELF" self config --mirror GLOBAL - bash tests/e2e/run_all.sh - - # A run that matched nothing is a green tick for having done nothing, - # and it looks exactly like a run that passed. E2E_ONLY is a glob typed - # by hand; if it stops matching, say so here rather than in a report - # nobody reads. - - name: Fail if the filter selected no tests - if: always() - shell: bash - run: | - n=$(ls tests/e2e/239_*.sh 2>/dev/null | wc -l) - test "$n" -gt 0 || { echo "FAIL: E2E_ONLY matched no tests"; exit 1; } - echo "selected $n test(s)" diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml deleted file mode 100644 index f9d0d38d9..000000000 --- a/.github/workflows/ci-windows.yml +++ /dev/null @@ -1,524 +0,0 @@ -name: ci-windows - -# Windows CI for mcpp — same flow as Linux (ci-linux.yml) and macOS (ci-macos.yml): -# xlings install mcpp → self-host build → smoke → package -# -# SHAPE: three INDEPENDENT jobs, no `needs:` between them; each restores the -# shared cache lineage (.github/actions/bootstrap-mcpp) and pays one warm -# `mcpp build` to get the PR's own binary. The e2e suite moved to -# ci-windows-e2e.yml (sharded ×2) — it was 9.7 of this job's 20.4 min. -# -# before: build → unit → xlings → stdin → e2e → toolchains → package ≈ 20 min -# after: max(build+unit+package, toolchains+integration) ≈ 8 min -# in parallel with ci-windows-e2e (≈ 8 min) - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-windows-${{ github.ref }} - cancel-in-progress: true - -env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - -jobs: - build-test: - name: build + test + package (windows x64, self-host) - runs-on: windows-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - 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: Unit + integration tests via mcpp test - shell: bash - run: | - export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - "$MCPP_SELF" test - - - name: Package Windows release zip - id: package - shell: bash - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - WRAPPER="mcpp-${VERSION}-windows-x86_64" - ZIPNAME="${WRAPPER}.zip" - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; } - - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe" - printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - XLINGS_EXE="$USERPROFILE/.xlings/subos/default/bin/xlings.exe" - [ -f "$XLINGS_EXE" ] && cp "$XLINGS_EXE" "$STAGING/$WRAPPER/registry/bin/xlings.exe" - - mkdir -p dist - (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") - cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" - (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") - echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the packaged zip - shell: bash - run: | - ZIPNAME="${{ steps.package.outputs.zipname }}" - WRAPPER="${ZIPNAME%.zip}" - SMOKE=$(mktemp -d) - (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") - "$SMOKE/$WRAPPER/bin/mcpp.exe" --version - test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" - test -f "$SMOKE/$WRAPPER/mcpp.bat" - echo "Smoke-test passed" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-windows-x86_64 - path: | - dist/*.zip - dist/*.sha256 - - # Everything that needs a toolchain other than the default, plus the two - # Windows-specific behavioural regressions. Kept on one runner: each leg is - # seconds-to-2-minutes, so per-leg runners would cost more setup than they - # save. - # A Windows machine WITHOUT Visual Studio — the shape of an ordinary user's - # box, and the one shape no GitHub image provides. Every runner ships VS, so - # a bare-Windows regression was structurally invisible here; the fresh-install - # workflow now covers it too, but that one only runs post-release, which is - # far too late to learn that `mcpp new && mcpp build` no longer works on a - # stock machine. - # - # Order matters: mcpp is built while Visual Studio is still present (the - # self-host build uses llvm, which targets the MSVC ABI and needs it), and - # only then is VS masked. e2e 182 opens by asserting that MSVC detection - # FAILS, so an incomplete mask fails the job instead of quietly testing the - # ordinary path. - no-msvc-fallback: - name: "bare Windows: no Visual Studio (windows x64)" - # Takes the binary build-test already produced instead of building here. - # Building in this job means compiling with clang, which reads the MSVC - # STL — the open handles that leaves make the VS directories unrenamable, - # so the masking below silently did nothing. - needs: build-test - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Fetch the PR's mcpp.exe - uses: actions/download-artifact@v4 - with: - name: mcpp-windows-x86_64 - path: dist - - - name: Unpack it - shell: bash - run: | - ZIP=$(ls dist/*.zip | head -1) - test -n "$ZIP" || { echo "FAIL: no zip artifact"; exit 1; } - unzip -q "$ZIP" -d unpacked - MCPP_SELF=$(find unpacked -name "mcpp.exe" | head -1) - test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe in $ZIP"; exit 1; } - MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") - "$MCPP_SELF" --version - echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" - - # Masked before anything else touches Visual Studio, so no process of - # ours is holding a handle into it. - - name: Mask Visual Studio - shell: pwsh - run: | - $ErrorActionPreference = 'Continue' - - # All three of msvc.cppm's discovery strategies converge on - # \VC\Tools\MSVC — vswhere returns an installationPath that - # find_latest_msvc_tools then resolves through it, the env strategy - # checks it explicitly, and the well-known-path scan tests for it. - # So mask the VC directory rather than the Visual Studio root: the - # root is held open on the runner and renaming it is denied, while - # VC one level down renames fine. The runner is disposable, so this - # is both safe and closer to "absent" than any env-only trick. - $vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" } - - # -Path with a trailing wildcard segment lists the CONTENTS of the - # matches, not the matches themselves, so `…\*\*\VC` would hand back - # VC's children. Resolve-Path returns the directories themselves. - # Errors are reported, not swallowed: a silent failure here is how - # the first attempt "masked" nothing and still looked fine. - Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" ` - -ErrorAction SilentlyContinue | ForEach-Object { - $p = $_.Path - Write-Host "masking $p" - try { Rename-Item -LiteralPath $p -NewName "VC.masked" -ErrorAction Stop } - catch { Write-Host " rename failed: $($_.Exception.Message)" } - } - - foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir', - 'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) { - "$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 - } - - # Check the mask's own postcondition here, where the cause is - # obvious, instead of letting it surface three steps later as a - # confusing pass. - $left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" ` - -Directory -ErrorAction SilentlyContinue - if ($left) { - Write-Host "FAIL: VC tools still present after masking:" - $left | ForEach-Object { Write-Host " $($_.FullName)" } - exit 1 - } - Write-Host "Visual Studio masked: no VC\Tools\MSVC remains." - - # After masking, so nothing this action does can be holding Visual - # Studio open. It installs xlings (which mcpp resolves the winlibs - # toolchain through) and a released mcpp; neither needs a C++ compiler, - # so a masked VS is irrelevant to it. - - uses: ./.github/actions/bootstrap-mcpp - - - name: "No Visual Studio: fallback to winlibs GCC (e2e 182)" - shell: bash - env: - MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }} - run: | - MCPP="$MCPP_SELF" bash tests/e2e/182_windows_no_msvc_fallback.sh - - toolchains: - name: "toolchains + regressions (windows x64)" - runs-on: windows-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - 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") - echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" - - # Integration: the mcpp built from THIS PR's source ($MCPP_SELF, the - # self-hosted binary) builds & runs a real external C++ project — xlings - # (openxlings/xlings ships its own mcpp.toml). MCPP_VENDORED_XLINGS only - # supplies the xlings package backend mcpp resolves deps through. - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - shell: bash - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - "$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \ - --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP_SELF" self config --mirror GLOBAL - "$MCPP_SELF" build - "$MCPP_SELF" run - - # Regression test for the Windows first-run "press Enter to advance" hang. - # Launches mcpp with an OPEN, EMPTY, never-closing stdin pipe. Without - # seal_stdin's Windows fix, any grandchild that reads stdin would inherit - # our pipe and block forever — caught by the timeout below. With the fix, - # every subprocess stdin is redirected from NUL → no possibility of hang. - - name: "Regression: mcpp survives open-empty-stdin (Windows hang fix)" - shell: pwsh - timeout-minutes: 15 - env: - MCPP_VENDORED_XLINGS: ${{ env.XLINGS_BIN }} - run: | - $ErrorActionPreference = 'Stop' - - # MCPP_SELF was set in a bash step as an MSYS-style path - # (e.g. /d/a/mcpp/...). PowerShell can't exec that — convert it - # to a native Windows path via the git-bash cygpath that ships - # on the runner. - $mcppExe = (& 'C:\Program Files\Git\usr\bin\cygpath.exe' -w $env:MCPP_SELF).Trim() - Write-Host "Resolved MCPP_SELF (Windows form): $mcppExe" - if (-not (Test-Path $mcppExe)) { - throw "MCPP_SELF after cygpath not found: $mcppExe" - } - - $tmp = Join-Path $env:RUNNER_TEMP ("stdin-hang-test-" + [guid]::NewGuid().ToString('N')) - New-Item -ItemType Directory -Path $tmp | Out-Null - Set-Location $tmp - & $mcppExe new hello_stdin - Set-Location hello_stdin - - function Invoke-McppWithOpenStdin { - param([string]$McppPath, [string]$McppArgs, [int]$TimeoutSeconds = 300) - - $psi = [System.Diagnostics.ProcessStartInfo]::new() - $psi.FileName = $McppPath - $psi.Arguments = $McppArgs - $psi.WorkingDirectory = (Get-Location).Path - $psi.UseShellExecute = $false - $psi.RedirectStandardInput = $true # parent holds child's stdin - $psi.RedirectStandardOutput = $true - $psi.RedirectStandardError = $true - # By default the child inherits the parent's env (we did not - # touch $psi.Environment) so MCPP_VENDORED_XLINGS / PATH / etc. - # propagate. - - $p = [System.Diagnostics.Process]::Start($psi) - - # Async-drain stdout/stderr so a full output buffer doesn't - # itself deadlock the child (separate failure mode from the - # stdin hang we're testing). - $stdoutTask = $p.StandardOutput.ReadToEndAsync() - $stderrTask = $p.StandardError.ReadToEndAsync() - - # NEVER write or close $p.StandardInput — the pipe stays open - # and empty for the lifetime of the child. Any grandchild that - # reads stdin will block on this pipe → caught by WaitForExit. - - if (-not $p.WaitForExit($TimeoutSeconds * 1000)) { - try { $p.Kill($true) } catch {} - Write-Host "----- captured stdout -----" - Write-Host $stdoutTask.Result - Write-Host "----- captured stderr -----" - Write-Host $stderrTask.Result - throw "REGRESSION: 'mcpp $McppArgs' HUNG with open-empty stdin after ${TimeoutSeconds}s. The Windows seal_stdin fix is not effective." - } - - Write-Host "----- stdout -----" - Write-Host $stdoutTask.Result - Write-Host "----- stderr -----" - Write-Host $stderrTask.Result - - if ($p.ExitCode -ne 0) { - throw "'mcpp $McppArgs' exited with code $($p.ExitCode) (no hang, but failed)." - } - } - - Write-Host '=== T1: mcpp --version (sanity, fast path) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs '--version' -TimeoutSeconds 30 - - Write-Host '=== T2: mcpp build (full bootstrap + toolchain + dep resolve + compile) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'build' -TimeoutSeconds 600 - - Write-Host '=== T3: mcpp run (post-build run path) ===' - Invoke-McppWithOpenStdin -McppPath $mcppExe -McppArgs 'run' -TimeoutSeconds 120 - - Write-Host 'SUCCESS: mcpp completes with open-empty stdin → Windows seal_stdin fix verified.' - - - name: "Toolchain: LLVM — mcpp new → run" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - TMP=$(mktemp -d) - cd "$TMP" - "$MCPP_SELF" new hello_win - cd hello_win - "$MCPP_SELF" run - - # MinGW-w64 GCC via the xlings ecosystem (xim:mingw-gcc → xlings-res - # winlibs mirror): install → default → modules build/run → standalone - # exe. Same flow as e2e 97 but as a visible CI step. Payload is cached - # via the mcpp sandbox cache after the first run. - - name: "Toolchain: MinGW — install → build → run (xim:mingw-gcc)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - MCPP="$MCPP_SELF" bash tests/e2e/97_mingw_toolchain.sh - - # windows-latest ships VS 2022 Enterprise with the VC workload, so - # msvc@system detection MUST succeed here — a failure is a regression - # in the discovery/identification chain (vswhere → env → paths). - # Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds - # target/, invalidating this job's $MCPP_SELF fingerprint path. - - name: "Toolchain: MSVC — detection & selection (msvc@system)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - # Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow - # the global default in `toolchain list` / doctor output. - TMP=$(mktemp -d); cd "$TMP" - - out=$("$MCPP_SELF" toolchain default msvc); echo "$out" - grep -q "Detected" <<<"$out" - grep -q "msvc@system" <<<"$out" - - "$MCPP_SELF" toolchain list | tee tc-list.txt - grep -E '\*\s*msvc' tc-list.txt - - "$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true - grep -qi "msvc" doctor.txt - - # native cl.exe build: full e2e (modules, import std, incremental) - cd "$GITHUB_WORKSPACE" - MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh - - # build.mcpp under cl.exe. `MSVC x build.mcpp` was an empty cell in - # this matrix and the feature was correspondingly at zero — the ten - # build.mcpp e2e all run under clang, whose payload path has no - # spaces in it either. Both gaps closed here. - MCPP="$MCPP_SELF" bash tests/e2e/180_msvc_build_mcpp.sh - - # restore the LLVM default for the remaining steps - "$MCPP_SELF" toolchain default llvm@20.1.7 - - - # GRAPHICS ON THIS HOST, BUILD ONLY, AND THAT IS THE WHOLE CLAIM. - # - # This runner has no Vulkan device, so what is asserted is what this - # platform decides: that the shader compiler THIS platform uses -- the - # rule declares `xim:shaderc` here and `xim:glslang` on Linux -- produces - # both SPIR-V headers, and that the Vulkan half compiles and links - # against the loader package. Running it is the Linux job's criterion, - # where a software device (`xim:mesa-lavapipe`) is published and the two - # legs' pixels are compared. - # - # The example is otherwise built only on Linux (`build_examples.sh` runs - # there), which is exactly the shape this change exists to remove: the - # half of a lane written for a host is the half that host never - # exercises. - - name: "Graphics: the offscreen example builds on this host" - shell: bash - run: | - set -e - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cd "$GITHUB_WORKSPACE/examples/10-graphics/offscreen" - # The toolchain is NAMED rather than inherited: leaving it to whatever - # a neighbouring step happened to select makes this step's subject - # depend on step order, which is not a property anybody reads. - "$MCPP_SELF" build --toolchain "llvm@20.1.7" - for f in triangle_vert triangle_frag; do - d="target/.build-mcpp/out/spirv" - test -f "$d/$f.h" || { echo "missing $d/$f.h"; exit 1; } - # THE MAGIC IS NOT ALWAYS IN THE HEADER, AND THAT IS THE POINT OF - # THIS JOB. The rule chooses the shader compiler this platform - # publishes -- glslang on Linux, glslc here -- and the two split - # the declaration differently: glslang writes a complete `const - # uint32_t ...[] = {...}`, glslc an initialiser list the rule - # declares around, so the words land in `.inc`. An assertion - # naming only the header is an assertion about ONE compiler, which - # is exactly the shape this step exists to catch. - # ONE FILE AT A TIME, because `grep -qs a b` exits 2 when `b` - # does not exist -- even on a match in `a`, and even with `-s`, - # which suppresses the message and not the status. Written as one - # grep over both names, this criterion fails whenever the route - # that produces only a header is taken, which is a failure about - # the criterion and not about the shader. - found="" - for g in "$d/$f.h" "$d/$f.inc"; do - [ -f "$g" ] && grep -q '0x07230203' "$g" && found=1 - done - [ -n "$found" ] \ - || { echo "$f carries no SPIR-V magic in either $f.h or $f.inc"; exit 1; } - done - echo "ok: both shader stages compiled and the Vulkan half linked" - - # WINDOWS STAYS AT "BUILDS", AND FOUR MEASUREMENTS SAY WHY IT IS NOT - # WAITING ON ANY OF THE THINGS IT WAS THOUGHT TO BE. - # - # 1. Not a missing `vulkan-1.dll`. The program printed `render - # unavailable`, which `src/main.cpp` writes after the render function - # returns nothing. A process that could not resolve `vkCreateInstance` - # from that DLL fails during image load and prints nothing at all. - # mcpp-index's own `vulkan-tests` member calls - # `vkEnumerateInstanceVersion` on the windows shards and passes. - # - # 2. Not an unparseable ICD manifest, though that WAS a real defect. - # `xim:mesa-lavapipe` wrote its rewritten `library_path` into the - # JSON string unescaped, so `C:\Users\...` carried `\U` and the - # loader's cJSON parser rejected the file -- an ICD it skips with no - # error. Fixed in openxlings/xim-pkgindex#781. With the fix the runner - # reads - # "library_path": "C:/Users/.../lib/vulkan_lvp.dll" - # and the manifest parses. The program still prints `render - # unavailable`. - # - # 3. Not a failed `LoadLibrary`. `vulkan_lvp.dll` imports only - # ADVAPI32, GDI32, KERNEL32, ntdll, ole32, SHELL32 and USER32, all of - # which any Windows has; the payload ships nothing else that could be - # missing. - # - # 4. Not the environment variable's vintage. Both `VK_DRIVER_FILES` and - # `VK_ICD_FILENAMES` were set, which covers loaders on either side of - # 1.3.234. - # - # What the loader itself said under `VK_LOADER_DEBUG=all`: - # - # INFO: Loader is running with elevated permissions. - # Environment variable VK_DRIVER_FILES will be ignored - # INFO: Loader is running with elevated permissions. - # Environment variable VK_ICD_FILENAMES will be ignored - # DRIVER: Found no registry files in - # HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers - # ERROR | DRIVER: windows_read_data_files_in_registry: Registry lookup - # failed to get ICD manifest files. Possibly missing Vulkan driver? - # - # THAT IS THE WHOLE ANSWER, AND NO PACKAGE CHANGE REACHES IT. A GitHub - # Windows runner runs elevated, and the loader discards every driver-path - # environment variable when it is -- it will not let a path a - # non-administrator could write inject a driver into an elevated process. - # It then falls back to the registry, which has no ICD. The mechanism this - # example uses on Linux and macOS is simply unavailable here. - # - # Reaching lavapipe on Windows therefore means registering the ICD under - # `HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers`, or running the - # program unelevated. `xim:mesa-lavapipe`'s `config()` says outright that - # it places the payload and leaves naming the ICD to the consumer, so the - # registry entry is a decision for that package or for this repository, - # not a defect in either - # - # So the platform builds the Vulkan half and runs the CPU fallback, and - # the next attempt starts from here rather than from the top. - - - name: "Toolchain: LLVM — build mcpp (self-host)" - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - cp "$MCPP_SELF" /tmp/mcpp-fresh.exe - MCPP=/tmp/mcpp-fresh.exe - "$MCPP" toolchain default llvm@20.1.7 - "$MCPP" clean --bmi-cache - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml deleted file mode 100644 index bed5d332d..000000000 --- a/.github/workflows/cross-build-test.yml +++ /dev/null @@ -1,501 +0,0 @@ -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 -musl` build to the triple-named cross -# gcc musl toolchain from the xlings ecosystem (xim:-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 ─────────────────────────────────────────────────────────────── -# * The e2e scripts this job names explicitly (102, 198, 240, 248) are the -# ones the ordinary Linux shards SKIP for want of `mingw-cross`. They are -# listed in the job rather than left to run_all's cap gating precisely so -# they cannot end up skipping everywhere at once. -# * 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 ` + 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: | - # The runner image carries third-party apt lists (Google Chrome - # among them) that this job does not use, and a transient - # `Hash Sum mismatch` on one of them fails the whole update -- which - # killed two cross-build jobs in setup, before a single byte was - # compiled. Dropping the lists this job has no use for is what makes - # the step's failure mean something about this job. - # - # BY CONTENT, NOT BY FILENAME. The first attempt removed - # `google-chrome.list` and the update failed on the same URL: on - # ubuntu-24.04 the runner writes deb822 `.sources` files, so the - # name was a guess and the guess was wrong. - sudo grep -rlE 'dl[.]google[.]com|packages[.]microsoft[.]com' \ - /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -f - 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/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@" 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.9.16.1' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "/tmp/${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" - # A hosted runner's DNS hiccup is not a red build — see - # .github/tools/git_clone_retry.sh for the measurement. - "$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \ - --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 - # The runner image carries third-party apt lists (Google Chrome - # among them) that this job does not use, and a transient - # `Hash Sum mismatch` on one of them fails the whole update -- which - # killed two cross-build jobs in setup, before a single byte was - # compiled. Dropping the lists this job has no use for is what makes - # the step's failure mean something about this job. - # - # BY CONTENT, NOT BY FILENAME. The first attempt removed - # `google-chrome.list` and the update failed on the same URL: on - # ubuntu-24.04 the runner writes deb822 `.sources` files, so the - # name was a guess and the guess was wrong. - sudo grep -rlE 'dl[.]google[.]com|packages[.]microsoft[.]com' \ - /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -f - 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.9.16.1' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "/tmp/${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 - - # mcpp#365. This is the only job with a MinGW cross toolchain, so it is - # the only place the GNU half of resource compilation (windres -O coff, - # because GNU ld cannot consume a .res) can run at all — the Linux e2e - # shards skip it for want of the `mingw-cross` capability. Named - # explicitly for the same reason 102 is. - - name: "e2e: windows resources (windres / COFF)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - bash tests/e2e/198_windows_resources_cross.sh - - # Packaging a Windows program FROM LINUX — and this job is the only - # place that can happen, for the same reason as the two above. - # - # Running it on a Windows runner would prove nothing: the point of - # reading the import table instead of executing the artifact - # (mcpp.pack.binfmt) is precisely that the packaging host need not be - # the target. A same-OS pack cannot tell the two implementations apart. - - name: "e2e: pack a PE from Linux (zip + DLL closure)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - bash tests/e2e/240_pack_pe_zip_cross.sh - - # A LIBRARY package whose legs cross an OS boundary, for the same reason - # as the three above: this is the only job with a MinGW cross toolchain. - # - # It is not redundant with 245 (which covers the fat-package mechanism - # with gnu + musl and therefore runs on every ordinary Linux shard). The - # leg added here changes BINARY FORMAT, and it is the case that proves - # `lib/` has to be keyed by triple rather than by OS: MinGW and MSVC are - # both "windows" and write `libfoo.a` and `foo.lib` respectively. - # - # Without this step the test would carry `# requires: mingw-cross` and - # skip in every job that exists — verified on a developer's machine and - # nowhere else, while the suite reported green. - - name: "e2e: pack a library across an OS boundary (PE leg)" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - bash tests/e2e/248_pack_library_fat_pe_leg.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" diff --git a/.github/workflows/homebrew-publish.yml b/.github/workflows/homebrew-publish.yml deleted file mode 100644 index 858e6e494..000000000 --- a/.github/workflows/homebrew-publish.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: homebrew-publish - -# Tell the Homebrew tap (mcpp-community/homebrew-mcpp) that a release is out. -# -# The tap owns the formula rewrite — it reads the .sha256 sidecars from the -# release and commits the new url/version itself. All this workflow does is -# fire the starting gun, so nothing here needs to know what a formula is. -# -# Triggers on COMPLETION of the `release` workflow rather than on -# `release: published`, for the same reason aur-publish.yml does: release.yml -# creates the GitHub Release in its first job but uploads the macOS / aarch64 -# assets in later jobs, and the tap needs every sidecar to exist. -# -# Requires one repository secret: -# HOMEBREW_TAP_TOKEN — fine-grained PAT scoped to mcpp-community/homebrew-mcpp -# with "Contents: read and write" (repository_dispatch -# is a write-level API). -# -# The secret is OPTIONAL. Without it this workflow logs a notice and exits 0; -# the tap runs the same bump on a daily schedule, so a missing token costs -# freshness (up to 24h), not correctness. That keeps a release from failing -# over a credential the release itself doesn't need. - -on: - workflow_run: - workflows: [release] - types: [completed] - workflow_dispatch: - inputs: - version: - description: "Version to publish (default: [package].version in mcpp.toml)" - required: false - -concurrency: - group: homebrew-publish - cancel-in-progress: false - -jobs: - notify-tap: - runs-on: ubuntu-latest - # On the workflow_run trigger, only proceed if the release actually - # succeeded (skip failed/cancelled release runs). - if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' - steps: - - name: Checkout released commit - uses: actions/checkout@v4 - with: - # workflow_run: the exact commit the release was built from. - # workflow_dispatch: default ref (HEAD of the branch). - ref: ${{ github.event.workflow_run.head_sha || github.ref }} - - - name: Resolve version - id: resolve - run: | - VER="${{ github.event.inputs.version }}" - if [ -z "$VER" ]; then - # mcpp.toml at the released commit carries the right version. - VER=$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/') - fi - [ -n "$VER" ] || { echo "cannot resolve version"; exit 1; } - echo "version=$VER" >> "$GITHUB_OUTPUT" - echo ":: version $VER" - - - name: Ping the tap - env: - TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - VER: ${{ steps.resolve.outputs.version }} - run: | - set -eu - if [ -z "${TAP_TOKEN}" ]; then - echo "::notice::HOMEBREW_TAP_TOKEN is not configured; skipping the ping. mcpp-community/homebrew-mcpp bumps itself on a daily schedule, so ${VER} reaches the tap within 24h." - exit 0 - fi - curl -fsS -X POST \ - -H "Authorization: Bearer ${TAP_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/mcpp-community/homebrew-mcpp/dispatches \ - -d "{\"event_type\":\"mcpp-release\",\"client_payload\":{\"version\":\"${VER}\"}}" - echo ":: dispatched mcpp-release ${VER} to mcpp-community/homebrew-mcpp" - echo "Pinged the Homebrew tap for **${VER}**." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml deleted file mode 100644 index 323dbdefe..000000000 --- a/.github/workflows/openkal-cross.yml +++ /dev/null @@ -1,478 +0,0 @@ -name: openkal cross-build (3 hosts × 3 targets) - -# WHAT THIS WORKFLOW ASSERTS, AND WHY IT IS A MATRIX RATHER THAN A ROW. -# -# `cross-build-test.yml` verifies the crosses served by a PAYLOAD: a toolchain -# whose driver has exactly one target. There the host and the target are joined -# — `x86_64-w64-mingw32-g++` is the Windows cross and nothing else — so one row -# per supported combination is the honest shape. -# -# openkal changes the shape of the question. The target side — the C library, -# the C++ runtime, the platform's own implementation — is a set of PACKAGES in -# the dependency graph, and the compiler is an ordinary retargetable clang. The -# claim that follows is that N hosts × N targets collapses to N implementations -# plus one tool: **the machine doing the building stops being a variable.** -# -# THAT IS A CLAIM, AND CLAIMS OF THIS SHAPE HAVE BEEN WRONG IN THIS -# REPOSITORY. Reaching PE from a Linux host needed four separate repairs, and -# adding the other two hosts found seven more — every one of them a decision -# that had been keyed on which machine was building rather than on which machine -# the output was for: -# -# the link line's three host-shaped branches, only one of which carried -# `--target=`; the `std` module command's Windows branch, which dropped the -# package's own include paths; `cd X && …` not changing the drive in cmd.exe; -# the artefact-format test matching LLVM's `apple` rather than mcpp's `macos`; -# the C++ runtime contract naming a library to link when one was already in -# the objects; `-nostdinc` missing so a host SDK header could be found; and -# `-lgcc` naming GCC's runtime on a link whose compiler is clang. -# -# None of those was visible from one host. So the matrix is the test. -# -# ── The shape ────────────────────────────────────────────────────────────── -# -# THREE build jobs, one per host, each producing THREE artefacts — nine builds. -# THREE run jobs, one per system, each executing the artefact FOR that system -# produced by ALL THREE hosts. -# -# build on Linux build on macOS build on Windows -# run Linux -# run macOS -# run Windows -# -# The diagonal is an ordinary native build. The six off-diagonal cells are -# the claim, and they are what a single-host workflow cannot reach. -# -# THE RUN JOBS INSTALL NOTHING — not mcpp, not a compiler, not a C runtime. -# A program above openkal carries its C library, its C++ runtime and its -# unwinder; what remains is the operating system it was built for. If a -# toolchain step is ever added to one of them because "the program needs it", -# that is the finding rather than the fix. -# -# AND THE ASSERTION IS ON THE OUTPUT, NOT THE EXIT STATUS. The program prints -# four lines, and `unwound: true` is the one a link cannot fake: it says a -# destructor ran while an exception was being carried out of a frame, which -# means the unwinder found this image's own frame descriptions. - -on: - pull_request: - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # No mcpp or xlings version here. `bootstrap-mcpp` owns both, and a second - # statement of them is a second thing to keep in step — the pin check - # (.github/tools/check_version_pins.sh) enforces the ones that exist and would - # not know about a copy in this file. - XLINGS_NON_INTERACTIVE: '1' - # The branch of the openkal packages this change is verified against. - # - # IT WAS `feat/openkal-closure` UNTIL 2026-08-25, LONG AFTER THAT BRANCH - # MERGED. A fixed name here is a pin nobody is reminded to move: the comment - # said "when they are on `main` this becomes `main`" and the moment for that - # passed without anyone reading it again. Every run since was verifying this - # engine against a tree the ecosystem had left behind — and the two - # regressions found today both hid behind exactly this shape, a pin that - # keeps a check green by keeping it out of date. - OPENKAL_BRANCH: main - -jobs: - build: - name: build 3 targets on ${{ matrix.host }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - - { host: linux, runner: ubuntu-24.04 } - - { host: macos, runner: macos-14 } - - { host: windows, runner: windows-2022 } - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - # THE REPOSITORY'S OWN BOOTSTRAP, NOT A SECOND ONE. - # - # This job first wrote its own: fetch xlings, then - # `xlings install mcpp@`. It failed on the very first run: - # - # xlings: version '2026.8.17.1' not found for 'mcpp' - # available: 2026.8.19.4 - # - # `.xlings.json` at this repository's root pins the mcpp that BUILDS mcpp, - # and that pin does not move when mcpp is released — so it names a version - # the index no longer carries, and a bare install inside the checkout - # obeys the pin rather than the argument. `bootstrap-mcpp` already knows - # this (it runs `install_pinned_mcpp.sh`), works on all three systems, and - # shares the cache lineage every other job lands on. - # - # ⇒ Two bootstraps would be two things to keep correct, and the second one - # was wrong within a day of being written. - - uses: ./.github/actions/bootstrap-mcpp - - # THE mcpp UNDER TEST. Everything after this uses the binary this step - # produces; the bootstrapped one above is only what compiles it. - - name: Build the mcpp in this pull request - run: | - set -euo pipefail - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # `--dev` and not `--release`. What is under test is a set of - # decisions about compile and link flags; an optimisation level - # changes none of them and a release self-build is most of the budget - # of a job on a two-core runner. - # REMOVE ANY mcpp ALREADY UNDER `target/` FIRST, so that after the - # build there is exactly ONE and it is the one this step produced. - # - # `target/` is a RESTORED CACHE whose key hashes `src/**`. A source - # change misses the key, `restore-keys` hands back the nearest older - # tree anyway — that is the point of the layered restore — and the - # build then writes its output into a NEW `//` - # directory beside the ones already there. `find … | head -1` returns - # whichever directory the walk reaches first, which is not the newest - # and is not this build's. - # - # MEASURED, LOCALLY AND ON CI. On this developer's own tree the - # command picks `007bc0f2b78f7fa5` while the build just wrote - # `148448dec6c4a7a7`. On CI it picked a stale binary whose ELF - # interpreter names a glibc payload the runner no longer has: - # - # …/bin/mcpp: cannot execute: required file not found (exit 127) - # - # — which reads as a broken build of THIS commit and is nothing of the - # kind. Two runs, same stale fingerprint, after seven green ones: the - # trigger is simply a source change large enough to leave two - # directories in the restored tree. - # - # mtime CANNOT BE THE CRITERION HERE. A cache restore writes every - # file at extraction time, so "newest" is meaningless on exactly the - # tree where this goes wrong — `ls -t | head -1` has picked a stale - # fingerprint in this repository before. Absence can't be faked: - # delete them, and whatever exists afterwards was linked just now. - # - # The objects and BMIs stay cached — this costs one link. - find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) -delete 2>/dev/null || true - "$MCPP" build --dev - # EXACTLY ONE, not "at least one". Two would mean the delete above - # missed a tree and the ambiguity this step exists to remove is back. - # - # NO `mapfile`. This job also runs on macos, whose `/bin/bash` is - # 3.2 and has no such builtin — a bashism here would fail on one row - # of the matrix for a reason unrelated to what the row tests. - find target -type f \( -name 'mcpp' -o -name 'mcpp.exe' \) > /tmp/mcpp-built.txt - COUNT=$(wc -l < /tmp/mcpp-built.txt | tr -d ' ') - if [ "$COUNT" -ne 1 ]; then - echo "::error::expected exactly one built mcpp, found $COUNT" - sed 's/^/ /' /tmp/mcpp-built.txt - exit 1 - fi - BUILT=$(cat /tmp/mcpp-built.txt) - BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT") - echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV" - "$BUILT" --version - - - name: Select the toolchain the openkal packages ask for - run: | - set -euo pipefail - # Install, then select. `toolchain default` names one and does not - # fetch it. - "$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP_UNDER_TEST" toolchain install llvm 22.1.8 - "$MCPP_UNDER_TEST" toolchain default 'llvm@22.1.8' - - # WHAT THIS RUNNER ACTUALLY PROVIDES FOR THE mingw TARGET. On a Windows - # host `openkal-windows`'s build program generates no import libraries - # ("the system's own are present"), so `-lntdll` and its neighbours are - # found by lld only where the host, the sandbox or the payload puts - # them. This job was green with one sandbox lineage and red with the - # next (2026-09-13, PR #629, same sources, same image), which is the - # signature of a dependency on cached state nobody declared. The lines - # below say where the libraries come from, so the next such reading is - # diagnosed from the log rather than from a bisect over caches. - - name: What this host provides for x86_64-w64-windows-gnu - if: matrix.host == 'windows' - run: | - echo "PATH=$PATH" | tr ':' '\n' | head -40 - ls "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs" 2>/dev/null || echo "(no xpkgs dir)" - CLANG=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/22.1.8/bin/clang++.exe 2>/dev/null | head -1) - echo "clang=$CLANG" - [ -n "$CLANG" ] && "$CLANG" --target=x86_64-w64-windows-gnu -print-search-dirs - [ -n "$CLANG" ] && "$CLANG" --target=x86_64-w64-windows-gnu -print-file-name=libntdll.a - [ -n "$CLANG" ] && "$CLANG" --target=x86_64-w64-windows-gnu -print-file-name=libkernel32.a - command -v x86_64-w64-mingw32-gcc gcc 2>/dev/null || true - - - name: The program — one source, three targets - run: | - set -euo pipefail - # THE SEVENTH CALL SITE. `git_clone_retry.sh` was written because a - # runner's DNS hiccup is not a red build, and its own note counts - # "six call sites, one failure mode" — this workflow was not among - # them, and the mode duly arrived here. Measured on this job, - # 2026-08-25: - # - # fatal: unable to access '…/openkal-llvm-runtime/': - # Could not resolve host: github.com - # - # One name that did not resolve ended a 120-minute job in its first - # minute, beside a real failure it had nothing to do with. - "$GITHUB_WORKSPACE/.github/tools/git_clone_retry.sh" \ - --quiet --depth 1 -b "$OPENKAL_BRANCH" \ - https://github.com/mcpplibs/openkal-llvm-runtime "$RUNNER_TEMP/okl" - cd "$RUNNER_TEMP/okl/examples/same-source" - mkdir -p "$RUNNER_TEMP/out" - # The three HOSTED targets. Bare metal is verified by - # `openkal-llvm-runtime`'s own CI under qemu; it has no runner here to - # execute on, and a build-only cell in a workflow whose point is - # running would be the weaker claim. - for t in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do - rm -rf target - "$MCPP_UNDER_TEST" build --target "$t" - a=$(find target -type f \( -name 'openkal-same-source' -o -name 'openkal-same-source.exe' \) | head -1) - [ -n "$a" ] || { echo "::error::$t produced no artefact on ${{ matrix.host }}"; exit 1; } - case "$t" in - x86_64-windows-gnu) cp "$a" "$RUNNER_TEMP/out/windows.exe" ;; - aarch64-macos) cp "$a" "$RUNNER_TEMP/out/macos" ;; - *) cp "$a" "$RUNNER_TEMP/out/linux" ;; - esac - echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes" - done - - - uses: actions/upload-artifact@v4 - with: - name: openkal-built-on-${{ matrix.host }} - path: ${{ runner.temp }}/out/ - if-no-files-found: error - - run: - name: run 3 builds on ${{ matrix.system }} - needs: build - runs-on: ${{ matrix.runner }} - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - include: - - { system: linux, runner: ubuntu-24.04, file: linux } - - { system: macos, runner: macos-14, file: macos } - - { system: windows, runner: windows-2022, file: windows.exe } - defaults: - run: - shell: bash - steps: - # NO checkout AND NO toolchain. This job is the claim: a program built - # above openkal needs the operating system it was built for and nothing - # else. Anything installed here would weaken what a pass means. - - uses: actions/download-artifact@v4 - with: { pattern: openkal-built-on-*, path: art } - - - name: The same program, from all three build hosts - run: | - set -euo pipefail - fail=0 - for host in linux macos windows; do - bin="art/openkal-built-on-$host/${{ matrix.file }}" - echo "──────── built on $host, running on ${{ matrix.system }} ────────" - if [ ! -f "$bin" ]; then - echo "::error::$bin is missing"; fail=1; continue - fi - # The executable bit does not survive an artefact upload. - chmod +x "$bin" || true - # arm64 macOS refuses an unsigned image, so the signature is - # asserted before the run: a failure here is "the linker did not - # ad-hoc sign it", which is a different repair from "it crashed". - if [ "${{ matrix.system }}" = "macos" ]; then - codesign -dv "$bin" 2>&1 | grep -q 'adhoc\|Signature' \ - || { echo "::error::built on $host: no code signature"; fail=1; continue; } - fi - if ! "./$bin" > out.log 2>&1; then - echo "::error::built on $host: it did not run"; cat out.log; fail=1; continue - fi - cat out.log - ok=1 - grep -q 'sorted: 2 4 7' out.log || ok=0 - grep -q 'caught: 42' out.log || ok=0 - # The line a link cannot fake. - grep -q 'unwound: true' out.log || ok=0 - grep -q 'import std over openkal: ok' out.log || ok=0 - [ "$ok" = 1 ] || { echo "::error::built on $host: wrong output"; fail=1; } - done - [ "$fail" = 0 ] || exit 1 - echo "three builds, one system, same four lines" - - # ────────────────────────────────────────────────────────────────── - # The e2e scripts that BUILD the openkal ecosystem, on a runner that - # has what they ask for. - # - # THEY WERE WRITTEN AND THEY WERE NEVER RUN. `285`–`289` declare - # `# requires: llvm`, and the linux e2e shards report - # - # Detected capabilities: elf unix-shell fresh-sandbox gcc - # patchelf pack symlink python3 … - # - # — no `llvm`, on either shard, because the shard workflow never - # installs one. `run_all.sh` exits 0 on a skip, so the suite stayed - # green while the five tests measuring this ecosystem did not run. - # - # run_all.sh's own note says why no token can fix this: a hard-requires - # cannot tell "this runner is misconfigured" from "this platform - # legitimately lacks the capability". The guard has to know which - # runner it is, so it lives in the job — install the capability, then - # assert each script's PASS line actually appeared. Same shape as - # ci-linux-e2e.yml's `baremetal` job, for the same reason. - # ────────────────────────────────────────────────────────────────── - ecosystem-e2e: - name: openkal e2e (the scripts, on a runner that has llvm) - runs-on: ubuntu-24.04 - timeout-minutes: 90 - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/bootstrap-mcpp - - - name: Build the mcpp in this pull request - run: | - set -euo pipefail - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --dev - # NEWEST BY MTIME, NOT FIRST BY DIRECTORY ORDER. `target/` holds one - # directory per fingerprint and the runner restores a cache of it, so - # `find … | head -1` can return a binary an earlier run left behind. - # Measured: it reported the right VERSION STRING — the stale copy was - # built from an earlier push of this same release — while missing the - # last two commits, so one new test passed and two failed for reasons - # that were nowhere in the source. - BUILT=$(find target -type f -name 'mcpp' -printf '%T@ %p\n' \ - | sort -rn | head -1 | cut -d' ' -f2) - [ -n "$BUILT" ] || { echo "::error::mcpp did not build"; exit 1; } - BUILT=$(cd "$(dirname "$BUILT")" && pwd)/$(basename "$BUILT") - echo "MCPP_UNDER_TEST=$BUILT" >> "$GITHUB_ENV" - "$BUILT" --version - - - name: Install what the scripts declare - run: | - set -euo pipefail - "$MCPP_UNDER_TEST" self config --mirror GLOBAL 2>/dev/null || true - # Both, and both are load-bearing: 285 and 291's first half build - # with gcc (a backend running ON a platform, keeping the payload's - # C library), 286-289 and 291's second half with llvm (the whole - # stack from the graph, where openkal-llvm-runtime IS libc++). - "$MCPP_UNDER_TEST" toolchain install gcc 16.1.0 - "$MCPP_UNDER_TEST" toolchain install llvm 22.1.8 - - # THE EMULATORS, OR TWO OF THE SIX MEASURE HALF OF WHAT THEY SAY. - # - # 287 and 288 both end by RUNNING what they built — an aarch64 binary and - # a riscv64 machine image — and both degrade to a SKIP when no emulator - # is here. Measured on this job's first run: 288 printed - # - # SKIP no riscv64 machine emulator here — linking is not booting - # - # and still reached its OK line, so the PASS-line assertion below would - # have called that covered. Linking is not booting, as the script itself - # says. - # - # BOTH homes, for the reason ci-linux-e2e.yml's baremetal job records: - # the shim on PATH dispatches against whichever home owns it, so an - # emulator installed only in the ambient one answers "not installed" when - # mcpp asks. - - name: Install the emulators the last two scripts need - run: | - set -euo pipefail - # The runner image carries third-party apt lists (Google Chrome - # among them) that this job does not use, and a transient - # `Hash Sum mismatch` on one of them fails the whole update -- which - # killed two cross-build jobs in setup, before a single byte was - # compiled. Dropping the lists this job has no use for is what makes - # the step's failure mean something about this job. - # - # BY CONTENT, NOT BY FILENAME. The first attempt removed - # `google-chrome.list` and the update failed on the same URL: on - # ubuntu-24.04 the runner writes deb822 `.sources` files, so the - # name was a guess and the guess was wrong. - sudo grep -rlE 'dl[.]google[.]com|packages[.]microsoft[.]com' \ - /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -f - sudo apt-get update -qq && sudo apt-get install -y -qq qemu-user - "$XLINGS_BIN" install xim:qemu-riscv -y - XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ - "$XLINGS_BIN" install xim:qemu-riscv -y - # Reachable AND runnable, asserted before the tests: without this the - # scripts would simply skip and say so in a line nobody reads. - qemu-aarch64 --version | head -1 - "$XLINGS_BIN" run qemu-system-riscv64 --version 2>/dev/null | head -1 \ - || command -v qemu-system-riscv64 - - - name: The scripts - run: | - set -euo pipefail - export MCPP="$MCPP_UNDER_TEST" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Directly rather than through run_all.sh: it accepts no filter, and - # it exits 0 on a skip — which is the condition this job exists to - # detect. - fail=0 - for t in tests/e2e/285_*.sh tests/e2e/286_*.sh tests/e2e/287_*.sh \ - tests/e2e/288_*.sh tests/e2e/289_*.sh tests/e2e/291_*.sh \ - tests/e2e/292_*.sh tests/e2e/293_*.sh tests/e2e/294_*.sh; do - echo "=== $t ===" - bash "$t" 2>&1 | tee "$(basename "$t").log" || true - rc=${PIPESTATUS[0]} - [ "$rc" = "0" ] || { echo "::error::$t failed (exit $rc)"; fail=1; } - done - [ "$fail" = 0 ] || exit 1 - - - name: Each one RAN - run: | - set -euo pipefail - # THE ASSERTION THIS JOB EXISTS FOR. A zero exit code cannot - # distinguish "passed" from "skipped" — every one of these scripts - # has an early `exit 0` for a capability or an arrangement it did - # not find. The PASS line can. - check() { - grep -qF "$2" "$1".log || { - echo "::error::$1 did not run to its conclusion on the runner that must run it" - tail -5 "$1".log 2>/dev/null | sed 's/^/ /' - return 1 - } - echo " ok $1" - } - fail=0 - check 285_kernel_abi_from_graph_keeps_the_payload_c_library.sh \ - "OK: a graph-supplied kernel interface leaves the payload's C library reachable" || fail=1 - check 286_the_openkal_stack_still_builds.sh \ - "OK: the openkal stack builds, links statically and runs" || fail=1 - check 287_the_openkal_stack_crosses_to_aarch64.sh \ - "OK: the openkal stack crosses to aarch64, supplies its atomics helpers and runs" || fail=1 - # AND IT REACHED THE PARTS THAT NEED A TOOL. Both of 287's last two - # assertions degrade to a SKIP, and the OK line prints either way. - check 287_the_openkal_stack_crosses_to_aarch64.sh \ - "LSE instructions out of" || fail=1 - check 287_the_openkal_stack_crosses_to_aarch64.sh \ - "it runs under qemu-aarch64" || fail=1 - check 288_the_openkal_stack_on_a_machine_with_no_os.sh \ - "OK: openkal runs on a machine with no operating system and no C library" || fail=1 - # 288's name says "runs"; without this it can print that line - # having only linked. - check 288_the_openkal_stack_on_a_machine_with_no_os.sh \ - "it boots" || fail=1 - check 289_one_host_reaches_every_openkal_target.sh \ - "OK: one host reached" || fail=1 - check 291_dynamic_linkage_is_refused_only_when_the_c_library_is_the_graphs.sh \ - "OK: the C library decides whether 'dynamic' can be honoured" || fail=1 - check 292_a_package_that_names_a_layer_does_not_lose_the_targets_compiler.sh \ - "OK: naming a layer changes the system, not the compiler that emits the target" || fail=1 - check 293_the_requested_target_and_the_resolved_one_name_one_os.sh \ - "OK: the requested target and the resolved one name one operating system" || fail=1 - check 294_the_list_answers_what_can_be_built_not_what_has_a_payload.sh \ - "OK: the list answers what can be built, not what has a payload" || fail=1 - [ "$fail" = 0 ] || exit 1 diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml new file mode 100644 index 000000000..7f26294c2 --- /dev/null +++ b/.github/workflows/probe.yml @@ -0,0 +1,11 @@ +name: probe-macos27-libcxx +on: + pull_request: + branches: [ main ] +jobs: + probe: + runs-on: xcode-27 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - run: bash .github/probe/m27.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 425ca1a2f..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,928 +0,0 @@ -name: release - -# Self-host release: bootstrap mcpp from xlings (xim:mcpp), build the -# musl-static artefact via `mcpp pack --target x86_64-linux-musl -o ...`, -# inject xlings into the produced tarball for install.sh consumers, -# smoke-test, upload. - -on: - push: - tags: [ 'v*' ] - workflow_dispatch: - inputs: - tag: - description: 'tag to (re)build — leave blank to derive `v` from mcpp.toml and create the tag automatically' - required: false - -jobs: - build-release: - name: build + upload (linux / x86_64) - runs-on: ubuntu-24.04 - permissions: - contents: write # required to create releases + push tags - timeout-minutes: 60 - env: - # mcpp resolves MCPP_HOME from the binary's location by default, - # but here we want to share toolchains with the bootstrap sandbox, - # so we pin to a known path. - MCPP_HOME: /home/runner/.mcpp - steps: - # fetch-depth: 0 instead of fetch-tags: true — actions/checkout@v4 - # fails on push-tag triggers when both the ref'd tag and - # `fetch-tags: true` are set: - # "Cannot fetch both and refs/tags/vX.Y.Z to refs/tags/vX.Y.Z" - # Full-history fetch covers the resolve-tag step's needs without - # that contention. - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve target tag + commit - id: resolve - # Three trigger shapes converge here: - # 1. push: refs/tags/vX.Y.Z → use that tag, build at its commit - # 2. workflow_dispatch with `tag` input set: - # - tag exists on remote → check it out (rebuild scenario) - # - tag doesn't exist → use current HEAD; gh-release - # creates the tag at that commit on upload - # 3. workflow_dispatch with no input → derive `v` from - # mcpp.toml's [package].version, build at current HEAD; - # gh-release creates the tag. - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - test -n "$VER" || { echo 'failed to read [package].version from mcpp.toml'; exit 1; } - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - # If the tag exists on remote AND we're on workflow_dispatch, - # check it out so we rebuild that exact commit. push-tag runs - # already start at the tag commit. - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - echo "Resolved tag: $TAG (commit $(git rev-parse --short HEAD))" - - # Cache mcpp's sandbox: musl-gcc 15.1 + binutils + glibc + linux-headers - # + patchelf + ninja is ~800 MB on disk; without this every release - # rebuilds from cold install. Key on the workspace manifest so a - # toolchain change in mcpp.toml refreshes the cache. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-release- - - # Cache xlings + xim:mcpp install. - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-release-xl0462- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - # Pin xlings to a known-good version. The upstream install - # script always grabs `latest` (no version override), so we - # download + self-install manually to avoid broken releases. - XLINGS_VERSION: '2026.9.16.1' - run: | - if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "/tmp/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - fi - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Pinned to .xlings.json — a bare `xlings install mcpp` resolves - # "newest in this runner's index copy" and put 0.0.105 (below the - # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Build + pack release artefact (musl static) - id: stage - # Build for the musl-static target, strip the produced ELF, then - # let `mcpp pack` assemble the tarball (binary + top-level wrapper - # + README + LICENSE, contents at archive root). Inject xlings - # afterwards so install.sh consumers get a single self-contained - # bundle. - run: | - TAG="${{ steps.resolve.outputs.tag }}" - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-linux-x86_64.tar.gz" - - # Build first so we can strip the ELF before pack copies it. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build --target x86_64-linux-musl - ARTIFACT=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) - test -n "$ARTIFACT" - file "$ARTIFACT" | grep -q 'statically linked' - # NB: stripping here is pointless — `mcpp pack` below rebuilds the - # binary and overwrites it, which is why every release up to and - # including 2026.7.28.2 shipped an UNSTRIPPED bin/mcpp despite the - # strip that used to live on this line. The payload is slimmed after - # packing instead, in the inject step below. - - # Pack with the freshly-built mcpp (not the bootstrap) so any - # fixes to the pack code path are exercised in the same release - # they ship in. MCPP_HOME is forced so the new binary uses the - # pinned sandbox instead of resolving relative to its own - # location under target/. - MCPP_HOME="$MCPP_HOME" "$ARTIFACT" pack \ - --target x86_64-linux-musl \ - --mode static \ - -o "${TARBALL_NAME}" - - # Inject xlings: extract → add registry/bin/xlings to the wrapper - # dir → re-tar preserving the wrapper. Since 0.0.4 the bundled - # xlings lives at /registry/bin/xlings (= /bin/xlings). - TARBALL="target/dist/${TARBALL_NAME}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - test -f "$TARBALL" - INJECT=$(mktemp -d) - tar -xzf "$TARBALL" -C "$INJECT" - mkdir -p "$INJECT/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "$INJECT/$WRAPPER/registry/bin/xlings" - chmod +x "$INJECT/$WRAPPER/registry/bin/xlings" - # Slim AFTER pack (pack rebuilds bin/mcpp) and BEFORE tar. Asserts - # the result, so a strip that silently stops working fails the - # release instead of quietly shipping a 34.8MB tarball again. - bash .github/tools/slim_linux_payload.sh "$INJECT/$WRAPPER" - (cd "$INJECT" && tar -czf "$GITHUB_WORKSPACE/${TARBALL}" "$WRAPPER") - rm -rf "$INJECT" - - # Stage final dist/ (tarball + sidecars) for upload. - mkdir -p dist - cp "$TARBALL" "dist/${TARBALL_NAME}" - (cd dist && cp "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz") - (cd dist && sha256sum "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz" > SHA256SUMS) - (cd dist && sha256sum "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && sha256sum "mcpp-linux-x86_64.tar.gz" > "mcpp-linux-x86_64.tar.gz.sha256") - - # Top-level install.sh — fetched by `curl | bash`. - cp install.sh dist/install.sh - chmod +x dist/install.sh - - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "tarball=${TARBALL_NAME}" >> $GITHUB_OUTPUT - ls -la dist/ - - - name: Smoke-test the bundled tarball - # Extract to a scratch dir and run mcpp from there with MCPP_HOME - # unset — proves the release artefact is genuinely self-contained. - run: | - VERSION="${{ steps.stage.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - # Wrapper dir inside the tarball matches its stem (mcpp pack - # ties the two together). - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - ROOT="$SMOKE/$WRAPPER" - test -x "$ROOT/bin/mcpp" - test -x "$ROOT/registry/bin/xlings" - test -x "$ROOT/mcpp" - file "$ROOT/bin/mcpp" | grep -q 'statically linked' - env -u MCPP_HOME "$ROOT/bin/mcpp" --version - env -u MCPP_HOME "$ROOT/bin/mcpp" --help | head -10 - # Top-level wrapper reports the same version we're shipping. - env -u MCPP_HOME "$ROOT/mcpp" --version | grep -q "$VERSION" - # MCPP_HOME should auto-resolve to the extracted root. - out=$(env -u MCPP_HOME "$ROOT/bin/mcpp" self env) - echo "$out" | grep -q "MCPP_HOME *= *$ROOT" - - - name: Generate source tarball + xpkg.lua via mcpp publish - # Use the freshly-built mcpp to produce the source tarball + xpkg - # descriptor for mcpp-index. The release tarball wraps its - # contents in a `/` directory so the extract path - # is $PUB/$WRAPPER/bin/mcpp. - run: | - VERSION="${{ steps.stage.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - PUB=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$PUB" - MCPP_BIN="$PUB/$WRAPPER/bin/mcpp" - env -u MCPP_HOME "$MCPP_BIN" publish --dry-run --allow-dirty - test -f "target/dist/mcpp-${VERSION}.tar.gz" - test -f "target/dist/mcpp.lua" - cp "target/dist/mcpp-${VERSION}.tar.gz" dist/ - cp "target/dist/mcpp.lua" dist/ - ls -la dist/ - - - name: Extract release notes from CHANGELOG - id: notes - run: | - TAG="${{ steps.stage.outputs.tag }}" - VERSION="${{ steps.stage.outputs.version }}" - awk -v v="$VERSION" ' - /^## \[/ { - if (in_section) exit - if ($0 ~ "\\[" v "\\]") { in_section=1; next } - } - in_section { print } - ' CHANGELOG.md > dist/RELEASE_NOTES.md || true - if [ ! -s dist/RELEASE_NOTES.md ]; then - echo "(no CHANGELOG entry found for $VERSION)" > dist/RELEASE_NOTES.md - fi - echo "--- RELEASE_NOTES.md ---" - cat dist/RELEASE_NOTES.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.stage.outputs.tag }} - name: ${{ steps.stage.outputs.tag }} - body_path: dist/RELEASE_NOTES.md - draft: false - prerelease: false - files: | - dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz - dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz.sha256 - dist/mcpp-linux-x86_64.tar.gz - dist/mcpp-linux-x86_64.tar.gz.sha256 - dist/install.sh - dist/SHA256SUMS - dist/mcpp-${{ steps.stage.outputs.version }}.tar.gz - dist/mcpp.lua - - build-linux-aarch64: - name: build (linux / aarch64, cross) - runs-on: ubuntu-24.04 - needs: build-release - permissions: - contents: write - timeout-minutes: 70 - steps: - - uses: actions/checkout@v4 - - - name: Install system deps + qemu - run: | - # The runner image carries third-party apt lists (Google Chrome - # among them) that this job does not use, and a transient - # `Hash Sum mismatch` on one of them fails the whole update -- which - # killed two cross-build jobs in setup, before a single byte was - # compiled. Dropping the lists this job has no use for is what makes - # the step's failure mean something about this job. - # - # BY CONTENT, NOT BY FILENAME. The first attempt removed - # `google-chrome.list` and the update failed on the same URL: on - # ubuntu-24.04 the runner writes deb822 `.sources` files, so the - # name was a guess and the guess was wrong. - sudo grep -rlE 'dl[.]google[.]com|packages[.]microsoft[.]com' \ - /etc/apt/sources.list.d/ 2>/dev/null | xargs -r sudo rm -f - sudo apt-get update -qq - sudo apt-get install -y curl git build-essential qemu-user-static - qemu-aarch64-static --version | head -1 - - - name: Resolve tag + version - id: resolve - run: | - VERSION=$(grep -E '^version' mcpp.toml | head -1 | sed 's/.*"\([^"]*\)".*/\1/') - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.9.16.1' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "/tmp/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Bootstrap mcpp + refresh index (latest, GLOBAL) - run: | - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - "$MCPP" --version - - - name: Cross-build mcpp -> aarch64-linux-musl (this release's source) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # "$MCPP", not a bare `mcpp`: the bare form runs the PATH shim, which - # resolves to whatever version xvm has selected — so pinning the - # bootstrap would have looked correct and changed nothing here. - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - # The published bootstrap mcpp predates aarch64 cross-build support - # (the feature landed after the last release), so it resolves the - # x86_64 host musl-gcc for an aarch64 target. Two-stage instead: build - # THIS release's x86_64 mcpp first, then cross-build aarch64 with it. - "$MCPP" build --target x86_64-linux-musl - FRESH=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) - test -n "$FRESH" - "$FRESH" build --target aarch64-linux-musl - BIN=$(find target/aarch64-linux-musl -type f -name mcpp | head -1) - test -n "$BIN" - file "$BIN" | grep -q 'ARM aarch64' - file "$BIN" | grep -q 'statically linked' - echo "MCPP_AARCH64=$GITHUB_WORKSPACE/$BIN" >> "$GITHUB_ENV" - - - name: Package aarch64 release (+ bundle aarch64 xlings) - id: stage - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-linux-aarch64.tar.gz" - WRAPPER="mcpp-${VERSION}-linux-aarch64" - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" - cp "$MCPP_AARCH64" "$STAGING/$WRAPPER/bin/mcpp" - # The binary is aarch64, so the host x86_64 `strip` cannot touch it — - # resolve a cross-capable one. Required, not best-effort: the old - # `|| true` here meant a missing tool silently shipped a fat tarball. - # (slim_linux_payload.sh runs after xlings is staged, below.) - STRIP=$(find "$HOME/.mcpp" -name 'aarch64-linux-musl-strip' -type f 2>/dev/null | head -1) - [ -n "$STRIP" ] || STRIP=$(command -v llvm-strip 2>/dev/null || true) - [ -n "$STRIP" ] || STRIP=$(command -v aarch64-linux-gnu-strip 2>/dev/null || true) - [ -n "$STRIP" ] || { echo "no aarch64-capable strip found"; exit 1; } - echo "aarch64 strip: $STRIP" - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cat > "$STAGING/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "$STAGING/$WRAPPER/mcpp" - # Bundle the aarch64 xlings so install.sh consumers on aarch64 get an - # aarch64 xlings, not the x86_64 bootstrap one. The three literals - # below are pinned to the same version as XLINGS_VERSION; they are - # NOT interpolated from it, so check_version_pins.sh scans for them - # explicitly (they were absent from the old lock-step comment). - XLA="xlings-2026.9.16.1-linux-aarch64.tar.gz" - # NOT fetch_release.sh: this asset is OPTIONAL and the `if` is the - # point — an arch with no prebuilt xlings must fall through quietly, - # while the helper retries a 404 five times before giving up. The one - # flag that matters here is --retry-all-errors: `curl: (52) Empty - # reply from server` is a transport error, so plain --retry does not - # cover it. - if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \ - --connect-timeout 20 --max-time 600 -o "/tmp/$XLA" \ - "https://github.com/openxlings/xlings/releases/download/v2026.9.16.1/$XLA"; then - tar -xzf "/tmp/$XLA" -C /tmp - XLBIN=$(find /tmp/xlings-2026.9.16.1-linux-aarch64 -path '*/bin/xlings' -type f | head -1) - if [ -n "$XLBIN" ]; then - mkdir -p "$STAGING/$WRAPPER/registry/bin" - cp "$XLBIN" "$STAGING/$WRAPPER/registry/bin/xlings" - chmod +x "$STAGING/$WRAPPER/registry/bin/xlings" - fi - fi - # Slim both shipped ELFs with the cross strip resolved above, and - # assert the result (the vendored xlings was 86.9MB unstripped here). - bash .github/tools/slim_linux_payload.sh "$STAGING/$WRAPPER" "$STRIP" - mkdir -p dist - (cd "$STAGING" && tar -czf "$GITHUB_WORKSPACE/dist/${TARBALL_NAME}" "$WRAPPER") - cp "dist/${TARBALL_NAME}" "dist/mcpp-linux-aarch64.tar.gz" - (cd dist && sha256sum "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && sha256sum "mcpp-linux-aarch64.tar.gz" > "mcpp-linux-aarch64.tar.gz.sha256") - echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the aarch64 tarball (qemu) - run: | - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - ver=$(qemu-aarch64-static "$SMOKE/$WRAPPER/bin/mcpp" --version) - echo "$ver"; echo "$ver" | grep -q 'mcpp' - - - name: Upload aarch64 artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-linux-aarch64.tar.gz - dist/mcpp-${{ steps.resolve.outputs.version }}-linux-aarch64.tar.gz.sha256 - dist/mcpp-linux-aarch64.tar.gz - dist/mcpp-linux-aarch64.tar.gz.sha256 - - build-macos: - name: build (macOS / ARM64) - runs-on: macos-15 - needs: build-release - permissions: - contents: write - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve tag - id: resolve - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-macos15-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-macos15-release-xl0462- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.9.16.1' - run: | - if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" \ - "${WORK}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - fi - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Pinned to .xlings.json — a bare `xlings install mcpp` resolves - # "newest in this runner's index copy" and put 0.0.105 (below the - # index floor) into this job. See .github/tools/install_pinned_mcpp.sh. - MCPP=$(bash "$GITHUB_WORKSPACE/.github/tools/install_pinned_mcpp.sh" "$GITHUB_WORKSPACE") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Build mcpp from source (two-stage self-host) - env: - # macOS min-version support: target macOS 14 so the release runs - # on 14.0+ instead of only the runner's OS (the official LLVM - # static libc++ archives are built for macOS 14 — going lower - # needs a custom libc++ build, tracked as follow-up). Needs - # static LLVM libc++ — the system libc++ on older macOS lacks - # LLVM-20-era C++23 symbols (std::print's __is_posix_terminal - # etc.; minos-14 + dynamic libc++ dies at launch on macos-14 CI). - # See xlings .agents/docs/2026-06-05-macos-min-version-support.md. - MACOSX_DEPLOYMENT_TARGET: '14.0' - run: | - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - # Stage 1: the bootstrap mcpp builds this release's source. The - # bootstrap's macOS link path predates the staticStdlib - # implementation (hardcoded -lc++), so stage 1 links the system - # libc++ — fine, it only needs to RUN on this runner. - "$MCPP" build - STAGE1=$(find target -path "*/bin/mcpp" | head -1) - STAGE1=$(cd "$(dirname "$STAGE1")" && pwd)/$(basename "$STAGE1") - "$STAGE1" --version - - # Stage 2: this release's mcpp rebuilds itself — flags.cppm's - # native staticStdlib link produces the static minos-14 binary. - # NOTE: stage 2 lands in a NEW fingerprint directory (its - # fingerprint includes the deployment target; the bootstrap's - # did not) — pick the most recently modified binary, not the - # first find hit. - "$STAGE1" build --no-cache - MCPP_BIN=$(ls -t $(find target -path "*/bin/mcpp" -type f) | head -1) - MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") - test -x "$MCPP_BIN" - file "$MCPP_BIN" - otool -L "$MCPP_BIN" - echo "=== LC_BUILD_VERSION (must be minos 14.0) ===" - otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | head -6 - otool -l "$MCPP_BIN" | grep -A4 LC_BUILD_VERSION | grep -q "minos 14.0" \ - || { echo "FAIL: expected minos 14.0"; exit 1; } - if otool -L "$MCPP_BIN" | grep -q "libc++"; then - echo "FAIL: still linked against system libc++"; exit 1 - fi - "$MCPP_BIN" --version - echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - - - name: Package macOS release - id: stage - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - # Create release layout - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp" - # Strip (Mach-O) - strip "$STAGING/$WRAPPER/bin/mcpp" 2>/dev/null || true - # Copy metadata - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - - # Shell launcher (same as Linux) - cat > "$STAGING/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "$STAGING/$WRAPPER/mcpp" - - # Bundle xlings for install.sh consumers - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "$STAGING/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "$STAGING/$WRAPPER/registry/bin/xlings" - chmod +x "$STAGING/$WRAPPER/registry/bin/xlings" - fi - - # Create tarball - mkdir -p dist - (cd "$STAGING" && tar -czf "$GITHUB_WORKSPACE/dist/${TARBALL_NAME}" "$WRAPPER") - # Versionless alias - cp "dist/${TARBALL_NAME}" "dist/mcpp-macosx-arm64.tar.gz" - # SHA256 - (cd dist && shasum -a 256 "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && shasum -a 256 "mcpp-macosx-arm64.tar.gz" > "mcpp-macosx-arm64.tar.gz.sha256") - - echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the tarball - run: | - VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="${{ steps.stage.outputs.tarball }}" - WRAPPER="${TARBALL_NAME%.tar.gz}" - SMOKE=$(mktemp -d) - tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" - "$SMOKE/$WRAPPER/bin/mcpp" --version - "$SMOKE/$WRAPPER/mcpp" --version | grep -q "$VERSION" - - - name: Upload macOS artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz - dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz.sha256 - dist/mcpp-macosx-arm64.tar.gz - dist/mcpp-macosx-arm64.tar.gz.sha256 - - build-windows: - name: build (Windows / x86_64) - runs-on: windows-latest - needs: build-release - permissions: - contents: write - timeout-minutes: 45 - env: - MCPP_HOME: C:\Users\runneradmin\.mcpp - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve tag - id: resolve - shell: bash - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TAG="v$VER" - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ - && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then - git checkout --detach "refs/tags/$TAG" - fi - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~\.mcpp - key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-release- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~\.xlings - key: xlings-${{ runner.os }}-release-xl0462-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-release-xl0462- - - - name: Bootstrap mcpp via xlings - shell: bash - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '2026.9.16.1' - run: | - # Captured before the `cd` below, in POSIX form: this step never - # returns to the workspace, and GITHUB_WORKSPACE is a backslash - # Windows path that git-bash tools mangle. - REPO_DIR="$(pwd)" - WORK=$(mktemp -d) - zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" - bash "$GITHUB_WORKSPACE/.github/tools/fetch_release.sh" \ - "https://github.com/openxlings/xlings/releases/download/v${XLINGS_VERSION}/${zipfile}" \ - "${WORK}/${zipfile}" - cd "${WORK}" - unzip -q "${zipfile}" - "$WORK/xlings-${XLINGS_VERSION}-windows-x86_64/subos/default/bin/xlings.exe" self install - export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" - echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" - xlings.exe --version - # Pinned + version-scoped lookup. The old `find | head -1` returned - # whichever version the directory walk reached first. - MCPP=$(bash "$REPO_DIR/.github/tools/install_pinned_mcpp.sh" "$REPO_DIR") - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - XLINGS_BIN=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") - echo "XLINGS_BIN=$XLINGS_BIN" >> "$GITHUB_ENV" - echo "XLINGS_BIN_UNIX=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" - echo "XLINGS_XPKGS=$USERPROFILE/.xlings/data/xpkgs" >> "$GITHUB_ENV" - - - name: Build mcpp from source (self-host) - shell: bash - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - - "$MCPP" build - # Pick the NEWEST mcpp.exe, not an arbitrary one: `target/` is - # restored from cache and keeps a directory per build fingerprint, - # so after a version bump the freshly built binary sits alongside - # the previous release's. `find | head -1` returned whichever the - # directory walk hit first — which is how a 0.0.106 build ran the - # 0.0.105 binary and failed 01_help_and_version. - MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" -printf "%T@ %p\n" \ - | sort -rn | head -1 | cut -d" " -f2-) - test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; } - MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") - echo "Self-hosted binary: $MCPP_BIN" - "$MCPP_BIN" --version - echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - - - name: Package Windows release zip - id: stage - shell: bash - run: | - VERSION="${{ steps.resolve.outputs.version }}" - WRAPPER="mcpp-${VERSION}-windows-x86_64" - ZIPNAME="${WRAPPER}.zip" - - STAGING=$(mktemp -d) - mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" - cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe" - - # Windows batch launcher - printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" - cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true - cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true - - # Bundle xlings.exe for install consumers - if [ -f "$XLINGS_BIN_UNIX" ]; then - cp "$XLINGS_BIN_UNIX" "$STAGING/$WRAPPER/registry/bin/xlings.exe" - fi - - # Pack with 7z (available on windows-latest) - mkdir -p dist - (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") - cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" - # Versionless alias - cp "dist/$ZIPNAME" "dist/mcpp-windows-x86_64.zip" - # SHA256 - (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") - (cd dist && sha256sum "mcpp-windows-x86_64.zip" > "mcpp-windows-x86_64.zip.sha256") - - echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke-test the packaged zip - shell: bash - run: | - ZIPNAME="${{ steps.stage.outputs.zipname }}" - WRAPPER="${ZIPNAME%.zip}" - SMOKE=$(mktemp -d) - (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") - "$SMOKE/$WRAPPER/bin/mcpp.exe" --version - "$SMOKE/$WRAPPER/bin/mcpp.exe" --help | head -5 - test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" - test -f "$SMOKE/$WRAPPER/mcpp.bat" - echo "Smoke-test passed" - - - name: Upload Windows artifacts to release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.resolve.outputs.tag }} - files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip - dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip.sha256 - dist/mcpp-windows-x86_64.zip - dist/mcpp-windows-x86_64.zip.sha256 - - # Seal the complete, non-draft release inventory only after every platform - # uploader has finished. The manifest is desired state for downstream - # reconcilers: a rerun may reproduce it byte-for-byte, but may never replace - # it with different bytes for the same tag. - release-manifest: - name: validate + seal release manifest - needs: [build-release, build-linux-aarch64, build-macos, build-windows] - runs-on: ubuntu-24.04 - permissions: - contents: write - timeout-minutes: 20 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve immutable release identity - id: resolve - run: | - if [ "${{ github.event_name }}" = "push" ]; then - TAG="${{ github.ref_name }}" - elif [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - else - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - test -n "$VERSION" || { echo 'failed to read [package].version from mcpp.toml'; exit 1; } - TAG="v$VERSION" - fi - git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG" - COMMIT=$(git rev-list -n 1 "refs/tags/$TAG") - test -n "$COMMIT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - echo "commit=$COMMIT" >> "$GITHUB_OUTPUT" - - - name: Generate, upload once, and refetch manifest - run: | - TAG="${{ steps.resolve.outputs.tag }}" - VERSION="${{ steps.resolve.outputs.version }}" - COMMIT="${{ steps.resolve.outputs.commit }}" - AUDIT_ROOT=$(mktemp -d) - mkdir -p "$AUDIT_ROOT/assets" "$AUDIT_ROOT/publish" - - gh api "repos/${GITHUB_REPOSITORY}/releases/tags/$TAG" \ - > "$AUDIT_ROOT/release.json" - gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ - --dir "$AUDIT_ROOT/assets" - python3 scripts/release/generate_manifest.py \ - --release-json "$AUDIT_ROOT/release.json" \ - --assets-dir "$AUDIT_ROOT/assets" \ - --version "$VERSION" \ - --tag "$TAG" \ - --commit "$COMMIT" \ - --output "$AUDIT_ROOT/publish/mcpp-release.json" - - if [ -f "$AUDIT_ROOT/assets/mcpp-release.json" ]; then - cmp "$AUDIT_ROOT/assets/mcpp-release.json" \ - "$AUDIT_ROOT/publish/mcpp-release.json" - echo "Existing manifest is byte-identical; leaving it untouched." - else - gh release upload "$TAG" \ - "$AUDIT_ROOT/publish/mcpp-release.json" \ - --repo "$GITHUB_REPOSITORY" - fi - - # Do not trust the upload command alone. Fetch the final public - # inventory into a clean directory, recompute every payload digest, - # regenerate desired state, and compare the published bytes. - MANIFEST_VISIBLE=false - for attempt in {1..12}; do - if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/$TAG" \ - --jq '.assets[].name' | grep -Fxq mcpp-release.json; then - MANIFEST_VISIBLE=true - break - fi - echo "Waiting for mcpp-release.json API visibility ($attempt/12)" - sleep 5 - done - test "$MANIFEST_VISIBLE" = true - mkdir -p "$AUDIT_ROOT/final-assets" - gh api "repos/${GITHUB_REPOSITORY}/releases/tags/$TAG" \ - > "$AUDIT_ROOT/final-release.json" - gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ - --dir "$AUDIT_ROOT/final-assets" - test -f "$AUDIT_ROOT/final-assets/mcpp-release.json" - python3 scripts/release/generate_manifest.py \ - --release-json "$AUDIT_ROOT/final-release.json" \ - --assets-dir "$AUDIT_ROOT/final-assets" \ - --version "$VERSION" \ - --tag "$TAG" \ - --commit "$COMMIT" \ - --output "$AUDIT_ROOT/final-expected.json" - cmp "$AUDIT_ROOT/final-assets/mcpp-release.json" \ - "$AUDIT_ROOT/final-expected.json" - cat "$AUDIT_ROOT/final-assets/mcpp-release.json" - - # Publish this release into the xlings ecosystem, after ALL platform builds - # have uploaded their assets and the immutable manifest gate has passed: - # ① mirror binaries → xlings-res/mcpp (GitHub + GitCode) so XLINGS_RES - # downloads resolve on every platform (incl. the CN/GitCode path); - # ② open a PR against openxlings/xim-pkgindex bumping mcpp to this version - # (a maintainer merges it — index git source is not on the critical path). - # These publication steps are required for ecosystem completeness: their - # failure leaves the GitHub Release object available but keeps this workflow - # red, so post-release verification cannot report a complete release. - # Shared vendored scripts live in .github/tools/ (kept in sync with xlings). - publish-ecosystem: - needs: release-manifest - runs-on: ubuntu-latest - # A4 hardening: a single stuck upload once held this job >1h (6h default - # ceiling). The mirror script has per-file timeouts and (post-0.0.89) - # batch-upload + ranged-GET verification — normal runs are minutes; 30 - # is the generous backstop (20 was hit by the old per-asset verify loop). - timeout-minutes: 30 - env: - XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} - XIM_PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Determine version - id: version - run: echo "version=$(awk -F '\"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml)" >> "$GITHUB_OUTPUT" - - - name: Mirror binaries to xlings-res/mcpp (gh + gtc) - if: ${{ env.XLINGS_RES_TOKEN != '' }} - # THIS MUST BE LONGER THAN THE SCRIPT'S OWN LEG DEADLINE, AND FOR - # A LONG TIME IT WAS SHORTER. - # - # `mirror_res.sh` gives GitCode `MIRROR_LEG_DEADLINE_GTC=2400s` because - # that host shapes inbound traffic; this step killed it at 600s, so the - # script's budget could never be spent and the shorter of two - # disagreeing limits always won. Every release since has ended the same - # way — `The action … has timed out after 10 minutes`, GitHub's eight - # assets uploaded in seconds and GitCode's three large ones cut off, - # with only the small `.sha256` files through. v2026.8.25.1 and - # v2026.8.25.2 both, measured. - # - # The paragraph this replaces described a design that no longer - # exists: a per-asset `MIRROR_UPLOAD_TIMEOUT` of 180s. The script's own - # comment records why it went ("the old per-asset cap failed four - # releases in a row") — but this value, calibrated to it, stayed. - # - # 45 > 40 leaves the script room to reach its own deadline and report - # what it abandoned by name, which is the visibility the value exists - # for; the job's `timeout-minutes: 30` above is the outer backstop. - timeout-minutes: 45 - env: - GH_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} - run: | - chmod +x .github/tools/gtc .github/tools/mirror_res.sh - export PATH="$PWD/.github/tools:$PATH" - # A4: BLOCKING. Both mirror hosts serve users (GLOBAL + CN install - # paths); an incomplete mirror must fail here, visibly, instead of - # surfacing as a 404 in the first user's install (or fresh-install CI). - bash .github/tools/mirror_res.sh mcpp "${{ steps.version.outputs.version }}" - - - name: Open index bump PR (xim-pkgindex) - if: ${{ env.XIM_PKGINDEX_TOKEN != '' }} - env: - PKGINDEX_TOKEN: ${{ secrets.XIM_PKGINDEX_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - chmod +x .github/tools/bump_index.sh - # A4: BLOCKING — a missing bump PR means the index never learns the - # release exists and every post-release install of the new version 404s. - bash .github/tools/bump_index.sh mcpp "${{ steps.version.outputs.version }}" - - # Post-release verification (ci-fresh-install) is triggered via its - # `workflow_run: [release]` hook — a platform-generated event that is - # exempt from GITHUB_TOKEN trigger suppression and needs no cross-repo - # PAT. (A PAT-based dispatch step lived here briefly; it never worked — - # XIM_PKGINDEX_TOKEN's resource owner is the index org and cannot cover - # this repository.) From 30948b827f2e540bffd0fbf5df3bdb87d3cf47f1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:29:23 +0800 Subject: [PATCH 2/3] probe round 2: mechanism and candidate fixes --- .github/probe/m27.sh | 71 ++++++++++++++++++------------------- .github/workflows/probe.yml | 1 + 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/.github/probe/m27.sh b/.github/probe/m27.sh index 4321f560d..e1ef4c507 100644 --- a/.github/probe/m27.sh +++ b/.github/probe/m27.sh @@ -1,42 +1,39 @@ #!/usr/bin/env bash -# macOS 27: why libc++ 22's std.cppm loses INFINITY/NAN. One READING per fact. +# macOS 27: libc++ 22 std.cppm loses INFINITY/NAN. Round 2: mechanism and candidate fixes. set +e r() { echo "READING $*"; } -r "os $(sw_vers -productVersion) $(sw_vers -buildVersion) xcode=$(xcodebuild -version | tr '\n' ' ')" -r "xcrun sdk=$(xcrun --show-sdk-path) ver=$(xcrun --show-sdk-version)" -CLT=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -r "clt sdk link=$(readlink $CLT) exists=$([ -d $CLT ] && echo y || echo n) settings=$(plutil -extract Version raw $CLT/SDKSettings.plist 2>/dev/null)" -ls -la /Library/Developer/CommandLineTools/SDKs/ 2>&1 | sed 's/^/READING clt-sdks /' -XSDK=$(xcrun --show-sdk-path) -for s in $CLT $XSDK; do - r "math.h in $s: INFINITY defs: $(grep -rn 'define[[:space:]]*INFINITY' $s/usr/include/math.h $s/usr/include/_math.h $s/usr/include/math 2>/dev/null | head -3 | tr '\n' ' ')" - r "math.h head in $s: $(grep -n 'include\|__MATH' $s/usr/include/math.h 2>/dev/null | head -12 | tr '\n' ' ')" -done -# xlings llvm -curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.9.16.1 >/dev/null 2>&1 -export PATH="$HOME/.xlings/subos/current/bin:$PATH" -xlings install llvm@22.1.8 -y >/dev/null 2>&1 -L=$(ls -d $HOME/.xlings/data/xpkgs/xim-x-llvm/22.1.8) -r "llvm cfg: $(cat $L/bin/clang++.cfg | tr '\n' ' ')" -S=$L/share/libc++/v1/std.cppm +L="$LLVM_ROOT"; SDK=$(xcrun --show-sdk-path); S=$L/share/libc++/v1/std.cppm +r "llvm=$L cfg=$(tr '\n' ' ' < $L/bin/clang++.cfg)" +r "math.h 55-95:"; sed -n 55,95p $SDK/usr/include/math.h | sed 's/^/READING math.h| /' +FH=$($L/bin/clang -print-resource-dir)/include/float.h +r "clang float.h INFINITY region:"; grep -n -B4 -A3 'define INFINITY' $FH | sed 's/^/READING float.h| /' +AF=$(/usr/bin/clang -print-resource-dir)/include/float.h +r "apple float.h INFINITY region:"; grep -n -B4 -A3 'define INFINITY' $AF | sed 's/^/READING apple-float.h| /' W=$(mktemp -d); cd $W -try() { name=$1; shift; out=$("$@" 2>&1); rc=$?; r "$name rc=$rc $(printf '%s' "$out" | grep -m2 'error' | tr '\n' ' ')"; } -try cfg-clt $L/bin/clang++ -std=c++23 -Wno-reserved-module-identifier --precompile $S -o a.pcm -try isysroot-xc $L/bin/clang++ -std=c++23 -Wno-reserved-module-identifier --precompile $S -o b.pcm --sysroot=$XSDK -try no-cfg-xc $L/bin/clang++ --no-default-config -std=c++23 -nostdinc++ -isystem $L/include/c++/v1 -isysroot $XSDK -Wno-reserved-module-identifier --precompile $S -o c.pcm -printf '#include \n#ifndef INFINITY\n#error no INFINITY after cmath\n#endif\n#include \nint main(){}\n' > t.cpp -try hdr-clt $L/bin/clang++ -std=c++23 -fsyntax-only t.cpp -try hdr-xc $L/bin/clang++ -std=c++23 -fsyntax-only t.cpp --sysroot=$XSDK -printf '#include \n#ifndef INFINITY\n#error no INFINITY after math.h\n#endif\nint main(){}\n' > m.c -try c-math-clt $L/bin/clang -fsyntax-only m.c -try c-math-apple /usr/bin/clang -fsyntax-only m.c -try cxx-apple /usr/bin/clang++ -std=c++23 -fsyntax-only t.cpp -$L/bin/clang++ -std=c++23 -E -dD t.cpp 2>/dev/null | grep -n 'INFINITY\|# 1 ".*math' | head -10 | sed 's/^/READING pp-clt /' -$L/bin/clang++ -std=c++23 -H -fsyntax-only t.cpp 2>&1 | grep -i 'math' | head -12 | sed 's/^/READING includes-clt /' -# the released mcpp -xlings install mcpp@2026.9.16.2 -y >/dev/null 2>&1 -M=$(ls $HOME/.xlings/data/xpkgs/xim-x-mcpp/2026.9.16.2/bin/mcpp) -cd $(mktemp -d); "$M" new h >/dev/null 2>&1; cd h -out=$("$M" run 2>&1); rc=$? -r "mcpp-2026.9.16.2 new+run rc=$rc $(printf '%s' "$out" | grep -m3 -E 'error|Hello|sysroot' | tr '\n' ' ')" +BASE=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK -Wno-reserved-module-identifier) +try() { name=$1; shift; out=$("$@" 2>&1); rc=$?; r "$name rc=$rc $(printf '%s' "$out" | grep -m1 'error' | cut -c1-200)"; } +printf 'int main(){ return __has_feature(modules); }\n' > hf.cpp +try hasfeature-modules-in-precompile-of-plain $L/bin/clang++ "${BASE[@]}" -std=c++23 -fsyntax-only hf.cpp +printf 'export module m;\n#if __has_feature(modules)\n#error MODULES_ON\n#endif\n' > hf.cppm +try hasfeature-modules-in-module-interface $L/bin/clang++ "${BASE[@]}" -std=c++23 --precompile hf.cppm -o hf.pcm +try c++23 $L/bin/clang++ "${BASE[@]}" -std=c++23 --precompile $S -o a.pcm +try gnu++23 $L/bin/clang++ "${BASE[@]}" -std=gnu++23 --precompile $S -o b.pcm +try c++23-U-strict $L/bin/clang++ "${BASE[@]}" -std=c++23 -U__STRICT_ANSI__ --precompile $S -o c.pcm +try c++23-inc-math $L/bin/clang++ "${BASE[@]}" -std=c++23 -include math.h --precompile $S -o d.pcm +try c++23-fbuiltin-inf $L/bin/clang++ "${BASE[@]}" -std=c++23 "-DINFINITY=__builtin_inff()" "-DNAN=__builtin_nanf(\"\")" --precompile $S -o e.pcm +try c++26 $L/bin/clang++ "${BASE[@]}" -std=c++26 --precompile $S -o f.pcm +SDK26=/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk +BASE26=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK26 -Wno-reserved-module-identifier) +try c++23-sdk26.5 $L/bin/clang++ "${BASE26[@]}" -std=c++23 --precompile $S -o g.pcm +# a program importing std, end to end, with the variant that works +for std in c++23 gnu++23; do + rm -rf p; mkdir p; cd p + $L/bin/clang++ "${BASE[@]}" -std=$std --precompile $S -o std.pcm >/dev/null 2>&1 + $L/bin/clang++ "${BASE[@]}" -std=$std -c std.pcm -o std.o >/dev/null 2>&1 + printf 'import std;\nint main(){ std::println("inf={} nan={}", std::numeric_limits::infinity(), std::isnan(std::nan(""))); }\n' > m.cpp + $L/bin/clang++ "${BASE[@]}" -std=$std -fmodule-file=std=std.pcm -c m.cpp -o m.o >/dev/null 2>&1 + $L/bin/clang++ --no-default-config -isysroot $SDK -fuse-ld=lld -nostdlib++ m.o std.o $L/lib/libc++.a $L/lib/libc++abi.a -o m >/dev/null 2>&1 + r "program $std: $(./m 2>&1)" + cd .. +done exit 0 diff --git a/.github/workflows/probe.yml b/.github/workflows/probe.yml index 7f26294c2..550741f41 100644 --- a/.github/workflows/probe.yml +++ b/.github/workflows/probe.yml @@ -8,4 +8,5 @@ jobs: timeout-minutes: 40 steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-macos-llvm - run: bash .github/probe/m27.sh From 2201e7f1571222b1247bf531a98d74ec90228d7a Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:36:00 +0800 Subject: [PATCH 3/3] probe round 3: builtin headers in system modules vs macro definitions --- .github/probe/m27.sh | 67 +++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/.github/probe/m27.sh b/.github/probe/m27.sh index e1ef4c507..2a4e75d34 100644 --- a/.github/probe/m27.sh +++ b/.github/probe/m27.sh @@ -1,39 +1,42 @@ #!/usr/bin/env bash -# macOS 27: libc++ 22 std.cppm loses INFINITY/NAN. Round 2: mechanism and candidate fixes. +# macOS 27 round 3: which single flag makes libc++ 22's std module build in strict mode. set +e r() { echo "READING $*"; } -L="$LLVM_ROOT"; SDK=$(xcrun --show-sdk-path); S=$L/share/libc++/v1/std.cppm -r "llvm=$L cfg=$(tr '\n' ' ' < $L/bin/clang++.cfg)" -r "math.h 55-95:"; sed -n 55,95p $SDK/usr/include/math.h | sed 's/^/READING math.h| /' -FH=$($L/bin/clang -print-resource-dir)/include/float.h -r "clang float.h INFINITY region:"; grep -n -B4 -A3 'define INFINITY' $FH | sed 's/^/READING float.h| /' -AF=$(/usr/bin/clang -print-resource-dir)/include/float.h -r "apple float.h INFINITY region:"; grep -n -B4 -A3 'define INFINITY' $AF | sed 's/^/READING apple-float.h| /' +L="$LLVM_ROOT"; SDK=$(xcrun --show-sdk-path); S=$L/share/libc++/v1/std.cppm; SC=$L/share/libc++/v1/std.compat.cppm W=$(mktemp -d); cd $W -BASE=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK -Wno-reserved-module-identifier) -try() { name=$1; shift; out=$("$@" 2>&1); rc=$?; r "$name rc=$rc $(printf '%s' "$out" | grep -m1 'error' | cut -c1-200)"; } -printf 'int main(){ return __has_feature(modules); }\n' > hf.cpp -try hasfeature-modules-in-precompile-of-plain $L/bin/clang++ "${BASE[@]}" -std=c++23 -fsyntax-only hf.cpp -printf 'export module m;\n#if __has_feature(modules)\n#error MODULES_ON\n#endif\n' > hf.cppm -try hasfeature-modules-in-module-interface $L/bin/clang++ "${BASE[@]}" -std=c++23 --precompile hf.cppm -o hf.pcm -try c++23 $L/bin/clang++ "${BASE[@]}" -std=c++23 --precompile $S -o a.pcm -try gnu++23 $L/bin/clang++ "${BASE[@]}" -std=gnu++23 --precompile $S -o b.pcm -try c++23-U-strict $L/bin/clang++ "${BASE[@]}" -std=c++23 -U__STRICT_ANSI__ --precompile $S -o c.pcm -try c++23-inc-math $L/bin/clang++ "${BASE[@]}" -std=c++23 -include math.h --precompile $S -o d.pcm -try c++23-fbuiltin-inf $L/bin/clang++ "${BASE[@]}" -std=c++23 "-DINFINITY=__builtin_inff()" "-DNAN=__builtin_nanf(\"\")" --precompile $S -o e.pcm -try c++26 $L/bin/clang++ "${BASE[@]}" -std=c++26 --precompile $S -o f.pcm -SDK26=/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk -BASE26=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK26 -Wno-reserved-module-identifier) -try c++23-sdk26.5 $L/bin/clang++ "${BASE26[@]}" -std=c++23 --precompile $S -o g.pcm -# a program importing std, end to end, with the variant that works -for std in c++23 gnu++23; do - rm -rf p; mkdir p; cd p - $L/bin/clang++ "${BASE[@]}" -std=$std --precompile $S -o std.pcm >/dev/null 2>&1 - $L/bin/clang++ "${BASE[@]}" -std=$std -c std.pcm -o std.o >/dev/null 2>&1 - printf 'import std;\nint main(){ std::println("inf={} nan={}", std::numeric_limits::infinity(), std::isnan(std::nan(""))); }\n' > m.cpp - $L/bin/clang++ "${BASE[@]}" -std=$std -fmodule-file=std=std.pcm -c m.cpp -o m.o >/dev/null 2>&1 - $L/bin/clang++ --no-default-config -isysroot $SDK -fuse-ld=lld -nostdlib++ m.o std.o $L/lib/libc++.a $L/lib/libc++abi.a -o m >/dev/null 2>&1 - r "program $std: $(./m 2>&1)" +BASE=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK -Wno-reserved-module-identifier -std=c++23) +try() { name=$1; shift; out=$("$@" 2>&1); rc=$?; r "$name rc=$rc $(printf '%s' "$out" | grep -m1 -E 'error|warning' | cut -c1-220)"; } +r "modulemap infinity_nan: $(grep -n -A3 'infinity_nan' $($L/bin/clang -print-resource-dir)/include/module.modulemap | tr '\n' ' ')" +r "driver cc1 builtin-headers: $($L/bin/clang++ "${BASE[@]}" -### -fsyntax-only -x c++ /dev/null 2>&1 | tr ' ' '\n' | grep -i 'builtin-headers\|fmodules' | tr '\n' ' ')" +try baseline $L/bin/clang++ "${BASE[@]}" --precompile $S -o a.pcm +try builtin-in-sysmodules $L/bin/clang++ "${BASE[@]}" -Xclang -fbuiltin-headers-in-system-modules --precompile $S -o b.pcm +try no-modulemaps $L/bin/clang++ "${BASE[@]}" -fno-implicit-module-maps --precompile $S -o c.pcm +try need-inf-nan $L/bin/clang++ "${BASE[@]}" -D__need_infinity_nan --precompile $S -o d.pcm +try defs-sdk-spelling $L/bin/clang++ "${BASE[@]}" "-DINFINITY=HUGE_VALF" "-DNAN=__builtin_nanf(\"0x7fc00000\")" --precompile $S -o e.pcm +# the winner must also give std.compat and a working program, and must not warn in plain TUs +printf '#include \n#include \n#include \n#include \nint main(){ return INFINITY > 0 && NAN != NAN ? 0 : 1; }\n' > p.cpp +printf '#include \n#include \nint main(void){ return INFINITY > 0 && NAN != NAN ? 0 : 1; }\n' > p.c +for v in "builtin-in-sysmodules:-Xclang -fbuiltin-headers-in-system-modules" "defs-sdk-spelling:-DINFINITY=HUGE_VALF -DNAN=__builtin_nanf(\"0x7fc00000\")"; do + name=${v%%:*}; flags=${v#*:} + eval "F=($flags)" + try "$name plain-c++ -Werror" $L/bin/clang++ "${BASE[@]}" "${F[@]}" -Werror -Wall p.cpp -o pcpp + try "$name plain-c -Werror" $L/bin/clang --no-default-config -isysroot $SDK "${F[@]}" -std=c17 -Werror -Wall p.c -o pc + try "$name std.compat" $L/bin/clang++ "${BASE[@]}" "${F[@]}" -fmodule-file=std=a.pcm --precompile $SC -o sc.pcm + rm -rf prog; mkdir prog; cd prog + $L/bin/clang++ "${BASE[@]}" "${F[@]}" --precompile $S -o std.pcm >/dev/null 2>&1 + $L/bin/clang++ "${BASE[@]}" "${F[@]}" -c std.pcm -o std.o >/dev/null 2>&1 + printf 'export module mm;\nimport std;\n#include \nexport double inf() { return INFINITY; }\n' > mm.cppm + $L/bin/clang++ "${BASE[@]}" "${F[@]}" -fmodule-file=std=std.pcm --precompile mm.cppm -o mm.pcm 2> mm.err + $L/bin/clang++ "${BASE[@]}" "${F[@]}" -c mm.pcm -o mm.o >/dev/null 2>&1 + printf 'import std;\nimport mm;\nint main(){ std::println("inf={} nan={}", inf(), std::isnan(std::nan(""))); }\n' > m.cpp + $L/bin/clang++ "${BASE[@]}" "${F[@]}" -fmodule-file=std=std.pcm -fmodule-file=mm=mm.pcm -c m.cpp -o m.o 2> m.err + $L/bin/clang++ --no-default-config -isysroot $SDK -fuse-ld=lld -nostdlib++ m.o mm.o std.o $L/lib/libc++.a $L/lib/libc++abi.a -o m 2> l.err + r "$name program: $(./m 2>&1) | $(head -1 mm.err) $(head -1 m.err) $(head -1 l.err)" cd .. done +# same flags on the macOS 26.5 SDK: must stay green +SDK26=/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk +B26=(--no-default-config -nostdinc++ -isystem $L/include/c++/v1 -isysroot $SDK26 -Wno-reserved-module-identifier -std=c++23) +try sdk26-builtin-in-sysmodules $L/bin/clang++ "${B26[@]}" -Xclang -fbuiltin-headers-in-system-modules --precompile $S -o f.pcm +try sdk26-plain-c++-Werror $L/bin/clang++ "${B26[@]}" -Xclang -fbuiltin-headers-in-system-modules -Werror -Wall p.cpp -o p26 exit 0