From 587857bbdda84ef8839b7734d46435eab7014375 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Mon, 22 Jun 2026 17:05:45 +0100 Subject: [PATCH 1/5] Add dart bindings, using uniffi-dart --- .changeset/uniffi-dart-bindings.md | 5 + Cargo.lock | 352 ++++++++++++----------------- livekit-uniffi/Cargo.toml | 18 +- livekit-uniffi/Makefile.toml | 21 +- livekit-uniffi/bindgen-dart.rs | 65 ++++++ 5 files changed, 253 insertions(+), 208 deletions(-) create mode 100644 .changeset/uniffi-dart-bindings.md create mode 100644 livekit-uniffi/bindgen-dart.rs diff --git a/.changeset/uniffi-dart-bindings.md b/.changeset/uniffi-dart-bindings.md new file mode 100644 index 000000000..75ec6b146 --- /dev/null +++ b/.changeset/uniffi-dart-bindings.md @@ -0,0 +1,5 @@ +--- +livekit-uniffi: minor +--- + +Add a Dart bindings target. Bumps the crate's UniFFI dependency from 0.30 to 0.31 to match the bindgen. diff --git a/Cargo.lock b/Cargo.lock index 2aed71a33..d6430cf0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,7 +220,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -231,7 +231,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "askama" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" dependencies = [ "askama_derive", "itoa", @@ -348,9 +348,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" dependencies = [ "askama_parser", "basic-toml", @@ -365,9 +365,9 @@ dependencies = [ [[package]] name = "askama_parser" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" dependencies = [ "memchr", "serde", @@ -398,6 +398,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -990,6 +1003,20 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "cargo_metadata" version = "0.19.2" @@ -1207,7 +1234,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -2150,7 +2177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2533,6 +2560,28 @@ dependencies = [ "slab", ] +[[package]] +name = "genco" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35958104272e516c2a5f66a9d82fba4784d2b585fc1e2358b8f96e15d342995" +dependencies = [ + "genco-macros", + "relative-path", + "smallvec", +] + +[[package]] +name = "genco-macros" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eaff6bbc0b3a878361aced5ec6a2818ee7c541c5b33b5880dfa9a86c23e9e7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -2575,24 +2624,11 @@ dependencies = [ "cfg-if 1.0.4", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if 1.0.4", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "gif" version = "0.14.1" @@ -2619,7 +2655,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3111,7 +3147,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3222,12 +3258,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -3626,12 +3656,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -3959,12 +3983,14 @@ dependencies = [ name = "livekit-uniffi" version = "0.1.1" dependencies = [ + "camino", "livekit-api", "livekit-protocol", "log", "once_cell", "tokio", "uniffi", + "uniffi-dart", ] [[package]] @@ -4584,7 +4610,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5789,7 +5815,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5826,7 +5852,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -5846,12 +5872,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.5" @@ -6065,6 +6085,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -6251,7 +6277,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6664,9 +6690,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -6795,7 +6821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6859,6 +6885,18 @@ dependencies = [ "serde", ] +[[package]] +name = "stringcase" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" + +[[package]] +name = "stringcase" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" + [[package]] name = "strsim" version = "0.10.0" @@ -6962,10 +7000,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand 2.3.0", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7765,21 +7803,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "uniffi" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c866f627c3f04c3df068b68bb2d725492caaa539dd313e2a9d26bb85b1a32f4e" +checksum = "46eefd5468602930da46b1f49d3448c6dfc2e81295f93120f23f8174fd70267f" dependencies = [ "anyhow", "camino", - "cargo_metadata", + "cargo_metadata 0.19.2", "clap", "uniffi_bindgen", "uniffi_build", @@ -7788,16 +7820,37 @@ dependencies = [ "uniffi_pipeline", ] +[[package]] +name = "uniffi-dart" +version = "0.1.0+v0.30.0" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c#90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata 0.18.1", + "genco", + "heck 0.5.0", + "lazy_static", + "paste", + "proc-macro2", + "serde", + "stringcase 0.4.0", + "toml", + "uniffi", + "uniffi_bindgen", + "uniffi_dart_macro", +] + [[package]] name = "uniffi_bindgen" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8ca600167641ebe7c8ba9254af40492dda3397c528cc3b2f511bd23e8541a5" +checksum = "c4a0c9b375d32e1365cdb2bdd7cb495eecf6fac851ddbad077412b4ee1888514" dependencies = [ "anyhow", "askama", "camino", - "cargo_metadata", + "cargo_metadata 0.19.2", "fs-err", "glob", "goblin", @@ -7816,9 +7869,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e55c05228f4858bb258f651d21d743fcc1fe5a2ec20d3c0f9daefddb105ee4d" +checksum = "744fe15bcd3e2b1712a4573a45ce749af19cf28d69027ca5789619014955668c" dependencies = [ "anyhow", "camino", @@ -7827,21 +7880,35 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7a5a038ebffe8f4cf91416b154ef3c2468b18e828b7009e01b1b99938089f9" +checksum = "eec017b112701681f6fbbe5d92014b5c468eb0b177a94389de03ceec40665095" dependencies = [ "anyhow", + "async-compat", "bytes", "once_cell", "static_assertions", ] +[[package]] +name = "uniffi_dart_macro" +version = "0.1.0+v0.30.0" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart?rev=90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c#90f2c6f29cbf88c8bc2cf515e6a0c2314a48844c" +dependencies = [ + "futures", + "proc-macro2", + "quote", + "stringcase 0.3.0", + "syn 1.0.109", + "uniffi", +] + [[package]] name = "uniffi_internal_macros" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf" +checksum = "4641669b48fefbc5e80ff08c5004d9c7617fb91232131a6734ab6712779cb04c" dependencies = [ "anyhow", "indexmap 2.13.0", @@ -7852,9 +7919,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c6309fc36c7992afc03bc0c5b059c656bccbef3f2a4bc362980017f8936141" +checksum = "eeb8617ee814de22caf7417bf514715ba0b3f46bd9d5a5d794413fd8282cb737" dependencies = [ "camino", "fs-err", @@ -7869,9 +7936,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a138823392dba19b0aa494872689f97d0ee157de5852e2bec157ce6de9cdc22" +checksum = "58d5b94fc92803d21b2928bd15c6f06e57609b95caf98ea561c99cda1b6d2a25" dependencies = [ "anyhow", "siphasher", @@ -7881,9 +7948,9 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29" +checksum = "032739b3ec725576914c15899dedaf080163ced86b6934566c20ec2b20ce90ca" dependencies = [ "anyhow", "heck 0.5.0", @@ -7894,9 +7961,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.30.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0adacdd848aeed7af4f5af7d2f621d5e82531325d405e29463482becfdeafca" +checksum = "fc0a1d0a0252ce1af9e8ce78ba67ac0d8937fb2bedaf10cbddd43d3614d06ec6" dependencies = [ "anyhow", "textwrap", @@ -8041,15 +8108,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -8109,40 +8167,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.14" @@ -8738,7 +8762,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -9328,88 +9352,6 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] [[package]] name = "writeable" diff --git a/livekit-uniffi/Cargo.toml b/livekit-uniffi/Cargo.toml index 54a774c90..85daeb6bc 100644 --- a/livekit-uniffi/Cargo.toml +++ b/livekit-uniffi/Cargo.toml @@ -14,18 +14,32 @@ publish = false [dependencies] livekit-protocol = { workspace = true } livekit-api = { workspace = true } -uniffi = { version = "0.30.0", features = ["cli", "scaffolding-ffi-buffer-fns"] } +uniffi = { version = "0.31", features = ["cli", "scaffolding-ffi-buffer-fns"] } log = { workspace = true } tokio = { workspace = true, features = ["sync"] } once_cell = "1.21.3" +# 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 } +camino = { version = "1", optional = true } + +[features] +# Enables the `uniffi-bindgen-dart` bin. Not enabled for library builds. +dart-bindgen = ["dep:uniffi-dart", "dep:camino"] [build-dependencies] -uniffi = { version = "0.30.0", features = ["build", "scaffolding-ffi-buffer-fns"] } +uniffi = { version = "0.31", features = ["build", "scaffolding-ffi-buffer-fns"] } [[bin]] name = "uniffi-bindgen" path = "bindgen.rs" +[[bin]] +name = "uniffi-bindgen-dart" +path = "bindgen-dart.rs" +required-features = ["dart-bindgen"] + [profile.release] opt-level = "z" lto = true diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index 609061c36..b41e3a668 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -165,7 +165,8 @@ command = "tera" [tasks.bindgen] dependencies = [ "bindgen-kotlin", - "bindgen-python" + "bindgen-python", + "bindgen-dart", ] # MARK: - Kotlin @@ -612,3 +613,21 @@ run_task = "android-publish-maven-local" # Add other bindgen tasks here, following the same pattern # as above for the task name (e.g., "bindgen-"). + +# MARK: - Dart + +# uniffi-dart has no standalone CLI, so the `uniffi-bindgen-dart` bin drives its +# library API instead (gated behind the `dart-bindgen` feature to keep its heavy +# deps out of regular library builds). Reads metadata from the compiled cdylib +# in library mode, like the Kotlin/Python tasks. +[tasks.bindgen-dart] +description = "Generate Dart bindings" +dependencies = ["build", "locate-lib"] +env = { LANG = "dart" } +script_runner = "@duckscript" +script = """ +out_dir = join_path ${PACKAGES_DIR} ${LANG} +exec cargo run --features dart-bindgen --bin uniffi-bindgen-dart -- \ + --library ${LIB_PATH} \ + --out-dir ${out_dir} +""" \ No newline at end of file diff --git a/livekit-uniffi/bindgen-dart.rs b/livekit-uniffi/bindgen-dart.rs new file mode 100644 index 000000000..a3e751c12 --- /dev/null +++ b/livekit-uniffi/bindgen-dart.rs @@ -0,0 +1,65 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Dart bindings generator for the UniFFI interface. +//! +//! [_uniffi-dart_](https://github.com/Uniffi-Dart/uniffi-dart) ships no working +//! CLI of its own, so this thin binary drives its library API in +//! [library mode][lm] — reading UniFFI metadata directly from a compiled +//! `cdylib`, exactly as the Kotlin/Swift/Node tasks do via the stock +//! `uniffi-bindgen`. +//! +//! Usage: `uniffi-bindgen-dart --library --out-dir [--config ]` +//! +//! [lm]: https://mozilla.github.io/uniffi-rs/latest/bindings.html + +use camino::Utf8PathBuf; + +fn main() { + let mut library: Option = None; + let mut out_dir: Option = None; + let mut config: Option = None; + + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + let mut take = |name: &str| -> Utf8PathBuf { + args.next().unwrap_or_else(|| panic!("{name} requires a value")).into() + }; + match arg.as_str() { + "--library" => library = Some(take("--library")), + "--out-dir" => out_dir = Some(take("--out-dir")), + "--config" => config = Some(take("--config")), + // Tolerate a leading `generate` subcommand for symmetry with `uniffi-bindgen`. + "generate" => {} + other => panic!("unrecognized argument: {other}"), + } + } + + let library = library.expect("--library is required"); + let out_dir = out_dir.expect("--out-dir is required"); + // The crate's `uniffi.toml` carries any `[bindings.dart]` config; default to + // it so a bare invocation from the crate root picks it up. + let config = config.unwrap_or_else(|| Utf8PathBuf::from("uniffi.toml")); + + uniffi_dart::gen::generate_dart_bindings( + &config, // udl_file: only consulted to locate config in library mode + None, // config_file_override + Some(&out_dir), // out_dir_override + &library, // compiled cdylib to read metadata from + true, // library_mode + ) + .expect("failed to generate Dart bindings"); + + println!("Generated Dart bindings to {out_dir}"); +} From 7809a8357b7e2a2b25ef1b1ed70498ccf0a3f2f2 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Mon, 22 Jun 2026 17:59:38 +0100 Subject: [PATCH 2/5] Add pubspec.yaml and hook/build.dart --- livekit-uniffi/Makefile.toml | 58 ++++++++++++++++++- livekit-uniffi/bindgen-dart.rs | 8 +-- livekit-uniffi/support/dart/hook/build.dart | 36 ++++++++++++ livekit-uniffi/support/dart/pubspec.yaml.tera | 16 +++++ livekit-uniffi/uniffi.toml | 8 ++- 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 livekit-uniffi/support/dart/hook/build.dart create mode 100644 livekit-uniffi/support/dart/pubspec.yaml.tera diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index b41e3a668..8ea46cc05 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -19,6 +19,7 @@ LIB_NAME = "lib${CARGO_MAKE_CRATE_FS_NAME}" TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}" SPM_NAME = "LiveKitUniFFI" NPM_NAME = "@livekit/uniffi" +DART_PACKAGE_NAME = "livekit_uniffi" # Base URL for downloading release builds of the cdylib. # Currently, it is assumed the archives will be named "ffi--.zip". @@ -630,4 +631,59 @@ out_dir = join_path ${PACKAGES_DIR} ${LANG} exec cargo run --features dart-bindgen --bin uniffi-bindgen-dart -- \ --library ${LIB_PATH} \ --out-dir ${out_dir} -""" \ No newline at end of file +""" + +# 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. +# +# 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) + +[tasks.dart-move-code-into-lib] +private = true +script_runner = "@shell" +script = """ +mkdir -p ${PACKAGES_DIR}/${LANG}/lib +mv ${PACKAGES_DIR}/${LANG}/*.dart ${PACKAGES_DIR}/${LANG}/lib +""" + +[tasks.dart-generate-manifest] +private = true +extend = "tera" +args = ["--env-only", "-t", "${SUPPORT_DIR}/${LANG}/pubspec.yaml.tera", "-o", "${PACKAGES_DIR}/${LANG}/pubspec.yaml"] + +[tasks.dart-copy-hook] +private = true +script_runner = "@shell" +script = """ +mkdir -p ${PACKAGES_DIR}/${LANG}/hook +cp ${SUPPORT_DIR}/${LANG}/hook/build.dart ${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. +[tasks.dart-copy-lib] +private = true +script_runner = "@shell" +script = """ +cp ${LIB_PATH} ${PACKAGES_DIR}/${LANG}/ +""" + +[tasks.dart-package-flow] +private = true +dependencies = [ + "bindgen-dart", + "dart-move-code-into-lib", + "dart-generate-manifest", + "dart-copy-hook", + "dart-copy-lib" +] + +[tasks.dart-package] +description = "Build a consumable Dart package with bindings, hook, and (dev) native lib" +env = { LANG = "dart" } +run_task = "dart-package-flow" \ No newline at end of file diff --git a/livekit-uniffi/bindgen-dart.rs b/livekit-uniffi/bindgen-dart.rs index a3e751c12..84d7cba86 100644 --- a/livekit-uniffi/bindgen-dart.rs +++ b/livekit-uniffi/bindgen-dart.rs @@ -53,11 +53,11 @@ fn main() { let config = config.unwrap_or_else(|| Utf8PathBuf::from("uniffi.toml")); uniffi_dart::gen::generate_dart_bindings( - &config, // udl_file: only consulted to locate config in library mode - None, // config_file_override + &config, // udl_file (unused in library mode once a config override is set) + Some(&config), // config_file_override: routes through the supplier that reads [bindings.dart] Some(&out_dir), // out_dir_override - &library, // compiled cdylib to read metadata from - true, // library_mode + &library, // compiled cdylib to read metadata from + true, // library_mode ) .expect("failed to generate Dart bindings"); diff --git a/livekit-uniffi/support/dart/hook/build.dart b/livekit-uniffi/support/dart/hook/build.dart new file mode 100644 index 000000000..f4483375a --- /dev/null +++ b/livekit-uniffi/support/dart/hook/build.dart @@ -0,0 +1,36 @@ +// Native Assets build hook for the LiveKit UniFFI Dart bindings. +// +// The generated bindings call into the native library through `@Native` +// annotations bound to the asset id `package:/uniffi:livekit_uniffi`. +// This hook registers that code asset, resolving the prebuilt dynamic library +// from the package root. +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; + +// Matches `cdylib_name` in livekit-uniffi/uniffi.toml and the asset name baked +// into the generated Dart. +const _cdylibName = 'livekit_uniffi'; + +String _libFileName() { + if (Platform.isMacOS) return 'lib$_cdylibName.dylib'; + if (Platform.isWindows) return '$_cdylibName.dll'; + return 'lib$_cdylibName.so'; +} + +void main(List args) async { + await build(args, (input, output) async { + final libPath = input.packageRoot.resolve(_libFileName()); + + output.assets.code.add( + CodeAsset( + package: input.packageName, + // Dart prefixes this with `package:/`. + name: 'uniffi:$_cdylibName', + linkMode: DynamicLoadingBundled(), + file: libPath, + ), + ); + }); +} diff --git a/livekit-uniffi/support/dart/pubspec.yaml.tera b/livekit-uniffi/support/dart/pubspec.yaml.tera new file mode 100644 index 000000000..d92def583 --- /dev/null +++ b/livekit-uniffi/support/dart/pubspec.yaml.tera @@ -0,0 +1,16 @@ +name: {{ DART_PACKAGE_NAME }} +description: Dart bindings for the LiveKit UniFFI interface. +version: {{ CARGO_MAKE_CRATE_VERSION }} +publish_to: none + +environment: + sdk: ">=3.10.0 <4.0.0" + +dependencies: + ffi: ^2.1.0 + +dev_dependencies: + # Native Assets build-hook support. `any` tracks the SDK-bundled versions. + code_assets: any + hooks: any + test: ^1.25.0 diff --git a/livekit-uniffi/uniffi.toml b/livekit-uniffi/uniffi.toml index 18e24e501..2ed2769f6 100644 --- a/livekit-uniffi/uniffi.toml +++ b/livekit-uniffi/uniffi.toml @@ -4,4 +4,10 @@ ffi_module_name = "RustLiveKitUniFFI" [bindings.kotlin] android = true package_name = "io.livekit.uniffi" -cdylib_name = "livekit_uniffi" # the name of the so file to be loaded \ No newline at end of file +cdylib_name = "livekit_uniffi" # the name of the so file to be loaded + +[bindings.dart] +# Dart package name; must match the published pub package so the Native Assets +# asset id resolves to `package:livekit_uniffi/uniffi:livekit_uniffi`. +package_name = "livekit_uniffi" +cdylib_name = "livekit_uniffi" \ No newline at end of file From e30c53847c4790372f6efe3f8d048fda7651bb95 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Mon, 22 Jun 2026 19:31:57 +0100 Subject: [PATCH 3/5] Add cdylib publishing CI and release-mode Dart build hook Release/multi-platform delivery for the Dart package: - uniffi-cdylib.yml builds liblivekit_uniffi for desktop + mobile targets and attaches build-.zip (+ .sha256) to the livekit-uniffi release; wired into uniffi-packages.yml. Validated across all 10 targets via CI dry-run. - hook/build.dart.tera gains a download mode: fetches and SHA-256-verifies the prebuilt library for the target when no local build is present. --- .github/workflows/uniffi-cdylib.yml | 172 ++++++++++++++++++ .github/workflows/uniffi-packages.yml | 10 + livekit-uniffi/Makefile.toml | 22 ++- livekit-uniffi/support/dart/hook/build.dart | 36 ---- .../support/dart/hook/build.dart.tera | 141 ++++++++++++++ livekit-uniffi/support/dart/pubspec.yaml.tera | 4 + 6 files changed, 341 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/uniffi-cdylib.yml delete mode 100644 livekit-uniffi/support/dart/hook/build.dart create mode 100644 livekit-uniffi/support/dart/hook/build.dart.tera diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml new file mode 100644 index 000000000..cdcce2eb6 --- /dev/null +++ b/.github/workflows/uniffi-cdylib.yml @@ -0,0 +1,172 @@ +name: UniFFI cdylib builds + +# Builds the livekit-uniffi dynamic library for every Dart/Flutter target and +# attaches them to the livekit-uniffi release as `build-.zip` (+ a +# `.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. + +on: + workflow_call: + inputs: + version: + description: Release version (e.g. 0.1.1) + required: true + type: string + tag_name: + description: Source release tag (e.g. livekit-uniffi/v0.1.1) + required: true + type: string + dry_run: + description: Build artifacts but skip release upload + type: boolean + default: false + +permissions: + contents: write + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: aarch64-apple-darwin + platform: apple + - os: macos-latest + target: x86_64-apple-darwin + platform: apple + - os: macos-latest + target: aarch64-apple-ios + platform: apple + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + platform: linux + - os: windows-latest + target: x86_64-pc-windows-msvc + platform: windows + - os: windows-latest + target: aarch64-pc-windows-msvc + platform: windows + - os: ubuntu-latest + target: aarch64-linux-android + platform: android + - os: ubuntu-latest + target: armv7-linux-androideabi + platform: android + - os: ubuntu-latest + target: x86_64-linux-android + platform: android + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + submodules: true + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + target: ${{ matrix.target }} + cache: false + # This action defaults RUSTFLAGS to "-D warnings"; clear it so + # dependency warnings don't fail the build (matches ffi-builds.yml). + rustflags: "" + + - name: Install Protoc + uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2.1.0 + with: + version: "25.2" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + # Apple, Windows: build the cdylib directly for the target. + - name: Build (Apple / Windows) + if: ${{ matrix.platform == 'apple' || matrix.platform == 'windows' }} + shell: bash + run: cargo build --release --target ${{ matrix.target }} -p livekit-uniffi + + # Linux: native build for x64; cross toolchain for aarch64. + - name: Build (Linux) + if: ${{ matrix.platform == 'linux' }} + 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 + + # Android: build with cargo-ndk. + - name: Build (Android) + if: ${{ matrix.platform == 'android' }} + run: | + cargo install cargo-ndk + cargo ndk --target ${{ matrix.target }} build --release -p livekit-uniffi + + # macOS/Linux: zip + shasum are available. + - name: Package artifact (Unix) + if: ${{ matrix.platform != 'windows' }} + shell: bash + run: | + triple="${{ matrix.target }}" + dir="target/${triple}/release" + case "$triple" in + *apple*) lib="liblivekit_uniffi.dylib" ;; + *) lib="liblivekit_uniffi.so" ;; + esac + out="build-${triple}.zip" + # Zip with the bare library name at the archive root. + (cd "$dir" && zip "$GITHUB_WORKSPACE/$out" "$lib") + # SHA-256 sidecar, in `shasum -a 256` format (lowercase hex first). + shasum -a 256 "$out" > "$out.sha256" + + # Windows runners lack `zip`/`shasum`; use PowerShell equivalents and + # emit the same ` ` sidecar format the hook parses. + - name: Package artifact (Windows) + if: ${{ matrix.platform == 'windows' }} + shell: pwsh + run: | + $triple = "${{ matrix.target }}" + $lib = "target/$triple/release/livekit_uniffi.dll" + $out = "build-$triple.zip" + Compress-Archive -Path $lib -DestinationPath $out -Force + $hash = (Get-FileHash -Algorithm SHA256 $out).Hash.ToLower() + "$hash $out" | Out-File -Encoding ascii -NoNewline "$out.sha256" + + - name: Upload build artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: cdylib-${{ matrix.target }} + path: | + build-${{ matrix.target }}.zip + build-${{ matrix.target }}.zip.sha256 + + release: + name: Attach cdylibs to release + needs: build + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + steps: + - name: Download build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: cdylib-* + merge-multiple: true + path: cdylibs + + - name: Upload to release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ inputs.tag_name }}" cdylibs/* --repo "${{ github.repository }}" --clobber diff --git a/.github/workflows/uniffi-packages.yml b/.github/workflows/uniffi-packages.yml index 1bea67880..67acc827b 100644 --- a/.github/workflows/uniffi-packages.yml +++ b/.github/workflows/uniffi-packages.yml @@ -76,3 +76,13 @@ jobs: tag_name: ${{ needs.resolve-tag.outputs.tag_name }} dry_run: ${{ inputs.dry_run || false }} secrets: inherit + + # Raw cdylibs for the Dart package's build-hook download (build-.zip). + cdylib: + needs: resolve-tag + if: needs.resolve-tag.outputs.tag_name != '' + 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/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index 8ea46cc05..8be17e626 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -21,9 +21,10 @@ SPM_NAME = "LiveKitUniFFI" NPM_NAME = "@livekit/uniffi" DART_PACKAGE_NAME = "livekit_uniffi" -# Base URL for downloading release builds of the cdylib. -# Currently, it is assumed the archives will be named "ffi--.zip". -CDYLIB_DOWNLOAD_BASE_URL = "https://github.com/livekit/example/releases/download/example/build" +# 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" [config] skip_core_tasks = true @@ -656,13 +657,18 @@ private = true extend = "tera" args = ["--env-only", "-t", "${SUPPORT_DIR}/${LANG}/pubspec.yaml.tera", "-o", "${PACKAGES_DIR}/${LANG}/pubspec.yaml"] -[tasks.dart-copy-hook] +# Render the build hook from its template, baking in the package version and the +# release download base used by the hook's download (release) mode. +[tasks.dart-make-hook-dir] private = true script_runner = "@shell" -script = """ -mkdir -p ${PACKAGES_DIR}/${LANG}/hook -cp ${SUPPORT_DIR}/${LANG}/hook/build.dart ${PACKAGES_DIR}/${LANG}/hook/build.dart -""" +script = "mkdir -p ${PACKAGES_DIR}/${LANG}/hook" + +[tasks.dart-copy-hook] +private = true +dependencies = ["dart-make-hook-dir"] +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. diff --git a/livekit-uniffi/support/dart/hook/build.dart b/livekit-uniffi/support/dart/hook/build.dart deleted file mode 100644 index f4483375a..000000000 --- a/livekit-uniffi/support/dart/hook/build.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Native Assets build hook for the LiveKit UniFFI Dart bindings. -// -// The generated bindings call into the native library through `@Native` -// annotations bound to the asset id `package:/uniffi:livekit_uniffi`. -// This hook registers that code asset, resolving the prebuilt dynamic library -// from the package root. -import 'dart:io'; - -import 'package:code_assets/code_assets.dart'; -import 'package:hooks/hooks.dart'; - -// Matches `cdylib_name` in livekit-uniffi/uniffi.toml and the asset name baked -// into the generated Dart. -const _cdylibName = 'livekit_uniffi'; - -String _libFileName() { - if (Platform.isMacOS) return 'lib$_cdylibName.dylib'; - if (Platform.isWindows) return '$_cdylibName.dll'; - return 'lib$_cdylibName.so'; -} - -void main(List args) async { - await build(args, (input, output) async { - final libPath = input.packageRoot.resolve(_libFileName()); - - output.assets.code.add( - CodeAsset( - package: input.packageName, - // Dart prefixes this with `package:/`. - name: 'uniffi:$_cdylibName', - linkMode: DynamicLoadingBundled(), - file: libPath, - ), - ); - }); -} diff --git a/livekit-uniffi/support/dart/hook/build.dart.tera b/livekit-uniffi/support/dart/hook/build.dart.tera new file mode 100644 index 000000000..b54a3c95e --- /dev/null +++ b/livekit-uniffi/support/dart/hook/build.dart.tera @@ -0,0 +1,141 @@ +// Native Assets build hook for the LiveKit UniFFI Dart bindings. +// +// The generated bindings call the native library through `@Native` annotations +// bound to the asset id `package:/uniffi:livekit_uniffi`. This hook +// resolves the dynamic library for the target and registers that code asset. +// +// Two modes, chosen automatically: +// - Local: if a prebuilt library sits in the package root (placed there by +// `cargo make dart-package` during development), use it directly. +// - Download: otherwise fetch the prebuilt library for the target platform +// from the release host, verifying its SHA-256. +// +// Generated from support/dart/hook/build.dart.tera; do not edit the copy under +// packages/. +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:code_assets/code_assets.dart'; +import 'package:crypto/crypto.dart'; +import 'package:hooks/hooks.dart'; +import 'package:http/http.dart' as http; + +// Matches `cdylib_name` in uniffi.toml and the asset name in the generated Dart. +const _cdylibName = 'livekit_uniffi'; +const _version = '{{ CARGO_MAKE_CRATE_VERSION }}'; +// Per-release directory holding the `livekit_uniffi-.zip` assets. +const _downloadBase = '{{ CDYLIB_DOWNLOAD_BASE_URL }}'; + +void main(List args) async { + await build(args, (input, output) async { + if (!input.config.buildCodeAssets) return; + + final code = input.config.code; + final libFile = await _resolveLibrary( + input, + output, + code.targetOS, + code.targetArchitecture, + ); + + output.assets.code.add( + CodeAsset( + package: input.packageName, + // Dart prefixes this with `package:/`. + name: 'uniffi:$_cdylibName', + linkMode: DynamicLoadingBundled(), + file: libFile, + ), + ); + }); +} + +Future _resolveLibrary( + BuildInput input, + BuildOutputBuilder output, + OS os, + Architecture arch, +) async { + final libName = _libFileName(os); + + // Local mode: library copied into the package root for development. + final local = File.fromUri(input.packageRoot.resolve(libName)); + if (await local.exists()) return local.uri; + + // Download mode: fetch the prebuilt library for the target. The hooks-runner + // already caches hook results across builds, so there's no manual cache check. + final outDir = Directory.fromUri( + input.outputDirectoryShared.resolve('$_cdylibName/'), + ); + final libFile = File.fromUri(outDir.uri.resolve(libName)); + + final triple = _targetTriple(os, arch); + // Asset naming matches the existing node downloader convention so one set of + // release assets serves both: `/v/build-.zip`. + final zipUrl = Uri.parse('$_downloadBase/v$_version/build-$triple.zip'); + final bytes = await _downloadAndVerify(zipUrl); + + await outDir.create(recursive: true); + for (final entry in ZipDecoder().decodeBytes(bytes)) { + if (!entry.isFile) continue; + final out = File.fromUri(outDir.uri.resolve(entry.name)); + await out.create(recursive: true); + await out.writeAsBytes(entry.content as List); + } + + if (!await libFile.exists()) { + throw Exception('$libName not found in $zipUrl'); + } + output.dependencies.add(libFile.uri); + return libFile.uri; +} + +Future> _downloadAndVerify(Uri zipUrl) async { + final zipResp = await http.get(zipUrl); + if (zipResp.statusCode != 200) { + throw Exception('Failed to download $zipUrl: HTTP ${zipResp.statusCode}'); + } + final shaResp = await http.get(Uri.parse('$zipUrl.sha256')); + if (shaResp.statusCode != 200) { + throw Exception( + 'Failed to download checksum for $zipUrl: HTTP ${shaResp.statusCode}', + ); + } + // `shasum`/`sha256sum` format: " "; take the first token. + final expected = shaResp.body.trim().split(RegExp(r'\s+')).first; + final actual = sha256.convert(zipResp.bodyBytes).toString(); + if (actual != expected) { + throw Exception( + 'Checksum mismatch for $zipUrl: expected $expected, got $actual', + ); + } + return zipResp.bodyBytes; +} + +String _libFileName(OS os) { + if (os == OS.windows) return '$_cdylibName.dll'; + if (os == OS.macOS || os == OS.iOS) return 'lib$_cdylibName.dylib'; + return 'lib$_cdylibName.so'; // linux, android +} + +// Maps a target to the Rust triple used in the published asset name. Kept in +// sync with the build matrix in .github/workflows/uniffi-cdylib.yml. +String _targetTriple(OS os, Architecture arch) { + if (os == OS.macOS) { + if (arch == Architecture.arm64) return 'aarch64-apple-darwin'; + if (arch == Architecture.x64) return 'x86_64-apple-darwin'; + } else if (os == OS.linux) { + if (arch == Architecture.arm64) return 'aarch64-unknown-linux-gnu'; + if (arch == Architecture.x64) return 'x86_64-unknown-linux-gnu'; + } else if (os == OS.windows) { + if (arch == Architecture.arm64) return 'aarch64-pc-windows-msvc'; + if (arch == Architecture.x64) return 'x86_64-pc-windows-msvc'; + } else if (os == OS.android) { + if (arch == Architecture.arm64) return 'aarch64-linux-android'; + if (arch == Architecture.arm) return 'armv7-linux-androideabi'; + if (arch == Architecture.x64) return 'x86_64-linux-android'; + } else if (os == OS.iOS) { + if (arch == Architecture.arm64) return 'aarch64-apple-ios'; + } + throw UnsupportedError('No prebuilt $_cdylibName for $os $arch'); +} diff --git a/livekit-uniffi/support/dart/pubspec.yaml.tera b/livekit-uniffi/support/dart/pubspec.yaml.tera index d92def583..e40add49b 100644 --- a/livekit-uniffi/support/dart/pubspec.yaml.tera +++ b/livekit-uniffi/support/dart/pubspec.yaml.tera @@ -13,4 +13,8 @@ dev_dependencies: # Native Assets build-hook support. `any` tracks the SDK-bundled versions. code_assets: any hooks: any + # Used by hook/build.dart to download and verify prebuilt libraries. + archive: ^4.0.0 + crypto: ^3.0.0 + http: ^1.2.0 test: ^1.25.0 From 09b8e703cc1028d4f23e564a983c66ce709c9db9 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Tue, 23 Jun 2026 12:08:06 +0100 Subject: [PATCH 4/5] Add Dart package tests (smoke + access-token round-trip) Ship tests in support/dart/test, copied into the generated package by the dart-package flow. Covers the FFI smoke path (buildVersion) and a real JWT/HMAC round-trip (generate -> verify -> decode, plus wrong-secret rejection) to exercise Rust logic across the boundary. --- livekit-uniffi/Makefile.toml | 11 +++- .../support/dart/test/access_token_test.dart | 51 +++++++++++++++++++ .../support/dart/test/build_version_test.dart | 10 ++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 livekit-uniffi/support/dart/test/access_token_test.dart create mode 100644 livekit-uniffi/support/dart/test/build_version_test.dart diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index 8be17e626..5ec6e6764 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -679,6 +679,14 @@ script = """ cp ${LIB_PATH} ${PACKAGES_DIR}/${LANG}/ """ +[tasks.dart-copy-tests] +private = true +script_runner = "@shell" +script = """ +mkdir -p ${PACKAGES_DIR}/${LANG}/test +cp ${SUPPORT_DIR}/${LANG}/test/*.dart ${PACKAGES_DIR}/${LANG}/test/ +""" + [tasks.dart-package-flow] private = true dependencies = [ @@ -686,7 +694,8 @@ dependencies = [ "dart-move-code-into-lib", "dart-generate-manifest", "dart-copy-hook", - "dart-copy-lib" + "dart-copy-lib", + "dart-copy-tests" ] [tasks.dart-package] diff --git a/livekit-uniffi/support/dart/test/access_token_test.dart b/livekit-uniffi/support/dart/test/access_token_test.dart new file mode 100644 index 000000000..0fe5e5870 --- /dev/null +++ b/livekit-uniffi/support/dart/test/access_token_test.dart @@ -0,0 +1,51 @@ +import 'package:test/test.dart'; +import 'package:livekit_uniffi/livekit_uniffi.dart'; + +void main() { + // Exercises real Rust JWT/HMAC logic across the FFI boundary: generate a + // token, then verify and decode it. + final creds = ApiCredentials(key: 'devkey', secret: 'devsecret'); + + group('access token', () { + test('generate produces a well-formed JWT', () { + final token = tokenGenerate( + options: TokenOptions(identity: 'alice', name: 'Alice'), + credentials: creds, + ); + // header.payload.signature + expect(token.split('.'), hasLength(3)); + }); + + test('verify round-trips identity, name, and issuer', () { + final token = tokenGenerate( + options: TokenOptions(identity: 'alice', name: 'Alice'), + credentials: creds, + ); + final claims = tokenVerify(token: token, credentials: creds); + expect(claims.sub, equals('alice')); + expect(claims.name, equals('Alice')); + expect(claims.iss, equals('devkey')); + }); + + test('claims can be read without the secret', () { + final token = tokenGenerate( + options: TokenOptions(identity: 'bob'), + credentials: creds, + ); + final claims = tokenClaimsFromUnverified(token: token); + expect(claims.sub, equals('bob')); + }); + + test('verify rejects a token signed with a different secret', () { + final token = tokenGenerate( + options: TokenOptions(identity: 'alice'), + credentials: creds, + ); + final wrong = ApiCredentials(key: 'devkey', secret: 'wrongsecret'); + expect( + () => tokenVerify(token: token, credentials: wrong), + throwsA(isA()), + ); + }); + }); +} diff --git a/livekit-uniffi/support/dart/test/build_version_test.dart b/livekit-uniffi/support/dart/test/build_version_test.dart new file mode 100644 index 000000000..6080ff74d --- /dev/null +++ b/livekit-uniffi/support/dart/test/build_version_test.dart @@ -0,0 +1,10 @@ +import 'package:test/test.dart'; +import 'package:livekit_uniffi/livekit_uniffi.dart'; + +void main() { + // Smoke test for the whole FFI path: the build hook resolves the native + // library, Dart calls a Rust function, and the result crosses back. + test('buildVersion returns a non-empty version string', () { + expect(buildVersion(), isNotEmpty); + }); +} From c60ae5189321fa636ce5d80b1c37e5406968c810 Mon Sep 17 00:00:00 2001 From: James Hugman Date: Tue, 23 Jun 2026 12:43:22 +0100 Subject: [PATCH 5/5] Add CI job running the Dart package tests uniffi-dart-test.yml builds the Dart package (host cdylib + bindings via cargo make dart-package) and runs dart test on PRs/pushes touching livekit-uniffi or its dependencies. The dart build keeps symbols (CARGO_PROFILE_RELEASE_STRIP=false) so library-mode bindgen can read the UNIFFI_META_* metadata, which the release strip removes on Linux. --- .github/workflows/uniffi-dart-test.yml | 80 ++++++++++++++++++++++++++ livekit-uniffi/Makefile.toml | 8 ++- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/uniffi-dart-test.yml diff --git a/.github/workflows/uniffi-dart-test.yml b/.github/workflows/uniffi-dart-test.yml new file mode 100644 index 000000000..62f341752 --- /dev/null +++ b/.github/workflows/uniffi-dart-test.yml @@ -0,0 +1,80 @@ +name: UniFFI Dart tests + +# Builds the Dart package (dev mode: host cdylib + generated bindings) and runs +# its test suite, exercising the FFI boundary end-to-end. Scoped to changes that +# can affect the livekit-uniffi Dart bindings. + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + paths: + - "livekit-uniffi/**" + - "livekit-api/**" + - "livekit-protocol/**" + - "Cargo.lock" + - "Cargo.toml" + - ".cargo/**" + - ".github/workflows/uniffi-dart-test.yml" + +permissions: + contents: read + +jobs: + dart-test: + name: Dart package tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + submodules: true + + - name: Setup Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + cache: false + rustflags: "" + + - name: Install Protoc + uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2.1.0 + with: + version: "25.2" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Dart + uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + with: + sdk: stable + + - 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 cargo-make + run: cargo install --locked cargo-make + + - name: Build Dart package + working-directory: livekit-uniffi + run: cargo make dart-package + + - name: Run Dart tests + working-directory: livekit-uniffi/packages/dart + run: | + dart pub get + dart test diff --git a/livekit-uniffi/Makefile.toml b/livekit-uniffi/Makefile.toml index 5ec6e6764..810d0d829 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-uniffi/Makefile.toml @@ -625,7 +625,10 @@ run_task = "android-publish-maven-local" [tasks.bindgen-dart] description = "Generate Dart bindings" dependencies = ["build", "locate-lib"] -env = { LANG = "dart" } +# Library-mode bindgen reads UNIFFI_META_* symbols from the cdylib. The release +# profile's `strip = "symbols"` removes them on Linux (macOS strips less +# aggressively), leaving no metadata to generate from — so keep symbols here. +env = { LANG = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } script_runner = "@duckscript" script = """ out_dir = join_path ${PACKAGES_DIR} ${LANG} @@ -700,5 +703,6 @@ dependencies = [ [tasks.dart-package] description = "Build a consumable Dart package with bindings, hook, and (dev) native lib" -env = { LANG = "dart" } +# See bindgen-dart: keep symbols so library-mode bindgen can read metadata. +env = { LANG = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } run_task = "dart-package-flow" \ No newline at end of file