SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 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: | |
| ios-host-surface: | |
| name: iOS - what this runner actually provides | |
| runs-on: macos-15 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-macos-llvm | |
| # THE PREMISE, STATED AND THEN MEASURED. The design record schedules the | |
| # iOS rows on one assumption: that a GitHub macOS runner ships both an | |
| # iOS SDK and a bootable simulator. If it ships the SDK but no simulator, | |
| # the device row is still verifiable here and the two simulator rows | |
| # become a local-only claim -- which changes the tier they can reach and | |
| # nothing else. This step is the difference between those two worlds. | |
| # EVERY MEASUREMENT STEP CONTINUES, because the first version of this job | |
| # did not and the four steps after the failing one were SKIPPED -- so one | |
| # unmet assumption cost the whole measurement. A probe's value is the | |
| # complete set of answers; a probe that stops at the first `no` is a | |
| # build, not a measurement. (GitHub runs `run:` under `bash -e`, which is | |
| # what turned a failing clang into a terminated step.) | |
| - 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 | |
| # WHETHER A BARE MACH-O CAN BE RUN AT ALL, which decides how much the | |
| # runner program has to do. `simctl launch` needs an installed .app | |
| # bundle; `simctl spawn` takes an executable. If spawn works, the | |
| # `simctl-run` program is a boot-and-spawn wrapper; if it does not, it | |
| # has to synthesise a bundle, sign it and install it. Measuring this is | |
| # cheaper than designing for the harder case. | |
| # THE PAYLOAD'S CLANG READS A CONFIG FILE, AND THAT CONFIG NAMES A | |
| # DIFFERENT SDK. First run, with `-isysroot <iPhoneSimulator18.5.sdk>` | |
| # on the command line: | |
| # | |
| # 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) | |
| # | |
| # mcpp's own post-install writes the located macOS SDK into the | |
| # payload's `clang++.cfg` so that a native macOS build is deterministic, | |
| # and that cfg is read before the command line for search purposes. So | |
| # an Apple CROSS target has to suppress it -- which mcpp's cross path | |
| # already does (`--no-default-config` in hostflags.cppm), and which this | |
| # hand-written probe did not. Printed here because the explanation | |
| # belongs next to the measurement. | |
| - 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 | |
| - 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 <cstdio> | |
| 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 DEVICE ROW'S ARTEFACT. Nothing runs it here -- that needs a | |
| # signature the developer owns -- so the claim is about the ARTEFACT: | |
| # arm64 Mach-O naming the iOS platform in LC_BUILD_VERSION. An artefact | |
| # that says MACOS there is the failure this leg exists to catch, and it | |
| # is invisible to a build that merely succeeds. | |
| - name: "Device: the artefact names the iOS platform" | |
| continue-on-error: true | |
| run: | | |
| set -euo pipefail | |
| cat > /tmp/dev.cpp << 'CPP' | |
| #include <cstdio> | |
| int main() { std::puts("1-2-3"); return 0; } | |
| CPP | |
| SDK=$(xcrun --sdk iphoneos --show-sdk-path) | |
| "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ | |
| -target arm64-apple-ios18.0 \ | |
| -isysroot "$SDK" -o /tmp/dev /tmp/dev.cpp | |
| file /tmp/dev | |
| otool -l /tmp/dev | grep -A5 LC_BUILD_VERSION | |
| # THE C++ RUNTIME QUESTION, WHICH IS THE ONE THAT CAN SINK THIS. 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 -- so iOS may have to take | |
| # libc++ from the SDK instead. Both are tried here, because "which one | |
| # works" is the fact the flag builder needs and neither answer can be | |
| # reasoned out from a Linux desk. | |
| - 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 <string> | |
| #include <cstdio> | |
| 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 | |
| # `import std` FOR iOS, which is mcpp's default and therefore the real | |
| # bar. The module is precompiled from the PAYLOAD's libc++ headers | |
| # against the LOCATED SDK's C library -- the same split macOS already | |
| # uses, with a second SDK. If this cannot be made to work the iOS rows | |
| # are a non-module tier and the documentation has to say so. | |
| - name: "import std: precompile against the located SDK" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| SDK=$(xcrun --sdk iphoneos --show-sdk-path) | |
| STD_CPPM=$(find "$LLVM_ROOT" -name 'std.cppm' | head -1) | |
| echo "std.cppm=$STD_CPPM" | |
| MODDIR=$(dirname "$STD_CPPM") | |
| if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ | |
| -target arm64-apple-ios18.0 \ | |
| -isysroot "$SDK" -Wno-reserved-module-identifier \ | |
| -Xclang -emit-reduced-module-interface \ | |
| --precompile -o /tmp/std.pcm "$STD_CPPM" -I"$MODDIR"; then | |
| echo "PRECOMPILE-OK" | |
| else | |
| echo "PRECOMPILE-FAILED" | |
| fi | |
| cat > /tmp/mod.cpp << 'CPP' | |
| import std; | |
| int main() { std::println("1-2-3"); return 0; } | |
| CPP | |
| if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ | |
| -target arm64-apple-ios18.0 \ | |
| -isysroot "$SDK" -fmodule-file=std=/tmp/std.pcm \ | |
| -o /tmp/mod /tmp/mod.cpp /tmp/std.pcm; then | |
| echo "MODULE-LINK-OK" | |
| else | |
| echo "MODULE-LINK-FAILED" | |
| fi | |
| file /tmp/mod || true | |
| set +x | |
| 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" | |
| # THE THREE ROWS ARE `planned`, AND THAT IS THE STATE THIS JOB EXISTS TO | |
| # CHANGE. A planned row is refused with its own escape hatch named: | |
| # | |
| # error: target 'aarch64-ios' is registered but not yet supported | |
| # (planned) -- An explicit [target.aarch64-ios] toolchain | |
| # override can opt in early. | |
| # | |
| # so the fixture writes that override. Once this job is green the tiers | |
| # move and the override becomes unnecessary; keeping it until then is | |
| # what makes the measurement possible before the claim is made. | |
| - 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" | |
| [target.aarch64-ios] | |
| toolchain = "llvm@22.1.8" | |
| [target.aarch64-ios-sim] | |
| toolchain = "llvm@22.1.8" | |
| [target.x86_64-ios-sim] | |
| toolchain = "llvm@22.1.8" | |
| # D4: 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 engine learns nothing | |
| # about simulators, which is the boundary it keeps for every other | |
| # emulated target. | |
| # | |
| # The device row keeps `runner` unset: an artefact cannot be run off | |
| # an iOS device without a signature the developer owns. | |
| # | |
| # THE TOOL IS DECLARED AT THE TOP LEVEL HERE AND NOT IN | |
| # examples/13, and the difference is which hosts the manifest has to | |
| # work on. `deps` is not conditional on a target -- measured: | |
| # "[target.aarch64-ios-sim.xlings] does not accept 'deps'" -- and | |
| # `xim:apple-simulator-tools` exists for macosx alone, so an | |
| # unconditional declaration breaks a Linux build. This fixture is | |
| # macOS-only, so it can say it. | |
| [xlings] | |
| deps = ["xim:apple-simulator-tools"] | |
| TOML | |
| cat >> /tmp/iostest/mcpp.toml << 'TOML' | |
| [target.aarch64-ios-sim] | |
| runner = ["simctl-run"] | |
| TOML | |
| cat > /tmp/iostest/src/main.cpp << 'CPP' | |
| import std; | |
| int main() { | |
| std::vector<int> v{3, 1, 2}; | |
| std::ranges::sort(v); | |
| std::print("{}-{}-{}\n", v[0], v[1], v[2]); | |
| } | |
| CPP | |
| cat /tmp/iostest/mcpp.toml | |
| # D1/D2: THE DEVICE ROW'S ARTEFACT. Nothing runs it -- that needs a | |
| # signature the developer owns -- so the claim is about the artefact: | |
| # arm64 Mach-O naming the iOS platform and the project's deployment | |
| # target in LC_BUILD_VERSION. A build that merely succeeds cannot tell | |
| # those apart from a macOS binary. | |
| - name: "D1: mcpp build --target aarch64-ios" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| cd /tmp/iostest | |
| "$MCPP_DEV" build --target aarch64-ios | |
| set +x | |
| ART=$(find /tmp/iostest/target/aarch64-ios -name iostest -type f | head -1) | |
| echo "artefact=$ART" | |
| file "$ART" | |
| otool -l "$ART" | grep -A5 LC_BUILD_VERSION | |
| # D4's PREMISE: whether `simctl spawn` takes a bare Mach-O. If it does, | |
| # the `simctl-run` program in xim:apple-simulator-tools is a | |
| # boot-and-spawn wrapper; if it does not, it has to synthesise a bundle, | |
| # sign it and install it. This is the measurement that decides which. | |
| - name: "D4: mcpp build --target aarch64-ios-sim, then simctl spawn" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| cd /tmp/iostest | |
| "$MCPP_DEV" build --target aarch64-ios-sim | |
| set +x | |
| ART=$(find /tmp/iostest/target/aarch64-ios-sim -name iostest -type f | head -1) | |
| echo "artefact=$ART" | |
| file "$ART" | |
| otool -l "$ART" | grep -A5 LC_BUILD_VERSION | |
| 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 | |
| if xcrun simctl spawn "$UDID" "$ART"; then | |
| echo "SPAWN-OK" | |
| else | |
| echo "SPAWN-FAILED exit=$?" | |
| fi | |
| # D4: `mcpp run` THROUGH THE RUNNER, WHICH IS THE SUPPORTED PATH. | |
| # | |
| # The step above spawned the artefact by hand, which proves the platform | |
| # can run it and not that mcpp can. This repository has a standing note | |
| # that publishing and verifying an asset is not the same as being able to | |
| # use it through a supported path, and the difference here is a `runner` | |
| # a manifest declares and a program a package provides. | |
| - name: "D4: mcpp run --target aarch64-ios-sim prints 1-2-3" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| cd /tmp/iostest | |
| out=$("$MCPP_DEV" run --target aarch64-ios-sim 2>&1) || true | |
| set +x | |
| printf '%s\n' "$out" | tail -20 | |
| if printf '%s\n' "$out" | grep -q '1-2-3'; then | |
| echo "RUN-THROUGH-RUNNER-OK" | |
| else | |
| echo "RUN-THROUGH-RUNNER-FAILED" | |
| fi | |
| # x86_64-ios-sim IS THE THIRD ROW AND THIS RUNNER CANNOT RUN IT. | |
| # | |
| # The simulator runs the HOST's architecture, so an Apple-silicon machine | |
| # has no x86_64 iOS runtime to spawn into. What can be measured here is | |
| # the ARTEFACT, which is the claim the row's tier will rest on; a machine | |
| # with an Intel host is what would move it further. | |
| - name: "x86_64-ios-sim: the artefact, on a host that cannot run it" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| cd /tmp/iostest | |
| "$MCPP_DEV" build --target x86_64-ios-sim | |
| set +x | |
| ART=$(find /tmp/iostest/target/x86_64-ios-sim -name iostest -type f | head -1) | |
| file "$ART" | |
| otool -l "$ART" | grep -A5 LC_BUILD_VERSION | |
| # THE HOST SURFACE IS BOUNDED, AND ITS ABSENCE NAMES THE SDK. The | |
| # recorded rule is that a host dependency must be minimal, named, and | |
| # never a fallthrough -- so the refusal is a claim like any other, and | |
| # `SDKROOT` pointing at a macOS SDK is the cheapest way to make the iOS | |
| # SDK unlocatable without breaking the rest of the machine. | |
| # | |
| # AND NOT BY POINTING `DEVELOPER_DIR` AT NOTHING, WHICH MEASURED NOTHING. | |
| # | |
| # The first version of this step set `DEVELOPER_DIR=/nonexistent` and | |
| # expected the refusal. The build SUCCEEDED: `xcrun` ignores an invalid | |
| # developer directory and falls back to the recorded one, so the | |
| # environment change did not make the SDK unlocatable. The predicate was | |
| # right and the object was wrong -- the claim was never tested. | |
| # | |
| # The claim now lives where the SDK is genuinely absent, which is every | |
| # non-Apple host: `tests/e2e/641` asserts it on Linux and Windows, with | |
| # an explicit toolchain override so the tier gate does not answer first | |
| # and `MCPP_NO_AUTO_INSTALL=1` so no payload is needed. What is left for | |
| # this runner is the other half of the same claim -- that a machine which | |
| # DOES have the SDK is not refused -- and that is what the steps above | |
| # measure by building. | |
| - name: "D: the SDK is located here, which is the other half of the claim" | |
| continue-on-error: true | |
| run: | | |
| set -x | |
| xcrun --sdk iphoneos --show-sdk-path | |
| xcrun --sdk iphonesimulator --show-sdk-path | |
| set +x | |
| echo "both SDKs located; the refusal for a machine without them is" | |
| echo "asserted by tests/e2e/641 on every non-Apple host" | |
| cd /tmp/iostest && "$MCPP_DEV" build --target aarch64-ios 2>&1 | tail -3 |