build(qr): guard libzxingcpp_android.so like libarti_android.so

PR #4146 committed libzxingcpp_android.so for all four ABIs but landed it
outside both native-library guards main had just built for Arti, so two
properties the QR decoder's README claims are not actually true of the
APK:

* verifyArtiAbis only ever looked for libarti_android.so. An ABI split
  missing libzxingcpp_android.so — or holding a truncated one, or arm64's
  copied into x86/ — builds and installs clean, and the scanner then fails
  to load on that architecture with nothing in the build to catch it.
  Renamed verifyNativeAbis and driven from a map of committed libraries,
  so each one is checked on every shipped ABI and the error names the
  build command to re-run.

* keepDebugSymbols excluded only libarti_android.so, so AGP's llvm-strip
  pass rewrites the QR library on its way into the APK. That makes
  `unzip -p app.apk lib/<abi>/libzxingcpp_android.so | sha256sum` a
  function of whoever built the APK rather than of the committed bytes,
  which is exactly the comparison tools/zxing-cpp-build exists to make
  possible.

The same PR also added tools/zxing-cpp-build/ANDROID_NDK_VERSION as a
second copy of the NDK pin. c2071ee82c had just made :amethyst read
ndkVersion straight from tools/arti-build/ANDROID_NDK_VERSION rather than
duplicate it ("the two can then never drift"), and ea8326f759 deleted
verify-reproducible.sh's private copy of the ABI list for the same
reason. build-zxingcpp.sh now reads that one file too and the copy is
gone, so a bump moves both native builds and the strip toolchain
together.

Docs follow: BUILDING.md still described Arti as the only committed .so
and named verifyArtiAbis, as did tools/arti-build/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134jvyriixNTHST4WRbbqbX
This commit is contained in:
Claude
2026-09-18 22:03:47 +00:00
parent 3737f6dad6
commit f70205a5d4
6 changed files with 138 additions and 85 deletions
+31 -21
View File
@@ -89,8 +89,8 @@ cd amethyst
## Generated & vendored artifacts ## Generated & vendored artifacts
Two build inputs are **generated by tools but committed to the repo**, so a Three build inputs are **generated by tools but committed to the repo**, so a
normal build or release does **not** run either — Gradle just consumes the normal build or release does **not** run any of them — Gradle just consumes the
checked-in output. You only regenerate them under the specific conditions below, checked-in output. You only regenerate them under the specific conditions below,
and each has its own guide: and each has its own guide:
@@ -98,34 +98,44 @@ and each has its own guide:
|---|---|---|---| |---|---|---|---|
| **Material Symbols subset font** | `commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` | | **Material Symbols subset font** | `commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` |
| **Arti (Tor) native libs** | `amethyst/src/main/jniLibs/*.so` | You update the pinned Arti version, change the JNI wrapper, or want to reproduce the binaries | [`tools/arti-build/README.md`](tools/arti-build/README.md) | | **Arti (Tor) native libs** | `amethyst/src/main/jniLibs/*.so` | You update the pinned Arti version, change the JNI wrapper, or want to reproduce the binaries | [`tools/arti-build/README.md`](tools/arti-build/README.md) |
| **zxing-cpp (QR decoder) native libs** | `amethyst/src/main/jniLibs/*/libzxingcpp_android.so` | You bump `ZXING_CPP_VERSION`, change the JNI wrapper, or want to reproduce the binaries | [`tools/zxing-cpp-build/README.md`](tools/zxing-cpp-build/README.md) |
> **Material Symbols is mandatory after icon changes.** The bundled font is a > **Material Symbols is mandatory after icon changes.** The bundled font is a
> ~210-glyph subset; a new codepoint that isn't in it renders as tofu (□) at > ~210-glyph subset; a new codepoint that isn't in it renders as tofu (□) at
> runtime. Regenerate and commit the `.ttf` alongside the `MaterialSymbols.kt` > runtime. Regenerate and commit the `.ttf` alongside the `MaterialSymbols.kt`
> change. Reusing an existing codepoint needs no regeneration. > change. Reusing an existing codepoint needs no regeneration.
Both tools have their own prerequisites (`fonttools`/`brotli` for the font; a Each tool has its own prerequisites (`fonttools`/`brotli` for the font; a Rust
Rust toolchain + the exact Android NDK revision pinned in toolchain for Arti; `cmake` + `ninja` for zxing-cpp; and, for both native
`tools/arti-build/ANDROID_NDK_VERSION` for Arti) documented in their READMEs — builds, the exact Android NDK revision pinned in
they are **not** required to build Amethyst from the committed sources. `tools/arti-build/ANDROID_NDK_VERSION`) documented in their READMEs — none of
them are required to build Amethyst from the committed sources.
The NDK half of that Arti pin does reach the ordinary Android build, though. That NDK pin is a single file for the whole repo, not a copy per tool:
AGP strips every native library it packages with the NDK's `llvm-strip`, so `build-arti.sh`, `build-zxingcpp.sh` and `:amethyst`'s `ndkVersion` all read it,
`:amethyst` sets `ndkVersion` from `ANDROID_NDK_VERSION` — one revision for the so bumping it moves every native build and the packaging toolchain together and
libraries we build and the ones we merge from dependencies. `libarti_android.so` they cannot drift apart. (It lives under `tools/arti-build/` for history; it is
is then excluded from that strip step (`packaging.jniLibs.keepDebugSymbols`): not Arti's alone.)
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` The NDK half of that pin reaches the ordinary Android build. AGP strips every
inside an APK be compared byte-for-byte against the committed, independently native library it packages with the NDK's `llvm-strip`, so `:amethyst` sets
reproducible one. The Rust toolchain stays irrelevant either way; Studio/AGP `ndkVersion` from `ANDROID_NDK_VERSION` — one revision for the libraries we
fetches the pinned NDK on demand, or pre-install it with build and the ones we merge from dependencies. Both of our own libraries are
then excluded from that strip step (`packaging.jniLibs.keepDebugSymbols`):
they are already stripped by the pinned toolchain, 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)"`. `sdkmanager "ndk;$(cat tools/arti-build/ANDROID_NDK_VERSION)"`.
> **One Arti library per ABI split.** The APK is split four ways (`arm64-v8a`, > **Every ABI split needs its own copy of each library.** The APK is split four
> `x86_64`, `armeabi-v7a`, `x86`) and every split needs its own > ways (`arm64-v8a`, `x86_64`, `armeabi-v7a`, `x86`). A split missing
> `libarti_android.so`; a split without one installs and runs with Tor silently > `libarti_android.so` installs and runs with Tor silently unavailable; one
> unavailable. The `verifyArtiAbis` Gradle task fails the build if the two lists > missing `libzxingcpp_android.so` installs with the QR scanner broken. The
> drift, and names the `build-arti.sh --target=…` to run. > `verifyNativeAbis` Gradle task fails the build if a library's ABI list drifts
> from the split list, and names the `build-arti.sh --target=…` /
> `build-zxingcpp.sh --abi …` to run — and rejects a file that is not an ELF
> built for that architecture, which a missing-file check would pass.
--- ---
+85 -52
View File
@@ -67,14 +67,27 @@ afterEvaluate {
} }
} }
// Every ABI we split the APK for, and therefore every ABI that needs its own // Every ABI we split the APK for, and therefore every ABI that needs its own copy of each
// libarti_android.so under src/main/jniLibs/ (see tools/arti-build/). The two // library we build and commit ourselves under src/main/jniLibs/. The lists drifted once: the
// lists drifted once: the splits shipped four ABIs while Arti was built for two, // splits shipped four ABIs while Arti was built for two, so the armeabi-v7a and x86 APKs
// so the armeabi-v7a and x86 APKs installed and ran with the dependencies' native // installed and ran with the dependencies' native libraries all present (secp256k1's JNI ships
// libraries all present (secp256k1's JNI ships every ABI) and Tor alone dead for // every ABI) and Tor alone dead for the life of the install. `verifyNativeAbis` below keeps
// the life of the install. `verifyArtiAbis` below keeps them in step. // them in step.
val shippedAbis = listOf("x86", "x86_64", "arm64-v8a", "armeabi-v7a") val shippedAbis = listOf("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
// The libraries that guard covers, and how to rebuild one when it is missing. Both are built
// from source by tools/ rather than pulled prebuilt, so both can go missing the same way — and
// a QR scanner that cannot load is as silently broken on that install as a dead Tor.
val committedNativeLibs =
mapOf(
"libarti_android.so" to { abi: String, triple: String ->
"./tools/arti-build/build-arti.sh --target=$triple"
},
"libzxingcpp_android.so" to { abi: String, _: String ->
"./tools/zxing-cpp-build/build-zxingcpp.sh --abi $abi"
},
)
android { android {
namespace = "com.vitorpamplona.amethyst" namespace = "com.vitorpamplona.amethyst"
compileSdk = compileSdk =
@@ -92,10 +105,14 @@ android {
// libraries") — three different APKs from the same source, which is // libraries") — three different APKs from the same source, which is
// exactly what F-Droid's rebuild verification cannot have. // exactly what F-Droid's rebuild verification cannot have.
// //
// Read straight from the Arti pin rather than copied into the version // Read straight from the pin rather than copied into the version catalog:
// catalog: the two can then never drift, and bumping ANDROID_NDK_VERSION // the two can then never drift, and bumping ANDROID_NDK_VERSION (which also
// (which also means rebuilding the .so) moves the packaging toolchain with // means rebuilding the .so files) moves the packaging toolchain with it.
// it. See tools/arti-build/README.md → "Reproducible builds". // That one file is the repo's only NDK pin -- tools/arti-build/build-arti.sh
// and tools/zxing-cpp-build/build-zxingcpp.sh read it too, so every
// committed .so is produced and stripped by the same revision. It lives
// under tools/arti-build for history; it is not Arti's alone. See
// tools/arti-build/README.md → "Reproducible builds".
ndkVersion = ndkVersion =
providers providers
.fileContents(layout.settingsDirectory.file("tools/arti-build/ANDROID_NDK_VERSION")) .fileContents(layout.settingsDirectory.file("tools/arti-build/ANDROID_NDK_VERSION"))
@@ -341,6 +358,17 @@ android {
// symbols to drop) and makes that comparison exact. Dependency .so files // symbols to drop) and makes that comparison exact. Dependency .so files
// are still stripped, with the NDK pinned by ndkVersion above. // are still stripped, with the NDK pinned by ndkVersion above.
keepDebugSymbols += "**/libarti_android.so" keepDebugSymbols += "**/libarti_android.so"
// Same guarantee for the QR decoder, for a different reason. Unlike Arti's, this
// library is *not* currently rewritten by AGP's pass -- verified by running the
// pinned NDK's `llvm-strip --strip-unneeded` over the committed file and getting
// identical bytes -- because tools/zxing-cpp-build strips it with that very same
// llvm-strip, which makes a second pass idempotent. That idempotence is a property
// of one NDK revision, though, and reading it back from the APK should not depend
// on a strip pass staying a no-op across bumps. Excluding it makes
// `unzip -p app.apk lib/<abi>/libzxingcpp_android.so | sha256sum` match
// src/main/jniLibs by construction, at no size cost.
keepDebugSymbols += "**/libzxingcpp_android.so"
} }
} }
@@ -381,20 +409,20 @@ android {
// only surfaces at System.loadLibrary time on a user's device — where // only surfaces at System.loadLibrary time on a user's device — where
// TorManager's flow swallows the UnsatisfiedLinkError and leaves the status Off // TorManager's flow swallows the UnsatisfiedLinkError and leaves the status Off
// forever. Checked at build time instead, against the same list the splits use. // forever. Checked at build time instead, against the same list the splits use.
val verifyArtiAbis = val verifyNativeAbis =
tasks.register("verifyArtiAbis") { tasks.register("verifyNativeAbis") {
group = "verification" group = "verification"
description = "Checks that every ABI in the APK splits has a libarti_android.so for that architecture." description = "Checks that every ABI in the APK splits has each committed native library, built for that architecture."
val jniLibs = file("src/main/jniLibs") val jniLibs = file("src/main/jniLibs")
val abis = shippedAbis val abis = shippedAbis
// Per ABI: the Rust target triple (so a failure names the exact build val libs = committedNativeLibs
// command) and the ELF identity the library must have — 32/64-bit class // Per ABI: the Rust target triple (so an Arti failure names the exact build command) and
// (header byte 4) and e_machine (bytes 18-19, little-endian on every // the ELF identity every library must have — 32/64-bit class (header byte 4) and
// Android ABI we ship). Existence alone is not enough: a truncated file, // e_machine (bytes 18-19, little-endian on every Android ABI we ship). Existence alone is
// an empty placeholder, or arm64's .so copied into x86/ all load as // not enough: a truncated file, an empty placeholder, or arm64's .so copied into x86/ all
// nothing on device, which is the same silent dead Tor this task exists // load as nothing on device, which is the same silent failure this task exists to prevent
// to prevent — and unlike a missing file, those look fine in git. // — and unlike a missing file, those look fine in git.
val expected = val expected =
mapOf( mapOf(
"arm64-v8a" to Triple("aarch64-linux-android", 2, 0xB7), "arm64-v8a" to Triple("aarch64-linux-android", 2, 0xB7),
@@ -405,48 +433,51 @@ val verifyArtiAbis =
doLast { doLast {
val bitness = mapOf(1 to "32-bit", 2 to "64-bit") val bitness = mapOf(1 to "32-bit", 2 to "64-bit")
val problems = mutableListOf<Pair<String, String>>() val problems = mutableListOf<Triple<String, String, String>>()
abis.forEach { abi -> libs.keys.forEach { libName ->
val lib = File(jniLibs, "$abi/libarti_android.so") abis.forEach { abi ->
val want = expected[abi] val lib = File(jniLibs, "$abi/$libName")
val header = ByteArray(20) val want = expected[abi]
val read = if (lib.isFile) lib.inputStream().use { it.read(header) } else -1 val header = ByteArray(20)
val read = if (lib.isFile) lib.inputStream().use { it.read(header) } else -1
val problem = val problem =
when { when {
!lib.isFile -> "no libarti_android.so" !lib.isFile -> "no $libName"
want == null -> "no expected ELF identity recorded for this ABI" want == null -> "no expected ELF identity recorded for this ABI"
read < header.size || read < header.size ||
header[0] != 0x7F.toByte() || header[0] != 0x7F.toByte() ||
header[1] != 'E'.code.toByte() || header[1] != 'E'.code.toByte() ||
header[2] != 'L'.code.toByte() || header[2] != 'L'.code.toByte() ||
header[3] != 'F'.code.toByte() -> "not an ELF file (truncated or corrupt)" header[3] != 'F'.code.toByte() -> "not an ELF file (truncated or corrupt)"
header[4].toInt() != want.second -> header[4].toInt() != want.second ->
"${bitness[header[4].toInt()] ?: "unknown-class"} ELF, expected ${bitness[want.second]}" "${bitness[header[4].toInt()] ?: "unknown-class"} ELF, expected ${bitness[want.second]}"
else -> { else -> {
val machine = (header[18].toInt() and 0xFF) or ((header[19].toInt() and 0xFF) shl 8) val machine = (header[18].toInt() and 0xFF) or ((header[19].toInt() and 0xFF) shl 8)
if (machine != want.third) { if (machine != want.third) {
"built for ELF machine 0x%02x, expected 0x%02x".format(machine, want.third) "built for ELF machine 0x%02x, expected 0x%02x".format(machine, want.third)
} else { } else {
null null
}
} }
} }
}
if (problem != null) problems += abi to problem if (problem != null) problems += Triple(libName, abi, problem)
}
} }
if (problems.isNotEmpty()) { if (problems.isNotEmpty()) {
throw GradleException( throw GradleException(
buildString { buildString {
appendLine("libarti_android.so is missing or wrong for ${problems.size} ABI split(s):") appendLine("Committed native libraries are missing or wrong for ${problems.size} (library, ABI split) pair(s):")
problems.forEach { (abi, problem) -> appendLine(" $abi: $problem") } problems.forEach { (libName, abi, problem) -> appendLine(" $libName / $abi: $problem") }
appendLine("Those APK splits would install with Tor permanently unavailable.") appendLine("Those APK splits would install with that library permanently unavailable.")
appendLine("Rebuild them (tools/arti-build/README.md):") appendLine("Rebuild them:")
problems.forEach { (abi, _) -> problems.forEach { (libName, abi, _) ->
val triple = expected[abi]?.first ?: "<add the Rust target for $abi>" val triple = expected[abi]?.first ?: "<add the Rust target for $abi>"
appendLine(" ./tools/arti-build/build-arti.sh --target=$triple") val rebuild = libs[libName]?.invoke(abi, triple) ?: "<no rebuild command recorded for $libName>"
appendLine(" $rebuild")
} }
append("…or drop the ABI from `shippedAbis` in amethyst/build.gradle.kts.") append("…or drop the ABI from `shippedAbis` in amethyst/build.gradle.kts.")
}, },
@@ -455,7 +486,9 @@ val verifyArtiAbis =
} }
} }
tasks.named("preBuild") { dependsOn(verifyArtiAbis) } tasks.named("preBuild") {
dependsOn(verifyNativeAbis)
}
// androidx.appfunctions-compiler runs in a per-module mode by default, // androidx.appfunctions-compiler runs in a per-module mode by default,
// emitting only the dispatcher Kotlin code. The aggregator that builds // emitting only the dispatcher Kotlin code. The aggregator that builds
+3 -2
View File
@@ -196,8 +196,9 @@ 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` `amethyst/build.gradle.kts`, `targets` in `rust-toolchain.toml`, and `TARGETS`
in `build-arti.sh`. (`verify-reproducible.sh` has no copy of its own — it asks in `build-arti.sh`. (`verify-reproducible.sh` has no copy of its own — it asks
`build-arti.sh --print-abis`, so it can never hash a different set than the one `build-arti.sh --print-abis`, so it can never hash a different set than the one
it just rebuilt.) The `verifyArtiAbis` Gradle task, wired into `preBuild`, fails it just rebuilt.) The `verifyNativeAbis` Gradle task, wired into `preBuild`, fails
the build when an ABI split has no `libarti_android.so` **or** has one that is the build when an ABI split is missing any committed native library
(`libarti_android.so`, `libzxingcpp_android.so`) **or** has one that is
not an ELF of that architecture — a truncated file or arm64's library copied not an ELF of that architecture — a truncated file or arm64's library copied
into `x86/` loads as nothing on device, exactly like a missing one, and unlike a into `x86/` loads as nothing on device, exactly like a missing one, and unlike a
missing one it looks fine in `git status`. missing one it looks fine in `git status`.
@@ -1 +0,0 @@
30.0.16248370
+5 -3
View File
@@ -30,8 +30,10 @@ verify the binaries, bump the zxing-cpp version, or change the build flags.
``` ```
Prerequisites: `git`, `cmake`, `ninja`, and the exact NDK revision in Prerequisites: `git`, `cmake`, `ninja`, and the exact NDK revision in
[`ANDROID_NDK_VERSION`](ANDROID_NDK_VERSION) — which is deliberately the same revision [`tools/arti-build/ANDROID_NDK_VERSION`](../arti-build/ANDROID_NDK_VERSION). That is the
`tools/arti-build` pins, so one NDK install serves both native builds. repo's single NDK pin, not a copy: `build-zxingcpp.sh`, `build-arti.sh` and `:amethyst`'s
`ndkVersion` all read that one file, so one NDK install serves every native build and the
three can never drift apart.
## Reproducible builds ## Reproducible builds
@@ -39,7 +41,7 @@ Five things have to be fixed, and each is:
| Source of non-determinism | Pinned by | | Source of non-determinism | Pinned by |
|---|---| |---|---|
| compiler + linker version | [`ANDROID_NDK_VERSION`](ANDROID_NDK_VERSION); `build-zxingcpp.sh` refuses any other revision | | compiler + linker version | [`tools/arti-build/ANDROID_NDK_VERSION`](../arti-build/ANDROID_NDK_VERSION); `build-zxingcpp.sh` refuses any other revision |
| upstream source | [`ZXING_CPP_VERSION`](ZXING_CPP_VERSION), cloned at that tag and nothing else | | upstream source | [`ZXING_CPP_VERSION`](ZXING_CPP_VERSION), cloned at that tag and nothing else |
| absolute paths baked into `__FILE__`, assertions, debug records | `-ffile-prefix-map` / `-fdebug-prefix-map` in [`repro-env.sh`](repro-env.sh) | | absolute paths baked into `__FILE__`, assertions, debug records | `-ffile-prefix-map` / `-fdebug-prefix-map` in [`repro-env.sh`](repro-env.sh) |
| timestamps | `SOURCE_DATE_EPOCH`, derived from the pinned tag's commit rather than from build time; `__DATE__`/`__TIME__` redacted | | timestamps | `SOURCE_DATE_EPOCH`, derived from the pinned tag's commit rather than from build time; `__DATE__`/`__TIME__` redacted |
+14 -6
View File
@@ -13,15 +13,22 @@
# ./build-zxingcpp.sh --out DIR # write .so somewhere else (verify uses this) # ./build-zxingcpp.sh --out DIR # write .so somewhere else (verify uses this)
# #
# Prerequisites: git, cmake, ninja, and the exact NDK revision in # Prerequisites: git, cmake, ninja, and the exact NDK revision in
# ANDROID_NDK_VERSION. Any other revision is refused — it would change the # tools/arti-build/ANDROID_NDK_VERSION. Any other revision is refused — it
# output bytes, which is the whole point. # would change the output bytes, which is the whole point.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ZXING_VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ZXING_CPP_VERSION")" ZXING_VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ZXING_CPP_VERSION")"
NDK_VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ANDROID_NDK_VERSION")" # One pin for the whole repo, deliberately not a copy of our own. :amethyst
# already reads this same file for `ndkVersion` (the NDK that strips whatever
# lands in src/main/jniLibs), so a second copy here could only ever be a way
# to disagree with the toolchain that packages the .so we produce — the exact
# byte-level drift the pin exists to prevent. Bumping it rebuilds both
# libarti_android.so and libzxingcpp_android.so, which is correct: they must
# be built and stripped by one NDK.
NDK_VERSION="$(tr -d '[:space:]' < "$PROJECT_ROOT/tools/arti-build/ANDROID_NDK_VERSION")"
# Canonical build path. Codegen and link ordering can key on the real build # Canonical build path. Codegen and link ordering can key on the real build
# directory even with path remapping in place, so everyone builds here or # directory even with path remapping in place, so everyone builds here or
@@ -33,9 +40,10 @@ OUTPUT_DIR="$PROJECT_ROOT/amethyst/src/main/jniLibs"
LIB_NAME="libzxingcpp_android.so" LIB_NAME="libzxingcpp_android.so"
MIN_SDK_VERSION=26 MIN_SDK_VERSION=26
# Every ABI the app splits on. Unlike Tor — an optional feature that ships on # Every ABI the app splits on. A QR scanner that does not load is a broken core
# two ABIs — a QR scanner that does not load is a broken core feature, and the # feature — the decoder this replaced was pure Java and worked everywhere — so
# decoder this replaced was pure Java and worked everywhere. # every split gets one, and :amethyst's verifyNativeAbis fails the build if one
# is missing or built for the wrong architecture.
ABIS=(arm64-v8a armeabi-v7a x86 x86_64) ABIS=(arm64-v8a armeabi-v7a x86 x86_64)
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do