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:
Ben Dean-Kawamura 2024-09-18 11:29:34 -04:00 коммит произвёл bendk
Родитель 7aafd5efa6
Коммит c35705a06f
18 изменённых файлов: 737 добавлений и 414 удалений

Просмотреть файл

@ -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"] suggest-debug-ingestion-sizes = ["run", "-p", "suggest", "--bin", "debug_ingestion_sizes", "--features", "benchmark_api"]
relevancy = ["run", "-p", "examples-relevancy-cli", "--"] relevancy = ["run", "-p", "examples-relevancy-cli", "--"]
suggest = ["run", "-p", "examples-suggest-cli", "--"] suggest = ["run", "-p", "examples-suggest-cli", "--"]
start-bindings = ["run", "-p", "start-bindings", "--"]

Просмотреть файл

@ -4,6 +4,13 @@
# v132.0 (_2024-09-30_) # 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 🦊 ## 🦊 What's Changed 🦊
### Glean ### Glean

135
Cargo.lock сгенерированный
Просмотреть файл

@ -278,7 +278,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"itoa 1.0.9", "itoa 1.0.11",
"matchit", "matchit",
"memchr", "memchr",
"mime", "mime",
@ -382,7 +382,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash", "rustc-hash 1.1.0",
"shlex", "shlex",
"syn 1.0.109", "syn 1.0.109",
] ]
@ -1768,7 +1768,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap 2.1.0", "indexmap 2.5.0",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1869,7 +1869,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
"itoa 1.0.9", "itoa 1.0.11",
] ]
[[package]] [[package]]
@ -1931,7 +1931,7 @@ dependencies = [
"http-body", "http-body",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa 1.0.9", "itoa 1.0.11",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
@ -2005,9 +2005,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown 0.14.3",
@ -2087,9 +2087,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]] [[package]]
name = "jexl-eval" name = "jexl-eval"
@ -2157,7 +2157,7 @@ dependencies = [
"fraction", "fraction",
"getrandom", "getrandom",
"iso8601", "iso8601",
"itoa 1.0.9", "itoa 1.0.11",
"memchr", "memchr",
"num-cmp", "num-cmp",
"once_cell", "once_cell",
@ -2921,6 +2921,18 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 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]] [[package]]
name = "oorandom" name = "oorandom"
version = "11.1.3" version = "11.1.3"
@ -3661,6 +3673,49 @@ dependencies = [
"viaduct", "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]] [[package]]
name = "rkv" name = "rkv"
version = "0.19.0" version = "0.19.0"
@ -3741,6 +3796,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
@ -3915,7 +3976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [ dependencies = [
"indexmap 1.9.1", "indexmap 1.9.1",
"itoa 1.0.9", "itoa 1.0.11",
"ryu", "ryu",
"serde", "serde",
] ]
@ -3945,7 +4006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"itoa 1.0.9", "itoa 1.0.11",
"ryu", "ryu",
"serde", "serde",
] ]
@ -3969,7 +4030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
dependencies = [ dependencies = [
"indexmap 1.9.1", "indexmap 1.9.1",
"itoa 1.0.9", "itoa 1.0.11",
"ryu", "ryu",
"serde", "serde",
"unsafe-libyaml", "unsafe-libyaml",
@ -4095,6 +4156,26 @@ dependencies = [
"thiserror", "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]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -4415,7 +4496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa 1.0.9", "itoa 1.0.11",
"libc", "libc",
"num-conv", "num-conv",
"num_threads", "num_threads",
@ -4528,6 +4609,23 @@ dependencies = [
"serde", "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]] [[package]]
name = "tower" name = "tower"
version = "0.4.13" version = "0.4.13"
@ -5460,6 +5558,15 @@ version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"

Просмотреть файл

@ -49,6 +49,7 @@ members = [
"megazords/ios-rust/focus", "megazords/ios-rust/focus",
"tools/protobuf-gen", "tools/protobuf-gen",
"tools/embedded-uniffi-bindgen", "tools/embedded-uniffi-bindgen",
"tools/start-bindings",
"automation/swift-components-docs", "automation/swift-components-docs",
"examples/*/", "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), [iana-time-zone](https://github.com/strawlab/iana-time-zone),
[id-arena](https://github.com/fitzgen/id-arena), [id-arena](https://github.com/fitzgen/id-arena),
[idna](https://github.com/servo/rust-url/), [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), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes),
[ipnet](https://github.com/krisprice/ipnet), [ipnet](https://github.com/krisprice/ipnet),
[itertools](https://github.com/rust-itertools/itertools), [itertools](https://github.com/rust-itertools/itertools),

Просмотреть файл

@ -1,9 +1,6 @@
# Adding a new component to Application Services # Adding a new component to Application Services
Each component in the Application Services repository has three parts (the Rust code, This is a rapid-fire list for adding a component from scratch and generating Kotlin/Swift bindings.
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.
## The Rust Code ## The Rust Code
@ -16,14 +13,18 @@ advice on designing and structuring the actual Rust code, and follow the
introduces any new dependencies. introduces any new dependencies.
Use [UniFFI](https://mozilla.github.io/uniffi-rs/) to define how your crate's 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 API will get exposed to foreign-language bindings. Place the following in your `Cargo.toml`:
[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`:
``` ```
[dependencies] [dependencies]
uniffi = { workspace = true } 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] [build-dependencies]
uniffi = { workspace = true } 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 it to the `members` and `default-members` lists in the `Cargo.toml` at
the root of the repository. 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 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 things are configured properly. This will also have the side-effect of updating
`Cargo.lock` to contain your new crate and its dependencies. `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 Run the `start-bindings android <your_crate_name> <component_description>` command to auto-generate the initial code. Follow the directions in the output.
Kotlin- and Android-specific code. This directory will contain a gradle
project for building your Kotlin bindings.
Copy the `build.gradle` file from `./components/crashtest/android/` into You will end up with a directory structure something like this:
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:
* `components/<your_crate_name>/` * `components/<your_crate_name>/`
* `Cargo.toml` * `Cargo.toml`
@ -97,26 +54,42 @@ So you would end up with a directory structure something like this:
* `src/` * `src/`
* `main/` * `main/`
* `AndroidManifest.xml` * `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` ### Dependent crates
to confirm that this is all working correctly.
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 You will end up with a directory structure something like this:
### 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:
* `components/<your_crate_name>/` * `components/<your_crate_name>/`
* `Cargo.toml` * `Cargo.toml`
@ -124,31 +97,20 @@ So you would end up with a directory structure something like this:
* `src/` * `src/`
* Rust code here. * Rust code here.
* `ios/` * `ios/`
* `<your_crate_name>/`
* Hand-written Swift code here.
* `Generated/` * `Generated/`
* Generated Swift code will be written into this directory. * Generated Swift code will be written into this directory.
### Adding your component to the Swift Package Manager Megazord ### 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)* > *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: 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. 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.
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 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: the `./ios/` directory you created above. Add the following entries to the Group:
* Any hand-written `.swift `files for your component * Any hand-written `.swift `files for your component
> Make sure that the "Copy items if needed" option is **unchecked**, and that > Make sure that the "Copy items if needed" option is **unchecked**, and that
nothing is checked in the "Add to targets" list. 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 Use the Xcode Test Navigator to run your tests and check whether
they're passing. 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). 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. 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), [iana-time-zone](https://github.com/strawlab/iana-time-zone),
[id-arena](https://github.com/fitzgen/id-arena), [id-arena](https://github.com/fitzgen/id-arena),
[idna](https://github.com/servo/rust-url/), [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), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes),
[ipnet](https://github.com/krisprice/ipnet), [ipnet](https://github.com/krisprice/ipnet),
[itertools](https://github.com/rust-itertools/itertools), [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), [iana-time-zone](https://github.com/strawlab/iana-time-zone),
[id-arena](https://github.com/fitzgen/id-arena), [id-arena](https://github.com/fitzgen/id-arena),
[idna](https://github.com/servo/rust-url/), [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), [io-lifetimes](https://github.com/sunfishcode/io-lifetimes),
[ipnet](https://github.com/krisprice/ipnet), [ipnet](https://github.com/krisprice/ipnet),
[itertools](https://github.com/rust-itertools/itertools), [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 }}