From 476439c79e9f52c07401c719752a11a46a9f2691 Mon Sep 17 00:00:00 2001 From: Imod7 Date: Tue, 11 Aug 2026 08:42:47 +0200 Subject: [PATCH 1/2] ci: extract iOS binding sync into its own script --- ios/truapi-host/scripts/rebuild.sh | 20 +---- ios/truapi-host/scripts/sync-bindings.sh | 103 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 16 deletions(-) create mode 100755 ios/truapi-host/scripts/sync-bindings.sh diff --git a/ios/truapi-host/scripts/rebuild.sh b/ios/truapi-host/scripts/rebuild.sh index 24c45e45..22666fdc 100755 --- a/ios/truapi-host/scripts/rebuild.sh +++ b/ios/truapi-host/scripts/rebuild.sh @@ -15,22 +15,10 @@ TRUAPI_ROOT="$(cd "$PACKAGE_ROOT/../.." && pwd)" make -C "$TRUAPI_ROOT" xcframework -UNIFFI_OUT="$TRUAPI_ROOT/target/uniffi-swift-out" -for namespace in truapi truapi_platform truapi_server; do - mkdir -p "$PACKAGE_ROOT/Sources/${namespace}FFI/include" - cp "$UNIFFI_OUT/${namespace}.swift" \ - "$PACKAGE_ROOT/Sources/TrUAPIHost/${namespace}.swift" - cp "$UNIFFI_OUT/${namespace}FFI.h" \ - "$PACKAGE_ROOT/Sources/${namespace}FFI/include/${namespace}FFI.h" - cp "$UNIFFI_OUT/${namespace}FFI.modulemap" \ - "$PACKAGE_ROOT/Sources/${namespace}FFI/include/module.modulemap" - # UniFFI templates emit trailing spaces around optional fragments. Keep the - # committed bindings stable so rebuilding only records API changes. - perl -pi -e 's/[ \t]+$//' \ - "$PACKAGE_ROOT/Sources/TrUAPIHost/${namespace}.swift" \ - "$PACKAGE_ROOT/Sources/${namespace}FFI/include/${namespace}FFI.h" \ - "$PACKAGE_ROOT/Sources/${namespace}FFI/include/module.modulemap" -done +# The binding copy and normalization live in sync-bindings.sh so that CI's +# --check mode and this in-place write share one definition of what the +# committed bindings should contain. +sh "$PACKAGE_ROOT/scripts/sync-bindings.sh" rm -rf "$PACKAGE_ROOT/Binaries/truapi_server.xcframework" mkdir -p "$PACKAGE_ROOT/Binaries" diff --git a/ios/truapi-host/scripts/sync-bindings.sh b/ios/truapi-host/scripts/sync-bindings.sh new file mode 100755 index 00000000..2305ca71 --- /dev/null +++ b/ios/truapi-host/scripts/sync-bindings.sh @@ -0,0 +1,103 @@ +#!/bin/sh +# Copy the uniffi-generated Swift bindings from target/uniffi-swift-out into the +# TrUAPIHost package, normalizing them the way the committed files are stored. +# +# Requires `make uniffi` (or `make xcframework`, which depends on it) to have run +# first; this script generates nothing itself. +# +# Usage: +# ./scripts/sync-bindings.sh write the bindings in place +# ./scripts/sync-bindings.sh --check report stale committed bindings, write nothing +set -eu + +PACKAGE_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TRUAPI_ROOT="$(cd "$PACKAGE_ROOT/../.." && pwd)" +UNIFFI_OUT="$TRUAPI_ROOT/target/uniffi-swift-out" + +NAMESPACES="truapi truapi_platform truapi_server" + +CHECK_ONLY=0 +case "${1-}" in + --check) CHECK_ONLY=1 ;; + "") ;; + *) + echo "usage: $0 [--check]" >&2 + exit 2 + ;; +esac + +if [ ! -d "$UNIFFI_OUT" ]; then + echo "error: $UNIFFI_OUT is missing." >&2 + echo "Run 'make uniffi' at the repo root first; this script only copies." >&2 + exit 66 +fi + +# Stage into $1 (a directory root laid out like the package), so that --check +# never touches the working tree and an interrupted write cannot leave half of a +# namespace synced. +stage_bindings() { + dest="$1" + for namespace in $NAMESPACES; do + mkdir -p "$dest/Sources/TrUAPIHost" "$dest/Sources/${namespace}FFI/include" + cp "$UNIFFI_OUT/${namespace}.swift" \ + "$dest/Sources/TrUAPIHost/${namespace}.swift" + cp "$UNIFFI_OUT/${namespace}FFI.h" \ + "$dest/Sources/${namespace}FFI/include/${namespace}FFI.h" + cp "$UNIFFI_OUT/${namespace}FFI.modulemap" \ + "$dest/Sources/${namespace}FFI/include/module.modulemap" + # UniFFI templates emit trailing spaces around optional fragments. Keep the + # committed bindings stable so rebuilding only records API changes. + perl -pi -e 's/[ \t]+$//' \ + "$dest/Sources/TrUAPIHost/${namespace}.swift" \ + "$dest/Sources/${namespace}FFI/include/${namespace}FFI.h" \ + "$dest/Sources/${namespace}FFI/include/module.modulemap" + done +} + +relative_paths() { + for namespace in $NAMESPACES; do + echo "Sources/TrUAPIHost/${namespace}.swift" + echo "Sources/${namespace}FFI/include/${namespace}FFI.h" + echo "Sources/${namespace}FFI/include/module.modulemap" + done +} + +if [ "$CHECK_ONLY" -eq 0 ]; then + stage_bindings "$PACKAGE_ROOT" + echo "Bindings synced into $PACKAGE_ROOT/Sources" + exit 0 +fi + +STAGING="$(mktemp -d)" +trap 'rm -rf "$STAGING"' EXIT +stage_bindings "$STAGING" + +stale=0 +# Report every stale file rather than stopping at the first: a HostCallbacks +# change usually touches all three namespaces, and one-at-a-time reporting +# invites partial fixes. +for path in $(relative_paths); do + if ! diff -u "$PACKAGE_ROOT/$path" "$STAGING/$path" \ + --label "committed/$path" --label "generated/$path"; then + stale=$((stale + 1)) + fi +done + +if [ "$stale" -ne 0 ]; then + cat >&2 <<'EOF' + +error: the committed iOS bindings do not match the current Rust surface. + +Regenerate and commit them: + + make uniffi && ./ios/truapi-host/scripts/sync-bindings.sh + +Regenerating alone is usually not enough: the hand-written conformers +(ios/truapi-host/Sources/TrUAPIHost/TrUAPIHost.swift and +android/truapi-host/.../TrUAPIHost.kt) need a matching change whenever +HostCallbacks gains or changes a requirement. +EOF + exit 1 +fi + +echo "Committed iOS bindings are current." From 6a7da7bd7497178df2a9548fc1a44cfad0bddd67 Mon Sep 17 00:00:00 2001 From: Imod7 Date: Tue, 11 Aug 2026 08:44:16 +0200 Subject: [PATCH 2/2] ci: fail pull requests with stale iOS bindings --- .../skills/truapi-definition-of-done/SKILL.md | 22 ++++++++++-- .github/workflows/ci.yml | 35 ++++++++++++++++++- CLAUDE.md | 6 +++- ios/truapi-host/README.md | 17 ++++++++- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/.claude/skills/truapi-definition-of-done/SKILL.md b/.claude/skills/truapi-definition-of-done/SKILL.md index 30d456f5..1d804c54 100644 --- a/.claude/skills/truapi-definition-of-done/SKILL.md +++ b/.claude/skills/truapi-definition-of-done/SKILL.md @@ -31,6 +31,16 @@ git submodule update --init --recursive - [ ] **Codegen** — only if Rust trait surface changed. Invoke the `regen-codegen` skill, then commit `js/packages/truapi/src/{generated,playground}/`. +- [ ] **iOS bindings** — only if UniFFI-exposed types changed + (`HostCallbacks`, `NativeTrUApiCore`, the native mirror types in + `rust/crates/truapi-server/src/native*`). Run + `make uniffi && ./ios/truapi-host/scripts/sync-bindings.sh`, then + commit `ios/truapi-host/Sources/`. Also update every hand-written + conformer — `HostCallbackAdapter` in `TrUAPIHost.swift`, + `StubHostCallbacks` in `Tests/TrUAPIWsBridgeTests.swift`, and + `TrUAPIHost.kt` — since regenerating alone leaves the package + non-compiling. `rebuild.sh` does all of the above plus the + xcframework and container, but needs Xcode. - [ ] **`@parity/truapi`** — invoke the `ts-client-checks` skill. `npm run build && npm test` clean. - [ ] **Playground snapshot** — only if codegen ran or @@ -50,6 +60,12 @@ common cause of the codegen ↔ snapshot mismatch. GitHub Actions in `.github/workflows/ci.yml` runs the same chain on every PR. A green CI run is sufficient evidence for the static layers -(rust, codegen-drift, ts-client, playground); the e2e job runs the -Playwright suite from the `e2e-dotli` skill against a freshly built -dotli host. +(rust, codegen-drift, ios-bindings, ts-client, playground); the e2e job +runs the Playwright suite from the `e2e-dotli` skill against a freshly +built dotli host. + +The `ios-bindings` job only compares the committed bindings against +freshly generated ones. No CI job compiles Swift or Kotlin, so a +hand-written conformer that misses a new protocol requirement stays +green here and fails at release time instead. Green CI is not evidence +that the iOS package builds. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d583e5b3..ddbcfe10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,6 +117,28 @@ jobs: js/packages/truapi-host/src/generated playground/test/generated + ios-bindings: + name: iOS bindings (uniffi) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # stable + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + # Swift bindgen is pure Rust codegen, so this needs neither Xcode nor the + # iOS targets, unlike rebuild.sh which also builds the xcframework. + - name: Generate Swift bindings + run: make uniffi + + - name: Check committed iOS bindings are current + run: ./ios/truapi-host/scripts/sync-bindings.sh --check + ts-client: name: "@parity/truapi" runs-on: ubuntu-latest @@ -327,7 +349,17 @@ jobs: if: always() runs-on: ubuntu-latest needs: - [rust, licenses, codegen, ts-client, ts-host, playground, explorer, e2e] + [ + rust, + licenses, + codegen, + ios-bindings, + ts-client, + ts-host, + playground, + explorer, + e2e, + ] steps: - name: Check all jobs run: | @@ -335,6 +367,7 @@ jobs: "${{ needs.rust.result }}" "${{ needs.licenses.result }}" "${{ needs.codegen.result }}" + "${{ needs.ios-bindings.result }}" "${{ needs.ts-client.result }}" "${{ needs.ts-host.result }}" "${{ needs.playground.result }}" diff --git a/CLAUDE.md b/CLAUDE.md index 5bd34814..80b86223 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,11 @@ scripts/battery.sh run the generated battery against both headless CLI h locally before relying on the browser host. - After changing UniFFI-exposed types or native bindings, run `./ios/truapi-host/scripts/rebuild.sh` and commit the generated bindings and - container output. To publish the binary, include `@parity/ios-host ` + container output. When only the bindings changed, `make uniffi && + ./ios/truapi-host/scripts/sync-bindings.sh` does that part without Xcode. CI + enforces it: the `ios-bindings` job regenerates and diffs the committed + bindings, but compiles no Swift, so the hand-written conformers are still on + you. To publish the binary, include `@parity/ios-host ` in the `release:` PR title. The release workflow rebuilds and simulator-tests the XCFramework, uploads it, and makes the `Package.swift` follow-up commit only after the asset is live. `publish.sh ` is the manual fallback. diff --git a/ios/truapi-host/README.md b/ios/truapi-host/README.md index 389e161f..43292fbe 100644 --- a/ios/truapi-host/README.md +++ b/ios/truapi-host/README.md @@ -25,6 +25,21 @@ The generated bindings and the container bundle are committed build outputs; the # (URL + checksum) ``` +When only the bindings need refreshing — a Rust surface change with no container +or xcframework impact — skip the full rebuild, which needs Xcode and the iOS +targets: + +```bash +# from the repo root +make uniffi && ./ios/truapi-host/scripts/sync-bindings.sh +``` + +CI's `iOS bindings (uniffi)` job runs the same two commands with +`sync-bindings.sh --check`, which diffs the committed bindings against freshly +generated ones and writes nothing. It runs on Linux, so it verifies the +generated files only; the hand-written conformers in `TrUAPIHost.swift` and +`Tests/` are not compiled by any CI job. + Run `rebuild.sh` after changing anything host-visible — the `NativeTrUApiCore` methods, `HostCallbacks`, the native mirror types in `rust/crates/truapi-server/src/native*`, or `js/container/src` — and commit the regenerated bindings/container together with the source change. To publish from a release PR, add `@parity/ios-host ` to its `release:` title. After the release commit passes CI, the release workflow rebuilds and simulator-tests the XCFramework on macOS, uploads it, and makes the `Package.swift` follow-up commit only after the asset is live. `publish.sh` remains available for an ad hoc manual release. For local iteration without publishing, flip `useLocalBinary = true` in the root `Package.swift` to build against `Binaries/` directly; flip it back before committing. @@ -222,5 +237,5 @@ The product page reads `window.__truapi_localhost.url` (set by the bootstrap scr `./scripts/rebuild.sh` orchestrates everything; the underlying pieces, should you need one in isolation: - **xcframework** — `make xcframework` (repo root) builds `truapi-server` for `aarch64-apple-ios` and `aarch64-apple-ios-sim` and bundles `target/truapi_server.xcframework`; the script copies it into `Binaries/` and strips the per-slice `module.modulemap` (module resolution comes from the `systemLibrary` target; the slice copy collides with other xcframeworks in Xcode's flat include dir). -- **bindings** — `make uniffi` (run automatically by `make xcframework`) emits the Swift bindings into `target/uniffi-swift-out/` via the workspace `uniffi-bindgen-cli`; the script copies them into `Sources/TrUAPIHost/truapi_server.swift` and `Sources/truapi_serverFFI/include/`, renaming the emitted `truapi_serverFFI.modulemap` to `module.modulemap` so the SwiftPM `systemLibrary` target picks it up. +- **bindings** — `make uniffi` (run automatically by `make xcframework`) emits the Swift bindings into `target/uniffi-swift-out/` via the workspace `uniffi-bindgen-cli`; `scripts/sync-bindings.sh` copies them into `Sources/TrUAPIHost/truapi_server.swift` and `Sources/truapi_serverFFI/include/`, renaming the emitted `truapi_serverFFI.modulemap` to `module.modulemap` so the SwiftPM `systemLibrary` target picks it up. `rebuild.sh` calls it, and CI's `--check` mode compares against it. - **container** — `npm run build` in `js/container/` (repo root) bundles `src/index.ts` into `Sources/TrUAPIHost/Resources/truapi-container.js`.