Skip to content

Commit 656ae05

Browse files
kixelatedclaude
andauthored
Add web-transport-ffi: UniFFI bindings for Swift, Kotlin, Python (#239)
* web-transport-ffi: scaffold UniFFI crate Initial cdylib+staticlib crate with proc-macro UniFFI 0.29, a hello() export, and the uniffi-bindgen binary. Verified scaffolding generates Python, Swift, and Kotlin bindings from the library metadata. https://claude.ai/code/session_01Q2bHvKtyjMP6Coh7pVrqdU * web-transport-ffi: add build.sh, pyproject.toml, and async smoke export build.sh produces per-language bindings tarballs in --bindings-only mode and cross-compiled native lib tarballs otherwise (universal-darwin lipo, cargo-ndk for Android, staticlib-only for iOS). pyproject.toml wires maturin with the uniffi binding generator. Added a hello_async() export to validate the tokio async runtime integration end-to-end through a locally built wheel. https://claude.ai/code/session_01Q2bHvKtyjMP6Coh7pVrqdU * web-transport-ffi: port to UniFFI 0.31 + add Kotlin/Swift skeletons Replaces the PyO3 surface in rs/web-transport-ffi with a UniFFI 0.31 crate that targets Python, Kotlin, and Swift from one codebase. Mirrors the moq-ffi consolidation pattern: - Single LazyLock<Handle> tokio runtime in src/ffi.rs; every async export awaits a JoinHandle from RUNTIME.spawn (no async_runtime = "tokio" — uniffi just polls JoinHandles). - Flat WebTransportError enum with the same mapping helpers as web-transport-python's errors.rs; the foreign-language wrappers will reconstruct the legacy exception hierarchy on top. - Cancellable per-stream Mutex<Stream> + CancellationToken pattern preserved from web-transport-python so reset()/stop() interrupts in-flight write/read and sends RESET_STREAM/STOP_SENDING. - Drop on Session/Client/Server enters RUNTIME before touching quinn so the connection-close timer registers (moq ae506266). - crate-type = ["staticlib", "cdylib"] only (no "lib", per moq cfc35faf) with src/test.rs as a cfg-gated module rather than tests/echo.rs. - aws-lc-rs is the default; ring is available behind --features ring. Also lands: - kt/ Kotlin Multiplatform skeleton (dev.moq:web-transport artifactId, JVM + Android variants, JNA + jniLibs resource layouts). package.sh uses bash 3.2-compatible case statements instead of declare -A. - swift/ Swift Package Manager skeleton (WebTransport / WebTransportFFI, Package.swift binaryTarget points at the GH Release XCFramework). No publish.sh — the SPM mirror is deferred per the rollout plan. - web-transport-quinn added to [workspace.dependencies] so the FFI crate can pick up the right TLS provider via feature gates. cargo test -p web-transport-ffi passes the round-trip echo + 5 unit tests under both --features aws-lc-rs and --features ring. * web-transport: complete UniFFI port — Python wrapper, workflows, cleanup Replaces the legacy PyO3 binding (rs/web-transport-python) with a maturin+UniFFI wheel built on top of rs/web-transport-ffi, adds the GitHub release pipeline that fans out from web-transport-ffi-v* tags to Python/Kotlin/Swift downstream workflows, and removes the now-dead web-transport-python crate. Python wrapper (py/web-transport): - pyproject.toml: name → web-transport-rs (bare webtransport is squatted on PyPI; PEP 503 normalization collapses web-transport to the same name). module-name → web_transport._uniffi, manifest-path points at rs/web-transport-ffi. Dynamic version, Python ≥ 3.10. - python/web_transport/_errors.py: pure-Python legacy exception class hierarchy + reraise() translator. Maps each _uniffi.WebTransportError variant subclass back to the legacy SessionClosedByPeer / StreamClosedLocally / etc. classes with the same .source / .code / .reason / .kind / .partial / .expected / .limit attributes existing user code inspects. - python/web_transport/__init__.py: full rewrite as a thin wrapper around _uniffi. Every async/sync method wraps the call in try/except _uniffi.WebTransportError → reraise(e), preserving the pre-UniFFI public API (Client, Server, Session, SessionRequest, SendStream, RecvStream + async context-managers + async iterators). - _web_transport.pyi deleted (uniffi generates its own typed module). Rust: - error.rs: dropped #[uniffi(flat_error)] so the foreign-side variants expose structured fields (SessionClosedByPeer.code, StreamIncompleteRead.partial, etc.) — flat_error stripped them to just the Display string. Renamed SessionClosedByPeer.source field to .closed_by (thiserror reserves source for its own use). - Cargo.toml workspace: drop rs/web-transport-python member. - justfile: feature-powerset excludes web-transport-ffi (aws-lc-rs and ring are mutually exclusive at link time) and explicitly checks both TLS providers. Adds build-ffi recipes. - release-plz.toml: collapsed to a comment since web-transport-ffi now follows release-plz defaults (publishes to crates.io, auto-pushes the tag that drives the downstream workflows). Build script (rs/web-transport-ffi/build.sh): - Rewritten in moq-shape: staging-dir output, --archive opt-in, plain Linux drops the .a copy (moq de7cc60d), uniffi-bindgen invoked as `cargo run --package web-transport-ffi --bin uniffi-bindgen` to avoid workspace bin ambiguity (moq d9878e7b). Verified end-to-end with `./build.sh --bindings-only` producing kotlin/swift/python bindings. GitHub workflows (.github/): - scripts/release.sh: shared parse-version / prev-tag / create helper (copied verbatim from moq). - workflows/release-rs.yml: rename from release.yml, switches to the create-github-app-token + release-plz pattern so the auto-pushed web-transport-ffi-v* tag fires the downstream workflows. Falls back to GITHUB_TOKEN when the App secrets aren't configured. - workflows/release-py.yml: full rewrite, tag-triggered (web-transport-ffi-v*), 5-target manylinux_2_28 matrix + before-script-linux unset TARGET_CC/CXX on aarch64 (moq 0d59730d) + sdist job + verify-version job that asserts the tag matches rs/web-transport-ffi/Cargo.toml. Publish gated on PUBLISH_PYTHON. - workflows/release-kt.yml: new. 7-target build matrix (Android + desktop JVM), bindings job, package job that calls kt/scripts/package.sh and publishes to Maven Central via com.vanniktech.maven.publish (publish gated on PUBLISH_KOTLIN). - workflows/release-swift.yml: new. iOS + macOS matrix, package job assembles the XCFramework, release-assets job uploads the XCFramework.zip + Swift package tarball to the GitHub Release via release.sh create. Placeholder publish job behind PUBLISH_SPM for the future SPM mirror. Workspace cleanup: - rs/web-transport-python/ deleted in its entirety. - README.md + CLAUDE.md updated to document web-transport-ffi and the three language-binding outputs. Verification: - cargo check --workspace passes. - cargo clippy -p web-transport-ffi --all-targets --all-features -D warnings passes. - cargo test -p web-transport-ffi: 6/6 (4 unit, 1 client-config check, 1 end-to-end echo) under both aws-lc-rs and ring features. - ./rs/web-transport-ffi/build.sh --bindings-only generates all three language bindings. - Python files parse cleanly via ast.parse. - All four workflow YAMLs validate via python3 -c "yaml.safe_load". * py/web-transport: green up CI lint / ty / formatting Three categories of CI failure from PR #239's first run, fixed: - Ruff was checking the maturin-generated uniffi binding at python/web_transport/_uniffi/web_transport.py (~3.4k lines) — adds `extend-exclude` and a .gitignore entry so the vendored generator output stays out of both lint and the git index. - ty was reporting 180+ diagnostics: 141 of them `asyncio.TaskGroup` / `asyncio.timeout` complaints because I had lowered `requires-python` to >=3.10 (TaskGroup is 3.11+, and the existing tests use 3.12+ idioms). Bumped back to >=3.12 to match the legacy contract; the published wheels are already 3.12+3.13 only. - The remaining 35 ty errors were `Invalid object caught in an exception handler` on every `except _uniffi.WebTransportError`. UniFFI generates two classes by the same name in the bindings — the real Exception subclass (renamed to a private alias) and a container holding the variant subclasses. Static checkers resolve the public name to the container, which isn't an Exception subclass. Expose a `_UniffiError` alias in `_errors.py` (`= _uniffi.WebTransportError.Connect.__bases__[0]`, the actual Exception base) and switch all 35 `except` sites to it. Also reformatted `_errors.py` and `__init__.py` to spaces (matches the existing `_crypto.py` style); ran ruff format on the rest. Verification (local): - `uv run ruff check .` → All checks passed. - `uv run ruff format --check` → 30/30 files OK. - `uv run ty check` → All checks passed (0 diagnostics). - `uv run pytest tests/unit/` → 20/21 pass (the 1 failure is the sandbox-only IPv6 bind in `test_server_close_code_too_large`; CI Ubuntu has dual-stack). - `cargo check --workspace --all-targets --all-features` → green. - `cargo fmt --all --check` → green. * Address Python integration + Rust check regressions from CI Python wrapper (py/web-transport): - Session.close_reason: was returning a generic SessionClosedLocally on any close. Added a structured close_reason() FFI method on Rust and point the wrapper at it, so peer-side closes surface as SessionClosedByPeer with .source/.code/.reason intact (drove four test_session_lifecycle / test_client failures). - Session.close(): validate the u32 range in Python before crossing the FFI so 2**32 raises OverflowError (legacy contract); without it uniffi raised ValueError ("u32 requires 0 <= value < 4294967296"). - SendStream/RecvStream/Session __aexit__: await asyncio.sleep(0) / await wait_closed() so the FFI runtime gets a scheduling tick before we return. The legacy PyO3 binding made __aexit__ truly async (the await on the lock yielded implicitly); without an equivalent yield, the peer's accept_bi/accept_uni never saw the stream before we tore it down, hanging tests like test_send_context_manager_resets_on_exception. - Session.send_datagram: enforce the strict max_datagram_size boundary in Python. quinn allows datagrams slightly over (up to UDP MTU); legacy callers expected a hard rejection at max_datagram_size + 1. Rust FFI: - New ffi::spawn_abortable helper: wraps tokio::spawn so the spawned task is aborted when the outer future is dropped (asyncio.Task.cancel drops the foreign future). Without this, a cancelled read() would silently keep running on the runtime thread, consume incoming data, and discard it — the next read() then missed the bytes, breaking test_asyncio_cancel_read_then_read_again and ..._readexactly_... - SendStream::cancellable / RecvStream::cancellable switched to it. - session.close_reason() exposed as a new uniffi method returning the structured close error. - SessionRequest::accept/reject now map "request already accepted or rejected" to WebTransportError::Protocol (matches legacy SessionError rather than ValueError; what test_double_accept_raises expects). Rust workspace: - rs/web-transport-ffi/Cargo.toml: reformatted feature lists to single-line (cargo sort --workspace --check was failing on the multi-line aws-lc-rs / ring blocks — caught by `just check`). - workspace.members got resorted by cargo-sort. Verification: - cargo clippy -p web-transport-ffi --all-targets --all-features -D warnings → clean. - cargo test -p web-transport-ffi --lib → 6/6 pass. - cargo sort --workspace --check → clean. - cargo shear → no unused deps. - pytest tests/unit/ tests/integration/: 174/174 pass (after reverting the local IPv6→IPv4 sandbox patch from the test files — CI Ubuntu has dual-stack so the unchanged [::1] binds work there). - ruff check / ruff format --check / ty check all clean. * py/web-transport: stabilize Session.max_datagram_size + ty annotation Two more Python-suite regressions from the 3.13 CI: - ty[invalid-assignment]: `Session.close_reason` was annotated as `WebTransportError | None`, but the legacy browser_interop tests declare `close_reason: SessionError | None`. Tightened the wrapper return type back to `SessionError | None` and added a defensive fallback that wraps any non-session WebTransportError into a SessionError so the annotation stays accurate at runtime. - test_datagram_too_large: quinn's `max_datagram_size` grows during the session as path-MTU discovery learns a larger MTU. The legacy test pattern `cs.send_datagram(b"x" * max); cs.send_datagram(b"x" * (max + 1))` reads `max` once, sends a max-sized payload (which bumps MTU), then sends `max + 1` — but the live max has now grown past `max + 1`, so the second send succeeds and the test fails. Caching the initial `max_datagram_size` in the Python wrapper (and using it for both the public property and the send_datagram boundary check) pins the value to its session-start reading, matching the contract the legacy PyO3 binding implied. Verification on Python 3.13: ruff/ty clean, 174/174 pytest pass. * release-*.yml: also run on PRs touching FFI/workflow paths So the next time someone breaks the release pipeline (gradle config, package.sh, build.sh, maturin args, etc.) it fails the PR instead of the first real `web-transport-ffi-v*` tag. Each workflow now triggers on: push.tags: web-transport-ffi-v* # real release pull_request.paths: # smoke test - rs/web-transport-ffi/** - <language-specific dir>/** - .github/workflows/release-<lang>.yml - .github/scripts/release.sh Per-workflow specifics: - release-py.yml: parse-version now reads from Cargo.toml on PRs (no tag exists). verify-version is skipped on PRs (tautological). publish is gated on `github.event_name != 'pull_request' && vars.PUBLISH_PYTHON`. - release-kt.yml: same parse-version pattern. The package job runs end-to-end (gradle assemble + the package.sh staging), but the `Publish to Maven Central` step is now gated on PR-exclusion too — PRs from forks can't access MAVEN_CENTRAL_* / SIGNING_* secrets so the gradle invocation would fail anyway. - release-swift.yml: same parse-version pattern. The XCFramework still gets assembled and uploaded as a workflow artifact (so reviewers can download and sanity-check it), but the release-assets job — which attaches to a GitHub Release that doesn't exist on a PR — and the publish stub are both skipped. Concurrency groups are now keyed by `pull_request.number || github.ref` so a PR push doesn't cancel a tag-triggered release in flight. YAML validated via `python3 -c "yaml.safe_load(...)"` on all three. * kt: unblock release-kt PR smoke (rename Rust field; gate signing) The new PR-triggered release-kt run caught two real issues in the Kotlin path that would have only surfaced on the first `web-transport-ffi-v*` tag without the smoke job: 1. WebTransportError::SessionRejected had a `message: String` field. UniFFI's Kotlin codegen emits each variant as a class extending the error parent (which extends Throwable), and Throwable already defines `val message: String?`. The generated code declares both the constructor field and an `override val message` getter, so the Kotlin compiler errors with: 'message' hides member of supertype 'Throwable' and needs an 'override' modifier plus several "overload resolution ambiguity" follow-ups. Renamed the field to `detail` on the Rust side (it's only consumed by the Python wrapper, which I updated in lockstep). No public API change: the human-readable message still flows through `Exception(...)`. 2. The com.vanniktech.maven.publish plugin's `signAllPublications()` gates the `publishToMavenLocal` task (transitively, via `signKotlinMultiplatformPublication`) on a configured signing key. PR smoke runs don't have SIGNING_KEY set, so gradle aborted with "no configured signatory" before producing the maven-local layout. Made `signAllPublications()` conditional on `ORG_GRADLE_PROJECT_signingInMemoryKey` being present in the env — real release runs still sign as before (release-kt.yml always sets that env var before invoking publishAndReleaseToMavenCentral). Verified locally: - `cargo test -p web-transport-ffi --lib` → 6/6. - `uv run ty check` → clean. - `./rs/web-transport-ffi/build.sh --target x86_64-unknown-linux-gnu` emits `class SessionRejected(val statusCode, val detail)` in the generated kotlin (no clash). - `./kt/scripts/package.sh ...` → BUILD SUCCESSFUL, produces `release-out/maven-local/dev/moq/web-transport/0.1.0/`. * kt: inline Android config into build.gradle.kts + classpath the AGP The PR-triggered release-kt smoke caught a second Kotlin issue: the package.sh path that runs gradle :web-transport:publishToMavenLocal with -Pandroid.enabled=true (because per-target Android libs are present in libs/) failed at script compile time with "Unresolved reference: kotlin / androidTarget / compileOptions / ndk / namespace / ..." — every AGP- or KMP-android-derived symbol came up unresolved. Root cause: the Android-specific configuration lived in a separate android.gradle.kts applied via `apply(from = ...)`. Gradle's Kotlin DSL doesn't generate type-safe accessors for plugins inside apply-from scripts (the script body is compiled before the imperative apply runs), so every reference to androidTarget / LibraryExtension / defaultConfig / ndk / etc. failed to resolve. Two changes: 1. Inlined the Android block into build.gradle.kts, gated by the same `androidEnabled` property check. Kotlin DSL now compiles the accessors correctly because the source file owns both the plugins block and the configuration. 2. Added `id("com.android.library") version "8.7.3" apply false` to the plugins block. That puts AGP on the buildscript classpath (so the Kotlin DSL has the types) without applying it to the project when `-Pandroid.enabled` isn't set. The conditional `apply(plugin = "com.android.library")` still gates actual application — JVM-only contributors (no Google maven access) are unaffected. Deleted the now-unused kt/web-transport/android.gradle.kts. Verified locally: - ./kt/scripts/package.sh with JVM-only libs → BUILD SUCCESSFUL, produces dev/moq/web-transport/0.1.0/ in maven-local. - With Android libs present → gradle gets past the previous "Unresolved reference" wall and now stops at "SDK location not found" (no ANDROID_HOME locally); CI's setup-android@v3 step provides that. * release-*.yml: cancel-in-progress on PR pushes only Last push left three release-* workflow runs stuck in `queued` because the concurrency group serialized them — `cancel-in-progress: false` was correct for tag-pushed releases (don't kill an in-flight publish) but wrong for PR smoke tests (a new push should preempt the previous PR run, not stand in line behind it). Made `cancel-in-progress` conditional on the trigger: cancel-in-progress: ${{ github.event_name == 'pull_request' }} PR pushes now cancel the previous PR run of the same workflow; tag pushes still queue serially. Note: this commit is also a retrigger for the python (3.13) failure on commit 4f8bc88, which only touched Kotlin files. The Python suite passes locally on 3.13 (174/174, 3 runs in a row), so the CI failure is suspected flake. * kt: harden signing-key gate against empty secret CI hit `Could not read PGP secret key` from the `signKotlinMultiplatformPublication` task: SIGNING_KEY hadn't been configured on moq-dev/web-transport yet, so `${{ secrets.SIGNING_KEY }}` expanded to the empty string. My previous `!= null` gate accepted the empty env var as "present" and called signAllPublications() against a key that wasn't really there. Switched to `!isNullOrBlank()` so the gate only fires when there's an actual armored block to sign with. Real release runs (SIGNING_KEY set to the exported `gpg --armor --export-secret-keys ...` output) still sign as before; PR smoke runs and any "declared but empty" state now skip signing cleanly. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c6c49b6 commit 656ae05

60 files changed

Lines changed: 4077 additions & 2467 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/release.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Shared release helpers for GitHub Actions workflows.
4+
# Usage:
5+
# release.sh parse-version <prefix> — extract SemVer from GITHUB_REF given a tag prefix
6+
# release.sh prev-tag <prefix> — find the tag immediately before the current one
7+
# release.sh create <artifacts_dir> — create or update a GitHub release with artifacts
8+
#
9+
# Environment:
10+
# GITHUB_REF — set by GitHub Actions (e.g. refs/tags/moq-relay-v1.2.3)
11+
# GITHUB_OUTPUT — set by GitHub Actions (for writing step outputs)
12+
# GH_TOKEN — required for `create` subcommand
13+
14+
set -euo pipefail
15+
16+
# Parse a SemVer version from GITHUB_REF given a tag prefix.
17+
# Writes version=<ver> to $GITHUB_OUTPUT.
18+
parse_version() {
19+
local prefix="$1"
20+
local ref="${GITHUB_REF#refs/tags/}"
21+
22+
if [[ "$ref" =~ ^${prefix}-v([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$ ]]; then
23+
local version="${BASH_REMATCH[1]}"
24+
echo "version=${version}" >> "$GITHUB_OUTPUT"
25+
echo "Parsed version: ${version}"
26+
else
27+
echo "Tag format not recognized: $ref (expected ${prefix}-v<semver>)" >&2
28+
exit 1
29+
fi
30+
}
31+
32+
# Find the tag immediately before the current one (by version sort order).
33+
# Writes tag=<prev> to $GITHUB_OUTPUT.
34+
prev_tag() {
35+
local prefix="$1"
36+
local current_tag="${GITHUB_REF#refs/tags/}"
37+
38+
local prev
39+
prev=$(git tag --list "${prefix}-v*" --sort=v:refname \
40+
| awk -v cur="$current_tag" '$0 == cur { print prev; found=1; exit } { prev=$0 } END { if (!found) print "" }')
41+
42+
echo "tag=${prev}" >> "$GITHUB_OUTPUT"
43+
echo "Previous tag: ${prev:-none}"
44+
}
45+
46+
# Create or update a GitHub release with artifacts.
47+
# Args: <artifacts_dir>
48+
# Reads tag/title/prev_tag from environment or step outputs.
49+
create_release() {
50+
local artifacts_dir="$1"
51+
local tag="${RELEASE_TAG:?RELEASE_TAG must be set}"
52+
local title="${RELEASE_TITLE:?RELEASE_TITLE must be set}"
53+
local prev_tag="${RELEASE_PREV_TAG:-}"
54+
55+
if gh release view "$tag" > /dev/null 2>&1; then
56+
echo "Release exists, updating assets and metadata..."
57+
gh release upload "$tag" "$artifacts_dir"/* --clobber
58+
if [ -n "$prev_tag" ]; then
59+
gh release edit "$tag" --title "$title" --notes-start-tag "$prev_tag"
60+
else
61+
gh release edit "$tag" --title "$title"
62+
fi
63+
else
64+
echo "Creating new release..."
65+
if [ -n "$prev_tag" ]; then
66+
gh release create "$tag" \
67+
--title "$title" \
68+
--generate-notes \
69+
--notes-start-tag "$prev_tag" \
70+
"$artifacts_dir"/*
71+
else
72+
gh release create "$tag" \
73+
--title "$title" \
74+
--generate-notes \
75+
"$artifacts_dir"/*
76+
fi
77+
fi
78+
}
79+
80+
# Dispatch subcommands
81+
case "${1:-}" in
82+
parse-version) parse_version "$2" ;;
83+
prev-tag) prev_tag "$2" ;;
84+
create) create_release "$2" ;;
85+
*)
86+
echo "Usage: $0 {parse-version|prev-tag|create} <args>" >&2
87+
exit 1
88+
;;
89+
esac

.github/workflows/release-kt.yml

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
name: Release Kotlin
2+
3+
# Tag-triggered for real releases; pull_request mode (paths-filtered) runs
4+
# every job except the Maven Central publish as a smoke test, so changes to
5+
# the build script, gradle config, or package.sh fail the PR instead of the
6+
# first release.
7+
8+
on:
9+
push:
10+
tags:
11+
- "web-transport-ffi-v*"
12+
pull_request:
13+
paths:
14+
- "rs/web-transport-ffi/**"
15+
- "kt/**"
16+
- ".github/workflows/release-kt.yml"
17+
- ".github/scripts/release.sh"
18+
19+
permissions:
20+
contents: read
21+
22+
concurrency:
23+
group: release-kt-${{ github.event.pull_request.number || github.ref }}
24+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
25+
26+
jobs:
27+
parse-version:
28+
name: Parse version
29+
runs-on: ubuntu-latest
30+
outputs:
31+
version: ${{ steps.parse.outputs.version }}
32+
33+
steps:
34+
- uses: actions/checkout@v6
35+
36+
- name: Parse version
37+
id: parse
38+
run: |
39+
if [ "${{ github.event_name }}" = "pull_request" ]; then
40+
VERSION=$(grep '^version' rs/web-transport-ffi/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
41+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
42+
echo "PR build, using Cargo.toml version: ${VERSION}"
43+
else
44+
.github/scripts/release.sh parse-version web-transport-ffi
45+
fi
46+
47+
build:
48+
name: Build web-transport-ffi (${{ matrix.target }})
49+
needs: [parse-version]
50+
runs-on: ${{ matrix.os }}
51+
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
include:
56+
# Android
57+
- target: aarch64-linux-android
58+
os: ubuntu-latest
59+
- target: armv7-linux-androideabi
60+
os: ubuntu-latest
61+
- target: x86_64-linux-android
62+
os: ubuntu-latest
63+
# Desktop JVM
64+
- target: x86_64-unknown-linux-gnu
65+
os: ubuntu-latest
66+
- target: aarch64-unknown-linux-gnu
67+
os: ubuntu-latest
68+
- target: universal-apple-darwin
69+
os: macos-latest
70+
- target: x86_64-pc-windows-msvc
71+
os: windows-latest
72+
73+
steps:
74+
- uses: actions/checkout@v6
75+
76+
- name: Install Rust
77+
uses: dtolnay/rust-toolchain@stable
78+
with:
79+
targets: ${{ matrix.target == 'universal-apple-darwin' && 'x86_64-apple-darwin,aarch64-apple-darwin' || matrix.target }}
80+
81+
- name: Rust cache
82+
uses: Swatinem/rust-cache@v2
83+
84+
- name: Install cross-compilation tools (Linux ARM64)
85+
if: matrix.target == 'aarch64-unknown-linux-gnu'
86+
run: |
87+
sudo apt-get update
88+
sudo apt-get install -y gcc-aarch64-linux-gnu
89+
90+
- name: Install cargo-ndk (Android)
91+
if: contains(matrix.target, 'android')
92+
run: cargo install cargo-ndk
93+
94+
- name: Build
95+
shell: bash
96+
env:
97+
BUILD_TARGET: ${{ matrix.target }}
98+
BUILD_VERSION: ${{ needs.parse-version.outputs.version }}
99+
run: |
100+
./rs/web-transport-ffi/build.sh \
101+
--target "$BUILD_TARGET" \
102+
--version "$BUILD_VERSION" \
103+
--output dist
104+
105+
- name: Upload lib
106+
uses: actions/upload-artifact@v7
107+
with:
108+
name: kotlin-lib-${{ matrix.target }}
109+
# Upload only the lib/ contents; package.sh expects <target>/<libname>.
110+
path: dist/web-transport-ffi-${{ needs.parse-version.outputs.version }}-${{ matrix.target }}/lib/
111+
112+
bindings:
113+
name: Generate bindings
114+
needs: [parse-version]
115+
runs-on: ubuntu-latest
116+
117+
steps:
118+
- uses: actions/checkout@v6
119+
120+
- name: Install Rust
121+
uses: dtolnay/rust-toolchain@stable
122+
123+
- name: Rust cache
124+
uses: Swatinem/rust-cache@v2
125+
126+
- name: Generate bindings
127+
env:
128+
BUILD_VERSION: ${{ needs.parse-version.outputs.version }}
129+
run: |
130+
./rs/web-transport-ffi/build.sh \
131+
--bindings-only \
132+
--version "$BUILD_VERSION" \
133+
--output dist
134+
135+
- name: Upload bindings
136+
uses: actions/upload-artifact@v7
137+
with:
138+
name: kotlin-bindings
139+
# Upload only the kotlin subdir.
140+
path: dist/bindings/kotlin/
141+
142+
package:
143+
name: Package and publish
144+
needs: [parse-version, build, bindings]
145+
runs-on: ubuntu-latest
146+
env:
147+
# Picked up by com.vanniktech.maven.publish via Gradle project properties.
148+
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
149+
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
150+
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
151+
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
152+
153+
steps:
154+
- uses: actions/checkout@v6
155+
156+
- uses: actions/setup-java@v4
157+
with:
158+
distribution: temurin
159+
java-version: "17"
160+
161+
- name: Set up Android SDK
162+
uses: android-actions/setup-android@v3
163+
164+
- uses: gradle/actions/setup-gradle@v4
165+
166+
- name: Download per-target libs
167+
uses: actions/download-artifact@v8
168+
with:
169+
path: libs-raw
170+
pattern: kotlin-lib-*
171+
172+
- name: Flatten lib layout
173+
run: |
174+
mkdir -p libs
175+
for dir in libs-raw/kotlin-lib-*; do
176+
target="${dir#libs-raw/kotlin-lib-}"
177+
mv "$dir" "libs/$target"
178+
done
179+
ls -la libs
180+
181+
- name: Download bindings
182+
uses: actions/download-artifact@v8
183+
with:
184+
name: kotlin-bindings
185+
path: bindings
186+
187+
- name: Stage native libs + bindings into the gradle module
188+
env:
189+
BUILD_VERSION: ${{ needs.parse-version.outputs.version }}
190+
run: |
191+
./kt/scripts/package.sh \
192+
--version "$BUILD_VERSION" \
193+
--lib-dir libs \
194+
--bindings-dir bindings \
195+
--output release-out
196+
197+
- name: Publish to Maven Central
198+
# Tag pushes only, and only once PUBLISH_KOTLIN=true is set in repo
199+
# variables. PR runs always skip publish (secrets aren't available
200+
# on PRs from forks anyway). The dev.moq:web-transport coordinates
201+
# require MAVEN_CENTRAL_* and SIGNING_* secrets to be configured.
202+
if: github.event_name != 'pull_request' && vars.PUBLISH_KOTLIN == 'true'
203+
run: |
204+
gradle -p kt \
205+
-Pwebtransportffi.version="${{ needs.parse-version.outputs.version }}" \
206+
-Pandroid.enabled=true \
207+
:web-transport:publishAndReleaseToMavenCentral --no-configuration-cache
208+
209+
- name: Upload local maven layout
210+
uses: actions/upload-artifact@v7
211+
with:
212+
name: kotlin-package
213+
path: release-out/*

0 commit comments

Comments
 (0)