From c35705a06ffeb47790825e550dfa4511315dd1b7 Mon Sep 17 00:00:00 2001 From: Ben Dean-Kawamura Date: Wed, 18 Sep 2024 11:29:34 -0400 Subject: [PATCH] Bug 1919574 - CLI tool to generate the android/ios directories I was going to update the adding a new component docs, but while I was reading them that the best way to simplify them would be to automate the process. * Added the `cargo start-bindings` tool. * Removed the "Converting an existing Component to UniFFI" HOWTO. I believe that all our existing components have been converted so there's no need to keep updating this. * Added a section about dependencies and the build.gradle file. * Removed the `pub use` items from the megazord lib.rs files. I don't believe they were needed. --- .cargo/config.toml | 1 + CHANGELOG.md | 7 + Cargo.lock | 135 +++++++- Cargo.toml | 1 + DEPENDENCIES.md | 2 +- docs/howtos/adding-a-new-component.md | 156 ++++----- .../converting-a-component-to-uniffi.md | 304 ------------------ megazords/ios-rust/DEPENDENCIES.md | 2 +- megazords/ios-rust/focus/DEPENDENCIES.md | 2 +- tools/start-bindings/Cargo.toml | 14 + tools/start-bindings/src/android.rs | 194 +++++++++++ tools/start-bindings/src/cargo_metadata.rs | 36 +++ tools/start-bindings/src/ios.rs | 133 ++++++++ tools/start-bindings/src/lib.rs | 11 + tools/start-bindings/src/main.rs | 43 +++ tools/start-bindings/src/toml.rs | 93 ++++++ tools/start-bindings/templates/build.gradle | 10 + .../templates/buildconfig.android.fragment | 7 + 18 files changed, 737 insertions(+), 414 deletions(-) delete mode 100644 docs/howtos/converting-a-component-to-uniffi.md create mode 100644 tools/start-bindings/Cargo.toml create mode 100644 tools/start-bindings/src/android.rs create mode 100644 tools/start-bindings/src/cargo_metadata.rs create mode 100644 tools/start-bindings/src/ios.rs create mode 100644 tools/start-bindings/src/lib.rs create mode 100644 tools/start-bindings/src/main.rs create mode 100644 tools/start-bindings/src/toml.rs create mode 100644 tools/start-bindings/templates/build.gradle create mode 100644 tools/start-bindings/templates/buildconfig.android.fragment diff --git a/.cargo/config.toml b/.cargo/config.toml index 54b03a601..15a86008e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -8,3 +8,4 @@ suggest-bench = ["bench", "-p", "suggest", "--features", "benchmark_api"] suggest-debug-ingestion-sizes = ["run", "-p", "suggest", "--bin", "debug_ingestion_sizes", "--features", "benchmark_api"] relevancy = ["run", "-p", "examples-relevancy-cli", "--"] suggest = ["run", "-p", "examples-suggest-cli", "--"] +start-bindings = ["run", "-p", "start-bindings", "--"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 665ab9b64..0edb6b2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ # v132.0 (_2024-09-30_) +## ✨ What's New ✨ + +### General + +- Simplified the process of adding a new component by adding a tool that can autogenerate the + initial UniFFI/bindings code. + ## 🦊 What's Changed 🦊 ### Glean diff --git a/Cargo.lock b/Cargo.lock index 548594ef9..16d7ec497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,7 +278,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa 1.0.11", "matchit", "memchr", "mime", @@ -382,7 +382,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 1.0.109", ] @@ -1768,7 +1768,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1869,7 +1869,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", - "itoa 1.0.9", + "itoa 1.0.11", ] [[package]] @@ -1931,7 +1931,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", + "itoa 1.0.11", "pin-project-lite", "socket2", "tokio", @@ -2005,9 +2005,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -2087,9 +2087,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jexl-eval" @@ -2157,7 +2157,7 @@ dependencies = [ "fraction", "getrandom", "iso8601", - "itoa 1.0.9", + "itoa 1.0.11", "memchr", "num-cmp", "once_cell", @@ -2921,6 +2921,18 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "once_map" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c7f82d6d446dd295845094f3a76bcdc5e6183b66667334e169f019cd05e5a0" +dependencies = [ + "ahash", + "hashbrown 0.14.3", + "parking_lot", + "stable_deref_trait", +] + [[package]] name = "oorandom" version = "11.1.3" @@ -3661,6 +3673,49 @@ dependencies = [ "viaduct", ] +[[package]] +name = "rinja" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28580fecce391f3c0e65a692e5f2b5db258ba2346ee04f355ae56473ab973dc" +dependencies = [ + "humansize", + "itoa 1.0.11", + "num-traits", + "percent-encoding", + "rinja_derive", +] + +[[package]] +name = "rinja_derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1ae91455a4c82892d9513fcfa1ac8faff6c523602d0041536341882714aede" +dependencies = [ + "basic-toml", + "memchr", + "mime", + "mime_guess", + "once_map", + "proc-macro2", + "quote", + "rinja_parser", + "rustc-hash 2.0.0", + "serde", + "syn 2.0.72", +] + +[[package]] +name = "rinja_parser" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea17639e1f35032e1c67539856e498c04cd65fe2a45f55ec437ec55e4be941" +dependencies = [ + "memchr", + "nom", + "serde", +] + [[package]] name = "rkv" version = "0.19.0" @@ -3741,6 +3796,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3915,7 +3976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "indexmap 1.9.1", - "itoa 1.0.9", + "itoa 1.0.11", "ryu", "serde", ] @@ -3945,7 +4006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa 1.0.11", "ryu", "serde", ] @@ -3969,7 +4030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" dependencies = [ "indexmap 1.9.1", - "itoa 1.0.9", + "itoa 1.0.11", "ryu", "serde", "unsafe-libyaml", @@ -4095,6 +4156,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "start-bindings" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap 4.2.2", + "rinja", + "serde_yaml 0.8.24", + "toml", + "toml_edit", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -4415,7 +4496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa 1.0.11", "libc", "num-conv", "num_threads", @@ -4528,6 +4609,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.5.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -5460,6 +5558,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 9dadaab42..2cbf2cc97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "megazords/ios-rust/focus", "tools/protobuf-gen", "tools/embedded-uniffi-bindgen", + "tools/start-bindings", "automation/swift-components-docs", "examples/*/", diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 2c1f0d03e..f9a5f4672 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -506,7 +506,7 @@ The following text applies to code linked from these dependencies: [iana-time-zone](https://github.com/strawlab/iana-time-zone), [id-arena](https://github.com/fitzgen/id-arena), [idna](https://github.com/servo/rust-url/), -[indexmap](https://github.com/bluss/indexmap), +[indexmap](https://github.com/indexmap-rs/indexmap), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes), [ipnet](https://github.com/krisprice/ipnet), [itertools](https://github.com/rust-itertools/itertools), diff --git a/docs/howtos/adding-a-new-component.md b/docs/howtos/adding-a-new-component.md index cbf227248..91495ee47 100644 --- a/docs/howtos/adding-a-new-component.md +++ b/docs/howtos/adding-a-new-component.md @@ -1,9 +1,6 @@ # Adding a new component to Application Services -Each component in the Application Services repository has three parts (the Rust code, -the Kotlin wrapper, and the Swift wrapper) so there are quite a few moving -parts involved in adding a new component. This is a rapid-fire list of all -the things you'll need to do if adding a new component from scratch. +This is a rapid-fire list for adding a component from scratch and generating Kotlin/Swift bindings. ## The Rust Code @@ -16,14 +13,18 @@ advice on designing and structuring the actual Rust code, and follow the introduces any new dependencies. Use [UniFFI](https://mozilla.github.io/uniffi-rs/) to define how your crate's -API will get exposed to foreign-language bindings. Prefer using the -[proc-macro](https://mozilla.github.io/uniffi-rs/latest/proc_macro/index.html) approach to creating -a UDL file. Place the following entries in your `Cargo.toml`: +API will get exposed to foreign-language bindings. Place the following in your `Cargo.toml`: ``` [dependencies] uniffi = { workspace = true } +``` +New components should prefer using the +[proc-macro](https://mozilla.github.io/uniffi-rs/latest/proc_macro/index.html) approach rather than +a UDL file based approach. If you do use a UDL file, add this to `Cargo.toml` as well. + +``` [build-dependencies] uniffi = { workspace = true } ``` @@ -32,60 +33,16 @@ Include your new crate in the `application-services` workspace, by adding it to the `members` and `default-members` lists in the `Cargo.toml` at the root of the repository. -In order to be published to consumers, your crate must be included in the -["megazord"](../design/megazords.md) crate for each target platform: - -* For Android, add it as a dependency in `./megazords/full/Cargo.toml` and - add a `pub use ` to `./megazords/full/src/lib.rs`. -* For iOS, add it as a dependency in `./megazords/ios-rust/rust/Cargo.toml` and - add a `pub use ` to `./megazords/ios-rust/src/lib.rs`. - Run `cargo check -p ` in the repository root to confirm that things are configured properly. This will also have the side-effect of updating `Cargo.lock` to contain your new crate and its dependencies. -## The Kotlin Bindings +## The Android Bindings -Make a `./components//android` subdirectory to contain -Kotlin- and Android-specific code. This directory will contain a gradle -project for building your Kotlin bindings. +Run the `start-bindings android ` command to auto-generate the initial code. Follow the directions in the output. -Copy the `build.gradle` file from `./components/crashtest/android/` into -your own component's directory. Update the `ext.configureUniFFIBindgen("crashtest")` line, -replacing "crashtest" with the crate name of your component. - -Create a file `./components//uniffi.toml` with the -following contents: - -```toml -[bindings.kotlin] -package_name = "mozilla.appservices." -``` - -Create a file `./components//android/src/main/AndroidManifest.xml` -with the following contents: - -```xml - -``` - -In the root of the repository, edit `.buildconfig-android.yml`to add -your component's metadata. This will cause it to be included in the -gradle workspace and in our build and publish pipeline. Check whether -it builds correctly by running: -* `./gradlew :assembleDebug` - -You can include hand-written Kotlin code alongside the automatically -generated bindings, by placing `.kt`` files in a directory named: -* `./android/src/test/java/mozilla/appservices//` - -You can write Kotlin-level tests that consume your component's API, -by placing `.kt`` files in a directory named: -* `./android/src/test/java/mozilla/appservices//`. - -So you would end up with a directory structure something like this: +You will end up with a directory structure something like this: * `components//` * `Cargo.toml` @@ -97,26 +54,42 @@ So you would end up with a directory structure something like this: * `src/` * `main/` * `AndroidManifest.xml` - * `java/mozilla/appservices//` - * Hand-written Kotlin code here. - * `test/java/mozilla/appservices//` - * Kotlin test-cases here. -Run your component's Kotlin tests with `./gradlew :test` -to confirm that this is all working correctly. +### Dependent crates + +If your crate uses types from another crate in it's public API, you need to include a dependency for +the corresponding project in your `android/build.gradle` file. + +For example, suppose use the `remote_settings::RemoteSettingsServer` type in your public API so that +consumers can select which server they want. In that case, you need to a dependency on the +remotesettings project: + +``` +dependencies { + api project(":remotesettings") +} +``` + +### Hand-written code + +You can include hand-written Kotlin code alongside the automatically +generated bindings, by placing `.kt`` files in a directory named: +* `./android/src/test/java/mozilla/appservices//` + +You can write Kotlin-level tests that consume your component's API, +by placing `.kt`` files in a directory named: +* `./android/src/test/java/mozilla/appservices//`. + +You can run the tests with `./gradlew :test` + +## The iOS Bindings + +* Run the `start-bindings ios ` command to auto-generate the initial code +* Run `start-bindings ios-focus ` if you also want to expose your component to Focus. +* Follow the directions in the output. -## The Swift Bindings -### Creating the directory structure -Make a `./components//ios` subdirectory to contain -Swift- and iOS-specific code. The UniFFI-generated swift bindings will -be written to a subdirectory named `Generated`. - -You can include hand-written Swift code alongside the automatically -generated bindings, by placing `.swift` files in a directory named: -`./ios//`. - -So you would end up with a directory structure something like this: +You will end up with a directory structure something like this: * `components//` * `Cargo.toml` @@ -124,31 +97,20 @@ So you would end up with a directory structure something like this: * `src/` * Rust code here. * `ios/` - * `/` - * Hand-written Swift code here. * `Generated/` * Generated Swift code will be written into this directory. ### Adding your component to the Swift Package Manager Megazord + > *For more information on our how we ship components using the Swift Package Manager, check the [ADR that introduced the Swift Package Manager](../adr/0003-swift-packaging.md)* -You will need to do the following steps to include the component in the megazord: -1. Update its `uniffi.toml` to include the following settings: - ```toml - [bindings.swift] - ffi_module_name = "MozillaRustComponents" - ffi_module_filename = "FFI" - ``` -1. Add the component as a dependency to the `Cargo.toml` in [`megazords/ios-rust/`](https://github.com/mozilla/application-services/blob/main/megazords/ios-rust/Cargo.toml) -1. Add a `pub use` declaration for the component in [`megazords/ios-rust/src/lib.rs`](https://github.com/mozilla/application-services/blob/main/megazords/ios-rust/src/lib.rs) -1. Add an `#import` for its header file to [`megazords/ios-rust/MozillaRustComponents.h`](https://github.com/mozilla/application-services/blob/main/megazords/ios-rust/MozillaRustComponents.h) -1. Add your component into the iOS ["megazord"](../design/megazords.md) through the Xcode project, which can only really by done using the Xcode application, which can only really be done if you're on a Mac. +Add your component into the iOS ["megazord"](../design/megazords.md) through the Xcode project, which can only really by done using the Xcode application, which can only really be done if you're on a Mac. - 1. Open `megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj` in Xcode. +1. Open `megazords/ios-rust/MozillaTestServices/MozillaTestServices.xcodeproj` in Xcode. - 1. In the Project navigator, add a new Group for your new component, pointing to - the `./ios/` directory you created above. Add the following entries to the Group: - * Any hand-written `.swift `files for your component +1. In the Project navigator, add a new Group for your new component, pointing to +the `./ios/` directory you created above. Add the following entries to the Group: + * Any hand-written `.swift `files for your component > Make sure that the "Copy items if needed" option is **unchecked**, and that nothing is checked in the "Add to targets" list. @@ -191,11 +153,19 @@ The result should look something like this: Use the Xcode Test Navigator to run your tests and check whether they're passing. -## Distribute your component with `rust-components-swift` +### Hand-written code + +You can include hand-written Swift code alongside the automatically +generated bindings, by placing `.swift` files in a directory named: +`./ios//`. + +Make sure that this code gets distributed. Edit `taskcluster/scripts/build-and-test-swift.py` and: + +- Add the path to the directory containing any hand-written swift code to `SOURCE_TO_COPY` +- Optionally also to `FOCUS_SOURCE_TO_COPY` if your component is also targeting Firefox Focus + + +### Distribute your component with `rust-components-swift` The Swift source code and generated UniFFI bindings are distributed to consumers (eg: Firefox iOS) through [`rust-components-swift`](https://github.com/mozilla/rust-components-swift). -A nightly taskcluster job prepares the `rust-component-swift` packages from the source code in the application-services repository. To distribute your component with `rust-component-swift`, add the following to the taskcluster script in `taskcluster/scripts/build-and-test-swift.py`: -- Add the path to the directory containing any hand-written swift code to `SOURCE_TO_COPY` - - Optionally also to `FOCUS_SOURCE_TO_COPY` if your component is also targeting Firefox Focus - Your component should now automatically get included in the next `rust-component-swift` nightly release. diff --git a/docs/howtos/converting-a-component-to-uniffi.md b/docs/howtos/converting-a-component-to-uniffi.md deleted file mode 100644 index 8ad4ddcf0..000000000 --- a/docs/howtos/converting-a-component-to-uniffi.md +++ /dev/null @@ -1,304 +0,0 @@ -# Converting an existing Component to use UniFFI - -When we started building the components in this repo, exposing Rust code to -Kotlin and Swift was a manual process and each component had its own -hand-written FFI layer and foreign-language bindings. - -As we've gained more experience with building components in this way, we've -started to automate bindings generation and capture best practices in a -tool called [UniFFI](https://mozilla.github.io/uniffi-rs/), which is the -currently recommended approach when [adding a new component from scratch]( -./adding-a-new-component.md). - -We expect that existing components will gradually be ported over to use -UniFFI, and this document is a guide to doing that port. - -## First, get familiar with UniFFI - -First, make sure you've perused the [UniFFI guide](https://mozilla.github.io/uniffi-rs/) -to understand the overall architecture of a UniFFI component, and take a look -at the [guide to adding a new component](./adding-a-new-component.md) to understand -how such components fit in to this repo. The aim of porting will be to have a component -that looks like it was added by the process described therein. - -## Next, get familiar with the target component - -Pre-UniFFI components typically consist of four main parts: - -* A Rust crate implementing the core functionality of the component -* A separate Rust crate that exposes the core functionality over a C-style FFI. -* An Android package that imports the C-style FFI into idiomatic Kotlin. -* A Swift module that imports the C-style FFI into idiomatic Swift. - -The code for these parts will be laid out something like this: - -* `components//` - * `Cargo.toml` - * `src/` - * Rust code for the core functionality of the component goes here. - * `ffi/` - * `Cargo.toml` - * `src/` - * Rust code specifically for exposing the C-style FFI goes here. - * `android/` - * `build.gradle` - * `src/` - * `main/` - * `AndroidManifest.xml` - * `java/mozilla/appservices//` - * `LibFFI.kt` (low-level bindings to the C-style FFI) - * Higher-level hand-written Kotlin that wraps the FFI. - * `ios/` - * `/` - * `RustAPI.h` (low-level bindings to the C-style FFI) - * Higher-level hand-written Swift that wraps the FFI. - -The goal here is to replace much of the hand-written wrapper layers with autogenerated -code: - -* The `./ffi/` crate will disappear entirely, its work is automated by UniFFI - * If you still need some hand-written `pub extern "C"` functions, perhaps to - implement features not currently supported by UniFFI, then they should move - into `lib.rs` of the main component crate. -* The low-level `LibFFI.kt` file will disappear entirely, as will some of the - code that converts it back into nice high-level Kotlin classes and interfaces. - * Some of the hand-written Kotlin code may remain, if it provides functionality that - cannot be implemented in Rust. -* The low-level `RustAPI.h` file will disappear entirely, as will some of the - code that converts it back into nice high-level Swift classes and interfaces. - * Some of the hand-written Swift code may remain, if it provides functionality that - cannot be implemented in Rust. - -You'll aim to end up with a simplified file structure that looks like this: - - -* `components//` - * `Cargo.toml` - * `uniffi.toml` - * `src/` - * `.udl` (abstract interface definition) - * Rust code here. - * `android/` - * `build.gradle` - * `src/` - * `main/` - * `AndroidManifest.xml` - * `java/mozilla/appservices//` - * Optional hand-written Kotlin code here. - * `ios/` - * `/` - * Optional hand-written Swift code here. - -## Write a first draft of the `.udl` file for the component's interface - -Make sure you've got the `uniffi-bindgen` command available; `cargo install uniffi_bindgen` will -ensure you have the latest version. - -Create `./src/.udl` and try to describe the intended interface for the component -using [UniFFI's interface definition language](https://mozilla.github.io/uniffi-rs/udl_file_spec.html). -You'll probably need to reverse-engineer it a little bit from the existing hand-written Kotlin and/or -Swift code. - -Don't spend too much time on trying to match every minute detail of the existing hand-written API. -There are likely to be small differences between how UniFFI likes to do things and how the hand-written -APIs were structured, and it's in everyone's best long-term interests to just push ahead and update -consumers to accommodate any breaking API changes, rather than e.g. trying to convince UniFFI to -capitalize enum variant names in the same style that the hand-written code was using. - -To check whether the `.udl` file is syntactically valid, you can use `uniffi-bindgen` to generate -the Rust FFI scaffolding like so: - -``` -uniffi-bindgen scaffolding ./src/.udl -``` - -If this succeeds, it will generate a file `./src/.uniffi.rs` with a bunch of -thorny auto-generated Rust code. If it fails, it will likely fail with an inscrutable error message. -Unfortunately the error reporting in UniFFI is currently a known pain point, and it can take a -bit of trial-and-error to identify what part of the file is causing the issue. Sorry :-( - -The aim at this point is to ensure that the intended interface of the component can be expressed -in terms that UniFFI understands. Most cases should be supported, but you may find some aspect of -the existing component that is hard to express in UniFFI, perhaps even uncovering new functionality -that needs to be added to UniFFI itself! - -The `.udl` file is definitely a first draft at this point. It is normal and expected to need -to iterate on this file as you port over the underlying Rust code. - - -## Restructure the Rust code to introduce UniFFI - -You will now restructure the existing Rust crate so that its public API surface -and overall "shape" match what you defined in the `.udl` file. - -Start by deleting the `./ffi` sub-crate, because you're going to use UniFFI to generate -all of that code. You'll also need to remove it from the workspace in the top-level -`Cargo.toml` file, as well as change the crates under `/megazords` to import the core -Rust crate for the component rather than importing the FFI sub-crate. - -Add UniFFI to the crate's dependencies and configure its `build.rs` script to invoke the -UniFFI scaffolding generator, as described in ["adding a new component"](adding-a-new-component.md). - -Now, edit `./lib.rs` so that it matches the interface defined in the `.udl` file as closely -as possible. If the `.udl` has an `interface Example` then `lib.rs` should contain a -`pub struct Example`, if the `.udl` contains an `enum ExampleItem` then `lib.rs` should -contain a `pub enum ExampleItem`, and so-on. - -The details of this step will depend heavily on the specific crate, but some tips include: - -* You may find it useful to move all of the existing code into a sub-module named `internal`, - and then make a brand new `lib.rs` that imports or re-defines just the pieces it needs - in order to implement the interface from the `.udl` file. The `fxa-client` crate is an - example of a case where this worked out well, though of course your mileage may vary. - -* If the existing crate contains a file named like `_msg_types.proto`, then - it was using Protocol Buffers to serialize data to pass over the FFI. The message types - defined in the `.proto` file will need to be converted into `dictionary` or `enum` definitions - in your `.udl` file. See the section below for more details. - -As noted above, don't be afraid to accept some API churn during the conversion process. -We're willing to accept some breaking API changes as the cost of getting bindings generated -for free, as long as the core functionality and mental model of the component remain intact. - -At this point, in theory the crate should be buildable with UniFFI, although it's likely -to require some iteration to get it all working! Run `cargo check` to check for any -compilation errors without having to do a full build. - -### Removing Protobuf Messages - -Passing rich structured data over the FFI is the most complex part of our hand-written bindings, -and was previously done by [serializing data via Protocol Buffers]( -https://hacks.mozilla.org/2019/04/crossing-the-rust-ffi-frontier-with-protocol-buffers/). -This is something that UniFFI tries to make as simple as possible. - -Start by locating the `_msg_types.proto` file for the component. This file defines -the structured messages that can be passed over the FFI, and you should see that they correspond -to various types of structured data that the component wants to receive from, or return to, -the foreign-language code. - -Find the places in your `.udl` interface that correspond to these message types and make sure -that you've got a similarly-shaped `dictionary` or `enum` for each one. You should find that -representing this structured data in UDL is simpler than protobuf in many cases - for example -many of our `.protobuf` files need to use a separate `ExampleStructs` message in order to -pass a list of `ExampleStruct` messages over the FFI, but in UniFFI this is represented -directly as `sequence`. - -Find the places in the Rust code that are using these message types to return structured data. -In simple cases, you may be able to directly replace uses of `msg_types::ExampleStruct` with -the corresponding `crate::ExampleStruct` from your public API. -For more complex cases, you may find it helpful to define an `Into` mapping between the -UniFFI dictionary/enum in the crate's public interface, and a more complex struct designed -for internal use. - -As noted above, don't be afraid to accept some API churn during this conversion process. - -Once you have replaced all uses of the `msg_types` structs in the Rust code: - -* Delete `./src/_msg_types.proto`. -* Delete `./src/mozilla.appservices..protobuf.rs`, which is generated from the `.proto` file. -* Remote `prost` and `prost-derive` from the crate's dependencies. -* Delete the crate from the list in `/tools/protobuf_files.toml`. - -If you happen to find that you've deleted the last crate from the list in `protobuf_files.toml`, -congratulations! You've successfully removed protocol buffers from this repo entirely, and should -file a bug to track the complete removal of protobuf from our tooling and dependency chain. - -## Document the Public API in the Rust code - -Write consumer-facing documentation on the public API in `lib.rs` using Rust's standard -[rustdoc](https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html) conventions -and tools. The `fxa-client` crate may serve as a good example. - -You can view the generated documentation by running: - -``` -cargo doc --no-deps --open -``` - -In future, we intend to automatically extract documentation from the Rust code -and make it easily available to consumers of the generated bindings. - -(In fact there is some work-in-progress code in [uniffi-rs#416](https://github.com/mozilla/uniffi-rs/pull/416) -that can read docs from the Rust code and write them back into the `.udl` file, which you're -welcome to try out if you're feeling adventurous. But it's just a very hacky prototype.) - -## Set up the Kotlin wrapper - -It's easiest to start by removing all of the hand-written Kotlin code under `android/src/main/java` -and then restoring parts of it later if necessary. Leave the `AndroidManifest.xml` file and any tests -in place. - -Delete the `android/build.gradle` file and then follow the instructions for [adding Kotlin bindings -for a new component](adding-a-new-component.md#the-kotlin-bindings) to create a new `build.gradle` -file and a corresponding `uniffi.toml`. - -This should be all that's required to set up UniFFI to build the Kotlin bindings. Try building -the Android package to confirm: - -* `./gradlew :assembleDebug` - -The UniFFI-generated Kotlin code will be under `./android/build/generated/source/uniffi/` and -may be useful for debugging. - -If there are existing Kotlin tests for the component, the next step is to get those passing: - -* `./gradlew :test` - -As noted above, it is normal and expected for the autogenerated bindings to be subtly different -from the previous hand-written ones. For example, UniFFI insists on using SHOUTY_SNAKE_CASE -variant names in Kotlin enums while the hand-written code may have used CamelCase. Some components -also have small naming differences between the Rust code and the hand-written Kotlin bindings, -which UniFFI will not allow. - -If the component had functionality in its Kotlin layer that was not part of the Rust API, -then you'll need to add some hand-written Kotlin code under `android/src/main/java` to -implement it. The `fxa-client` component may be a good example here: its Rust layer exposes -a `FirefoxAccount` struct that the Kotlin code wraps into a `PersistedFirefoxAccount` class, -adding the ability to set a persistence callback. - -Finally, you will need to try out the new bindings with a consuming app. For Kotlin code you should -[make a local build of android-components and Fenix](locally-published-components-in-fenix.md), -updating them to accommodate any changes in the component's public API. - - -## Set up the Swift wrapper - -It's easiest to start by removing all of the hand-written Swift code under `./ios` and then -restoring parts of it later if necessary. - -Edit `/megazords/ios-rust/MozillaTestServices.h` to remove any references to `RustAPI.h`, -replacing them with the UniFFI-generated header file name `FFI.h`. - -Open `/megazords/ios-rust/MozillaTestServices.xcodeproj` in Xcode and follow the instructions for -[adding Swift bindings for a new component](adding-a-new-component.md#the-swift-bindings) to -configure Xcode to build your UniFFI-generated bindings. - -While you are in the Xcode Project Navigator, you should also delete any references to -`RustAPI.h` or to the old hand-written Swift wrappers. (They should be highlighted -in red in the Project Navigator, because the files will be missing from disk after you -deleted them above). - -This should be all that's required to set up UniFFI to build the Swift bindings. Try building -the project in Xcode to confirm. - -The UniFFI-generated Swift code will be under `ios/Generated` and may be useful for debugging. - -If there are existing Swift tests for the component, the next step is to get those passing: - -* `./automation/run_ios_tests.sh` -* (or run them from the Xcode GUI) - -As noted above, it is normal and expected for the autogenerated bindings to be subtly different -from the previous hand-written ones. Many existing components have small naming differences -between the Rust code and the hand-written Swift bindings, which UniFFI will not allow. - -If the component had functionality in its Swift layer that was not part of the Rust API, -then you'll need to add some hand-written Swift code under `./ios/` to -implement it. The `fxa-client` component may be a good example here: its Rust layer exposes -a `FirefoxAccount` struct that the Swift code wraps into a `PersistedFirefoxAccount` class, -adding the ability to set a persistence callback. - -You will need to add any such file to the "Compile Sources" list in Xcode, in the same way -that you added the `.udl` file. - -Finally, you will need to try out the new bindings with a consuming app. For Swift code you should make a local build of Firefox iOS, you can do that by following the steps in [this document](./locally-published-components-in-firefox-ios.md) diff --git a/megazords/ios-rust/DEPENDENCIES.md b/megazords/ios-rust/DEPENDENCIES.md index 2c6590a90..aaaefb1fd 100644 --- a/megazords/ios-rust/DEPENDENCIES.md +++ b/megazords/ios-rust/DEPENDENCIES.md @@ -497,7 +497,7 @@ The following text applies to code linked from these dependencies: [iana-time-zone](https://github.com/strawlab/iana-time-zone), [id-arena](https://github.com/fitzgen/id-arena), [idna](https://github.com/servo/rust-url/), -[indexmap](https://github.com/bluss/indexmap), +[indexmap](https://github.com/indexmap-rs/indexmap), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes), [ipnet](https://github.com/krisprice/ipnet), [itertools](https://github.com/rust-itertools/itertools), diff --git a/megazords/ios-rust/focus/DEPENDENCIES.md b/megazords/ios-rust/focus/DEPENDENCIES.md index e628c3332..431c7926a 100644 --- a/megazords/ios-rust/focus/DEPENDENCIES.md +++ b/megazords/ios-rust/focus/DEPENDENCIES.md @@ -475,7 +475,7 @@ The following text applies to code linked from these dependencies: [iana-time-zone](https://github.com/strawlab/iana-time-zone), [id-arena](https://github.com/fitzgen/id-arena), [idna](https://github.com/servo/rust-url/), -[indexmap](https://github.com/bluss/indexmap), +[indexmap](https://github.com/indexmap-rs/indexmap), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes), [ipnet](https://github.com/krisprice/ipnet), [itertools](https://github.com/rust-itertools/itertools), diff --git a/tools/start-bindings/Cargo.toml b/tools/start-bindings/Cargo.toml new file mode 100644 index 000000000..3dca950ab --- /dev/null +++ b/tools/start-bindings/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "start-bindings" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +camino = "1" +cargo_metadata = "0.15" +clap = { version = "4.2", features = ["derive"] } +rinja = "0.3.3" +serde_yaml = "0.8" +toml = "0.5" +toml_edit = "0.22.21" diff --git a/tools/start-bindings/src/android.rs b/tools/start-bindings/src/android.rs new file mode 100644 index 000000000..47d12a341 --- /dev/null +++ b/tools/start-bindings/src/android.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{ + fs::{create_dir_all, read_to_string, File}, + io::Write, +}; + +use anyhow::{bail, Result}; +use camino::Utf8Path; +use rinja::Template; + +use crate::{ + cargo_metadata::CargoMetadataInfo, + toml::{add_cargo_toml_dependency, update_uniffi_toml}, +}; + +pub fn generate_android(crate_name: String, description: String) -> Result<()> { + let metadata = CargoMetadataInfo::new(&crate_name)?; + let android_root = metadata.crate_root.join("android"); + + println!(); + write_file( + BuildGradle { + crate_name: crate_name.clone(), + } + .render()?, + &android_root.join("build.gradle"), + )?; + write_file( + ANDROID_MANIFEST, + &android_root + .join("src") + .join("main") + .join("AndroidManifest.xml"), + )?; + write_file(PROGUARD_RULES, &android_root.join("proguard-rules.pro"))?; + update_uniffi_toml( + &metadata.crate_root, + "kotlin", + [( + "package_name", + format!("mozilla.appservices.{crate_name}").into(), + )], + )?; + add_cargo_toml_dependency( + &metadata.android_megazord_root, + &metadata.crate_root, + &crate_name, + )?; + update_buildconfig( + &metadata.workspace_root, + &crate_name, + &android_root, + &description, + )?; + + println!(); + println!("Android bindings successfully started!"); + println!(); + println!("Run `./gradlew :assembleDebug` from the app-services root directory to test that this is working"); + println!(); + println!("Does crate use types from another crate in it's public API? If so, you'll need to tweak the `android/build.gradle` file:"); + println!("https://mozilla.github.io/application-services/book/howtos/adding-a-new-component.html#dependent-crates"); + println!(); + println!("Optional steps:"); + println!( + " - Add hand-written Android code in {}", + metadata + .crate_root + .join("android") + .join("src") + .join("main") + .join("java") + .join("mozilla") + .join("appservices") + .join(&crate_name) + .strip_prefix(&metadata.workspace_root) + .unwrap() + ); + println!( + " - Add tests in {}", + metadata + .crate_root + .join("android") + .join("src") + .join("test") + .join("java") + .join("mozilla") + .join("appservices") + .join(&crate_name) + .strip_prefix(&metadata.workspace_root) + .unwrap() + ); + + Ok(()) +} + +fn write_file(contents: impl AsRef, path: &Utf8Path) -> Result<()> { + let contents = contents.as_ref(); + create_dir_all(path.parent().unwrap())?; + + let mut file = File::create(path)?; + write!(file, "{contents}")?; + println!("{path} generated"); + + Ok(()) +} + +// Update .buildconfig-android.yml +// +// We don't have anything like toml-edit that can edit YAML files while maintaining the formatting. +// Instead, if we need to update the file, append a manually constructed YAML fragment. +fn update_buildconfig( + workspace_root: &Utf8Path, + crate_name: &str, + android_root: &Utf8Path, + description: &str, +) -> Result<()> { + let path = workspace_root.join(".buildconfig-android.yml"); + if !buildconfig_needs_update(&path, crate_name)? { + println!("{path} skipped ([projects.{crate_name}] key already exists)"); + return Ok(()); + } + + let fragment = BuildConfigFragementTemplate { + crate_name: crate_name.to_owned(), + android_root: android_root + .strip_prefix(workspace_root) + .unwrap() + .to_string(), + description: description.to_owned(), + } + .render()?; + + let mut file = File::options().append(true).open(&path)?; + write!(file, "{fragment}")?; + println!("{path} updated"); + + Ok(()) +} + +fn buildconfig_needs_update(path: &Utf8Path, crate_name: &str) -> Result { + let config: serde_yaml::Value = serde_yaml::from_str(&read_to_string(path)?)?; + let projects = config + .as_mapping() + .and_then(|m| m.get(&"projects".into())) + .and_then(|v| v.as_mapping()); + match projects { + None => bail!("buildconfig.yaml does not have projects key"), + Some(projects) => Ok(!projects.contains_key(&crate_name.into())), + } +} + +#[derive(Template)] +#[template(path = "build.gradle", escape = "none")] +struct BuildGradle { + crate_name: String, +} + +const ANDROID_MANIFEST: &str = + "\n"; +const PROGUARD_RULES: &str = "\ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +"; + +#[derive(Template)] +#[template(path = "buildconfig.android.fragment", escape = "none")] +struct BuildConfigFragementTemplate { + crate_name: String, + android_root: String, + description: String, +} diff --git a/tools/start-bindings/src/cargo_metadata.rs b/tools/start-bindings/src/cargo_metadata.rs new file mode 100644 index 000000000..d896aa3e1 --- /dev/null +++ b/tools/start-bindings/src/cargo_metadata.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use anyhow::{bail, Result}; +use camino::Utf8PathBuf; +use cargo_metadata::{Metadata, MetadataCommand}; + +pub struct CargoMetadataInfo { + pub workspace_root: Utf8PathBuf, + pub crate_root: Utf8PathBuf, + pub android_megazord_root: Utf8PathBuf, + pub ios_megazord_root: Utf8PathBuf, + pub ios_focus_megazord_root: Utf8PathBuf, +} + +impl CargoMetadataInfo { + pub fn new(crate_name: &str) -> Result { + let metadata = MetadataCommand::new().exec().unwrap(); + Ok(Self { + crate_root: find_crate_root(&metadata, crate_name)?, + android_megazord_root: find_crate_root(&metadata, "megazord")?, + ios_megazord_root: find_crate_root(&metadata, "megazord_ios")?, + ios_focus_megazord_root: find_crate_root(&metadata, "megazord_focus")?, + workspace_root: metadata.workspace_root, + }) + } +} + +fn find_crate_root(metadata: &Metadata, crate_name: &str) -> Result { + let package = metadata.packages.iter().find(|pkg| pkg.name == crate_name); + match package { + Some(pkg) => Ok(pkg.manifest_path.parent().unwrap().to_owned()), + None => bail!("Crate not found: {crate_name}"), + } +} diff --git a/tools/start-bindings/src/ios.rs b/tools/start-bindings/src/ios.rs new file mode 100644 index 000000000..d35c632b7 --- /dev/null +++ b/tools/start-bindings/src/ios.rs @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::{ + fs::{read_to_string, File}, + io::Write, + process::Command, +}; + +use anyhow::Result; +use camino::Utf8Path; + +use crate::{ + cargo_metadata::CargoMetadataInfo, + toml::{add_cargo_toml_dependency, update_uniffi_toml}, +}; + +pub fn generate_ios(crate_name: String) -> Result<()> { + generate(crate_name, IosMegazord::Ios) +} + +pub fn generate_ios_focus(crate_name: String) -> Result<()> { + generate(crate_name, IosMegazord::Focus) +} + +enum IosMegazord { + Ios, + Focus, +} + +impl IosMegazord { + fn root_dir<'a>(&self, metadata_info: &'a CargoMetadataInfo) -> &'a Utf8Path { + match self { + Self::Ios => &metadata_info.ios_megazord_root, + Self::Focus => &metadata_info.ios_focus_megazord_root, + } + } + + fn name(&self) -> &'static str { + match self { + Self::Ios => "Ios", + Self::Focus => "Ios Focus", + } + } + + fn crate_name(&self) -> &'static str { + match self { + Self::Ios => "megazord_ios", + Self::Focus => "megazord_focus", + } + } +} + +fn generate(crate_name: String, megazord: IosMegazord) -> Result<()> { + let metadata = CargoMetadataInfo::new(&crate_name)?; + add_cargo_toml_dependency( + megazord.root_dir(&metadata), + &metadata.crate_root, + &crate_name, + )?; + update_uniffi_toml( + &metadata.crate_root, + "swift", + [ + ("ffi_module_name", "MozillaRustComponents".into()), + ("ffi_module_filename", format!("{crate_name}FFI").into()), + ], + )?; + update_megazord_lib_rs( + megazord.root_dir(&metadata), + megazord.crate_name(), + &crate_name, + )?; + println!(); + println!("{} bindings successfully started!", megazord.name()); + println!(); + println!( + "The next step is to update the iOS Xcode project. See the application-services docs:" + ); + println!("https://mozilla.github.io/application-services/book/howtos/adding-a-new-component.html#adding-your-component-to-the-swift-package-manager-megazord"); + println!(); + println!("Optional steps:"); + println!( + " - Add hand-written code in {}", + metadata + .crate_root + .join("ios") + .strip_prefix(&metadata.workspace_root) + .unwrap() + ); + Ok(()) +} + +/// Add `pub use ` to lib.rs for the megazord. +/// +/// This is needed for iOS, but not for Android. Maybe because iOS uses a static lib. +fn update_megazord_lib_rs( + crate_root: &Utf8Path, + megazord_crate_name: &str, + crate_name: &str, +) -> Result<()> { + let path = crate_root.join("src").join("lib.rs"); + let contents = read_to_string(&path)?; + let mut lines: Vec<_> = contents.split('\n').collect(); + let new_use_statement = format!("pub use {crate_name};"); + + let mut last_pub_use = None; + for (i, line) in lines.iter().enumerate() { + if line.trim() == new_use_statement { + // The use statement is already present, don't change anything + return Ok(()); + } else if line.trim().starts_with("pub use") { + last_pub_use = Some(i); + } + } + let insert_pos = match last_pub_use { + Some(i) => i + 1, + None => lines.len(), + }; + lines.insert(insert_pos, &new_use_statement); + let mut file = File::create(&path)?; + write!(file, "{}", lines.join("\n"))?; + println!("{path} generated"); + + // Run cargo fmt to ensure the imports are sorted in the correct order. + Command::new("cargo") + .args(["fmt", "-p", megazord_crate_name]) + .spawn()? + .wait()?; + + Ok(()) +} diff --git a/tools/start-bindings/src/lib.rs b/tools/start-bindings/src/lib.rs new file mode 100644 index 000000000..e9a9bb6c2 --- /dev/null +++ b/tools/start-bindings/src/lib.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod android; +mod cargo_metadata; +mod ios; +mod toml; + +pub use android::generate_android; +pub use ios::{generate_ios, generate_ios_focus}; diff --git a/tools/start-bindings/src/main.rs b/tools/start-bindings/src/main.rs new file mode 100644 index 000000000..9bf9d56dd --- /dev/null +++ b/tools/start-bindings/src/main.rs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use clap::{Parser, Subcommand}; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + Android { + crate_name: String, + description: String, + }, + Ios { + crate_name: String, + }, + IosFocus { + crate_name: String, + }, +} + +fn main() { + let args = Args::parse(); + let result = match args.command { + Command::Android { + crate_name, + description, + } => start_bindings::generate_android(crate_name, description), + Command::Ios { crate_name } => start_bindings::generate_ios(crate_name), + Command::IosFocus { crate_name } => start_bindings::generate_ios_focus(crate_name), + }; + if let Err(e) = result { + eprintln!("{e}"); + std::process::exit(1); + } +} diff --git a/tools/start-bindings/src/toml.rs b/tools/start-bindings/src/toml.rs new file mode 100644 index 000000000..214092d66 --- /dev/null +++ b/tools/start-bindings/src/toml.rs @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::fs::{read_to_string, File}; +use std::io::Write; + +use anyhow::Result; +use camino::{Utf8Path, Utf8PathBuf}; +use toml_edit::{DocumentMut, Table, Value}; + +/// A toml file that we're editing +/// +/// This wraps toml_edit's DocumentMut for a particular file path +pub struct TomlFile { + path: Utf8PathBuf, + doc: DocumentMut, +} + +impl TomlFile { + pub fn open(path: &Utf8Path) -> Result { + let doc = if path.exists() { + read_to_string(path)?.parse()? + } else { + DocumentMut::new() + }; + Ok(Self { + path: path.to_owned(), + doc, + }) + } + + pub fn write(&self) -> Result<()> { + let mut file = File::create(&self.path)?; + write!(file, "{}", self.doc)?; + println!("{} updated", self.path); + Ok(()) + } +} + +impl std::ops::Deref for TomlFile { + type Target = DocumentMut; + + fn deref(&self) -> &DocumentMut { + &self.doc + } +} + +impl std::ops::DerefMut for TomlFile { + fn deref_mut(&mut self) -> &mut DocumentMut { + &mut self.doc + } +} + +pub fn add_cargo_toml_dependency( + megazord_root: &Utf8Path, + crate_root: &Utf8Path, + crate_name: &str, +) -> Result<()> { + // Find the relative path from the megazord to the crate root + let megazord_components: Vec<_> = megazord_root.components().collect(); + let crate_root_components: Vec<_> = crate_root.components().collect(); + let mut i = 0; + while i < megazord_components.len() + && i < crate_root_components.len() + && megazord_components[i] == crate_root_components[i] + { + i += 1; + } + let mut relpath_components = vec![".."; megazord_components.len() - i]; + for component in crate_root_components.iter().skip(i) { + relpath_components.push(component.as_str()); + } + + let mut toml = TomlFile::open(&megazord_root.join("Cargo.toml"))?; + toml["dependencies"][crate_name]["path"] = relpath_components.join("/").into(); + toml.write() +} + +pub fn update_uniffi_toml( + crate_root: &Utf8Path, + bindings_name: &str, + values: [(&str, Value); N], +) -> Result<()> { + let mut toml = TomlFile::open(&crate_root.join("uniffi.toml"))?; + if !toml.contains_key("bindings") { + let mut table = Table::new(); + table.set_implicit(true); + toml["bindings"] = toml_edit::Item::Table(table); + } + toml["bindings"][bindings_name] = toml_edit::Item::Table(Table::from_iter(values)); + toml.write() +} diff --git a/tools/start-bindings/templates/build.gradle b/tools/start-bindings/templates/build.gradle new file mode 100644 index 000000000..77aafa0ee --- /dev/null +++ b/tools/start-bindings/templates/build.gradle @@ -0,0 +1,10 @@ +apply from: "$rootDir/build-scripts/component-common.gradle" +apply from: "$rootDir/publish.gradle" + +android { + namespace 'org.mozilla.appservices.{{ crate_name }}' +} + +ext.configureUniFFIBindgen("{{ crate_name }}") +ext.dependsOnTheMegazord() +ext.configurePublish() diff --git a/tools/start-bindings/templates/buildconfig.android.fragment b/tools/start-bindings/templates/buildconfig.android.fragment new file mode 100644 index 000000000..762cfdcff --- /dev/null +++ b/tools/start-bindings/templates/buildconfig.android.fragment @@ -0,0 +1,7 @@ + {{ crate_name }}: + path: {{ android_root }} + artifactId: {{ crate_name }} + publications: + - name: {{ crate_name }} + type: aar + description: {{ description }}