Skip to content

SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web #5

SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web

SDK toolchains, the payload/engine seam, and openkal across iOS, Android and Web #5

Workflow file for this run

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. A tool declaration 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.workspace]` and not `[xlings] deps`, which mcpp reports as
# superseded: "deps is superseded by [xlings.workspace] and will stop
# being read. It is honoured for now."
[xlings.workspace]
"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