From 119e1ebb8f198b546d882a4e6be99276719c783d Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:10:54 +0900 Subject: [PATCH 01/12] Pin uniffi-dart to the multi-crate codegen fixes and re-enable Dart tests The upstream rev pinned by #1183 predates data tracks, so the bindgen emitted analysis errors (Bytes vs NativeType) for the multi-crate surface and the Dart tests have been disabled since #1034. Pin the fork rev that fixes multi-crate, custom-type and name-collision codegen; the pin is temporary until upstream merges the fixes or a livekit fork exists, see the Cargo.toml comment. Verified locally: cargo make dart-package passes all 5 FFI tests across the FFI boundary. --- .github/workflows/uniffi-dart-test.yml | 2 -- Cargo.lock | 8 ++++---- livekit-uniffi/Cargo.toml | 6 +++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/uniffi-dart-test.yml b/.github/workflows/uniffi-dart-test.yml index 1e694562b..d5b1aefc5 100644 --- a/.github/workflows/uniffi-dart-test.yml +++ b/.github/workflows/uniffi-dart-test.yml @@ -23,8 +23,6 @@ permissions: jobs: dart-test: - # TODO: Fix tests then re-enable. - if: false name: Dart package tests runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 5da102a03..7bc464ae5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8093,8 +8093,8 @@ dependencies = [ [[package]] name = "uniffi-dart" -version = "0.1.0+v0.30.0" -source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c#90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c" +version = "0.2.1+v0.31.2" +source = "git+https://github.com/1egoman/uniffi-dart?rev=ee04fd038659a9bd1438cbd297707861a55fd2f7#ee04fd038659a9bd1438cbd297707861a55fd2f7" dependencies = [ "anyhow", "camino", @@ -8164,8 +8164,8 @@ dependencies = [ [[package]] name = "uniffi_dart_macro" -version = "0.1.0+v0.30.0" -source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c#90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c" +version = "0.2.1+v0.31.2" +source = "git+https://github.com/1egoman/uniffi-dart?rev=ee04fd038659a9bd1438cbd297707861a55fd2f7#ee04fd038659a9bd1438cbd297707861a55fd2f7" dependencies = [ "futures", "proc-macro2", diff --git a/livekit-uniffi/Cargo.toml b/livekit-uniffi/Cargo.toml index 665f2ca55..d4c42b4eb 100644 --- a/livekit-uniffi/Cargo.toml +++ b/livekit-uniffi/Cargo.toml @@ -27,7 +27,11 @@ thiserror = { workspace = true } # Dart binding generator. Not published to crates.io, so pinned by git rev. The # rev must target the same uniffi-rs release (0.31) as the `uniffi` dependency # above, or it cannot read this crate's compiled metadata. -uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart", rev = "90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c", optional = true } +# TEMPORARY: pinned to the multi-crate codegen fixes on ryangaus's fork +# (upstream is dormant; PR Uniffi-Dart/uniffi-dart#150 is one of them). Move +# the pin back to upstream once merged, or to a livekit-maintained fork if a +# release needs cutting before that (CLT-2872). +uniffi-dart = { git = "https://github.com/1egoman/uniffi-dart", rev = "ee04fd038659a9bd1438cbd297707861a55fd2f7", optional = true } camino = { version = "1", optional = true } [features] From 2c299132af4865a32c3bafed71de0cdbcd8a79a9 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:11:04 +0900 Subject: [PATCH 02/12] Make the generated livekit_uniffi Dart package publishable Split the dart-package flow by cargo-make profile: dev builds keep the embedded host dylib and publish_to: none, release builds omit the dylib (consumers use the hook's download mode) and clear publish_to so pub.dev accepts the package. The publish_to guard fails closed, an unknown profile still renders none. A dart-clean task starts every build from scratch so a release build over a previous dev tree cannot ship the stale host dylib, which the hook would prefer over download on every platform. Also add the metadata pub.dev requires or scores: LICENSE, README, CHANGELOG, repository/homepage/issue_tracker pubspec fields, real version constraints for code_assets/hooks (wide ranges, flutter_test pins meta exactly so carets break older stable channels), and an analysis_options.yaml so unused-import lints in generated code do not fail publish validation. Verified: both profiles build, all 5 FFI tests pass on the dev package, dart pub publish --dry-run exits 0 on the release package, and a release build over a dev tree contains no dylib. --- livekit-uniffi/Makefile.toml | 54 +++++++++++++++++-- livekit-uniffi/support/dart/README.md | 40 ++++++++++++++ .../support/dart/analysis_options.yaml | 8 +++ livekit-uniffi/support/dart/pubspec.yaml.tera | 24 +++++++-- 4 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 livekit-uniffi/support/dart/README.md create mode 100644 livekit-uniffi/support/dart/analysis_options.yaml diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index ec3e57746..5516b2a8e 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -20,12 +20,20 @@ TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}" SPM_NAME = "LiveKitUniFFI" NPM_NAME = "@livekit/uniffi" DART_PACKAGE_NAME = "livekit_uniffi" +# Rendered into pubspec.yaml by dart-generate-manifest. Fail closed: every +# profile gets `publish_to: none` (dev packages embed a host dylib and must +# never reach pub.dev), and only an explicit release build clears it so +# `dart pub publish` accepts the package. +PUBSPEC_PUBLISH_TO = "none" # Per-release directory holding the cdylib archives published by # .github/workflows/uniffi-cdylib.yml. Downloaders append # `/v/build-.zip` (and a `.sha256` sidecar). CDYLIB_DOWNLOAD_BASE_URL = "https://github.com/livekit/rust-sdks/releases/download/livekit-uniffi" +[env.release] +PUBSPEC_PUBLISH_TO = "" + [config] skip_core_tasks = true @@ -697,14 +705,33 @@ exec cargo run --features dart-bindgen --bin uniffi-bindgen-dart -- \ """ # Assemble a consumable Dart package around the generated bindings: idiomatic -# lib/ layout, a pubspec, the Native Assets build hook, and (dev) the locally -# built dynamic library so a consumer can load it without a download step. +# lib/ layout, a pubspec, the Native Assets build hook, package metadata, and +# (dev only) the locally built dynamic library so a consumer can load it +# without a download step. Release builds omit the dylib, so consumers use the +# hook's download mode against the release assets. # # packages/dart/ # pubspec.yaml (from support/dart/pubspec.yaml.tera) # lib/livekit_uniffi.dart # hook/build.dart (from support/dart/hook/build.dart) -# liblivekit_uniffi. (dev: copied local build) +# LICENSE, README.md, CHANGELOG.md +# liblivekit_uniffi. (dev only: copied local build) +# +# Publishing note: pub builds its archive from git's file listing and this +# whole tree is gitignored, so `dart pub publish` must run from a copy of +# packages/dart placed outside the repository (uniffi-dart-publish.yml does +# this; do the same for a manual publish). + +# Start from a clean slate: leftovers from a previous build with a different +# profile would otherwise survive into this one. The dev-profile dylib is the +# dangerous case, a release package must never carry it (the build hook +# prefers a package-root library over download, on every platform). +[tasks.dart-clean] +private = true +script_runner = "@shell" +script = """ +rm -rf ${PACKAGES_DIR}/${LANG} +""" [tasks.dart-move-code-into-lib] private = true @@ -733,14 +760,31 @@ extend = "tera" args = ["--env-only", "-t", "${SUPPORT_DIR}/${LANG}/hook/build.dart.tera", "-o", "${PACKAGES_DIR}/${LANG}/hook/build.dart"] # Dev delivery: copy the locally built dylib into the package root, where the -# build hook resolves it. A release flow would replace this with a download. +# build hook resolves it. Development profile only. A published package must +# not contain the host dylib, its consumers get the right library per target +# from the hook's download mode instead. [tasks.dart-copy-lib] private = true +condition = { profiles = ["development"] } script_runner = "@shell" script = """ cp ${LIB_PATH} ${PACKAGES_DIR}/${LANG}/ """ +# Package metadata: pub.dev requires a LICENSE and scores README/CHANGELOG. +# The LICENSE comes from the repo root (same source as the Swift package), +# the changelog from the crate, and the README and analysis options from +# support/dart. +[tasks.dart-copy-meta] +private = true +script_runner = "@shell" +script = """ +cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/LICENSE ${PACKAGES_DIR}/${LANG}/ +cp ${SUPPORT_DIR}/${LANG}/README.md ${PACKAGES_DIR}/${LANG}/ +cp ${SUPPORT_DIR}/${LANG}/analysis_options.yaml ${PACKAGES_DIR}/${LANG}/ +cp CHANGELOG.md ${PACKAGES_DIR}/${LANG}/ +""" + [tasks.dart-copy-tests] private = true script_runner = "@shell" @@ -752,11 +796,13 @@ cp ${SUPPORT_DIR}/${LANG}/test/*.dart ${PACKAGES_DIR}/${LANG}/test/ [tasks.dart-package-flow] private = true dependencies = [ + "dart-clean", "bindgen-dart", "dart-move-code-into-lib", "dart-generate-manifest", "dart-copy-hook", "dart-copy-lib", + "dart-copy-meta", "dart-copy-tests" ] diff --git a/livekit-uniffi/support/dart/README.md b/livekit-uniffi/support/dart/README.md new file mode 100644 index 000000000..90fd7cb32 --- /dev/null +++ b/livekit-uniffi/support/dart/README.md @@ -0,0 +1,40 @@ +# livekit_uniffi + +Dart bindings for the LiveKit Rust SDK core, generated from the +[`livekit-uniffi`](https://github.com/livekit/rust-sdks/tree/main/livekit-uniffi) +crate with [UniFFI](https://mozilla.github.io/uniffi-rs/) and +[uniffi-dart](https://github.com/Uniffi-Dart/uniffi-dart). + +This is a low-level package. It is consumed by +[`livekit_client`](https://pub.dev/packages/livekit_client) and is not +intended to be used directly in applications. + +## How it works + +The bindings call into a prebuilt Rust dynamic library through Dart's +[Native Assets](https://dart.dev/tools/hooks). The package's `hook/build.dart` +resolves the library for your target platform at build time, downloading it +from the matching [`livekit-uniffi` release](https://github.com/livekit/rust-sdks/releases) +and verifying its SHA-256 checksum. No Rust toolchain is required to consume +this package. + +Requires Dart >= 3.10 (Flutter >= 3.38), where build hooks and code assets are +stable. There is no web support: guard usage behind a conditional import. + +## Supported platforms + +macOS (arm64, x64), iOS (device and simulator), Android (arm64, armv7, x64), +Linux (arm64, x64), and Windows (arm64, x64). + +## Development + +This package is generated; do not edit it directly. To build it from source: + +```sh +cd rust-sdks/livekit-uniffi +cargo make dart-package # generates packages/dart with a locally built library +``` + +See the [crate README](https://github.com/livekit/rust-sdks/tree/main/livekit-uniffi) +for details. Issues and pull requests go to +[livekit/rust-sdks](https://github.com/livekit/rust-sdks). diff --git a/livekit-uniffi/support/dart/analysis_options.yaml b/livekit-uniffi/support/dart/analysis_options.yaml new file mode 100644 index 000000000..74aa836c5 --- /dev/null +++ b/livekit-uniffi/support/dart/analysis_options.yaml @@ -0,0 +1,8 @@ +# The package contents are generated by uniffi-dart, which emits imports and +# helpers that are not always used by the exported surface. Silence those +# lints so pub.dev validation stays clean; real analysis errors still fail. +analyzer: + errors: + unused_import: ignore + unnecessary_import: ignore + unused_element: ignore diff --git a/livekit-uniffi/support/dart/pubspec.yaml.tera b/livekit-uniffi/support/dart/pubspec.yaml.tera index bfd6a7ddb..443d7fea3 100644 --- a/livekit-uniffi/support/dart/pubspec.yaml.tera +++ b/livekit-uniffi/support/dart/pubspec.yaml.tera @@ -1,16 +1,30 @@ name: {{ DART_PACKAGE_NAME }} -description: Dart bindings for the LiveKit UniFFI interface. +description: Dart bindings for the LiveKit Rust SDK core, delivered as a prebuilt native library via Native Assets. version: {{ CARGO_MAKE_CRATE_VERSION }} -publish_to: none +repository: https://github.com/livekit/rust-sdks +issue_tracker: https://github.com/livekit/rust-sdks/issues +homepage: https://livekit.io/ +{#- Dev packages embed a host dylib and must never be published; release + packages omit publish_to so pub.dev accepts them. Fails closed: the base + [env] in Makefile.toml sets "none" for every profile and only + [env.release] clears it. #} +{%- if PUBSPEC_PUBLISH_TO %} +publish_to: {{ PUBSPEC_PUBLISH_TO }} +{%- endif %} environment: sdk: ">=3.10.0 <4.0.0" dependencies: ffi: ^2.1.0 - # Native Assets build-hook support. `any` tracks the SDK-bundled versions. - code_assets: any - hooks: any + # Native Assets build-hook support. pub.dev requires bounded constraints + # (`any` fails publish validation), but these must stay wide: the resolvable + # version is coupled to the consumer's Dart SDK via a shared `meta` pin + # (Flutter 3.44 back-solves to code_assets 1.0.0 / hooks 1.0.2, while a bare + # Dart 3.12 resolves 1.2.1 / 2.1.0). Carets here break `flutter pub get` + # for consumers on older stable channels. + code_assets: ">=1.0.0 <2.0.0" + hooks: ">=1.0.2 <3.0.0" # Used by hook/build.dart to download and verify prebuilt libraries. archive: ^4.0.0 crypto: ^3.0.0 From d131b1effa45a9ab6b57b1940850bc318419a7f3 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:28:53 +0900 Subject: [PATCH 03/12] Attach Dart/Flutter cdylib assets to livekit-uniffi releases Wire the existing uniffi-cdylib.yml into uniffi-packages.yml so every livekit-uniffi release carries the build-.zip archives (and sha256 sidecars) the Dart build hook downloads at consumer build time. The assets attach to the already-published release, so the knope assets marker stays off (a draft-based flow stranded releases before, #1256). Since this activates the previously dormant uniffi-cdylib.yml on every release, also bind its tag name through env instead of template expanding it into the upload script: tag names may contain shell metacharacters (Actions script injection). The changeset cuts the release that carries the first assets. --- .changeset/uniffi_dart_release_automation.md | 5 ++++ .github/workflows/uniffi-cdylib.yml | 6 ++++- .github/workflows/uniffi-packages.yml | 24 ++++++++++++++++---- knope.toml | 5 ++-- 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 .changeset/uniffi_dart_release_automation.md diff --git a/.changeset/uniffi_dart_release_automation.md b/.changeset/uniffi_dart_release_automation.md new file mode 100644 index 000000000..f1f9d1e65 --- /dev/null +++ b/.changeset/uniffi_dart_release_automation.md @@ -0,0 +1,5 @@ +--- +livekit-uniffi: patch +--- + +Attach Dart/Flutter cdylib assets to releases and prepare livekit_uniffi for pub.dev publishing diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index dc5c4ba08..eaffecc56 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -167,6 +167,10 @@ jobs: path: cdylibs - name: Upload to release + # TAG_NAME is bound through env rather than template-expanded into the + # script: tag names may contain shell metacharacters (Actions script + # injection). GITHUB_REPOSITORY is provided by the runner already. env: GH_TOKEN: ${{ github.token }} - run: gh release upload "${{ inputs.tag_name }}" cdylibs/* --repo "${{ github.repository }}" --clobber + TAG_NAME: ${{ inputs.tag_name }} + run: gh release upload "$TAG_NAME" cdylibs/* --repo "$GITHUB_REPOSITORY" --clobber diff --git a/.github/workflows/uniffi-packages.yml b/.github/workflows/uniffi-packages.yml index 0ebab424c..a03c76062 100644 --- a/.github/workflows/uniffi-packages.yml +++ b/.github/workflows/uniffi-packages.yml @@ -1,16 +1,18 @@ name: UniFFI packages # Publishes the per-language livekit-uniffi wrapper packages (Swift xcframework → -# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) when a -# livekit-uniffi release is published. +# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) and attaches +# the Dart/Flutter cdylib archives to the release when a livekit-uniffi release +# is published. # # knope publishes the `livekit-uniffi/v*` GitHub release (and its git tag) # directly on the release merge — no draft — so this reacts to the published # release event, which fires only for genuine livekit-uniffi releases (the gate # is free). workflow_dispatch allows a manual re-run with an explicit tag. # -# The Dart/Flutter cdylib target is experimental and not published for now; add a -# `cdylib` job (uniffi-cdylib.yml) back when it ships. +# The Dart package itself is published from uniffi-dart-publish.yml (tag-push +# triggered, as pub.dev requires); it waits for the cdylib assets this workflow +# attaches, since the package's build hook downloads them at consumer build time. on: release: @@ -64,3 +66,17 @@ jobs: tag_name: ${{ needs.resolve-tag.outputs.tag_name }} dry_run: ${{ inputs.dry_run || false }} secrets: inherit + + # Attaches build-.zip (+ .sha256) for every Dart/Flutter target to the + # release. The livekit_uniffi package's hook/build.dart downloads these at + # consumer build time, so a release consumed from pub.dev must carry them. + cdylib: + needs: resolve-tag + permissions: + # The upload step attaches assets to the release with the workflow token. + contents: write + uses: ./.github/workflows/uniffi-cdylib.yml + with: + version: ${{ needs.resolve-tag.outputs.version }} + tag_name: ${{ needs.resolve-tag.outputs.tag_name }} + dry_run: ${{ inputs.dry_run || false }} diff --git a/knope.toml b/knope.toml index 4e8783902..182f2f394 100644 --- a/knope.toml +++ b/knope.toml @@ -130,8 +130,9 @@ versioned_files = [ changelog = "livekit-uniffi/CHANGELOG.md" # No `assets` marker: knope publishes the release + tag directly, and # uniffi-packages.yml reacts to the published release to build/publish the Swift -# and Android wrapper packages. (Re-add the marker if the release itself needs -# to host assets, e.g. the Dart/Flutter cdylibs.) +# and Android wrapper packages. The Dart/Flutter cdylibs are attached to the +# already-published release by its cdylib job (gh release upload --clobber), so +# the marker stays off; a draft-based flow stranded releases before (#1256). scopes = ["uniffi", "livekit-uniffi"] [packages.livekit-wakeword] From c3437557d994ceddeda8e6988d78955c978e4ca8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:11:25 +0900 Subject: [PATCH 04/12] Add a tag-push workflow publishing livekit_uniffi to pub.dev pub.dev's automated publishing only accepts workflows triggered by a push of a tag matching the configured pattern, so this cannot be a job in the release-event-triggered uniffi-packages.yml. The workflow builds the release-profile package (cached, before the asset wait), stages it outside the work tree (pub archives from git's file listing and the generated packages/ tree is gitignored), refuses to publish a package containing a local native library, gates on every cdylib zip having its sha256 sidecar, and fast-fails on mis-pointed tags or a missing release. Tag names are env-bound, never template-expanded into scripts, since the job holds id-token: write for pub.dev token minting. PUBLISHING IS NOT ENABLED YET (PUBLISH_ENABLED=false): every run stops at dart pub publish --dry-run until the uniffi-dart pin moves off the personal fork, a livekit.io publisher admin has done the first manual publish, and automated publishing is configured on pub.dev. See the header for the full enablement steps. --- .github/workflows/uniffi-dart-publish.yml | 203 ++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 .github/workflows/uniffi-dart-publish.yml diff --git a/.github/workflows/uniffi-dart-publish.yml b/.github/workflows/uniffi-dart-publish.yml new file mode 100644 index 000000000..f5454ffaa --- /dev/null +++ b/.github/workflows/uniffi-dart-publish.yml @@ -0,0 +1,203 @@ +name: UniFFI Dart package + +# Publishes the generated livekit_uniffi Dart package to pub.dev on a +# livekit-uniffi release. +# +# This is a separate workflow from uniffi-packages.yml because pub.dev's +# automated publishing only accepts workflows triggered by a tag push matching +# the tag pattern configured on the package (livekit-uniffi/v{{version}}); +# a release-event-triggered workflow is rejected. knope-bot creates the tag via +# the API, and App-created events do trigger workflows here (see the note in +# uniffi-packages.yml / #1256). +# +# The package's hook/build.dart downloads build-.zip assets from the +# GitHub release at consumer build time. Those assets are attached by the +# cdylib job in uniffi-packages.yml, which runs from the same release, so this +# workflow waits for them before publishing: a pub.dev version whose assets +# are missing would be broken on arrival, and pub.dev versions cannot be +# unpublished. +# +# PUBLISHING IS NOT ENABLED YET. Until the steps below are done, every run +# stops after `dart pub publish --dry-run`. Things the first live tag +# confirms: that knope-bot's API-created tag fires this push trigger at all +# (expected for App events, never exercised here; workflow_dispatch is the +# fallback), and that the asset wait sees the cdylib job's uploads. +# Enablement steps: +# 1. The temporary uniffi-dart pin on ryangaus's personal fork (see +# livekit-uniffi/Cargo.toml) has moved to upstream or a livekit fork, +# so releases don't depend on a personal repo. +# 2. A first manual `dart pub publish` by a livekit.io publisher admin has +# created the package on pub.dev (pub.dev only automates existing packages). +# 3. Automated publishing from GitHub Actions is enabled in the package's +# pub.dev admin settings for livekit/rust-sdks with tag pattern +# `livekit-uniffi/v{{version}}`, and PUBLISH_ENABLED below is flipped. + +on: + push: + tags: ["livekit-uniffi/v*"] + workflow_dispatch: + inputs: + tag_name: + description: "Release tag (e.g. livekit-uniffi/v0.1.9)" + required: true + type: string + +env: + # Flip to "true" once the enablement steps in the header are done. Real + # publishing additionally requires a tag-push trigger; workflow_dispatch + # runs always stop at the dry run (pub.dev rejects their OIDC tokens). + PUBLISH_ENABLED: "false" + +permissions: + contents: read + +jobs: + build-and-publish: + name: Build & publish Dart package + runs-on: ubuntu-latest + permissions: + contents: read + # Exchanged for a temporary pub.dev token by dart-lang/setup-dart. + id-token: write + steps: + - name: Resolve tag + id: tag + # Bound through env, never template-expanded into the script: tag names + # and dispatch inputs may contain shell metacharacters (Actions script + # injection), and this job can mint the pub.dev publish token. + env: + PUSHED_TAG: ${{ github.ref_name }} + INPUT_TAG: ${{ inputs.tag_name }} + run: | + TAG="${INPUT_TAG:-$PUSHED_TAG}" + case "$TAG" in + livekit-uniffi/v*) ;; + *) echo "Unexpected tag: $TAG"; exit 1 ;; + esac + echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + # Build the tagged source, not the dispatch ref (which defaults to main). + ref: ${{ steps.tag.outputs.tag_name }} + submodules: true + + # Fast fail on a mis-pointed tag before spending runner time: the tag + # version must match the crate version the checkout carries (it is also + # what the pubspec, and pub.dev's tag-pattern check, will see). + - name: Check tag matches crate version + env: + VERSION: ${{ steps.tag.outputs.version }} + run: | + crate_version=$(grep -m1 '^version = ' livekit-uniffi/Cargo.toml | cut -d'"' -f2) + if [ "$crate_version" != "$VERSION" ]; then + echo "Tag version $VERSION does not match livekit-uniffi crate version $crate_version." >&2 + exit 1 + fi + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: false + rustflags: "" + + # Same keys as uniffi-dart-test.yml, so pushes to main keep them warm. + - name: Cache cargo registry + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Cache cargo target + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: target/ + key: ${{ runner.os }}-cargo-target-dart-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-target-dart- + + - name: Install Common Deps for UniFFI Tasks + uses: ./.github/actions/uniffi-deps + + - name: Setup Dart + uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + with: + sdk: stable + + - name: Build Dart package (release) + working-directory: livekit-uniffi + run: cargo make --profile release dart-package + + # pub builds the publish archive from git's file listing, and the + # generated packages/ tree is gitignored, so publishing from inside the + # work tree produces an empty archive. Stage a copy outside it. + # + # The guard is a backstop for the dart-clean task: a package that ships + # a local native library would shadow the hook's download mode for every + # consumer, and pub.dev versions cannot be unpublished. + - name: Stage package outside the work tree + run: | + rm -rf "$RUNNER_TEMP/livekit_uniffi" + cp -R livekit-uniffi/packages/dart "$RUNNER_TEMP/livekit_uniffi" + if find "$RUNNER_TEMP/livekit_uniffi" \( -name 'liblivekit_uniffi.*' -o -name 'livekit_uniffi.dll' \) | grep -q .; then + echo "Staged package contains a local native library; refusing to publish." >&2 + exit 1 + fi + + # The cdylib job in uniffi-packages.yml attaches the per-target archives + # to the release after knope publishes it, so they can land up to an + # hour after this workflow starts; the build above does not need them, + # only the validate/publish steps do. The hook needs a + # build-.zip AND its .zip.sha256 sidecar per target, so gate on + # both. 12 targets; keep the count in sync with the matrix in + # uniffi-cdylib.yml. + # + # Fails fast when the release itself is missing (mis-tag, or knope never + # published). Recovery from a failed cdylib build: fix it, dispatch + # uniffi-packages.yml with this tag to attach the assets, then re-run + # this workflow's failed run (or dispatch it with the tag). + - name: Wait for cdylib release assets + timeout-minutes: 90 + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag_name }} + run: | + expected=12 + missing=0 + while true; do + if ! assets=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '[.assets[].name]'); then + missing=$((missing + 1)) + echo "release $TAG not found ($missing/10)" + if [ "$missing" -ge 10 ]; then + echo "Release $TAG does not exist; is the tag mis-pointed or the release unpublished?" >&2 + exit 1 + fi + sleep 60 + continue + fi + missing=0 + zips=$(echo "$assets" | jq '[.[] | select(test("^build-.*\\.zip$"))] | length') + sums=$(echo "$assets" | jq '[.[] | select(test("^build-.*\\.zip\\.sha256$"))] | length') + echo "cdylib assets on $TAG: $zips/$expected zips, $sums/$expected sha256 sidecars" + if [ "$zips" -ge "$expected" ] && [ "$sums" -ge "$zips" ]; then + break + fi + sleep 120 + done + + - name: Validate package + working-directory: ${{ runner.temp }}/livekit_uniffi + run: | + dart pub get + dart pub publish --dry-run + + - name: Publish to pub.dev + if: ${{ env.PUBLISH_ENABLED == 'true' && github.event_name == 'push' }} + working-directory: ${{ runner.temp }}/livekit_uniffi + run: dart pub publish --force From ec291cfc64831116bd7ecfb4a2f72216ea342f03 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:46:28 +0800 Subject: [PATCH 05/12] Move the uniffi-dart pin back to upstream The multi-crate codegen fixes (Uniffi-Dart/uniffi-dart#150, #154, #156) are all merged upstream, so the temporary pin to the contributor fork is no longer needed. Pin the current upstream main rev, as no release tag has been cut since v0.2.1. Upstream codegen now emits ignore_for_file hints and drops unused imports, but it introduces unused locals in async callback trampolines, so add unused_local_variable to the generated-code lint ignores. --- Cargo.lock | 4 ++-- livekit-uniffi/Cargo.toml | 10 +++++----- livekit-uniffi/support/dart/analysis_options.yaml | 8 +++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f7bf58a8..badbc823a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7737,7 +7737,7 @@ dependencies = [ [[package]] name = "uniffi-dart" version = "0.2.1+v0.31.2" -source = "git+https://github.com/1egoman/uniffi-dart?rev=ee04fd038659a9bd1438cbd297707861a55fd2f7#ee04fd038659a9bd1438cbd297707861a55fd2f7" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=67547701d849c9ef6302f79bb2189bb6075f69fa#67547701d849c9ef6302f79bb2189bb6075f69fa" dependencies = [ "anyhow", "camino", @@ -7808,7 +7808,7 @@ dependencies = [ [[package]] name = "uniffi_dart_macro" version = "0.2.1+v0.31.2" -source = "git+https://github.com/1egoman/uniffi-dart?rev=ee04fd038659a9bd1438cbd297707861a55fd2f7#ee04fd038659a9bd1438cbd297707861a55fd2f7" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=67547701d849c9ef6302f79bb2189bb6075f69fa#67547701d849c9ef6302f79bb2189bb6075f69fa" dependencies = [ "futures", "proc-macro2", diff --git a/livekit-uniffi/Cargo.toml b/livekit-uniffi/Cargo.toml index 802d5cfcb..5d45d4015 100644 --- a/livekit-uniffi/Cargo.toml +++ b/livekit-uniffi/Cargo.toml @@ -29,11 +29,11 @@ thiserror = { workspace = true } # Dart binding generator. Not published to crates.io, so pinned by git rev. The # rev must target the same uniffi-rs release (0.31) as the `uniffi` dependency # above, or it cannot read this crate's compiled metadata. -# TEMPORARY: pinned to the multi-crate codegen fixes on ryangaus's fork -# (upstream is dormant; PR Uniffi-Dart/uniffi-dart#150 is one of them). Move -# the pin back to upstream once merged, or to a livekit-maintained fork if a -# release needs cutting before that (CLT-2872). -uniffi-dart = { git = "https://github.com/1egoman/uniffi-dart", rev = "ee04fd038659a9bd1438cbd297707861a55fd2f7", optional = true } +# Pinned to upstream main from 2026-08-31, which carries the multi-crate +# codegen fixes (Uniffi-Dart/uniffi-dart#150/#154/#156) this crate needs, +# previously consumed from a contributor fork. Upstream has published no +# release tag since v0.2.1, hence a rev pin rather than a tag. +uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart", rev = "67547701d849c9ef6302f79bb2189bb6075f69fa", optional = true } camino = { version = "1", optional = true } [features] diff --git a/livekit-uniffi/support/dart/analysis_options.yaml b/livekit-uniffi/support/dart/analysis_options.yaml index 74aa836c5..03e3b5a1e 100644 --- a/livekit-uniffi/support/dart/analysis_options.yaml +++ b/livekit-uniffi/support/dart/analysis_options.yaml @@ -1,8 +1,10 @@ -# The package contents are generated by uniffi-dart, which emits imports and -# helpers that are not always used by the exported surface. Silence those -# lints so pub.dev validation stays clean; real analysis errors still fail. +# The package contents are generated by uniffi-dart, which emits imports, +# helpers, and locals that are not always used by the exported surface. +# Silence those lints so pub.dev validation stays clean; real analysis +# errors still fail. analyzer: errors: unused_import: ignore unnecessary_import: ignore unused_element: ignore + unused_local_variable: ignore From 873a6e05565a107e15b25064525addfb914d4af5 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:59:24 +0800 Subject: [PATCH 06/12] Bind the release tag through env in resolve-tag The tag name was template-expanded into the shell script. It only comes from knope or a workflow_dispatch input and the job token is read-only, but binding it through env matches how the cdylib and Dart publish jobs already handle it and closes the pattern off. --- .github/workflows/uniffi-packages.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/uniffi-packages.yml b/.github/workflows/uniffi-packages.yml index a03c76062..0c1395ab8 100644 --- a/.github/workflows/uniffi-packages.yml +++ b/.github/workflows/uniffi-packages.yml @@ -42,8 +42,12 @@ jobs: version: ${{ steps.get-tag.outputs.version }} steps: - id: get-tag + # TAG is bound through env rather than template-expanded into the + # script: tag names may contain shell metacharacters (Actions script + # injection). + env: + TAG: ${{ github.event.release.tag_name || inputs.tag_name }} run: | - TAG="${{ github.event.release.tag_name || inputs.tag_name }}" echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" # Strip the "livekit-uniffi/v" prefix to get the package version (e.g. 0.1.6). echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT" From 051ca2eba6cbc179fa8d2029cdc2fcba2bcecf07 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:00:21 +0800 Subject: [PATCH 07/12] Check out the release tag in the cdylib job The Swift and Android reusable workflows already build from inputs.tag_name. Without it, a manual re-run of the cdylib job for an older release built whatever ref the dispatch UI selected and uploaded the result under that release's version with --clobber. --- .github/workflows/uniffi-cdylib.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index eaffecc56..3fb43ad36 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -75,6 +75,10 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: + # Build the tagged source, not the ref the workflow was dispatched + # from. A manual re-run for an older release would otherwise upload + # libraries built from main under that release's version. + ref: ${{ inputs.tag_name }} submodules: true - name: Setup Rust toolchain From 8ee4f631bf02eddc3e16f903c9a7630d1d7f0527 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:00:21 +0800 Subject: [PATCH 08/12] Build the Linux cdylibs in manylinux_2_28 containers Native builds on ubuntu-latest inherit its glibc 2.39 floor, so the published .so would fail to load on Debian 12, Ubuntu 22.04 and Amazon Linux 2023, which is where server-side Dart consumers of the access token API run. Build inside quay.io/pypa/manylinux_2_28 images instead, the same approach ffi-builds.yml uses for livekit-ffi. The arm64 target now runs on a native arm64 runner, which removes the cross-linker setup. --- .github/workflows/uniffi-cdylib.yml | 43 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index 3fb43ad36..38b1250d4 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -53,9 +53,11 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu platform: linux - - os: ubuntu-latest + build_image: quay.io/pypa/manylinux_2_28_x86_64 + - os: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu platform: linux + build_image: quay.io/pypa/manylinux_2_28_aarch64 - os: windows-latest target: x86_64-pc-windows-msvc platform: windows @@ -82,6 +84,8 @@ jobs: submodules: true - name: Setup Rust toolchain + # Linux builds run inside a container with their own toolchain. + if: ${{ matrix.platform != 'linux' }} uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: target: ${{ matrix.target }} @@ -96,21 +100,34 @@ jobs: shell: bash run: cargo build --release --target ${{ matrix.target }} -p livekit-uniffi - # Linux: native build for x64; cross toolchain for aarch64. + # Linux: build inside a manylinux_2_28 container so the shared library + # links against glibc 2.28 and loads on the distributions the Dart package + # is used on (Debian 12, Ubuntu 22.04, Amazon Linux 2023 and newer). A + # native build on the runner would inherit its glibc 2.39 floor. Same + # approach as ffi-builds.yml. The arm64 entry runs on a native arm64 + # runner, so no cross toolchain is needed, and livekit-uniffi has no C + # dependencies, so the image's stock gcc is enough. The toolchain version + # comes from rust-toolchain.toml. - name: Build (Linux) if: ${{ matrix.platform == 'linux' }} + env: + TARGET: ${{ matrix.target }} + BUILD_IMAGE: ${{ matrix.build_image }} run: | - if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc - # .cargo/config.toml forces -fuse-ld=lld for this target to link - # libwebrtc; livekit-uniffi doesn't use libwebrtc. A non-empty global - # RUSTFLAGS overrides that per-target config, selecting the GNU bfd - # linker the cross binutils provides. - export RUSTFLAGS="-C link-arg=-fuse-ld=bfd" - fi - cargo build --release --target ${{ matrix.target }} -p livekit-uniffi + docker run --rm -e TARGET -v "$PWD:/workspace" -w /workspace "$BUILD_IMAGE" bash -euo pipefail -c ' + curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none + export PATH="/root/.cargo/bin:$PATH" + if [ "$TARGET" = "aarch64-unknown-linux-gnu" ]; then + # .cargo/config.toml forces -fuse-ld=lld for this target to link + # libwebrtc, which livekit-uniffi does not use. A non-empty global + # RUSTFLAGS overrides that per-target config, selecting the GNU + # bfd linker the image provides. + export RUSTFLAGS="-C link-arg=-fuse-ld=bfd" + fi + cargo build --release --target "$TARGET" -p livekit-uniffi + ' + # The container ran as root, so hand the outputs back to the runner user. + sudo chown -R "$USER:$USER" target # Android: build with cargo-ndk. - name: Build (Android) From 08bbf55c5edd28e8344ee2198aff44a00b2f481a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:29:09 +0900 Subject: [PATCH 09/12] Add a description to the uniffi-deps composite action Action metadata formally requires one and actionlint flags every workflow referencing the action without it. Cosmetic, GitHub runs the action fine either way. --- .github/actions/uniffi-deps/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/uniffi-deps/action.yml b/.github/actions/uniffi-deps/action.yml index 1dfc527de..1bb3d37ec 100644 --- a/.github/actions/uniffi-deps/action.yml +++ b/.github/actions/uniffi-deps/action.yml @@ -1,4 +1,5 @@ name: Install Common Deps for UniFFI Tasks +description: Installs the tooling shared by the UniFFI packaging workflows (cargo-make) runs: using: composite steps: From c8efa1443e728fed83e000e25bb3cccddb98756c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:29:34 +0900 Subject: [PATCH 10/12] Install cargo-ndk as a prebuilt binary in the cdylib matrix Each of the three Android matrix jobs compiled cargo-ndk from source on every run, roughly 3-5 minutes per job on the release critical path. taiki-e/install-action fetches the prebuilt binary in seconds, matching how the repo installs cargo-make in the uniffi-deps action. --- .github/workflows/uniffi-cdylib.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index 38b1250d4..d2c42ca33 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -129,12 +129,16 @@ jobs: # The container ran as root, so hand the outputs back to the runner user. sudo chown -R "$USER:$USER" target - # Android: build with cargo-ndk. + # Android: build with cargo-ndk. Installed as a prebuilt binary; a + # source build costs 3-5 minutes per matrix job on the release path. + - name: Install cargo-ndk + if: ${{ matrix.platform == 'android' }} + uses: taiki-e/install-action@682e7d9e49c5e653d371fc6adbda67653461378a # v2.82.4 + with: { tool: cargo-ndk } + - name: Build (Android) if: ${{ matrix.platform == 'android' }} - run: | - cargo install cargo-ndk - cargo ndk --target ${{ matrix.target }} build --release -p livekit-uniffi + run: cargo ndk --target ${{ matrix.target }} build --release -p livekit-uniffi # macOS/Linux: zip + shasum are available. - name: Package artifact (Unix) From 2345030c67ea0cf2a0fbb14bac7d995a2b230f02 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:11:07 +0800 Subject: [PATCH 11/12] Build the cdylib assets inside the Dart publish workflow The publish job used to poll the release for up to 90 minutes waiting for the cdylib job in uniffi-packages.yml to attach its assets, because pub.dev only accepts publishes from a tag-push workflow while the assets came from the release-event workflow. Call the reusable cdylib workflow from the tag-push workflow instead and make the publish job depend on it, so assets and package are produced by the same run and there is nothing to poll. uniffi-packages.yml goes back to Swift and Android only. The tag/crate version check moves to the resolve-tag job with a sparse checkout, so a mis-pointed tag still fails before the build matrix starts. A dry_run input skips the asset upload for exercising the workflow by hand. Also drops the completed fork-pin enablement step from the header. --- .github/workflows/uniffi-cdylib.yml | 5 +- .github/workflows/uniffi-dart-publish.yml | 130 +++++++++++----------- .github/workflows/uniffi-packages.yml | 26 +---- 3 files changed, 71 insertions(+), 90 deletions(-) diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index d2c42ca33..f1cea9c24 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -5,8 +5,9 @@ name: UniFFI cdylib builds # `.sha256` sidecar). The Dart build hook (support/dart/hook/build.dart.tera) # downloads these at consumer build time. # -# Reusable workflow, invoked from uniffi-packages.yml once the release tag has -# been resolved. Mirrors the structure of ffi-builds.yml. Not triggered directly. +# Reusable workflow, invoked from uniffi-dart-publish.yml once the release tag +# has been resolved, so the Dart publish job can depend on the assets being +# attached. Mirrors the structure of ffi-builds.yml. Not triggered directly. on: workflow_call: diff --git a/.github/workflows/uniffi-dart-publish.yml b/.github/workflows/uniffi-dart-publish.yml index f5454ffaa..3d453a701 100644 --- a/.github/workflows/uniffi-dart-publish.yml +++ b/.github/workflows/uniffi-dart-publish.yml @@ -1,7 +1,7 @@ name: UniFFI Dart package # Publishes the generated livekit_uniffi Dart package to pub.dev on a -# livekit-uniffi release. +# livekit-uniffi release, together with the native libraries it loads. # # This is a separate workflow from uniffi-packages.yml because pub.dev's # automated publishing only accepts workflows triggered by a tag push matching @@ -11,26 +11,28 @@ name: UniFFI Dart package # uniffi-packages.yml / #1256). # # The package's hook/build.dart downloads build-.zip assets from the -# GitHub release at consumer build time. Those assets are attached by the -# cdylib job in uniffi-packages.yml, which runs from the same release, so this -# workflow waits for them before publishing: a pub.dev version whose assets -# are missing would be broken on arrival, and pub.dev versions cannot be -# unpublished. +# GitHub release at consumer build time, so a pub.dev version whose assets are +# missing would be broken on arrival, and pub.dev versions cannot be +# unpublished. The cdylib job below builds and attaches those assets in this +# same run, and the publish job depends on it, so the package can only be +# published once every asset is on the release. # # PUBLISHING IS NOT ENABLED YET. Until the steps below are done, every run -# stops after `dart pub publish --dry-run`. Things the first live tag -# confirms: that knope-bot's API-created tag fires this push trigger at all -# (expected for App events, never exercised here; workflow_dispatch is the -# fallback), and that the asset wait sees the cdylib job's uploads. +# stops after `dart pub publish --dry-run`. The first live tag confirms that +# knope-bot's API-created tag fires this push trigger at all (expected for App +# events, never exercised here; workflow_dispatch is the fallback). # Enablement steps: -# 1. The temporary uniffi-dart pin on ryangaus's personal fork (see -# livekit-uniffi/Cargo.toml) has moved to upstream or a livekit fork, -# so releases don't depend on a personal repo. -# 2. A first manual `dart pub publish` by a livekit.io publisher admin has +# 1. A first manual `dart pub publish` by a livekit.io publisher admin has # created the package on pub.dev (pub.dev only automates existing packages). -# 3. Automated publishing from GitHub Actions is enabled in the package's +# 2. Automated publishing from GitHub Actions is enabled in the package's # pub.dev admin settings for livekit/rust-sdks with tag pattern # `livekit-uniffi/v{{version}}`, and PUBLISH_ENABLED below is flipped. +# +# Recovery: if a run fails, fix the cause and re-run the failed jobs, or +# dispatch this workflow with the tag. A dispatch rebuilds and re-attaches the +# assets (with --clobber) and re-validates the package, but never publishes, +# since pub.dev rejects OIDC tokens from dispatch runs. Set dry_run to skip the +# asset upload when only exercising the workflow. on: push: @@ -41,6 +43,10 @@ on: description: "Release tag (e.g. livekit-uniffi/v0.1.9)" required: true type: string + dry_run: + description: "Build everything but skip the release asset upload." + type: boolean + default: false env: # Flip to "true" once the enablement steps in the header are done. Real @@ -52,19 +58,18 @@ permissions: contents: read jobs: - build-and-publish: - name: Build & publish Dart package + resolve-tag: runs-on: ubuntu-latest - permissions: - contents: read - # Exchanged for a temporary pub.dev token by dart-lang/setup-dart. - id-token: write + outputs: + tag_name: ${{ steps.tag.outputs.tag_name }} + version: ${{ steps.tag.outputs.version }} steps: - name: Resolve tag id: tag # Bound through env, never template-expanded into the script: tag names # and dispatch inputs may contain shell metacharacters (Actions script - # injection), and this job can mint the pub.dev publish token. + # injection), and the outputs feed jobs that write release assets and + # can mint the pub.dev publish token. env: PUSHED_TAG: ${{ github.ref_name }} INPUT_TAG: ${{ inputs.tag_name }} @@ -77,15 +82,16 @@ jobs: echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT" + # Fast fail on a mis-pointed tag before spending runner time on the + # cdylib matrix: the tag version must match the crate version the tag + # carries (it is also what the pubspec, and pub.dev's tag-pattern check, + # will see). Only the manifest is needed here. - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: - # Build the tagged source, not the dispatch ref (which defaults to main). ref: ${{ steps.tag.outputs.tag_name }} - submodules: true + sparse-checkout: livekit-uniffi/Cargo.toml + sparse-checkout-cone-mode: false - # Fast fail on a mis-pointed tag before spending runner time: the tag - # version must match the crate version the checkout carries (it is also - # what the pubspec, and pub.dev's tag-pattern check, will see). - name: Check tag matches crate version env: VERSION: ${{ steps.tag.outputs.version }} @@ -96,6 +102,35 @@ jobs: exit 1 fi + # Builds build-.zip (+ .sha256) for every target and attaches them to + # the release. Runs here rather than in uniffi-packages.yml so the publish + # job can depend on it directly instead of polling the release for assets. + cdylib: + needs: resolve-tag + permissions: + # The upload step attaches assets to the release with the workflow token. + contents: write + uses: ./.github/workflows/uniffi-cdylib.yml + with: + version: ${{ needs.resolve-tag.outputs.version }} + tag_name: ${{ needs.resolve-tag.outputs.tag_name }} + dry_run: ${{ inputs.dry_run || false }} + + build-and-publish: + name: Build & publish Dart package + needs: [resolve-tag, cdylib] + runs-on: ubuntu-latest + permissions: + contents: read + # Exchanged for a temporary pub.dev token by dart-lang/setup-dart. + id-token: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + # Build the tagged source, not the dispatch ref (which defaults to main). + ref: ${{ needs.resolve-tag.outputs.tag_name }} + submodules: true + - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 with: @@ -150,47 +185,6 @@ jobs: exit 1 fi - # The cdylib job in uniffi-packages.yml attaches the per-target archives - # to the release after knope publishes it, so they can land up to an - # hour after this workflow starts; the build above does not need them, - # only the validate/publish steps do. The hook needs a - # build-.zip AND its .zip.sha256 sidecar per target, so gate on - # both. 12 targets; keep the count in sync with the matrix in - # uniffi-cdylib.yml. - # - # Fails fast when the release itself is missing (mis-tag, or knope never - # published). Recovery from a failed cdylib build: fix it, dispatch - # uniffi-packages.yml with this tag to attach the assets, then re-run - # this workflow's failed run (or dispatch it with the tag). - - name: Wait for cdylib release assets - timeout-minutes: 90 - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.tag.outputs.tag_name }} - run: | - expected=12 - missing=0 - while true; do - if ! assets=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '[.assets[].name]'); then - missing=$((missing + 1)) - echo "release $TAG not found ($missing/10)" - if [ "$missing" -ge 10 ]; then - echo "Release $TAG does not exist; is the tag mis-pointed or the release unpublished?" >&2 - exit 1 - fi - sleep 60 - continue - fi - missing=0 - zips=$(echo "$assets" | jq '[.[] | select(test("^build-.*\\.zip$"))] | length') - sums=$(echo "$assets" | jq '[.[] | select(test("^build-.*\\.zip\\.sha256$"))] | length') - echo "cdylib assets on $TAG: $zips/$expected zips, $sums/$expected sha256 sidecars" - if [ "$zips" -ge "$expected" ] && [ "$sums" -ge "$zips" ]; then - break - fi - sleep 120 - done - - name: Validate package working-directory: ${{ runner.temp }}/livekit_uniffi run: | diff --git a/.github/workflows/uniffi-packages.yml b/.github/workflows/uniffi-packages.yml index 0c1395ab8..862dd772e 100644 --- a/.github/workflows/uniffi-packages.yml +++ b/.github/workflows/uniffi-packages.yml @@ -1,18 +1,18 @@ name: UniFFI packages # Publishes the per-language livekit-uniffi wrapper packages (Swift xcframework → -# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) and attaches -# the Dart/Flutter cdylib archives to the release when a livekit-uniffi release -# is published. +# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) when a +# livekit-uniffi release is published. # # knope publishes the `livekit-uniffi/v*` GitHub release (and its git tag) # directly on the release merge — no draft — so this reacts to the published # release event, which fires only for genuine livekit-uniffi releases (the gate # is free). workflow_dispatch allows a manual re-run with an explicit tag. # -# The Dart package itself is published from uniffi-dart-publish.yml (tag-push -# triggered, as pub.dev requires); it waits for the cdylib assets this workflow -# attaches, since the package's build hook downloads them at consumer build time. +# The Dart package, and the cdylib release assets its build hook downloads, are +# handled by uniffi-dart-publish.yml. That workflow is tag-push triggered, as +# pub.dev requires, and builds the assets itself so publishing can depend on +# them directly. on: release: @@ -70,17 +70,3 @@ jobs: tag_name: ${{ needs.resolve-tag.outputs.tag_name }} dry_run: ${{ inputs.dry_run || false }} secrets: inherit - - # Attaches build-.zip (+ .sha256) for every Dart/Flutter target to the - # release. The livekit_uniffi package's hook/build.dart downloads these at - # consumer build time, so a release consumed from pub.dev must carry them. - cdylib: - needs: resolve-tag - permissions: - # The upload step attaches assets to the release with the workflow token. - contents: write - uses: ./.github/workflows/uniffi-cdylib.yml - with: - version: ${{ needs.resolve-tag.outputs.version }} - tag_name: ${{ needs.resolve-tag.outputs.tag_name }} - dry_run: ${{ inputs.dry_run || false }} From ef845a7ef66c7049c33c2f9b3fc567e5552ef19c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:11:08 +0800 Subject: [PATCH 12/12] Pin the Dart SDK used to test and publish the package A floating stable channel could pick up a new analyzer lint the week of a release and fail the publish dry run, which treats any analyzer finding as a warning. Pin both workflows to the same version so PR CI validates what the publish will run with, and bump them together. --- .github/workflows/uniffi-dart-publish.yml | 6 +++++- .github/workflows/uniffi-dart-test.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/uniffi-dart-publish.yml b/.github/workflows/uniffi-dart-publish.yml index 3d453a701..7bd09347d 100644 --- a/.github/workflows/uniffi-dart-publish.yml +++ b/.github/workflows/uniffi-dart-publish.yml @@ -163,7 +163,11 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 with: - sdk: stable + # Pinned, and kept identical to uniffi-dart-test.yml, so PR CI + # validates the SDK the publish runs with. A floating channel could + # introduce a new lint that fails the dry run only on release day. + # Bump both together. + sdk: 3.13.1 - name: Build Dart package (release) working-directory: livekit-uniffi diff --git a/.github/workflows/uniffi-dart-test.yml b/.github/workflows/uniffi-dart-test.yml index d5b1aefc5..84834c653 100644 --- a/.github/workflows/uniffi-dart-test.yml +++ b/.github/workflows/uniffi-dart-test.yml @@ -42,7 +42,11 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 with: - sdk: stable + # Pinned, and kept identical to uniffi-dart-publish.yml, so this job + # validates the SDK the publish will run with. A floating channel + # could introduce a new lint that fails the publish dry run only on + # release day. Bump both together. + sdk: 3.13.1 - name: Cache cargo registry uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0