|
| 1 | +name: ci-macos-ios |
| 2 | + |
| 3 | +# The iOS rows, measured on the only machine that can answer for them. |
| 4 | +# |
| 5 | +# iOS needs an ecosystem compiler and a LOCATED SDK: `xim:llvm` emits arm64 |
| 6 | +# Mach-O for an iOS deployment target, and only the machine's Xcode can supply |
| 7 | +# the iPhoneOS / iPhoneSimulator headers and stub libraries, which are not |
| 8 | +# redistributable. The simulator runtime is the same category. So every claim |
| 9 | +# about these three rows is a claim about a macOS runner, and this job is where |
| 10 | +# they are made. |
| 11 | +# |
| 12 | +# Kept out of ci-macos.yml deliberately: that job asserts mcpp's default quiet |
| 13 | +# output shape and tacking a differently-shaped leg onto it has broken that |
| 14 | +# assertion before. |
| 15 | + |
| 16 | +on: |
| 17 | + push: |
| 18 | + branches: [ main ] |
| 19 | + pull_request: |
| 20 | + branches: [ main ] |
| 21 | + workflow_dispatch: |
| 22 | + |
| 23 | +concurrency: |
| 24 | + group: ci-macos-ios-${{ github.ref }} |
| 25 | + cancel-in-progress: true |
| 26 | + |
| 27 | +jobs: |
| 28 | + # THE PREMISES, AS A PROBE THAT RUNS ON REQUEST. |
| 29 | + # |
| 30 | + # This job measured what the iOS rows were scheduled on: that a GitHub macOS |
| 31 | + # runner ships both SDKs and a bootable simulator, that `simctl spawn` takes a |
| 32 | + # bare Mach-O, that the payload's `clang++.cfg` names the macOS SDK, and |
| 33 | + # which C++ runtime an iOS link can use. Every answer is now encoded -- in |
| 34 | + # `simctl-run`, in `--no-default-config` on the Apple cross path, and in the |
| 35 | + # MachO contract cell -- and asserted by `ios-engine` below. |
| 36 | + # |
| 37 | + # Every step continues on error, because a probe's value is the complete set |
| 38 | + # of answers. That is also why it does not run on every change: a job that |
| 39 | + # cannot fail shown as a green check beside the gate reads as a second gate. |
| 40 | + # It stays available for the day a runner image changes one of the premises. |
| 41 | + ios-host-surface: |
| 42 | + name: iOS - what this runner actually provides |
| 43 | + if: github.event_name == 'workflow_dispatch' |
| 44 | + runs-on: macos-15 |
| 45 | + timeout-minutes: 30 |
| 46 | + steps: |
| 47 | + - uses: actions/checkout@v4 |
| 48 | + - uses: ./.github/actions/setup-macos-llvm |
| 49 | + |
| 50 | + - name: "Host surface: the two located SDKs and the simulator runtime" |
| 51 | + continue-on-error: true |
| 52 | + run: | |
| 53 | + set -x |
| 54 | + xcode-select -p |
| 55 | + xcrun --sdk iphoneos --show-sdk-path |
| 56 | + xcrun --sdk iphoneos --show-sdk-version |
| 57 | + xcrun --sdk iphonesimulator --show-sdk-path |
| 58 | + xcrun --sdk iphonesimulator --show-sdk-version |
| 59 | + set +x |
| 60 | + echo "--- runtimes ---" |
| 61 | + xcrun simctl list runtimes |
| 62 | + echo "--- devices available ---" |
| 63 | + xcrun simctl list devices available |
| 64 | +
|
| 65 | + # THE PAYLOAD'S CLANG READS A CONFIG FILE, AND THAT CONFIG NAMES A |
| 66 | + # DIFFERENT SDK. Measured with `-isysroot <iPhoneSimulator18.5.sdk>` on |
| 67 | + # the command line and the cfg not suppressed: |
| 68 | + # |
| 69 | + # ld64.lld: error: /Library/Developer/CommandLineTools/SDKs/ |
| 70 | + # MacOSX.sdk/usr/lib/libc++.tbd(/usr/lib/libc++.1.dylib) is |
| 71 | + # incompatible with arm64 (iOS Simulator18.0.0) |
| 72 | + # |
| 73 | + # which is why mcpp's Apple cross path carries `--no-default-config`. |
| 74 | + - name: "The payload's default config, which is why --no-default-config" |
| 75 | + continue-on-error: true |
| 76 | + run: | |
| 77 | + set -x |
| 78 | + ls -la "$LLVM_ROOT/bin/"*.cfg || true |
| 79 | + for f in "$LLVM_ROOT/bin/"*.cfg; do echo "=== $f"; cat "$f"; done || true |
| 80 | +
|
| 81 | + # WHETHER A BARE MACH-O CAN BE RUN AT ALL, which decided how much the |
| 82 | + # runner program has to do: `simctl launch` needs an installed .app, |
| 83 | + # `simctl spawn` takes an executable. |
| 84 | + - name: "Device: is one bootable, and does spawn take a bare executable" |
| 85 | + continue-on-error: true |
| 86 | + run: | |
| 87 | + set -uo pipefail |
| 88 | + UDID=$(xcrun simctl list devices available \ |
| 89 | + | grep -A50 -- '-- iOS' \ |
| 90 | + | grep -m1 -oE '[0-9A-F]{8}-[0-9A-F-]{27}' || true) |
| 91 | + echo "udid=[$UDID]" |
| 92 | + if [ -z "$UDID" ]; then |
| 93 | + echo "NO-IOS-SIMULATOR-DEVICE" |
| 94 | + exit 0 |
| 95 | + fi |
| 96 | + xcrun simctl boot "$UDID" || true |
| 97 | + xcrun simctl bootstatus "$UDID" -b 2>&1 | tail -3 || true |
| 98 | + cat > /tmp/hello.cpp << 'CPP' |
| 99 | + #include <cstdio> |
| 100 | + int main() { std::puts("1-2-3"); return 0; } |
| 101 | + CPP |
| 102 | + SDK=$(xcrun --sdk iphonesimulator --show-sdk-path) |
| 103 | + if ! "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ |
| 104 | + -target arm64-apple-ios18.0-simulator \ |
| 105 | + -isysroot "$SDK" -o /tmp/hello /tmp/hello.cpp; then |
| 106 | + echo "SIM-COMPILE-FAILED" |
| 107 | + exit 0 |
| 108 | + fi |
| 109 | + file /tmp/hello |
| 110 | + otool -l /tmp/hello | grep -A5 LC_BUILD_VERSION || true |
| 111 | + echo "--- simctl spawn on a bare Mach-O ---" |
| 112 | + if xcrun simctl spawn "$UDID" /tmp/hello; then |
| 113 | + echo "SPAWN-OK" |
| 114 | + else |
| 115 | + echo "SPAWN-FAILED exit=$?" |
| 116 | + fi |
| 117 | +
|
| 118 | + # THE C++ RUNTIME QUESTION. macOS links the PAYLOAD's static libc++ so |
| 119 | + # that mcpp's deployment floor is real; that archive is built for macOS, |
| 120 | + # and ld64 refuses an object built for one platform in a link for |
| 121 | + # another. Both routes are tried because "which one works" is the fact |
| 122 | + # the contract table needed. |
| 123 | + - name: "C++ runtime: SDK libc++ versus the payload static archive" |
| 124 | + continue-on-error: true |
| 125 | + run: | |
| 126 | + set -x |
| 127 | + SDK=$(xcrun --sdk iphoneos --show-sdk-path) |
| 128 | + cat > /tmp/cxx.cpp << 'CPP' |
| 129 | + #include <string> |
| 130 | + #include <cstdio> |
| 131 | + int main() { std::string s = "1-2-3"; std::puts(s.c_str()); return 0; } |
| 132 | + CPP |
| 133 | + echo "--- (a) SDK libc++, dynamic ---" |
| 134 | + if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ |
| 135 | + -target arm64-apple-ios18.0 \ |
| 136 | + -isysroot "$SDK" -o /tmp/cxx-sdk /tmp/cxx.cpp; then |
| 137 | + otool -L /tmp/cxx-sdk |
| 138 | + echo "SDK-LIBCXX-OK" |
| 139 | + else |
| 140 | + echo "SDK-LIBCXX-FAILED" |
| 141 | + fi |
| 142 | + echo "--- (b) payload static libc++ ---" |
| 143 | + if "$LLVM_ROOT/bin/clang++" -std=c++23 --no-default-config \ |
| 144 | + -target arm64-apple-ios18.0 \ |
| 145 | + -isysroot "$SDK" -nostdlib++ \ |
| 146 | + "$LLVM_ROOT/lib/libc++.a" "$LLVM_ROOT/lib/libc++abi.a" \ |
| 147 | + -o /tmp/cxx-static /tmp/cxx.cpp; then |
| 148 | + echo "PAYLOAD-STATIC-OK" |
| 149 | + else |
| 150 | + echo "PAYLOAD-STATIC-FAILED" |
| 151 | + fi |
| 152 | + set +x |
| 153 | +
|
| 154 | + # THE GATE. Every step here fails the job when its claim does not hold. |
| 155 | + # |
| 156 | + # The first version of this job was a probe too -- every step continued on |
| 157 | + # error -- and it stayed that way after the rows it measured moved to |
| 158 | + # `verified` and `preview`. That left a verified tier with no check that |
| 159 | + # could turn red: a regression in the Apple cross path would have printed |
| 160 | + # `RUN-THROUGH-RUNNER-FAILED` inside a green job. |
| 161 | + ios-engine: |
| 162 | + name: iOS - mcpp builds and the simulator runs it |
| 163 | + runs-on: macos-15 |
| 164 | + timeout-minutes: 40 |
| 165 | + steps: |
| 166 | + - uses: actions/checkout@v4 |
| 167 | + with: |
| 168 | + submodules: recursive |
| 169 | + - uses: ./.github/actions/setup-macos-llvm |
| 170 | + |
| 171 | + - name: Build mcpp from source (self-host) |
| 172 | + run: | |
| 173 | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" |
| 174 | + "$MCPP" build |
| 175 | + echo "MCPP_DEV=$(ls -td "$PWD"/target/*/*/bin/mcpp | head -1)" >> "$GITHUB_ENV" |
| 176 | +
|
| 177 | + # NO TOOLCHAIN IS DECLARED, AND THAT IS WHAT MAKES THIS THE USER'S PATH. |
| 178 | + # |
| 179 | + # While the rows were `planned` the fixture had to write |
| 180 | + # `[target.<row>] toolchain = "llvm@22.1.8"` to get past the tier gate, |
| 181 | + # which meant it measured an override and never the row's own pin. The |
| 182 | + # rows are now `verified` and `preview` and resolve `llvm@22.1.8` by |
| 183 | + # themselves, so the fixture says nothing and the default is what is |
| 184 | + # measured. |
| 185 | + - name: "Fixture: a project that imports std and prints 1-2-3" |
| 186 | + run: | |
| 187 | + set -euo pipefail |
| 188 | + mkdir -p /tmp/iostest/src |
| 189 | + cat > /tmp/iostest/mcpp.toml << 'TOML' |
| 190 | + [package] |
| 191 | + name = "iostest" |
| 192 | + version = "0.1.0" |
| 193 | +
|
| 194 | + [build] |
| 195 | + ios_deployment_target = "18.0" |
| 196 | +
|
| 197 | + # THE RUNNER IS AN ARGV PREFIX AND THE SESSION BELONGS TO A |
| 198 | + # PACKAGE. `simctl-run` comes from `xim:apple-simulator-tools`; it |
| 199 | + # chooses a device, boots it if it is not booted, waits, spawns, and |
| 200 | + # returns the program's own exit status. The device row keeps |
| 201 | + # `runner` unset: an artefact cannot be run off an iOS device without |
| 202 | + # a signature the developer owns. |
| 203 | + [target.aarch64-ios-sim] |
| 204 | + runner = ["simctl-run"] |
| 205 | +
|
| 206 | + # Declared at the top level here and not in examples/13, because a |
| 207 | + # tool declaration is not conditional on a target and this package |
| 208 | + # exists for macOS alone. This fixture is macOS-only, so it can say |
| 209 | + # it; a portable manifest cannot. |
| 210 | + [xlings.workspace] |
| 211 | + "xim:apple-simulator-tools" = "" |
| 212 | + TOML |
| 213 | + cat > /tmp/iostest/src/main.cpp << 'CPP' |
| 214 | + import std; |
| 215 | + int main() { |
| 216 | + std::vector<int> v{3, 1, 2}; |
| 217 | + std::ranges::sort(v); |
| 218 | + std::print("{}-{}-{}\n", v[0], v[1], v[2]); |
| 219 | + } |
| 220 | + CPP |
| 221 | + cat /tmp/iostest/mcpp.toml |
| 222 | +
|
| 223 | + # ONE ASSERTION FOR THE THREE ARTEFACTS. A build that succeeds cannot |
| 224 | + # tell an iOS binary from a macOS one; LC_BUILD_VERSION can. Each |
| 225 | + # value is read from the load command and compared whole, so an empty |
| 226 | + # reading is a failure and not a pass. |
| 227 | + cat > /tmp/assert-artefact.sh << 'SH' |
| 228 | + #!/usr/bin/env bash |
| 229 | + set -uo pipefail |
| 230 | + target=$1 arch=$2 platform=$3 minos=$4 |
| 231 | + arts=(/tmp/iostest/target/"$target"/*/bin/iostest) |
| 232 | + art=${arts[0]} |
| 233 | + if [ ! -f "$art" ]; then |
| 234 | + echo "FAIL: $target produced no artefact under /tmp/iostest/target/$target" |
| 235 | + exit 1 |
| 236 | + fi |
| 237 | + desc=$(file "$art") |
| 238 | + lc=$(otool -l "$art") |
| 239 | + echo "$desc" |
| 240 | + grep -A5 LC_BUILD_VERSION <<<"$lc" || true |
| 241 | + got_platform=$(awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="platform"{print $2; exit}' <<<"$lc") |
| 242 | + got_minos=$(awk '/cmd LC_BUILD_VERSION/{f=1} f && $1=="minos"{print $2; exit}' <<<"$lc") |
| 243 | + fail=0 |
| 244 | + if ! grep -qE "Mach-O 64-bit executable ${arch}\$" <<<"$desc"; then |
| 245 | + echo "FAIL: $target is not a Mach-O $arch executable"; fail=1 |
| 246 | + fi |
| 247 | + if [ "$got_platform" != "$platform" ]; then |
| 248 | + echo "FAIL: $target LC_BUILD_VERSION platform is '$got_platform', expected $platform"; fail=1 |
| 249 | + fi |
| 250 | + if [ "$got_minos" != "$minos" ]; then |
| 251 | + echo "FAIL: $target minos is '$got_minos', expected $minos from ios_deployment_target"; fail=1 |
| 252 | + fi |
| 253 | + [ "$fail" = 0 ] && echo "ok: $target is Mach-O $arch, platform $platform, minos $minos" |
| 254 | + exit "$fail" |
| 255 | + SH |
| 256 | + chmod +x /tmp/assert-artefact.sh |
| 257 | +
|
| 258 | + # THE DEVICE ROW. Nothing runs it -- that needs a signature the developer |
| 259 | + # owns -- so the claim is the artefact: `platform 2` is IOS. The refusal |
| 260 | + # for a machine WITHOUT the SDK is asserted by tests/e2e/641 on every |
| 261 | + # non-Apple host, which is the only place the SDK is genuinely absent: |
| 262 | + # pointing `DEVELOPER_DIR` at nothing here measured nothing, because |
| 263 | + # `xcrun` falls back to the recorded developer directory. |
| 264 | + - name: "aarch64-ios: the artefact names the iOS platform" |
| 265 | + run: | |
| 266 | + set -euo pipefail |
| 267 | + cd /tmp/iostest |
| 268 | + "$MCPP_DEV" build --target aarch64-ios |
| 269 | + /tmp/assert-artefact.sh aarch64-ios arm64 2 18.0 |
| 270 | +
|
| 271 | + # `platform 7` is IOSSIMULATOR. The number is what separates this row from |
| 272 | + # the device row; the architecture does not. |
| 273 | + - name: "aarch64-ios-sim: the artefact names the simulator platform" |
| 274 | + run: | |
| 275 | + set -euo pipefail |
| 276 | + cd /tmp/iostest |
| 277 | + "$MCPP_DEV" build --target aarch64-ios-sim |
| 278 | + /tmp/assert-artefact.sh aarch64-ios-sim arm64 7 18.0 |
| 279 | +
|
| 280 | + # THE SUPPORTED PATH, which is what the `verified` tier claims: a runner |
| 281 | + # the manifest declares and a program a package provides. The program's |
| 282 | + # own line is compared whole. An iOS-simulator Mach-O does not execute on |
| 283 | + # the macOS host directly, so the line appearing at all means the |
| 284 | + # simulator ran it. |
| 285 | + - name: "aarch64-ios-sim: mcpp run prints 1-2-3 through the runner" |
| 286 | + run: | |
| 287 | + set -uo pipefail |
| 288 | + cd /tmp/iostest |
| 289 | + out=$("$MCPP_DEV" run --target aarch64-ios-sim 2>&1) && rc=0 || rc=$? |
| 290 | + printf '%s\n' "$out" | tail -20 |
| 291 | + if [ "$rc" -ne 0 ]; then |
| 292 | + echo "FAIL: mcpp run --target aarch64-ios-sim exited $rc" |
| 293 | + exit 1 |
| 294 | + fi |
| 295 | + if ! grep -qx '1-2-3' <<<"$out"; then |
| 296 | + echo "FAIL: the program's output line '1-2-3' is absent" |
| 297 | + exit 1 |
| 298 | + fi |
| 299 | + echo "ok: mcpp run --target aarch64-ios-sim printed 1-2-3 and exited 0" |
| 300 | +
|
| 301 | + # THE THIRD ROW, ON A HOST THAT CANNOT RUN IT. A simulator runs the host's |
| 302 | + # architecture and this runner is Apple silicon, so the claim is the |
| 303 | + # artefact alone -- which is exactly the `preview` tier the row carries. |
| 304 | + - name: "x86_64-ios-sim: the artefact, on a host that cannot run it" |
| 305 | + run: | |
| 306 | + set -euo pipefail |
| 307 | + cd /tmp/iostest |
| 308 | + "$MCPP_DEV" build --target x86_64-ios-sim |
| 309 | + /tmp/assert-artefact.sh x86_64-ios-sim x86_64 7 18.0 |
0 commit comments