Merge branch 'main' into claude/elegant-lovelace-3qfhpj

main landed the other half of the Arti packaging story (#4142): :amethyst
now pins ndkVersion from ANDROID_NDK_VERSION and excludes
libarti_android.so from AGP's llvm-strip pass. Both changes touch the same
three files and compose cleanly — that exclusion is a glob, so the two new
32-bit libraries reach the APK unrewritten exactly like the existing pair,
and their byte-for-byte-vs-committed claim now covers four ABIs.

Only BUILDING.md conflicted: both sides appended a paragraph after the
"not required to build from the committed sources" note. Kept both, with
main's NDK prose first (it continues that paragraph) and the one-library-
per-ABI-split callout after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MX1p385SiQMd2Lhkbg2YKc
This commit is contained in:
Claude
2026-09-18 18:52:49 +00:00
3 changed files with 69 additions and 0 deletions
+12
View File
@@ -109,6 +109,18 @@ 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.
The NDK half of that Arti pin does reach the ordinary Android build, though.
AGP strips every native library it packages with the NDK's `llvm-strip`, so
`:amethyst` sets `ndkVersion` from `ANDROID_NDK_VERSION` — one revision for the
libraries we build and the ones we merge from dependencies. `libarti_android.so`
is then excluded from that strip step (`packaging.jniLibs.keepDebugSymbols`):
the Cargo release profile already stripped it, and llvm-strip would only rewrite
its `.comment` stamps, so skipping the pass costs no size and lets the `.so`
inside an APK be compared byte-for-byte against the committed, independently
reproducible one. The Rust toolchain stays irrelevant either way; Studio/AGP
fetches the pinned NDK on demand, or pre-install it with
`sdkmanager "ndk;$(cat tools/arti-build/ANDROID_NDK_VERSION)"`.
> **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
+39
View File
@@ -82,6 +82,29 @@ android {
.get()
.toInt()
// Packaging toolchain: AGP runs the NDK's llvm-strip over everything that
// lands in jniLibs — our committed libarti_android.so included — so the
// NDK revision is a build input for the APK, not just for whoever compiles
// Arti. Left unset it silently follows AGP's own default (28.2.13676358 on
// AGP 9.4.0), which moves with every AGP bump and is a different toolchain
// from the one that produced the .so, while a machine with no NDK at all
// packages the library unstripped ("Unable to strip the following
// libraries") — three different APKs from the same source, which is
// exactly what F-Droid's rebuild verification cannot have.
//
// Read straight from the Arti pin rather than copied into the version
// catalog: the two can then never drift, and bumping ANDROID_NDK_VERSION
// (which also means rebuilding the .so) moves the packaging toolchain with
// it. See tools/arti-build/README.md → "Reproducible builds".
ndkVersion =
providers
.fileContents(layout.settingsDirectory.file("tools/arti-build/ANDROID_NDK_VERSION"))
.asText
.orNull
?.trim()
?.takeIf { it.isNotEmpty() }
?: error("tools/arti-build/ANDROID_NDK_VERSION is missing or empty — it pins the NDK that strips src/main/jniLibs")
defaultConfig {
applicationId = "com.vitorpamplona.amethyst"
minSdk =
@@ -303,6 +326,22 @@ android {
resources {
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}", "**/libscrypt.dylib")
}
jniLibs {
// Reproducible builds, part two: ship the Arti library exactly as
// tools/arti-build produced it. Its Cargo release profile already
// strips it (no .symtab, no .debug_*), so AGP's
// `llvm-strip --strip-unneeded` pass has nothing left to remove — but it
// still rewrites the file: llvm-strip rebuilds .comment, the section that
// records the rustc / clang / lld version stamps, which measurably changes
// 273 bytes on arm64-v8a. That made the packaged bytes a function of
// whichever NDK did the stripping, so the .so in an APK could never be
// compared against the committed, independently reproducible one.
// Excluding it from the strip step costs nothing in size (there are no
// symbols to drop) and makes that comparison exact. Dependency .so files
// are still stripped, with the NDK pinned by ndkVersion above.
keepDebugSymbols += "**/libarti_android.so"
}
}
lint {
+18
View File
@@ -49,6 +49,24 @@ committed binary wasn't tampered with. **Five** things have to be fixed:
> pinned output was verified with. `cargo-ndk` only wraps the NDK, so a mismatch
> is a warning rather than an error — but it is the next thing to check if your
> rebuild does not match.
>
> **The app build reads this pin too.** `amethyst/build.gradle.kts` sets
> `ndkVersion` from `ANDROID_NDK_VERSION`, because AGP runs the NDK's
> `llvm-strip` over every native library it packages — the toolchain that strips
> a library is as much an APK input as the one that compiled it. Unset,
> `ndkVersion` follows AGP's own default (r28 on AGP 9.4.0) and moves with every
> AGP bump. So bumping this file changes what packagers need installed, not only
> what rebuilders need: bump it, rebuild the `.so`, and commit both.
>
> **`libarti_android.so` skips that strip step.** The release profile here
> already strips it — no `.symtab`, no `.debug_*` — so `llvm-strip
> --strip-unneeded` has nothing to remove and only rebuilds `.comment`, the
> section carrying the rustc/clang/lld stamps (273 bytes change on arm64-v8a).
> `packaging.jniLibs.keepDebugSymbols` in `amethyst/build.gradle.kts` therefore
> excludes it, which costs no APK size and means the library inside a built APK
> is byte-identical to the one committed in `src/main/jniLibs/`: `unzip -p
> app.apk lib/arm64-v8a/libarti_android.so | sha256sum` can be checked straight
> against the file this script reproduces.
`repro-env.sh` (sourced by both build scripts) also sets `CARGO_INCREMENTAL=0`
and a fixed `SOURCE_DATE_EPOCH` derived from the Arti tag. The size-optimized