Skip to content

Commit 710c5ae

Browse files
committed
Distribute Android component through pre-built Maven local repository
1 parent fdbf43b commit 710c5ae

File tree

12 files changed

+204
-61
lines changed

12 files changed

+204
-61
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
/.idea
44

55
/android/verification/
6+
7+
# Ignore all generated Maven local repository files and folders
8+
/android-release-support/maven/pom.xml
9+
/android-release-support/maven/rustls/rustls-platform-verifier/**/
10+
/android-release-support/maven/rustls/rustls-platform-verifier/maven-metadata-local.xml

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
[workspace]
2-
members = ["rustls-platform-verifier"]
2+
members = [
3+
"android-release-support",
4+
"rustls-platform-verifier",
5+
]
36

47
resolver = "2"

README.md

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -49,57 +49,49 @@ component must be included in your app's build to support `rustls-platform-verif
4949
`rustls-platform-verifier` bundles the required native components in the crate, but the project must be setup to locate them
5050
automatically and correctly.
5151

52-
Firstly, create an [init script](https://docs.gradle.org/current/userguide/init_scripts.html) in your Android
53-
Gradle project, with a filename of `init.gradle`. This is generally placed in your project's root. In your project's `settings.gradle`, add these lines:
52+
Inside of your project's `build.gradle` file, add the following code and Maven repository definition. `$PATH_TO_DEPENDENT_CRATE` is
53+
the relative path to the Cargo manifest (`Cargo.toml`) of any crate in your workspace that depends on `rustls-platform-verifier` from
54+
the location of your `build.gradle` file:
5455

5556
```groovy
56-
apply from: file("./init.gradle");
57-
// Cargo automatically handles finding the downloaded crate in the correct location
58-
// for your project.
59-
def veifierProjectPath = findRustlsPlatformVerifierProject()
60-
includeBuild("${verifierProjectPath}/android/")
61-
```
62-
63-
Next, the `rustls-platform-verifier` external dependency needs to be setup. Open the `init.gradle` file and add the following:
64-
`$PATH_TO_DEPENDENT_CRATE` is the relative path to the Cargo manifest (`Cargo.toml`) of any crate in your workspace that depends on `rustls-platform-verifier`
65-
from the location of your `init.gradle` file.
66-
67-
Alternatively, you can use `cmdProcessBuilder.directory(File("PATH_TO_ROOT"))` to change the working directory instead.
68-
69-
```groovy
70-
ext.findRustlsPlatformVerifierProject = {
71-
def cmdProcessBuilder = new ProcessBuilder(new String[] { "cargo", "metadata", "--format-version", "1", "--manifest-path", "$PATH_TO_DEPENDENT_CRATE" })
72-
def dependencyInfoText = new StringBuffer()
57+
import groovy.json.JsonSlurper
58+
import groovy.transform.Memoized
59+
60+
// ...Your own script code could be here...
61+
62+
allprojects {
63+
repositories {
64+
// ... Your other repositories could be here...
65+
maven {
66+
url = findRustlsPlatformVerifierProject()
67+
metadataSources.artifact()
68+
}
69+
}
70+
}
7371
74-
def cmdProcess = cmdProcessBuilder.start()
75-
cmdProcess.consumeProcessOutput(dependencyInfoText, null)
76-
cmdProcess.waitFor()
72+
@Memoized
73+
String findRustlsPlatformVerifierProject() {
74+
def dependencyText = providers.exec {
75+
it.workingDir = new File("../")
76+
commandLine("cargo", "metadata", "--format-version", "1", "--manifest-path", "$PATH_TO_DEPENDENT_CRATE/Cargo.toml")
77+
}.standardOutput.asText.get()
7778
78-
def dependencyJson = new groovy.json.JsonSlurper().parseText(dependencyInfoText.toString())
79-
def manifestPath = file(dependencyJson.packages.find { it.name == "rustls-platform-verifier" }.manifest_path)
80-
return manifestPath.parent
79+
def dependencyJson = new JsonSlurper().parseText(dependencyText)
80+
def manifestPath = file(dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }.manifest_path)
81+
return new File(manifestPath.parentFile, "maven").path
8182
}
8283
```
8384

84-
This script can be tweaked as best suits your project, but the `cargo metadata` invocation must be included so that the Android
85-
implementation source can be located on disk.
86-
87-
If your project often updates its Android Gradle Plugin versions, you should additionally consider setting your app's project
88-
up to override `rustls-platform-verifier`'s dependency versions. This allows your app to control what versions are used and avoid
89-
conflicts. To do so, advertise a `versions.path` system property from your `settings.gradle`:
90-
85+
Then, wherever you declare your dependencies, add the following:
9186
```groovy
92-
ext.setVersionsPath = {
93-
System.setProperty("versions.path", file("your/versions/path.toml").absolutePath)
94-
}
95-
96-
setVersionsPath()
87+
implementation "rustls:rustls-platform-verifier:latest.release"
9788
```
9889

99-
Finally, sync your gradle project changes. It should pick up on the `rustls-platform-verifier` Gradle project. It should finish
100-
successfully, resulting in a `rustls` group appearing in Android Studio's project view.
101-
After this, everything should be ready to use. Future updates of `rustls-platform-verifier` won't need any maintenance beyond the
102-
expected `cargo update`.
90+
Cargo automatically handles finding the downloaded crate in the correct location for your project. It also handles updating the version when
91+
new releases of `rustls-platform-verifier` are published. If you only use published releases, no extra maintainence should be required.
92+
93+
These script snippets can be tweaked as best suits your project, but the `cargo metadata` invocation must be included so that the Android
94+
implementation part can be located on disk.
10395

10496
#### Crate initialization
10597

admin/RELEASING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# How-to release `rustls-platform-verifier`
2+
3+
This document records the steps to publish new versions of the crate since it requires non-trivial preperation and ordering
4+
that needs to be remembered due to the Android component's distribution.
5+
6+
## Steps
7+
8+
1. Update main crate'a version in `rustls-platform-verifier/Cargo.toml`, and in any additional places.
9+
2. If any non-test changes have been made to the `android` directory since the last release:
10+
1. Update Android artifact version in `android-release-support/Cargo.toml`
11+
2. Bump dependency version of the Android support crate in `rustls-platform-verifier/Cargo.toml` to match the new one
12+
3. Commit version increase changes on the release branch
13+
4. Run `ci/package_android_release.sh` in a UNIX compatible shell
14+
5. (Optional) `cargo publish -p rustls-platform-verifier-android --dry-run`
15+
6. (Optional) Inspect extracted archive to ensure the local Maven repository artifacts are present
16+
7. Publish the Android artifacts' new version: `cargo publish -p rustls-platform-verifier-android`
17+
3. Commit main crate's version increase on the release branch
18+
4. Publish the main crate's new version: `cargo publish -p rustls-platform-verifier`

android-release-support/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "rustls-platform-verifier-android"
3+
version = "0.1.0"
4+
description = "The internal JVM support component of the rustls-platform-verifier crate. You shouldn't depend on this directly."
5+
repository = "https://github.com/1Password/rustls-platform-verifier"
6+
license = "MIT OR Apache-2.0"
7+
edition = "2021"
8+
9+
# Explicitly include the Maven local repository for the Android component.
10+
# While not checked into the repository, it is generated for releases and other contexts.
11+
include = [
12+
"src/*",
13+
"maven/pom.xml",
14+
"maven/rustls/rustls-platform-verifier/**/",
15+
"maven/rustls/rustls-platform-verifier/maven-metadata-local.xml",
16+
]
17+
18+
[dependencies]

android-release-support/maven/rustls/rustls-platform-verifier/.gitkeep

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4+
<modelVersion>4.0.0</modelVersion>
5+
<groupId>rustls</groupId>
6+
<artifactId>rustls-platform-verifier</artifactId>
7+
<version>$VERSION</version>
8+
<packaging>aar</packaging>
9+
<description>The internal JVM support component of the rustls-platform-verifier Rust crate</description>
10+
</project>

android-release-support/src/lib.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! # rustls-platform-verifier-android
2+
//!
3+
//! This crate is an implementation detail of the actual [rustls-platform-verifier](https://github.com/rustls/rustls-platform-verifier) crate.
4+
//!
5+
//! It contains no Rust code and is solely intended as a convenient delivery mechanism for the supporting Kotlin code that the main crate
6+
//! requires to perform TLS certificate validation using Android's APIs.
7+
//!
8+
//! Other crates should not directly depend on this crate in any way, as nothing about it is considered stable and probably useless elsewhere.
9+
//!
10+
//! ## Details
11+
//!
12+
//! Note: Everything in this section is subject to change at any time. Semver may not be followed either.
13+
//!
14+
//! ### Why?
15+
//!
16+
//! It was the best middle ground between several tradeoffs. The important ones, in priority order, are:
17+
//! - Automatically keeping component versions in sync
18+
//! - Allowing well-tested and well-known `cargo` dependency management patterns to apply everywhere
19+
//! - Providing a smooth developer experience as an Android consumer of `rustls-platform-verifier`
20+
//!
21+
//! Firstly, what alternatives are available for distributing the component? The other two known are source distribution n some form (here, it will be through crates.io)
22+
//! and Maven Central. Starting with the first, its become infeasible due to toolchain syncing requirements. If the Android component is
23+
//! built as part of the host app's Gradle build, then it becomes subject to any Gradle or AGP incompatibilites/requirements. In practice this means
24+
//! the AGP version between this project and the main application have to match all the time. Sometimes this works, but it becomes challenging/unfeasible
25+
//! during yearly toolchain/SDK upgrades and is not maintainable long term. Note that this is the _only_ option in this section which retains compatible
26+
//! with Cargo's Git dependency patching.
27+
//!
28+
//! Next, Maven Central. This is considered the standard way of distributing public Android dependencies. There are two downsides to this
29+
//! approach: version synchronization and publishing overhead. Version syncing is the hardest part: There's not a good way to know what version
30+
//! a crate is that doesn't hurt the Cargo part of the build or damage functionality. So instead of making assumptions at runtime, we would need to do
31+
//! clunky and manual version counting with an extra error case. Less importantly, the admin overhead of Maven Central is non-zero so its good to avoid
32+
//! if possible for such a small need.
33+
//!
34+
//! It is also worth calling out a third set of much worse options: requiring users to manually download and install the Android component
35+
//! on each update, which magnifies the version syncing problem with lots of user overhead and then deleting the component outright. A rewrite
36+
//! could be done with raw JNI calls, but this would easily be 3x the size of the existing implementation and require huge amounts of `unsafe`
37+
//! to review then audit.
38+
//!
39+
//! ### The solution
40+
//!
41+
//! The final design was built to avoid the pitfalls the previous two options mentioned. To build it, we rely on CI and packaging scripts to build
42+
//! the Android component into a prebuilt AAR file before creating a release. Next, a [on-disk Maven repository](https://maven.apache.org/repositories/local.html)
43+
//! is hosted inside of this repository. Only the unchanging file structure of it is kept checked-in, to avoid churn. The remaining parts are filled in
44+
//! during the packaging/release process, before being included in `cargo package` via an `include` Cargo.toml directive. Finally, once the repository has had
45+
//! its artifacts added, this crate is published to crates.io containing it. Then, the main crate ensures its downloaded when an Android target is compiled for via
46+
//! a platform-specific dependency.
47+
//!
48+
//! On the Gradle side, we include a very small snippet of code for users to include in their `settings.gradle` file to dyanmically locate the local maven repository
49+
//! on disk automatically based off Cargo's current version of it. The script is configuration cache friendly and doesn't impact performance either. When the script
50+
//! is run, it finds the cargo-cached download of the crate and tells Gradle it can find the `rustls-platform-verifier` Android component there when it gets sourced
51+
//! into the hosting application's build tree.
52+
//!
53+
//! ### Precompiled artifacts?
54+
//!
55+
//! For some, the notion of shipping something pre-compiled with an existing source distribution might seem incorrect, or insecure. However in this specific case,
56+
//! putting aside the fact shipping Kotlin code doesn't work (see above), there are many reasons this isn't the case:
57+
//! - Shipping pre-compiled artifacts is normal in the Java ecosystem. Maven Central and other package repositories do the same thing and serve `.jar` downloads.
58+
//! - Those not using Android will never download the pre-compiled AAR file.
59+
//! - The artifacts are incredibly easy to reproduce given an identicial compilation toolchain.
60+
//! - The artifacts are not native executables, or raw `.jar` files, so they can't be accidentally executed on a host system.
61+
//!
62+
//! ## Summary
63+
//!
64+
//! In summary, the selected distribution method avoids most of the previous pitfalls while still balancing a good experience for `cargo` annd Gradle users. Some of its
65+
//! positive properties include:
66+
//! - Full compatibility with Cargo's dependency management, including Git patching[^1]
67+
//! - No version checking or synchronization required
68+
//! - Painless and harmless to integrate into an Android app's build system
69+
//! - Low maintenance for the main crate maintainers'
70+
//!
71+
//! [^1]: The Git reference being used must have the local maven repository built and checked-in first.

android/settings.gradle

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,9 @@ dependencyResolutionManagement {
1212
mavenCentral()
1313
}
1414

15-
// We use a version catalog for two reasons:
16-
// 1. Ease of dependency management
17-
// 2. Supporting having our versions overridden by a larger project including us
18-
// as a composite build. This lets versions stay in sync when they would otherwise become
19-
// incompatible. Examples of this include AGP, where an actual app might have a newer one
20-
// then this library (which doesn't need to).
21-
//
22-
// This works by first trying to read global property that a parent module could advertise
23-
// and then falling back to our local definitions. In combination, both project-local tasks
24-
// still work as intended and use as a library in a full application.
2515
versionCatalogs {
2616
libs {
27-
from(files(System.getProperty("versions.path", "gradle/libraries.versions.toml")))
17+
from(files("gradle/libraries.versions.toml"))
2818
}
2919
}
3020
}

ci/package_android_release.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
3+
# This script's purpose is to automate the build + packaging steps for the pre-compiled Android verifier component.
4+
# It works with template files and directories inside the `android-release-support/` part of the repository to setup
5+
# a Maven local repository and then add the pre-compiled AAR file into it for distribution. The results of this packaging
6+
# are then included by `cargo` when publishing `rustls-platform-verifier-android`.
7+
8+
set -euo pipefail
9+
10+
if ! type mvn > /dev/null; then
11+
echo "The maven CLI, mvn, is required to run this script."
12+
echo "Download it from: https://maven.apache.org/download.cgi"
13+
exit 1
14+
fi
15+
16+
version=$(cat android-release-support/Cargo.toml | grep -m 1 "version = " | tr -d "version= " | tr -d '"')
17+
18+
echo "Packaging v$version of the Android support component"
19+
20+
pushd ./android
21+
22+
./gradlew assembleRelease
23+
24+
popd
25+
26+
artifact_name="rustls-platform-verifier-release.aar"
27+
28+
pushd ./android-release-support
29+
30+
artifact_path="../android/rustls-platform-verifier/build/outputs/aar/$artifact_name"
31+
32+
# Ensure no prior artifacts are present
33+
git clean -dfX "./maven/"
34+
35+
cp ./pom-template.xml ./maven/pom.xml
36+
sed -i "" "s/\$VERSION/$version/" ./maven/pom.xml
37+
38+
mvn install:install-file -Dfile="$artifact_path" -Dpackaging="aar" -DpomFile="./maven/pom.xml" -DlocalRepositoryPath="./maven/"

rustls-platform-verifier/Cargo.toml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@ license = "MIT OR Apache-2.0"
99
edition = "2021"
1010
rust-version = "1.64.0"
1111

12-
exclude = [
13-
"android/.run",
14-
"android/gradle/**",
15-
"android/gradle*",
16-
"android/settings.gradle",
17-
"android/src/androidTest",
18-
]
19-
2012
[lib]
2113
name = "rustls_platform_verifier"
2214
# Note: The `cdylib` specification is for testing only. The shared library
@@ -49,6 +41,7 @@ once_cell = "1.9"
4941
webpki = { package = "rustls-webpki", version = "0.101", features = ["alloc", "std"] }
5042

5143
[target.'cfg(target_os = "android")'.dependencies]
44+
rustls-platform-verifier-android = { path = "../android-release-support", version = "0.1.0" }
5245
jni = { version = "0.19", default-features = false }
5346
webpki = { package = "rustls-webpki", version = "0.101", features = ["alloc", "std"] }
5447
once_cell = "1.9"

0 commit comments

Comments
 (0)