diff --git a/.github/scripts/mcpp-under-review.sh b/.github/scripts/mcpp-under-review.sh new file mode 100755 index 0000000..e561deb --- /dev/null +++ b/.github/scripts/mcpp-under-review.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# The mcpp a job's steps run, when that is not the released one. +# +# `MCPP_SOURCE_REF` names a branch or tag of mcpp-community/mcpp. Empty, the +# steps run the release `MCPP_VERSION` names, which the step before this one +# fetched into `$MCPP`. Set, this script builds mcpp at that reference with the +# released mcpp and points `$MCPP` at the result, so that a change to the engine +# is measured against this collection before either is released. +# +# A SCRIPT, NOT A STEP COPIED INTO EACH JOB. Two copies drift, and a job that +# installs mcpp without this channel builds manifests written for the engine +# under review with the released engine, which accepts a key it does not know +# and proceeds without the semantics the key asks for. +# +# WHAT THIS SCRIPT WRITES IS NOT EVIDENCE THAT IT TOOK EFFECT. `GITHUB_ENV` +# governs the steps that follow, so the job's next step compares what `$MCPP` +# names and prints with what this script built. +# +# Environment: MCPP (the released mcpp), MCPP_SOURCE_REF, RUNNER_TEMP, GITHUB_ENV. +set -euo pipefail + +: "${MCPP:?the released mcpp, which the step before this one fetches}" +: "${GITHUB_ENV:?}" +: "${RUNNER_TEMP:?}" + +echo "MCPP_RELEASED=$MCPP" >> "$GITHUB_ENV" +if [ -z "${MCPP_SOURCE_REF:-}" ]; then + echo "MCPP_SOURCE_REF is empty: the steps run the released $("$MCPP" --version | head -1)" + exit 0 +fi + +# In Git Bash on Windows `RUNNER_TEMP` is a Windows path (`D:\a\_temp`), and the +# binary path this script exports is used by bash in every later step, so the +# directory is spelled in bash's own syntax. +temp="$RUNNER_TEMP" +if command -v cygpath > /dev/null; then temp=$(cygpath -u "$temp"); fi +src="$temp/mcpp-src" +rm -rf "$src" +git clone --quiet --depth 1 --branch "$MCPP_SOURCE_REF" \ + https://github.com/mcpp-community/mcpp.git "$src" +echo "READING source: mcpp-community/mcpp $MCPP_SOURCE_REF at $(git -C "$src" rev-parse HEAD)" + +# The clone's `.xlings.json` pins the mcpp that builds mcpp in that repository's +# own CI, and the pin does not move with this job's release: a build inside the +# checkout obeys it and installs a version this job did not choose. Removed, the +# released mcpp above builds the source. +rm -f "$src/.xlings.json" + +(cd "$src" && "$MCPP" build) + +# A fresh clone holds no earlier build, so what remains is this build's product; +# the count is asserted rather than assumed. +built=$(find "$src/target" -type f \( -name mcpp -o -name mcpp.exe \)) +count=$(printf '%s\n' "$built" | grep -c . || true) +if [ "$count" != 1 ]; then + echo "::error::expected one mcpp binary from $MCPP_SOURCE_REF, found $count" + printf '%s\n' "$built" | sed 's/^/ /' + exit 1 +fi +version=$("$built" --version | head -1) +echo "READING under review: $version at $built" +{ + echo "MCPP=$built" + echo "MCPP_UNDER_REVIEW=$built" + echo "MCPP_UNDER_REVIEW_VERSION=$version" +} >> "$GITHUB_ENV" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f75f4f7..35de1df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,11 @@ on: branches: [main] pull_request: workflow_dispatch: + inputs: + mcpp_source_ref: + description: "A branch or tag of mcpp-community/mcpp to build and run in place of the release MCPP_VERSION names" + required: false + default: "" env: # The mcpp release the consumers build with. Raising it is what admits a @@ -29,7 +34,27 @@ env: # every dispatched format was unreachable on macOS, including one that never # reads the staged tree. Staging is a service to the provider in 2026.9.11.2, # and `dist-apple` has been unreachable, not broken, since it was written. - MCPP_VERSION: 2026.9.13.2 + # + # 2026.9.14.2 IS THE RELEASE THAT STAGES THE CLOSURES `dist-apk` AND + # `dist-apple` READ (mcpp#634). It stages an Android application's native + # libraries under `lib/` (`lib//` for several targets) and a Mach-O + # program's dylibs beside it, naming each in the stage manifest's `needs` + # lines; it leaves an rpath that begins with `@executable_path` as written, + # which `dist-apple`'s framework rpath needs; and it runs a distributable + # through the runner named after its format, which is how `mcpp run --format + # app` reaches `macapp-run`. An engine below it stages no `needs` lines, and + # `dist-apk` refuses its tree naming this release. + MCPP_VERSION: 2026.9.14.2 + # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. + # + # Empty on every push and pull request, so the steps run the release above. + # A dispatch that names a branch or tag of mcpp-community/mcpp through the + # `mcpp_source_ref` input has `.github/scripts/mcpp-under-review.sh` build + # that reference with the release above and point `$MCPP` at the result, in + # each job that fetches mcpp, and the step after it checks that the steps run + # what it built. This is how a change to the engine is measured against this + # collection before either is released. + MCPP_SOURCE_REF: ${{ inputs.mcpp_source_ref || '' }} # PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING. # # A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env` @@ -55,7 +80,10 @@ jobs: # (319 MB) -- and a cold cache downloads all of them before the first # compile. The cache key includes the fixtures' manifests, so a run that # changes one of them is always the cold case. - timeout-minutes: 90 + # + # 150, which leaves room for the source build a dispatch naming + # `mcpp_source_ref` adds ahead of every step. + timeout-minutes: 150 steps: - uses: actions/checkout@v4 @@ -87,6 +115,31 @@ jobs: echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + # THE ENGINE THE STEPS BELOW RUN: the release fetched above, or mcpp built + # from `MCPP_SOURCE_REF` (see the workflow's `env`). A cold build of mcpp + # is the longest step of the job, which is what its timeout is sized for. + - name: the mcpp under review, built from MCPP_SOURCE_REF when it is set + timeout-minutes: 45 + run: bash .github/scripts/mcpp-under-review.sh + + # THE CRITERION FOR THE STEP ABOVE, IN A LATER STEP, because what that step + # writes to `GITHUB_ENV` governs only the steps after it. + - name: the steps below run the mcpp under review + if: env.MCPP_SOURCE_REF != '' + timeout-minutes: 5 + run: | + set -eu + [ "$MCPP" = "$MCPP_UNDER_REVIEW" ] || { + echo "::error::the steps run $MCPP, and the mcpp built from $MCPP_SOURCE_REF is $MCPP_UNDER_REVIEW"; exit 1; } + version=$("$MCPP" --version | head -1) + [ "$version" = "$MCPP_UNDER_REVIEW_VERSION" ] || { + echo "::error::$MCPP prints '$version', and the build printed '$MCPP_UNDER_REVIEW_VERSION'"; exit 1; } + # A reading, not a criterion: the two engines' versions, which may be + # equal when the reference has not moved the version yet, so the + # criterion above is the path and not the version. + echo "READING released: $("$MCPP_RELEASED" --version | head -1); under review: $version" + echo "ok: the steps run $version, built from $MCPP_SOURCE_REF" + # The rule's output is a header holding the SPIR-V module; the program # checks the magic number in its first word, so no Vulkan runtime is # needed and what is tested is the rule and the engine path feeding it. @@ -1123,6 +1176,23 @@ jobs: working-directory: tests/ios-app-consumer run: MCPP="$MCPP" ./check-ios-plan.sh + # dist-apple's framework directory, signature, runner and disk image, as + # planned for the macOS and iOS rows (mcpp#634, B1 to B3). The same + # fixture is built, signed, run and imaged on the macOS runner. + - name: dist-apple's framework, signature, runner and disk image, checked at the plan level + timeout-minutes: 15 + working-directory: tests/app-framework-consumer + run: MCPP="$MCPP" ./check-apple-plan.sh + + # dist-wix's `setup` format, as planned for a Windows target (mcpp#634, + # B4): the bundle follows the MSI, both generated documents are + # well-formed XML, and `setup.exe` is refused. The bundle itself is built + # on the Windows runner. + - name: dist-wix's MSI and Burn bundle, checked at the plan level + timeout-minutes: 15 + working-directory: tests/msi-consumer + run: MCPP="$MCPP" ./check-wix-plan.sh + # dist-web (#622 B3), end to end, the real `mcpp pack --format web # --target wasm32-emscripten` -- see `tests/web-consumer/ # check-web-plan.sh`'s own header for the host-toolchain defect this @@ -1223,23 +1293,20 @@ jobs: working-directory: tests/apk-consumer run: MCPP="$MCPP" ./check-apk-features.sh - # A dependency linked as a shared object on the Android row reaches - # `lib//` beside the app's own: walked from the app's NEEDED at - # command time, because the closure on this row is `not-walked` and the - # engine stages the app's object and the deployed files only (0.9.3). - - name: dist-apk carries the graph's shared libraries + # THE CLOSURE THE ENGINE STAGED (mcpp#634, B5). A dependency linked as a + # shared object on the Android row reaches `lib//` beside the app's + # own because the engine stages the closure and names it in the stage + # manifest (2026.9.14.2+), and the member reads that tree. Five criteria: + # two packs in a row carry the dependency's library both times; two + # triples give one signed APK listing both ABIs; `--format aab` gives an + # App Bundle bundletool validates and jarsigner verifies, and a universal + # APK built from it; a refusal prints its reason; and a stage manifest + # without `needs` lines is refused naming the engine floor. The script's + # header records what 0.9.3 did for each. + - name: dist-apk reads the staged closure, packs two ABIs into one APK, and builds an App Bundle + timeout-minutes: 30 working-directory: tests/apk-consumer-shared - run: | - "$MCPP" build --target x86_64-linux-android - "$MCPP" pack --format apk --target x86_64-linux-android | tee pack.log - apk=$(find target -name 'apk-consumer-shared.apk' | head -1) - test -n "$apk" || { cat pack.log; echo "FAIL: no apk-consumer-shared.apk"; exit 1; } - JDK=$(find "$MCPP_HOME/registry/data/xpkgs/xim-x-jdk-temurin" -mindepth 1 -maxdepth 1 -type d | head -1) - "$JDK/bin/jar" tf "$apk" | tee listing.log - for f in "lib/x86_64/libapk-consumer-shared.so" "lib/x86_64/libapk-consumer-dep.so"; do - grep -qxF "$f" listing.log || { echo "FAIL: the apk does not list $f"; exit 1; } - done - echo "ok: the dependency's shared object is in the apk beside the app's" + run: MCPP="$MCPP" ./check-apk-closure.sh # Compiles the device unit on a machine with no GPU: the clang route # produces sm_89 code from the payload toolkit. Running it needs a @@ -1498,7 +1565,9 @@ jobs: rules-cross-platform: name: rules (${{ matrix.name }}) runs-on: ${{ matrix.runs-on }} - timeout-minutes: 60 + # 150, which leaves room for the source build a dispatch naming + # `mcpp_source_ref` adds ahead of every step. + timeout-minutes: 150 strategy: fail-fast: false matrix: @@ -1593,6 +1662,31 @@ jobs: echo "MCPP=$MCPP" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + # THE ENGINE THE STEPS BELOW RUN: the release fetched above, or mcpp built + # from `MCPP_SOURCE_REF` (see the workflow's `env`). A cold build of mcpp + # is the longest step of the job, which is what its timeout is sized for. + - name: the mcpp under review, built from MCPP_SOURCE_REF when it is set + timeout-minutes: 45 + run: bash .github/scripts/mcpp-under-review.sh + + # THE CRITERION FOR THE STEP ABOVE, IN A LATER STEP, because what that step + # writes to `GITHUB_ENV` governs only the steps after it. + - name: the steps below run the mcpp under review + if: env.MCPP_SOURCE_REF != '' + timeout-minutes: 5 + run: | + set -eu + [ "$MCPP" = "$MCPP_UNDER_REVIEW" ] || { + echo "::error::the steps run $MCPP, and the mcpp built from $MCPP_SOURCE_REF is $MCPP_UNDER_REVIEW"; exit 1; } + version=$("$MCPP" --version | head -1) + [ "$version" = "$MCPP_UNDER_REVIEW_VERSION" ] || { + echo "::error::$MCPP prints '$version', and the build printed '$MCPP_UNDER_REVIEW_VERSION'"; exit 1; } + # A reading, not a criterion: the two engines' versions, which may be + # equal when the reference has not moved the version yet, so the + # criterion above is the path and not the version. + echo "READING released: $("$MCPP_RELEASED" --version | head -1); under review: $version" + echo "ok: the steps run $version, built from $MCPP_SOURCE_REF" + # FIRST, BECAUSE IT IS THE CHEAPER QUESTION AND THE MORE INFORMATIVE # ANSWER. Every other step here drives one rule end to end and needs # that rule's payload; this one names no accelerator, downloads nothing, @@ -1787,6 +1881,15 @@ jobs: exit 1; } echo "ok: the MSI installs msi-consumer.exe, $insidesize bytes, byte-for-byte the linked program" + # `--format setup` (mcpp#634, B4): a Burn bundle with WiX's stock + # bootstrapper application, chaining the MSI the step above measured, and + # built through the extension `xim:wix` 5.0.2-1 carries. + - name: dist-wix produces a Burn bundle that chains the MSI + if: ${{ !cancelled() && runner.os == 'Windows' }} + timeout-minutes: 20 + working-directory: tests/msi-consumer + run: MCPP="$MCPP" ./check-setup.sh + - name: dist-apple produces a bundle that launches if: runner.os == 'macOS' working-directory: tests/app-consumer @@ -1834,6 +1937,29 @@ jobs: grep -q 'closure = not-walked' pack.log || echo "note: the stage manifest's closure line was not echoed by pack" echo "ok: one bundle, a valid plist, it launches, and the resource is where NSBundle looks" + # THE CLOSURE AS A FRAMEWORK, ON THE BUNDLE (mcpp#634, B1 to B3). The + # plan-level half runs on Linux. This step, the Metal step and the iOS + # step measure independent things, so each runs when an earlier step of + # the job failed and the job reports every reading it can take. This half reads the linked program's + # load commands, verifies the bundle's ad-hoc signature, runs the program + # with and without its framework, runs the bundle through `mcpp run + # --format app` with no runner in the manifest, and attaches the image. + - name: dist-apple carries the closure as a signed framework, runs through macapp-run, and writes a disk image + if: ${{ !cancelled() && runner.os == 'macOS' }} + timeout-minutes: 30 + working-directory: tests/app-framework-consumer + run: MCPP="$MCPP" ./check-apple-bundle.sh + + # rules-metal (mcpp#634, B6): three Metal libraries from two shaders, and + # the compiler's dependency file in the graph. On a runner without the + # Metal toolchain the script asserts the rule's refusal and annotates the + # run with a warning that the two criteria are unmeasured. + - name: rules-metal compiles Metal libraries, or records that this runner has no Metal toolchain + if: ${{ !cancelled() && runner.os == 'macOS' }} + timeout-minutes: 20 + working-directory: tests/metal-consumer + run: MCPP="$MCPP" ./check-metal.sh + # THE iOS ROW, FOR REAL (#622 B1's other half). `tests/ios-app-consumer` # is the same fixture the Linux `consumers` job checks at the plan # level (see that fixture's own header); here it is built, packed and @@ -1852,7 +1978,7 @@ jobs: # gives: "a machine without Xcode should not download a compiler before # being told the compiler is not what is missing." - name: dist-apple's iOS row produces a bundle the simulator launches - if: runner.os == 'macOS' + if: ${{ !cancelled() && runner.os == 'macOS' }} working-directory: tests/ios-app-consumer shell: bash run: | @@ -1908,22 +2034,28 @@ jobs: # THE SIMULATOR, THROUGH THE DECLARED RUNNER. `mcpp run --format # app` hands the packaged bundle to `runner = ["simctl-run"]` - # (`xim:apple-simulator-tools` 0.2.0, declared on this target row), - # which installs and launches it rather than spawning a bare - # executable -- the branch that package's 0.2.0 header records as - # needed for an installed bundle. This is the same path a - # consumer's own `mcpp run --target aarch64-ios-sim --format app` - # takes, not a bare `simctl spawn` this step could call directly. + # (`xim:apple-simulator-tools` 0.3.0, declared on this target row), + # which installs the bundle and, because its executable does not + # load UIKit, runs the installed executable with `simctl spawn`. This + # is the same path a consumer's own `mcpp run --target aarch64-ios-sim + # --format app` takes, not a bare `simctl spawn` this step could call + # directly. + # + # THE PROGRAM EXITS 7, AND 7 IS THE CRITERION. `simctl launch`, which + # 0.2.0 used, returns 0 for an application that exits 7, so a fixture + # that exited 0 could not tell a runner that returns the status from + # one that loses it (mcpp#634's triage record, section 7.2). out=$("$MCPP" run --target aarch64-ios-sim --format app 2>&1) && rc=0 || rc=$? printf '%s\n' "$out" | tail -20 - [ "$rc" -eq 0 ] \ - || { echo "FAIL: mcpp run --target aarch64-ios-sim --format app exited $rc"; exit 1; } - # `simctl launch --console-pty` returns the program's output through - # a pty, so a line may end in a carriage return; the marker is asserted - # as a line after that is stripped. + [ "$rc" -eq 7 ] \ + || { echo "FAIL: mcpp run --target aarch64-ios-sim --format app exited $rc, and the program exits 7"; exit 1; } + # A line may end in a carriage return when the output passes through + # a pty, so the marker is asserted as a line after that is stripped. + # The diagnostic uses `od -c`, which BSD and GNU both accept; BSD + # `cat` refuses `-A`. tr -d '\r' <<<"$out" | grep -Eq '^[[:space:]]*1-2-3[[:space:]]*$' \ - || { echo "FAIL: the program's output line '1-2-3' is absent"; printf '%s\n' "$out" | cat -A | tail -8; exit 1; } - echo "ok: a flat iOS Simulator bundle, MinimumOSVersion 17.0, and the simulator ran it" + || { echo "FAIL: the program's output line '1-2-3' is absent"; printf '%s\n' "$out" | tail -8 | od -c; exit 1; } + echo "ok: a flat iOS Simulator bundle, MinimumOSVersion 17.0, and the simulator ran it and returned its status 7" - name: the rule declared its own compiler working-directory: tests/spirv-consumer diff --git a/README.md b/README.md index 08bab32..14b2f10 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,17 @@ engine's own module family and is not used here. | `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | `[build] accel = "ascend8.5+{dav-c220}"`, a constrained glob for `*.asc`. Compiles with BiSheng in MIXED mode, so the object carries the device binary and a host-callable launcher and joins the ordinary link -- no registration file and no device-link step. Its own engine needs are `.asc` in the device-source table and `mcpp::link_flag` for the `-rpath-link` the toolkit's shared libraries require, both 2026.9.6.5 | | `rules-cuda` | `mcpp.rules.cuda` | 2026.9.6.6 | `[build] accel = "cuda…"`, a constrained glob for `*.cu`; the clang route with an LLVM toolchain, the nvcc route with a GCC one | | `rules-hip` | `mcpp.rules.hip` | 2026.9.6.6 | `[build] accel = "hip, cuda12.9+{sm_89}"`, a constrained glob for `*.hip`. On the NVIDIA platform HIP is a header layer over the CUDA runtime, so the compiler is the project's own clang and there is no ROCm on the machine | +| `rules-metal` | `mcpp.rules.metal` | 2026.9.8.1 | the Metal toolchain of the macOS host's Xcode, located rather than installed: Xcode is not redistributable, so no payload is declared. `.metal` sources the project names on a macOS or iOS row become one `xcrun --sdk metal` action per shader (`-MMD`, so an edited `#include` recompiles the shaders that include it) and one `xcrun --sdk metallib` action per library, placed beside the program with `mcpp::deploy` under `metallib/`, which `dist-apple` maps into the bundle's resources. `compile(shaders)` compiles one source several times with definitions of its own, one library per `shader`; `options::library` links every shader into one library (`default` is the one `newDefaultLibrary` finds). Before planning anything the rule asks `xcrun --sdk --show-sdk-path` and `--find metal` / `--find metallib`, and refuses naming the command that answered nothing, because a missing SDK and a missing compiler have different remedies (Xcode 26 installs the Metal toolchain as a separate component). A shader on any other row is refused naming the row. CI compiles the fixture on `macos-15` and checks each library's magic, and that a header edit recompiles only the shaders that include it | | `rules-slang` | `mcpp.rules.slang` | 2026.9.7.1 | `[build] accel = "vulkan1.2"`, a constrained glob for `*.slang`. Slang is a different language from GLSL rather than a second driver for it -- its own module system, generics, and targets beyond SPIR-V -- so it is a rule of its own. `.slang` is **not** in the engine's device-source table: this feature declares `device_extensions = [".slang"]` and `rule_module = "mcpp.rules.slang"`, and the engine routes it from there. That is the criterion for the whole arrangement -- a new device language costs no engine release. Since 0.7.0 it has the same `options::storage` axis as `rules-spirv` (header / object / sidecar), `options::extra_args` for the arguments the rule has no field for, and `options::per_file` for what one shader gets that the others do not -- a project with a `-fvk-use-gl-layout` and one shader needing `-emit-spirv-via-glsl` writes both without leaving one `compile()` call | | `rules-spirv` | `mcpp.rules.spirv` | 2026.9.6.6 | `[build] accel = "vulkan1.2"`, a constrained glob for the shader stages; compiles each shader through a `role = "source"` action and states which of the two compilers produced it | | `rules-sycl` | `mcpp.rules.sycl` | 2026.9.6.6 | `[build] accel = "sycl"` or `"sycl, cuda12.9+{sm_89}"`, a constrained glob for `*.sycl`, and `compat:sycl-runtime` so the artifact can reach `libsycl.so.9` at run time. Its own engine need is `.sycl` in the device-source table, 2026.9.6.1 | | `tools-embed` | `mcpp.tools.embed` | 2026.9.5.4 | nothing beyond mcpp: it reads a file and writes a header while the build program runs. The floor is the release whose fast path compares a declared file input, without which an edit to the data does not reach the binary | | `tools-island` | `mcpp.tools.island` | 2026.9.7.1 | nothing beyond mcpp: it reads marked entry points out of an island's own source and writes the `extern "C"` boundary header its compiler reads and the module the C++ side imports. Not a device rule -- it claims no extension, and a project calls it from its own `build.mcpp` | | `dist-appimage` | `mcpp.dist.appimage` | 2026.9.11.1 | `xim:appimagetool`, which this feature declares on the `cfg(linux)` axis. Linux only. Turns the tree `mcpp pack` staged into one AppImage: the staged bundle is already an AppDir bar three files, so the member writes an `AppRun`, a `.desktop` entry and an icon into it and invokes one tool -- it never copies or re-lays-out a tree that can be hundreds of megabytes | -| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | `xim:wix`, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. Renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent | -| `dist-apple` | `mcpp.dist.apple` | 2026.9.11.2 (macOS), 2026.9.12.3 (iOS) | the base macOS install (`ditto`, and `codesign` only when an identity is given). macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why) and unchanged on the device row. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider. From 0.9.2 the staged tree's deployed files (`bin//...`, which the engine stages for a Mach-O program before the closure walk since the release for mcpp#630) land at the bundle's resource destination -- `Contents/Resources//...` on macOS, the bundle root on iOS -- and the launcher alone goes to the executable directory, so `CFBundleExecutable` names a file that is where it says. The iOS fixture declares `llvm.libcxx` and `llvm.compiler-rt-builtins` under `cfg(os = "ios")`, which is what an application that imports `std` on those rows declares | +| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | `xim:wix` 5.0.2-1, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. `--format msi` renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent. From 0.10.0 `--format setup` is a Burn bundle chaining that MSI, with WiX's stock bootstrapper application (`bal:WixStandardBootstrapperApplication`, theme `hyperlinkLicense`, `options::license_url` its link) loaded through the `WixToolset.BootstrapperApplications.wixext` extension the 5.0.2-1 payload carries (`options::extension` names another). The bundle is written as `-.exe` beside the MSI, with an UpgradeCode of its own, and `options::bundle_output` naming `setup.exe` is refused before `wix` runs, because `wix` refuses that name (WIX0388). A project with its own bootstrapper application supplies `options::bundle_wxs`, which receives the MSI as `$(Msi)`. CI builds the bundle on `windows-2022` and compares the MSI `wix burn extract` takes out of it with the one the first action wrote | +| `dist-apple` | `mcpp.dist.apple` | 2026.9.14.2 (0.10.0); 2026.9.11.2 (macOS) and 2026.9.12.3 (iOS) before it | the base macOS install (`ditto`, `codesign`, `hdiutil`), and `xim:macapp-run` for `mcpp run` on macOS, which this feature declares with `when = "run"`. macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why), and the device row signs only with an identity. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider. From 0.9.2 the staged tree's deployed files (`bin//...`, which the engine stages for a Mach-O program before the closure walk since the release for mcpp#630) land at the bundle's resource destination -- `Contents/Resources//...` on macOS, the bundle root on iOS -- and the launcher alone goes to the executable directory, so `CFBundleExecutable` names a file that is where it says. The iOS fixture declares `llvm.libcxx` and `llvm.compiler-rt-builtins` under `cfg(os = "ios")`, which is what an application that imports `std` on those rows declares. From 0.10.0, with mcpp 2026.9.14.2: the dylibs the engine stages beside a Mach-O program, which the stage manifest's `needs` lines name, go to `Contents/Frameworks/` (`Frameworks/` on iOS) and not to the resources; the program is linked with the rpath that finds them there (`@executable_path/../Frameworks`, `@executable_path/Frameworks` on iOS) through `mcpp::link_flag`, so no file is edited after the link; a macOS bundle without `options::identity` is signed ad hoc, frameworks first and the bundle second, which `codesign --verify --deep --strict` requires of a bundle that carries a framework; an incomplete closure is a `mcpp::warning` naming the unresolved libraries; every refusal is a `mcpp::warning` as well, because the engine discards a build program's output when it exits 0. On macOS the member supplies the runner named `app` (`macapp-run`), so `mcpp run --format app` runs the bundle's executable in the foreground and returns its status with no runner in the manifest; a manifest runner of that name wins. `--format dmg` stages the bundle beside an `Applications` link and writes a UDZO image with `hdiutil create` (`options::volume_name`, `options::dmg`); it is refused on iOS. An engine below 2026.9.14.2 stages no `needs` lines, so the bundle carries no framework, anchors the rpath to the package directory, and hands the bundle directory to the kernel under `mcpp run --format app` unless `--runner app` is typed. CI measures the bundle on `macos-15`: the load command, the signature, the program with and without its framework (exit 7, then "Library not loaded"), `mcpp run --format app` with and without `--runner app`, and `hdiutil verify` and an attached image | | `dist-web` | `mcpp.dist.web` | 2026.9.13.1, the release that carries `${mcpp.self}` and `mcpp stage`'s argument shape as an engine contract (`stage --verify content --output `) -- what lets this member's copy run on every host mcpp does, Windows included, in place of the `cp` this member used through 0.8.0 | nothing beyond mcpp: `wasm32-emscripten` only. Copies `${mcpp.stage_dir}/bin/` -- the `.js` launcher, the implicit `.wasm`, the `.data` when present, and every `mcpp::deploy`'d file, all of which #622 A5 and A4 already stage there -- to `/web/`, dropping the `bin/` prefix a browser has no use for, and writes an `index.html` rendered from a project template or a built-in default that loads the script with a plain `