Skip to content
Draft
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
35 changes: 35 additions & 0 deletions .github/workflows/ci-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Compile gate for the Android host module: regenerates the UniFFI Kotlin
# bindings and compiles the shell against them whenever the module or the
# native surface changes. No cross-compile, no publishing.
name: ci-android

on:
pull_request:
paths:
- "android/**"
- "rust/crates/truapi-server/**"
- "rust/crates/truapi-platform/**"
- "rust/crates/uniffi-bindgen-cli/**"
- ".github/workflows/ci-android.yml"

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.9"
- name: Generate UniFFI Kotlin bindings
run: make uniffi-kotlin
- name: Compile the shell against the bindings
run: gradle :truapi-host:assembleRelease
84 changes: 84 additions & 0 deletions .github/workflows/release-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Builds and publishes the Android host AAR (io.parity:truapi-host) to GitHub
# Packages. The AAR bundles the per-ABI Rust core (ws-bridge feature), the
# UniFFI Kotlin bindings generated from the same source, and the Kotlin shell.
name: release-android

on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. 0.1.0 or 0.1.0-rc.1)"
required: true
push:
tags:
- "truapi-host-android@*"
# Temporary, remove once the pipeline is proven: publish an rc build on
# every push to the introducing branch, since workflow_dispatch is only
# available after this file lands on the default branch.
branches:
- "vf/android-host-artifact"

permissions:
contents: read
packages: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Resolve version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
elif [[ "$GITHUB_REF" == refs/tags/* ]]; then
echo "version=${GITHUB_REF_NAME#truapi-host-android@}" >> "$GITHUB_OUTPUT"
else
# Branch push (temporary trigger): unique rc per run.
echo "version=0.1.0-rc.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
fi

- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android

- uses: Swatinem/rust-cache@v2

- name: Install cargo-ndk
run: cargo install cargo-ndk --locked

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.9"

- name: Generate UniFFI Kotlin bindings
run: make uniffi-kotlin

- name: Cross-compile libtruapi_server.so for all ABIs
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_NDK_LATEST_HOME }}
run: make android-jni

- name: Verify AAR inputs
run: |
set -e
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
test -f "android/truapi-host/src/main/jniLibs/$abi/libtruapi_server.so" \
|| { echo "missing .so for $abi"; exit 1; }
done
test -f android/truapi-host/src/main/kotlin/generated/uniffi/truapi_server/truapi_server.kt

- name: Publish to GitHub Packages
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gradle :truapi-host:publishReleasePublicationToGitHubPackagesRepository \
-PtruapiHostVersion="${{ steps.version.outputs.version }}"
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,20 @@ uniffi-kotlin: ## Regenerate Kotlin UniFFI bindings from the truapi-server cdyli
--out-dir $(UNIFFI_KOTLIN_OUT)

# Android ABIs to cross-compile the cdylib for. arm64 + armv7 cover physical
# devices; x86_64 covers the emulator on Intel/Apple-silicon hosts.
ANDROID_ABIS ?= arm64-v8a armeabi-v7a x86_64
# devices; x86 + x86_64 cover emulators.
ANDROID_ABIS ?= arm64-v8a armeabi-v7a x86 x86_64
ANDROID_JNILIBS := android/truapi-host/src/main/jniLibs

android-jni: ## Cross-compile libtruapi_server.so for Android ABIs into jniLibs (needs cargo-ndk + NDK).
@command -v cargo-ndk >/dev/null || { echo "cargo-ndk not found: cargo install cargo-ndk"; exit 1; }
$(CARGO) ndk $(foreach abi,$(ANDROID_ABIS),-t $(abi)) \
-o $(ANDROID_JNILIBS) \
build --release -p truapi-server --features ws-bridge
# cargo-ndk also copies dependency cdylib intermediates (hash-suffixed,
# statically linked into libtruapi_server.so already); keep only ours.
find $(ANDROID_JNILIBS) -name '*.so' ! -name 'libtruapi_server.so' -delete

android-publish-local: uniffi-kotlin ## Generate Kotlin bindings, then publish the AAR to ~/.m2 (needs Gradle + JDK 17). The AAR does not bundle the cdylib; consumers build it per ABI (see android-jni).
android-publish-local: uniffi-kotlin ## Generate Kotlin bindings, then publish the AAR to ~/.m2 as io.parity:truapi-host:0.0.0-local (needs Gradle + JDK 17). Run `make android-jni` first to bundle the per-ABI cdylibs into the AAR.
gradle :truapi-host:publishReleasePublicationToMavenLocal

test: ## Run Rust + TypeScript client tests.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ js/packages/
(shared host types), `/web` (iframe + Web Worker),
`/worker-runtime`
ios/truapi-host/ Swift host adapter package over the truapi-server UniFFI core
android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core
android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core;
published to GitHub Packages as io.parity:truapi-host (AAR with
per-ABI cdylibs; see android/truapi-host/README.md)
playground/ Interactive Next.js playground (truapi-playground.dot)
hosts/dotli/ dotli host, vendored as a submodule
hosts/ios/ polkadot-app-ios-v2, vendored as a submodule (build/test against the core)
Expand Down
85 changes: 27 additions & 58 deletions android/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,36 @@

*Kotlin wrapper around the TrUAPI Rust core (UniFFI). Wire decoding, request routing, and subscription lifecycle stay in the Rust core; products connect through the localhost WebSocket bridge.*

> **Status:** the JitPack distribution described below is the intended packaging but is **not yet wired up** — there is no `jitpack.yml` at the repo root, so the "add the JitPack repo and depend on the tag" flow does not work today. Until it is added, integrate locally with `make android-publish-local` + `mavenLocal()`, or build the module directly. The rest of this doc describes the target design.

Intended distribution: a Maven artifact built on demand from git tags by [JitPack](https://jitpack.io/), no Maven Central account required on either side.
Distribution: a Maven AAR published to GitHub Packages by the `release-android` workflow. Each release bundles, built from the same source tree: `libtruapi_server.so` for arm64-v8a, armeabi-v7a, x86 and x86_64 (built with the `ws-bridge` feature), the UniFFI Kotlin bindings (`uniffi.truapi_server.*`), and the Kotlin host adapter (`io.parity.truapi.*`). Consumers need no Rust toolchain or NDK.

## Consume

Add the JitPack Maven repository and the artifact to your app's Gradle build:
Add the GitHub Packages repository and the artifact to your app's Gradle build (GitHub Packages requires authentication even for public repos — any GitHub account token with `read:packages` works):

```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven {
url = uri("https://maven.pkg.github.com/paritytech/truapi")
credentials {
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
}
}
```

```kotlin
// app/build.gradle.kts
dependencies {
implementation("com.github.paritytech.truapi:truapi-host:0.1.0")
implementation("io.parity:truapi-host:0.1.0")
}
```

JitPack fetches the tag `0.1.0` from `paritytech/truapi`, runs `make android-publish-local` against it (driven by `jitpack.yml` at the repo root, including UniFFI binding generation), and serves the resulting AAR + POM + sources jar. First fetch takes ~1 minute while JitPack builds; subsequent consumers hit the cache.

The artifact bundles the Kotlin host adapter (`io.parity.truapi.*`) and the generated UniFFI bindings (`uniffi.truapi_server.*`). It does **not** bundle the native `libtruapi_server.so` cdylib, integrators build that per Android ABI and drop it into their app's `src/main/jniLibs/<abi>/` (see "Linking the cdylib" below).

The consuming app must declare `android.permission.INTERNET` — the localhost WebSocket bridge binds a `127.0.0.1` TCP socket, which requires it even for loopback.

### Compatibility
Expand Down Expand Up @@ -238,72 +238,41 @@ main.post {
core.disconnect()
```

## Linking the cdylib
## The cdylib

The native runtime ships separately. JNA looks for `libtruapi_server.so` in the standard `jniLibs` paths; bundle the per-ABI builds under:
The released AAR bundles `libtruapi_server.so` for all four ABIs under its `jni/` directory; JNA loads it from there without any consumer setup.

```
src/main/jniLibs/arm64-v8a/libtruapi_server.so
src/main/jniLibs/armeabi-v7a/libtruapi_server.so
src/main/jniLibs/x86_64/libtruapi_server.so
When iterating on the core from a source checkout instead of the published artifact, cross-compile into this module's `jniLibs` with:

```bash
make android-jni # needs cargo-ndk, the NDK, and the four Android rust targets
```

Cross-build the cdylib for each Android ABI from the truapi monorepo. Two options, pick whichever fits the host app's existing toolchain:
or point the `mozilla-rust-android-gradle` plugin at `rust/crates/truapi-server` from the host app's own build (polkadot-app-android-v2 does this while it still builds from a checkout).

**Option A: `mozilla-rust-android-gradle` plugin.** Recommended if the host app already uses it (polkadot-app-android-v2 does, for `bandersnatch-crypto`). Vendor `paritytech/truapi` as a git submodule, add a small Gradle module that points the plugin at `rust/crates/truapi-server`:
## Maintainers: cutting a release

```kotlin
// app/build.gradle.kts (or a dedicated :truapi-cdylib module)
plugins {
alias(libs.plugins.mozilla.rust.android)
}
Releases are built and published by `.github/workflows/release-android.yml`:

cargo {
module = "<path>/truapi/rust/crates/truapi-server"
libname = "truapi_server"
targets = listOf("arm64", "arm", "x86_64")
profile = "release"
features { defaultAnd(arrayOf("ws-bridge")) }
}
1. Tag the commit to release: `git tag truapi-host-android@0.1.0 && git push origin truapi-host-android@0.1.0` (or run the `release-android` workflow manually with a version input).
2. The workflow cross-compiles the cdylib for all four ABIs, regenerates the Kotlin bindings via the `codegen` cargo profile, and publishes `io.parity:truapi-host:<version>` to GitHub Packages.

tasks.matching { it.name.matches("merge.*JniLibFolders".toRegex()) }.configureEach {
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
dependsOn("cargoBuild")
}
```

**Option B: `cargo-ndk` from the command line.** Standalone, no Gradle plugin required:
Host apps that decode `UserConfirmationReview` payloads should regenerate their golden decoder fixtures against the release:

```bash
cargo install cargo-ndk
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \
-o app/src/main/jniLibs \
build --release -p truapi-server --features ws-bridge
cargo run -p truapi-platform --bin review-fixtures
```

Both options require the Android NDK installed and the matching Rust targets (`rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android`).

Pre-built per-ABI `.so` files bundled inside the AAR are tracked as a follow-up so consumers eventually don't need a Rust toolchain at all.

## Maintainers: cutting a release

JitPack builds on demand from any git tag in `paritytech/truapi`, so a release is just:

1. Bump `publicationVersion` in `android/truapi-host/build.gradle.kts`.
2. Commit. Open a PR. Merge.
3. Tag the merge commit with the version: `git tag truapi-host-android@0.1.0 && git push origin truapi-host-android@0.1.0`.

That's the entire release flow, the iOS Swift Package follows the same pattern. The first consumer to pull the tag will trigger JitPack to build the artifact; subsequent fetches hit the cache.
prints one `NAME=0x<hex>` line per review variant. The same hex is pinned by `rust/crates/truapi-platform/tests/review_fixtures.rs`, so a variant reorder or field change fails in this repo's CI before it can break a host's decoder.

For local development, publish into the dev `~/.m2`:
For local development, publish into `~/.m2`:

```bash
gradle :truapi-host:publishReleasePublicationToMavenLocal
# or
make android-jni # optional: bundle the cdylibs into the local AAR
make android-publish-local
```

The artifact lands under `~/.m2/repository/io/parity/truapi-host-android/<version>/`. Consumers pointing at `mavenLocal()` can resolve it via `io.parity:truapi-host-android:<version>`. These local coordinates differ from the JitPack consumer coordinate (`com.github.paritytech.truapi:truapi-host:<tag>`): JitPack derives the group and artifactId from the repo and Gradle subproject, overriding the `io.parity:truapi-host-android` coordinates set in `build.gradle.kts`.
The artifact lands under `~/.m2/repository/io/parity/truapi-host/0.0.0-local/`; consumers pointing at `mavenLocal()` resolve it as `io.parity:truapi-host:0.0.0-local`.

## Regenerating the UniFFI bindings

Expand Down
30 changes: 17 additions & 13 deletions android/truapi-host/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TrUAPI Android host adapter.
//
// Publishes `io.parity:truapi-host-android` to Maven. Products running in a
// Publishes `io.parity:truapi-host` to Maven. Products running in a
// `WebView` connect to the Rust core via its localhost WebSocket bridge
// (`TrUAPIHostCore.startWsBridge`); the Rust core (compiled to
// `libtruapi_server.so`) handles wire decoding, routing, subscription
Expand Down Expand Up @@ -58,14 +58,12 @@ dependencies {
api("net.java.dev.jna:jna:5.14.0@aar")
}

// Coordinates for the local Maven publication (`publishToMavenLocal`).
// Distribution is via JitPack: a git tag drives `jitpack.yml`, and JitPack
// derives the consumer coordinates from the repo + subproject as
// `com.github.paritytech.truapi:truapi-host:<tag>`, overriding the group and
// artifactId below. These fields only matter for local testing.
// Coordinates for the Maven publication. Releases are published to GitHub
// Packages by .github/workflows/release-android.yml, which passes the real
// version via -PtruapiHostVersion; local publishes default to 0.0.0-local.
val publicationGroup = "io.parity"
val publicationArtifact = "truapi-host-android"
val publicationVersion = "0.1.0"
val publicationArtifact = "truapi-host"
val publicationVersion = (findProperty("truapiHostVersion") as String?) ?: "0.0.0-local"

group = publicationGroup
version = publicationVersion
Expand Down Expand Up @@ -114,11 +112,17 @@ publishing {
}

repositories {
// Maven Local for `gradle publishToMavenLocal` during development
// and for JitPack's build environment (see `jitpack.yml`).
// Consumers fetch the published artifact via JitPack at
// `com.github.paritytech.truapi:truapi-host:<tag>` after the
// repo is tagged.
// Maven Local for `gradle publishToMavenLocal` during development.
mavenLocal()
// Release target: the release-android workflow publishes here with
// the workflow's GITHUB_TOKEN.
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/paritytech/truapi")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
}
2 changes: 1 addition & 1 deletion android/truapi-host/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ProGuard / R8 rules applied to consumers of `io.parity:truapi-host-android`.
# ProGuard / R8 rules applied to consumers of `io.parity:truapi-host`.
#
# JNA reflects into our generated UniFFI types at runtime, so the bindings
# package and the public Kotlin surface must survive shrinking.
Expand Down
9 changes: 8 additions & 1 deletion rust/crates/truapi-codegen/tests/golden_rust_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ fn produce_rustdoc_json_for_package(
package: &str,
) -> PathBuf {
let output = Command::new("cargo")
.args(["+nightly", "rustdoc", "-p", package, "--target-dir"])
.args([
"+nightly",
"rustdoc",
"-p",
package,
"--lib",
"--target-dir",
])
.arg(target_dir)
.args(["--", "-Z", "unstable-options", "--output-format", "json"])
.current_dir(workspace_root)
Expand Down
12 changes: 12 additions & 0 deletions rust/crates/truapi-platform/src/bin/review-fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Prints canonical SCALE hex for each [`truapi_platform::UserConfirmationReview`]
//! sample, one `NAME=0x<hex>` line per sample, for host apps to pin as decoder
//! fixtures.

use truapi_platform::review_fixtures;

fn main() {
for (name, review) in review_fixtures::all() {
let hex = review_fixtures::encode_hex(&review);
println!("{name}=0x{hex}");
}
}
2 changes: 2 additions & 0 deletions rust/crates/truapi-platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use truapi::latest::{
use truapi::v01::HostAccountSignVrfRequest;
use url::Url;

pub mod review_fixtures;

/// Role-neutral runtime configuration supplied by the embedding host.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
Loading
Loading