Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .claude/skills/truapi-definition-of-done/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
35 changes: 34 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -327,14 +349,25 @@ 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: |
results=(
"${{ needs.rust.result }}"
"${{ needs.licenses.result }}"
"${{ needs.codegen.result }}"
"${{ needs.ios-bindings.result }}"
"${{ needs.ts-client.result }}"
"${{ needs.ts-host.result }}"
"${{ needs.playground.result }}"
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>`
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 <version>`
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 <version>` is the manual fallback.
Expand Down
17 changes: 16 additions & 1 deletion ios/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>` 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.
Expand Down Expand Up @@ -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`.
20 changes: 4 additions & 16 deletions ios/truapi-host/scripts/rebuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
103 changes: 103 additions & 0 deletions ios/truapi-host/scripts/sync-bindings.sh
Original file line number Diff line number Diff line change
@@ -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."