2026.9.8.1: a package's host modules are ordered by what they import, and the design document aligned with what shipped #2013
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci-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 <iostream> | |
| #include <format> | |
| 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 <cstdio> | |
| 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 `<stem>.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 |