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.
This commit is contained in:
Родитель
7aafd5efa6
Коммит
c35705a06f
|
@ -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", "--"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -49,6 +49,7 @@ members = [
|
|||
"megazords/ios-rust/focus",
|
||||
"tools/protobuf-gen",
|
||||
"tools/embedded-uniffi-bindgen",
|
||||
"tools/start-bindings",
|
||||
"automation/swift-components-docs",
|
||||
|
||||
"examples/*/",
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 <your_crate_name>` 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 <your_crate_name>` to `./megazords/ios-rust/src/lib.rs`.
|
||||
|
||||
Run `cargo check -p <your_crate_name>` 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/<your_crate_name>/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 <your_crate_name> <component_description>` 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/<your_crate_name>/uniffi.toml` with the
|
||||
following contents:
|
||||
|
||||
```toml
|
||||
[bindings.kotlin]
|
||||
package_name = "mozilla.appservices.<your_crate_name>"
|
||||
```
|
||||
|
||||
Create a file `./components/<your_crate_name>/android/src/main/AndroidManifest.xml`
|
||||
with the following contents:
|
||||
|
||||
```xml
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.mozilla.appservices.<your_crate_name>" />
|
||||
```
|
||||
|
||||
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 <your_crate_name>: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/<your_crate_name>/`
|
||||
|
||||
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/<your_crate_name>/`.
|
||||
|
||||
So you would end up with a directory structure something like this:
|
||||
You will end up with a directory structure something like this:
|
||||
|
||||
* `components/<your_crate_name>/`
|
||||
* `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/<your_crate_name>/`
|
||||
* Hand-written Kotlin code here.
|
||||
* `test/java/mozilla/appservices/<your_crate_name>/`
|
||||
* Kotlin test-cases here.
|
||||
|
||||
Run your component's Kotlin tests with `./gradlew <your_crate_name>: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/<your_crate_name>/`
|
||||
|
||||
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/<your_crate_name>/`.
|
||||
|
||||
You can run the tests with `./gradlew <your_crate_name>:test`
|
||||
|
||||
## The iOS Bindings
|
||||
|
||||
* Run the `start-bindings ios <your_crate_name>` command to auto-generate the initial code
|
||||
* Run `start-bindings ios-focus <your_crate_name>` 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/<your_crate_name>/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/<your_crate_name>/`.
|
||||
|
||||
So you would end up with a directory structure something like this:
|
||||
You will end up with a directory structure something like this:
|
||||
|
||||
* `components/<your_crate_name>/`
|
||||
* `Cargo.toml`
|
||||
|
@ -124,31 +97,20 @@ So you would end up with a directory structure something like this:
|
|||
* `src/`
|
||||
* Rust code here.
|
||||
* `ios/`
|
||||
* `<your_crate_name>/`
|
||||
* 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 = "<crate_name>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/<your_crate_name>/`.
|
||||
|
||||
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.
|
||||
|
|
|
@ -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/<component_name>/`
|
||||
* `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/<component_name>/`
|
||||
* `Lib<ComponentName>FFI.kt` (low-level bindings to the C-style FFI)
|
||||
* Higher-level hand-written Kotlin that wraps the FFI.
|
||||
* `ios/`
|
||||
* `<component_name>/`
|
||||
* `Rust<ComponentName>API.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 `Lib<ComponentName>FFI.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 `Rust<ComponentName>API.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/<component_name>/`
|
||||
* `Cargo.toml`
|
||||
* `uniffi.toml`
|
||||
* `src/`
|
||||
* `<component_name>.udl` (abstract interface definition)
|
||||
* Rust code here.
|
||||
* `android/`
|
||||
* `build.gradle`
|
||||
* `src/`
|
||||
* `main/`
|
||||
* `AndroidManifest.xml`
|
||||
* `java/mozilla/appservices/<component_name>/`
|
||||
* Optional hand-written Kotlin code here.
|
||||
* `ios/`
|
||||
* `<component_name>/`
|
||||
* 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/<component_name>.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/<component_name>.udl
|
||||
```
|
||||
|
||||
If this succeeds, it will generate a file `./src/<component_name>.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 `<component_name>_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 `<component_name>_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<ExampleStruct>`.
|
||||
|
||||
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/<component_name>_msg_types.proto`.
|
||||
* Delete `./src/mozilla.appservices.<component_name>.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 <component_name>: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 <component_name>: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 `Rust<ComponentName>API.h`,
|
||||
replacing them with the UniFFI-generated header file name `<component_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
|
||||
`Rust<ComponentName>API.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/<ComponentName>` 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)
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
|
@ -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 <your_crate_name>: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<str>, 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<bool> {
|
||||
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 =
|
||||
"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"/>\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,
|
||||
}
|
|
@ -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<Self> {
|
||||
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<Utf8PathBuf> {
|
||||
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}"),
|
||||
}
|
||||
}
|
|
@ -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 <crate>` 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(())
|
||||
}
|
|
@ -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};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<Self> {
|
||||
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<const N: usize>(
|
||||
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()
|
||||
}
|
|
@ -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()
|
|
@ -0,0 +1,7 @@
|
|||
{{ crate_name }}:
|
||||
path: {{ android_root }}
|
||||
artifactId: {{ crate_name }}
|
||||
publications:
|
||||
- name: {{ crate_name }}
|
||||
type: aar
|
||||
description: {{ description }}
|
Загрузка…
Ссылка в новой задаче