Skip to content

ci(tauri): add CI guardrails for the mobile discovery FFI contract - #337

Merged
momics merged 4 commits into
mainfrom
momics-ci-mobile-discovery-ffi-guardrails
Jul 10, 2026
Merged

ci(tauri): add CI guardrails for the mobile discovery FFI contract#337
momics merged 4 commits into
mainfrom
momics-ci-mobile-discovery-ffi-guardrails

Conversation

@momics

@momics momics commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the CI gap on the mobile discovery contract (Refs #333). The Tauri
plugin's discovery commands cross a Rust ↔ Swift/Kotlin FFI boundary matched
by string at runtime (run_mobile_plugin("name")@objc func name
@Command fun name). Until now CI only cargo checked the Rust half, so a
rename or signature drift (as risked in #330) was a silent, device-only runtime
failure. This PR makes that drift fail CI.

What's added

  1. String-parity contract test (tests::ffi_contract in
    packages/iroh-http-tauri/src/tests.rs). Because mobile_mdns.rs is
    #[cfg(mobile)] and not host-compiled, it source-scans the three files
    and asserts the command-name sets are identical in every direction (fails on
    a Rust string with no native handler and on an orphaned native handler).
    Runs in the existing host-side cargo test for the crate — no new toolchain,
    milliseconds. Verified it fails on an injected rename and on an orphaned
    handler, in both directions. Would have caught feat(discovery): standardize desktop mDNS on DNS-SD for iOS/Android interop #330 with certainty.

  2. Native compile jobs (build-only; no device, emulator/simulator, NDK, or
    Rust mobile cross-compile). The Tauri mobile API ships inside the tauri
    crate (mobile/ios-api, mobile/android), so the scripts cargo fetch it,
    locate it via cargo metadata, and compile the native sources against the
    version this crate's Cargo.lock already resolves:

    • iOSswift build (iphonesimulator SDK), folded into the existing
      rust-check-macos job to avoid a second billed macOS runner.
    • Android — a standalone Gradle harness (android-ci/) runs
      :tauri-plugin-iroh-http:assembleDebug on ubuntu (no macOS cost).

    Both are gated to pull_request / tag builds like the other platform jobs,
    so a native contract break blocks merge (acceptance criterion 3).

  3. Two latent bugs fixed — surfaced immediately because the native sources
    had never been compiled in CI:

    • Kotlin drainResolveQueue() destructured a nullable Pair across a
      synchronized {} lambda — rejected by Kotlin 1.9 (what tauri android init
      generates). Restructured to preserve locking semantics.
    • build.gradle.kts referenced a missing consumer-rules.pro.
  4. Docsdocs/build-and-test.md gains a coverage matrix stating what CI
    verifies (parity + iOS/Android compile, all deterministic) vs. what still
    needs manual on-device verification (real multicast mDNS behavior, tracked in
    Verify DNS-SD discovery on real iOS and Android devices #334), plus local run instructions.

Validation

All jobs were validated locally on macOS (Xcode 26, JDK 21, Android SDK):
swift build and gradle assembleDebug both compile the plugin cleanly; the
parity test passes and fails correctly on injected drift; cargo fmt and the
full tauri crate test suite (29 tests) pass.

Explicitly out of scope

Real multicast mDNS behavior (advertise/browse/self-echo/endpoint resolution)
still requires a device on a LAN — see #334. This PR closes the gap on the
contract, not the behavior.

Refs #333, #334. (Leaving #333 open for the requester to close.)

momics and others added 4 commits July 10, 2026 15:24
The mobile mDNS/DNS-SD commands cross a Rust ↔ Swift/Kotlin boundary that
is matched by string at runtime: run_mobile_plugin("name") dispatches to
`@objc public func name` (Swift) and `@Command fun name` (Kotlin). Each
side compiles independently, so a rename or dropped handler is a silent
contract drift that only surfaces as an `unknown method` error during
manual on-device testing — the exact risk PR #330 introduced.

mobile_mdns.rs is #[cfg(mobile)] and not compiled on the host, so the
test scans the three source files as text and asserts the command-string
sets are identical in every direction (fails on a Rust string with no
native handler AND on an orphaned native handler). It runs in the
existing host-side `cargo test` for the tauri crate — no new toolchain —
in milliseconds, and would have caught the #330 rename with certainty.

Refs #333

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Kotlin discovery plugin was never compiled in CI (only the Rust half of
the FFI boundary was checked), so two latent build breaks shipped:

- `drainResolveQueue()` destructured a nullable `Pair` whose null-check and
  assignment happened inside a `synchronized {}` lambda. Kotlin 1.9 (the
  version `tauri android init` generates) rejects the smart-cast at the
  destructuring site. Restructure so the `synchronized` block yields a
  non-null value, preserving the exact locking semantics.
- `build.gradle.kts` referenced `consumer-rules.pro` via
  `consumerProguardFiles(...)`, but the file was missing, breaking any Gradle
  build of the module. Add it with keep rules for the reflectively-invoked
  `@Command` handlers.

Both are prerequisites for the new Android compile job (#333) to pass.

Refs #333

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add two build-only jobs that compile the Swift (iOS) and Kotlin (Android)
plugin sources against the Tauri mobile API this crate's Cargo.lock resolves,
so a renamed/removed `@objc`/`@Command` handler or a signature drift on the
native side of the string-matched FFI boundary fails CI instead of only
surfacing during manual on-device testing.

Neither job needs a device, an emulator/simulator, the NDK, or a Rust mobile
cross-compile: the Tauri mobile API ships inside the `tauri` crate
(`mobile/ios-api`, `mobile/android`), so the scripts `cargo fetch` the crate,
read its location from `cargo metadata`, and compile the native sources
against it — iOS via `swift build` (iphonesimulator SDK), Android via a
standalone Gradle harness running `:tauri-plugin-iroh-http:assembleDebug`.

The iOS step is folded into the existing `rust-check-macos` job to avoid a
second billed macOS runner; Android runs on ubuntu. Both are gated to
`pull_request` / tag builds like the other platform jobs, so a native
contract break blocks merge.

Refs #333

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a coverage matrix to build-and-test.md distinguishing what the new
FFI-contract checks verify (string-parity of command names across
Rust/Swift/Kotlin, plus iOS Swift and Android Kotlin compilation — all
deterministic, no device) from what still requires manual on-device
verification (real multicast mDNS advertise/browse/self-echo/resolution over
a LAN, tracked in #334). Document how to run each check locally.

Refs #333

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant