mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(android): build Arti for all four shipped ABIs
The APK is split four ways (arm64-v8a, x86_64, armeabi-v7a, x86) and create-release.yml publishes every one of them, but libarti_android.so existed only for arm64-v8a and x86_64. The armeabi-v7a and x86 APKs therefore installed with each dependency's native library present (secp256k1's JNI ships all four ABIs) and Arti's missing: System.loadLibrary throws, TorManager's status flow swallows the error, and Tor reports Off for the life of the install. With the defaults (TorType.INTERNAL, DM relays and unknown relays routed over Tor) those relays then dial a SOCKS port nothing listens on and never connect, so those ABIs lost their DMs as well as Tor. Build Arti for armv7-linux-androideabi and i686-linux-android, and keep the lists from drifting again: - rust-toolchain.toml / build-arti.sh: all four targets by default, plus --target=<triple> to add one ABI without rewriting the .so files already committed. - verify-reproducible.sh: verifies all four, or one ABI by triple. - verifyArtiAbis (wired into preBuild): fails the build when an ABI split has no libarti_android.so, and names the build-arti.sh command that produces it. Both new binaries come from the pinned Rust 1.98.1 + NDK 30.0.16248370, export every JNI symbol, and link only libc/libm/libdl like the existing two. 16 KB page alignment stays a 64-bit concern; the 32-bit libraries link at 4 KB as their targets specify. They have not been exercised on a 32-bit device or emulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MX1p385SiQMd2Lhkbg2YKc
This commit is contained in:
@@ -109,6 +109,12 @@ Rust toolchain + the exact Android NDK revision pinned in
|
||||
`tools/arti-build/ANDROID_NDK_VERSION` for Arti) documented in their READMEs —
|
||||
they are **not** required to build Amethyst from the committed sources.
|
||||
|
||||
> **One Arti library per ABI split.** The APK is split four ways (`arm64-v8a`,
|
||||
> `x86_64`, `armeabi-v7a`, `x86`) and every split needs its own
|
||||
> `libarti_android.so`; a split without one installs and runs with Tor silently
|
||||
> unavailable. The `verifyArtiAbis` Gradle task fails the build if the two lists
|
||||
> drift, and names the `build-arti.sh --target=…` to run.
|
||||
|
||||
---
|
||||
|
||||
## Per-format build commands
|
||||
|
||||
@@ -67,6 +67,14 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
|
||||
// Every ABI we split the APK for, and therefore every ABI that needs its own
|
||||
// libarti_android.so under src/main/jniLibs/ (see tools/arti-build/). The two
|
||||
// lists drifted once: the splits shipped four ABIs while Arti was built for two,
|
||||
// so the armeabi-v7a and x86 APKs installed and ran with the dependencies' native
|
||||
// libraries all present (secp256k1's JNI ships every ABI) and Tor alone dead for
|
||||
// the life of the install. `verifyArtiAbis` below keeps them in step.
|
||||
val shippedAbis = listOf("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
|
||||
|
||||
android {
|
||||
namespace = "com.vitorpamplona.amethyst"
|
||||
compileSdk =
|
||||
@@ -258,7 +266,7 @@ android {
|
||||
abi {
|
||||
isEnable = !disableAbiSplits
|
||||
reset()
|
||||
include("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
|
||||
include(*shippedAbis.toTypedArray())
|
||||
isUniversalApk = !disableUniversalApk
|
||||
}
|
||||
}
|
||||
@@ -305,8 +313,9 @@ android {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
// Lets TorArtiNativeIntegrationTest's System.loadLibrary("arti_android")
|
||||
// find the desktop-host build of our Arti JNI shim. The Android .so
|
||||
// variants live in src/main/jniLibs/{arm64-v8a,x86_64}/ and are loaded
|
||||
// on-device — this Linux x86_64 .so is just for JVM unit-test runs.
|
||||
// variants live in src/main/jniLibs/<abi>/ — one per ABI in [shippedAbis]
|
||||
// — and are loaded on-device; this Linux x86_64 .so is just for JVM
|
||||
// unit-test runs.
|
||||
// -Pamethyst.arti.integration=true opts the (slow, network-dependent)
|
||||
// tests in; see TorArtiNativeIntegrationTest.kdoc.
|
||||
unitTests.all { test ->
|
||||
@@ -321,6 +330,49 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// Every ABI split must carry Arti, or it ships an APK that is whole except for
|
||||
// Tor. Nothing else catches that: AGP happily assembles a split out of whatever
|
||||
// .so files the dependencies provide, the APK installs and runs, and the gap
|
||||
// only surfaces at System.loadLibrary time on a user's device — where
|
||||
// TorManager's flow swallows the UnsatisfiedLinkError and leaves the status Off
|
||||
// forever. Checked at build time instead, against the same list the splits use.
|
||||
val verifyArtiAbis =
|
||||
tasks.register("verifyArtiAbis") {
|
||||
group = "verification"
|
||||
description = "Checks that every ABI in the APK splits has a libarti_android.so."
|
||||
|
||||
val jniLibs = file("src/main/jniLibs")
|
||||
val abis = shippedAbis
|
||||
// Rust target triple per ABI, so the failure says exactly what to build.
|
||||
val triples =
|
||||
mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
||||
"x86" to "i686-linux-android",
|
||||
)
|
||||
|
||||
doLast {
|
||||
val missing = abis.filter { !File(jniLibs, "$it/libarti_android.so").exists() }
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("No libarti_android.so for ABI(s): ${missing.joinToString()}.")
|
||||
appendLine("Those APK splits would install with Tor permanently unavailable.")
|
||||
appendLine("Build them (tools/arti-build/README.md):")
|
||||
missing.forEach { abi ->
|
||||
val triple = triples[abi] ?: "<add the Rust target for $abi>"
|
||||
appendLine(" ./tools/arti-build/build-arti.sh --target=$triple")
|
||||
}
|
||||
append("…or drop the ABI from `shippedAbis` in amethyst/build.gradle.kts.")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("preBuild") { dependsOn(verifyArtiAbis) }
|
||||
|
||||
// androidx.appfunctions-compiler runs in a per-module mode by default,
|
||||
// emitting only the dispatcher Kotlin code. The aggregator that builds
|
||||
// the `app_functions.xml` asset (which the system reads to discover our
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
+45
-10
@@ -8,8 +8,8 @@ JNI wrapper built directly from Arti source.
|
||||
|
||||
| | Guardian Project AAR | Custom build |
|
||||
|---|---|---|
|
||||
| **Size** | ~140MB | ~11MB |
|
||||
| **16KB pages** | No | Yes (rustc aligns Android targets to 16 KiB) |
|
||||
| **Size** | ~140MB | ~22MB for all four ABIs — 4-6MB in the APK a device installs |
|
||||
| **16KB pages** | No | Yes on the 64-bit ABIs (rustc aligns them to 16 KiB) |
|
||||
| **Stop/restart** | Broken (state file lock) | Works (TorClient persists, only SOCKS proxy stops) |
|
||||
| **Version** | Behind | Pinned to latest (see [`ARTI_VERSION`](ARTI_VERSION)) |
|
||||
|
||||
@@ -71,8 +71,9 @@ release profile in `Cargo.toml` (`lto`, `codegen-units = 1`, `strip`,
|
||||
From `tools/arti-build/`, the helper builds twice from clean and diffs the output:
|
||||
|
||||
```bash
|
||||
./verify-reproducible.sh # both ABIs (arm64-v8a + x86_64)
|
||||
./verify-reproducible.sh # all four shipped ABIs
|
||||
./verify-reproducible.sh --release # arm64-v8a only (faster)
|
||||
./verify-reproducible.sh --target=armv7-linux-androideabi # one ABI
|
||||
```
|
||||
|
||||
It prints `✅ REPRODUCIBLE` when two clean builds produce identical bytes, then
|
||||
@@ -90,8 +91,12 @@ repo is checked out.
|
||||
|
||||
2. **Android targets**
|
||||
```bash
|
||||
rustup target add aarch64-linux-android x86_64-linux-android
|
||||
rustup target add aarch64-linux-android x86_64-linux-android \
|
||||
armv7-linux-androideabi i686-linux-android
|
||||
```
|
||||
One per ABI the APK is split for. They are also listed in
|
||||
`rust-toolchain.toml`, so a first invocation of the build scripts installs
|
||||
whatever is missing.
|
||||
|
||||
3. **cargo-ndk** — the release the pinned output was verified with:
|
||||
```bash
|
||||
@@ -120,12 +125,16 @@ repo is checked out.
|
||||
```bash
|
||||
cd tools/arti-build
|
||||
|
||||
# Build for all targets (arm64 + x86_64)
|
||||
# Build every shipped ABI (arm64-v8a, x86_64, armeabi-v7a, x86)
|
||||
./build-arti.sh
|
||||
|
||||
# Build arm64 only (for release APKs)
|
||||
# Build arm64-v8a only — a fast local loop, NOT enough to cut a release:
|
||||
# the APK splits ship four ABIs and each one needs its own libarti_android.so
|
||||
./build-arti.sh --release
|
||||
|
||||
# Build a single ABI without touching the other committed .so files
|
||||
./build-arti.sh --target=armv7-linux-androideabi
|
||||
|
||||
# Clean rebuild from scratch
|
||||
./build-arti.sh --clean
|
||||
```
|
||||
@@ -135,7 +144,7 @@ The script will:
|
||||
2. Check out the version pinned in `ARTI_VERSION`
|
||||
3. Copy the JNI wrapper into the source tree
|
||||
4. Compile with `cargo-ndk` for each target architecture
|
||||
5. Output `.so` files to `amethyst/src/main/jniLibs/{arm64-v8a,x86_64}/`
|
||||
5. Output `.so` files to `amethyst/src/main/jniLibs/{arm64-v8a,x86_64,armeabi-v7a,x86}/`
|
||||
6. Verify JNI symbols are exported correctly
|
||||
|
||||
## Output
|
||||
@@ -144,10 +153,30 @@ The script will:
|
||||
amethyst/src/main/jniLibs/
|
||||
├── arm64-v8a/
|
||||
│ └── libarti_android.so (~5-6 MB)
|
||||
└── x86_64/
|
||||
└── libarti_android.so (~6-7 MB, emulator support)
|
||||
├── x86_64/
|
||||
│ └── libarti_android.so (~6-7 MB, emulator support)
|
||||
├── armeabi-v7a/
|
||||
│ └── libarti_android.so (~3-4 MB, 32-bit ARM devices)
|
||||
└── x86/
|
||||
└── libarti_android.so (~6 MB, 32-bit x86 images)
|
||||
```
|
||||
|
||||
**One per ABI split, always.** `amethyst/build.gradle.kts` splits the APK four
|
||||
ways and `create-release.yml` publishes all four, so an ABI missing from this
|
||||
tree ships an APK that is complete except for Arti: the dependencies' native
|
||||
libraries are all there (secp256k1's JNI, for one, ships every ABI), the app
|
||||
installs and runs, and Tor alone is dead for that install. `System.loadLibrary`
|
||||
throws, `TorManager`'s status flow swallows the error, and Tor reports Off
|
||||
forever. With the defaults (`TorType.INTERNAL`, DM relays and unknown relays
|
||||
routed over Tor) those relays then dial a SOCKS port nothing listens on and
|
||||
never connect — so the ABI loses its DMs too, not just Tor.
|
||||
|
||||
The ABI list therefore lives in three places that must agree: `splits.abi` in
|
||||
`amethyst/build.gradle.kts`, `targets` in `rust-toolchain.toml`, and `TARGETS`
|
||||
in `build-arti.sh`. The `verifyArtiAbis` Gradle task (wired into `preBuild`)
|
||||
fails the build when an ABI split has no `libarti_android.so`, so the drift is
|
||||
caught here rather than on a user's phone.
|
||||
|
||||
## Verifying 16KB page alignment
|
||||
|
||||
Google Play requires 16KB page-aligned native libraries. Verify with:
|
||||
@@ -160,6 +189,11 @@ The first LOAD segment alignment should be `0x4000` (16384 bytes). This comes
|
||||
from rustc's Android target spec (`max-page-size=16384`), not from the NDK, so
|
||||
it holds for every NDK revision we could build with.
|
||||
|
||||
The requirement is a 64-bit one — 16 KB pages exist only on 64-bit Android
|
||||
devices — so it applies to `arm64-v8a` and `x86_64`. The 32-bit libraries
|
||||
(`armeabi-v7a`, `x86`) link at the 4 KB alignment their targets specify
|
||||
(`0x1000`), which is correct for them and not a regression to fix.
|
||||
|
||||
## Checking which toolchain built a `.so`
|
||||
|
||||
The shipped binaries say so themselves — useful when a rebuild does not match, or
|
||||
@@ -308,7 +342,8 @@ fatal.
|
||||
|
||||
### Rust targets not installed
|
||||
```bash
|
||||
rustup target add aarch64-linux-android x86_64-linux-android
|
||||
rustup target add aarch64-linux-android x86_64-linux-android \
|
||||
armv7-linux-androideabi i686-linux-android
|
||||
```
|
||||
|
||||
### Build fails with dependency errors
|
||||
|
||||
@@ -4,14 +4,20 @@
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Rust toolchain: rustup, cargo
|
||||
# - Android targets: rustup target add aarch64-linux-android x86_64-linux-android
|
||||
# - Android targets: rustup target add aarch64-linux-android x86_64-linux-android \
|
||||
# armv7-linux-androideabi i686-linux-android
|
||||
# - cargo-ndk: cargo install cargo-ndk
|
||||
# - Android NDK: the exact revision pinned in ANDROID_NDK_VERSION
|
||||
# (sdkmanager "ndk;<revision>") — see README.md -> "Reproducible builds"
|
||||
#
|
||||
# Usage:
|
||||
# ./build-arti.sh # Build for all targets (arm64 + x86_64)
|
||||
# ./build-arti.sh --release # Build arm64 only (for release)
|
||||
# ./build-arti.sh # Build every shipped ABI (all four)
|
||||
# ./build-arti.sh --release # arm64-v8a only (faster; NOT enough to cut a
|
||||
# # release — the APK splits ship four ABIs)
|
||||
# ./build-arti.sh --target=<triple>
|
||||
# # build just this target, repeatable. Use it to
|
||||
# # add one ABI without rewriting the other .so
|
||||
# # files already committed under jniLibs/.
|
||||
# ./build-arti.sh --clean # Clean and rebuild
|
||||
#
|
||||
set -euo pipefail
|
||||
@@ -56,26 +62,40 @@ OUTPUT_DIR="$PROJECT_ROOT/amethyst/src/main/jniLibs"
|
||||
LIB_NAME="libarti_android.so"
|
||||
MIN_SDK_VERSION=26
|
||||
|
||||
# Default targets
|
||||
TARGETS=("aarch64-linux-android" "x86_64-linux-android")
|
||||
RELEASE_ONLY=false
|
||||
# Default targets: one per ABI the APK is split for (amethyst/build.gradle.kts ->
|
||||
# splits.abi). They must stay in sync — an ABI that gets an APK split but no
|
||||
# libarti_android.so produces an install where every other native library is
|
||||
# present, so the app looks healthy right up to the moment it tries to reach Tor,
|
||||
# and then fails silently for the lifetime of that install.
|
||||
TARGETS=("aarch64-linux-android" "x86_64-linux-android" "armv7-linux-androideabi" "i686-linux-android")
|
||||
CLEAN=false
|
||||
REGEN_LOCK=false
|
||||
# Collects --target= selections; replaces TARGETS wholesale once any is given.
|
||||
# A space-separated string, not an array: macOS still ships bash 3.2, where
|
||||
# `${#empty_array[@]}` under `set -u` is an unbound-variable error. Target
|
||||
# triples never contain spaces, so word splitting is exact here.
|
||||
SELECTED_TARGETS=""
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--release) RELEASE_ONLY=true; TARGETS=("aarch64-linux-android") ;;
|
||||
--release) TARGETS=("aarch64-linux-android") ;;
|
||||
--target=*) SELECTED_TARGETS="$SELECTED_TARGETS ${arg#--target=}" ;;
|
||||
--clean) CLEAN=true ;;
|
||||
# Refresh the committed Cargo.lock from the pinned Arti tag, then exit
|
||||
# (no compile — needs only git + cargo, not the NDK). Use after bumping
|
||||
# ARTI_VERSION / Cargo.toml; the normal build is --locked and will fail
|
||||
# until the lock is regenerated and committed.
|
||||
--regen-lock) REGEN_LOCK=true; CLEAN=true ;;
|
||||
--help) echo "Usage: $0 [--release] [--clean] [--regen-lock] [--help]"; exit 0 ;;
|
||||
--help) echo "Usage: $0 [--release] [--target=<triple>]... [--clean] [--regen-lock] [--help]"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$SELECTED_TARGETS" ]; then
|
||||
# shellcheck disable=SC2206 # deliberate word splitting; see SELECTED_TARGETS
|
||||
TARGETS=($SELECTED_TARGETS)
|
||||
fi
|
||||
|
||||
print_header() { echo -e "\n${BLUE}=== $1 ===${NC}"; }
|
||||
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}✗ $1${NC}"; }
|
||||
@@ -254,12 +274,16 @@ PATCH
|
||||
# ============================================================================
|
||||
|
||||
# Android ABI directory (as laid out under jniLibs/) for a Rust target triple.
|
||||
# Unknown triples are fatal rather than empty: an empty answer would make the
|
||||
# caller write "$OUTPUT_DIR/" — i.e. drop the .so loose in jniLibs/, where no ABI
|
||||
# picks it up — and both verification loops would skip it without a word.
|
||||
abi_dir_for() {
|
||||
case "$1" in
|
||||
aarch64-linux-android) echo "arm64-v8a" ;;
|
||||
x86_64-linux-android) echo "x86_64" ;;
|
||||
armv7-linux-androideabi) echo "armeabi-v7a" ;;
|
||||
i686-linux-android) echo "x86" ;;
|
||||
*) print_error "Unknown Rust target '$1' — no jniLibs ABI maps to it" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,14 @@
|
||||
channel = "1.98.1"
|
||||
profile = "minimal"
|
||||
components = ["rustc", "cargo", "rust-std"]
|
||||
# Only the two ABIs we actually ship libarti_android.so for (see jniLibs/). If
|
||||
# build-arti.sh is extended to armv7/i686, add them here too — check_prerequisites
|
||||
# also adds any missing target on the fly.
|
||||
# One per ABI the APK is split for (see amethyst/build.gradle.kts -> splits.abi):
|
||||
# every split that ships native code ships libarti_android.so too, or Tor is dead
|
||||
# on that ABI for the life of the install. The `verifyArtiAbis` Gradle task holds
|
||||
# the two lists together. check_prerequisites also adds any missing target on the
|
||||
# fly.
|
||||
targets = [
|
||||
"aarch64-linux-android",
|
||||
"x86_64-linux-android",
|
||||
"armv7-linux-androideabi",
|
||||
"i686-linux-android",
|
||||
]
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
# "Reproducible builds".
|
||||
#
|
||||
# Usage:
|
||||
# ./verify-reproducible.sh # both ABIs (arm64-v8a + x86_64)
|
||||
# ./verify-reproducible.sh # all four shipped ABIs
|
||||
# ./verify-reproducible.sh --release # arm64-v8a only (faster)
|
||||
# ./verify-reproducible.sh --target=armv7-linux-androideabi
|
||||
# # one ABI, by Rust target triple
|
||||
#
|
||||
# Prerequisites are the same as build-arti.sh (rustup, cargo-ndk, and the exact
|
||||
# Android NDK revision pinned in ANDROID_NDK_VERSION — a different revision is
|
||||
@@ -28,15 +30,33 @@ sha256() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi
|
||||
}
|
||||
|
||||
# Mirrors abi_dir_for() in build-arti.sh — kept in step with it, since a triple
|
||||
# that maps to the wrong directory here would hash a library this run never
|
||||
# rebuilt and call it reproducible.
|
||||
abi_dir_for() {
|
||||
case "$1" in
|
||||
aarch64-linux-android) echo "arm64-v8a" ;;
|
||||
x86_64-linux-android) echo "x86_64" ;;
|
||||
armv7-linux-androideabi) echo "armeabi-v7a" ;;
|
||||
i686-linux-android) echo "x86" ;;
|
||||
*) echo "Unknown Rust target '$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Only the ABIs this run actually rebuilds. Hashing everything under jniLibs/
|
||||
# (what `find` used to do) made `--release` look like it had verified the
|
||||
# x86_64 library: that build never touches it, so the untouched file hashed
|
||||
# identically in both runs and the script reported the whole tree reproducible
|
||||
# and matching the commit.
|
||||
ABIS="arm64-v8a x86_64"
|
||||
ABIS="arm64-v8a x86_64 armeabi-v7a x86"
|
||||
SELECTED=""
|
||||
for arg in ${PASSTHRU[@]+"${PASSTHRU[@]}"}; do
|
||||
[ "$arg" = "--release" ] && ABIS="arm64-v8a"
|
||||
case "$arg" in
|
||||
--release) ABIS="arm64-v8a" ;;
|
||||
--target=*) SELECTED="$SELECTED $(abi_dir_for "${arg#--target=}")" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$SELECTED" ] && ABIS="${SELECTED# }"
|
||||
|
||||
# sha256 of each built .so, keyed by ABI dir (relative paths → stable keys).
|
||||
hashes() {
|
||||
|
||||
Reference in New Issue
Block a user