mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4144 from vitorpamplona/claude/elegant-lovelace-3qfhpj
Add 32-bit ABI support (armeabi-v7a, x86) to Arti build
This commit is contained in:
@@ -121,6 +121,12 @@ 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
|
||||
> 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 =
|
||||
@@ -281,7 +289,7 @@ android {
|
||||
abi {
|
||||
isEnable = !disableAbiSplits
|
||||
reset()
|
||||
include("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
|
||||
include(*shippedAbis.toTypedArray())
|
||||
isUniversalApk = !disableUniversalApk
|
||||
}
|
||||
}
|
||||
@@ -344,8 +352,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 ->
|
||||
@@ -360,6 +369,88 @@ 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 for that architecture."
|
||||
|
||||
val jniLibs = file("src/main/jniLibs")
|
||||
val abis = shippedAbis
|
||||
// Per ABI: the Rust target triple (so a failure names the exact build
|
||||
// command) and the ELF identity the library must have — 32/64-bit class
|
||||
// (header byte 4) and e_machine (bytes 18-19, little-endian on every
|
||||
// Android ABI we ship). Existence alone is not enough: a truncated file,
|
||||
// an empty placeholder, or arm64's .so copied into x86/ all load as
|
||||
// nothing on device, which is the same silent dead Tor this task exists
|
||||
// to prevent — and unlike a missing file, those look fine in git.
|
||||
val expected =
|
||||
mapOf(
|
||||
"arm64-v8a" to Triple("aarch64-linux-android", 2, 0xB7),
|
||||
"x86_64" to Triple("x86_64-linux-android", 2, 0x3E),
|
||||
"armeabi-v7a" to Triple("armv7-linux-androideabi", 1, 0x28),
|
||||
"x86" to Triple("i686-linux-android", 1, 0x03),
|
||||
)
|
||||
|
||||
doLast {
|
||||
val bitness = mapOf(1 to "32-bit", 2 to "64-bit")
|
||||
val problems = mutableListOf<Pair<String, String>>()
|
||||
|
||||
abis.forEach { abi ->
|
||||
val lib = File(jniLibs, "$abi/libarti_android.so")
|
||||
val want = expected[abi]
|
||||
val header = ByteArray(20)
|
||||
val read = if (lib.isFile) lib.inputStream().use { it.read(header) } else -1
|
||||
|
||||
val problem =
|
||||
when {
|
||||
!lib.isFile -> "no libarti_android.so"
|
||||
want == null -> "no expected ELF identity recorded for this ABI"
|
||||
read < header.size ||
|
||||
header[0] != 0x7F.toByte() ||
|
||||
header[1] != 'E'.code.toByte() ||
|
||||
header[2] != 'L'.code.toByte() ||
|
||||
header[3] != 'F'.code.toByte() -> "not an ELF file (truncated or corrupt)"
|
||||
header[4].toInt() != want.second ->
|
||||
"${bitness[header[4].toInt()] ?: "unknown-class"} ELF, expected ${bitness[want.second]}"
|
||||
else -> {
|
||||
val machine = (header[18].toInt() and 0xFF) or ((header[19].toInt() and 0xFF) shl 8)
|
||||
if (machine != want.third) {
|
||||
"built for ELF machine 0x%02x, expected 0x%02x".format(machine, want.third)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (problem != null) problems += abi to problem
|
||||
}
|
||||
|
||||
if (problems.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("libarti_android.so is missing or wrong for ${problems.size} ABI split(s):")
|
||||
problems.forEach { (abi, problem) -> appendLine(" $abi: $problem") }
|
||||
appendLine("Those APK splits would install with Tor permanently unavailable.")
|
||||
appendLine("Rebuild them (tools/arti-build/README.md):")
|
||||
problems.forEach { (abi, _) ->
|
||||
val triple = expected[abi]?.first ?: "<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.
+52
-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)) |
|
||||
|
||||
@@ -89,8 +89,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
|
||||
@@ -108,8 +109,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
|
||||
@@ -138,12 +143,19 @@ 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
|
||||
|
||||
# Print the jniLibs ABI dirs a given invocation would write, then exit
|
||||
./build-arti.sh --print-abis --release # -> arm64-v8a
|
||||
|
||||
# Clean rebuild from scratch
|
||||
./build-arti.sh --clean
|
||||
```
|
||||
@@ -153,7 +165,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
|
||||
@@ -162,10 +174,34 @@ 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`. (`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
|
||||
it just rebuilt.) The `verifyArtiAbis` Gradle task, wired into `preBuild`, fails
|
||||
the build when an ABI split has no `libarti_android.so` **or** has one that is
|
||||
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
|
||||
missing one it looks fine in `git status`.
|
||||
|
||||
## Verifying 16KB page alignment
|
||||
|
||||
Google Play requires 16KB page-aligned native libraries. Verify with:
|
||||
@@ -178,6 +214,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
|
||||
@@ -326,7 +367,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,15 +4,26 @@
|
||||
#
|
||||
# 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
|
||||
# ./build-arti.sh --print-abis # print the jniLibs ABI dirs this invocation
|
||||
# # would write (honours --release/--target=),
|
||||
# # then exit. This is how verify-reproducible.sh
|
||||
# # learns the ABI list instead of keeping its
|
||||
# # own copy of it.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -56,26 +67,42 @@ 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=""
|
||||
PRINT_ABIS=false
|
||||
|
||||
# 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=}" ;;
|
||||
--print-abis) PRINT_ABIS=true ;;
|
||||
--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] [--print-abis] [--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 +281,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
|
||||
}
|
||||
|
||||
@@ -395,6 +426,16 @@ verify_ndk_stamp() {
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
# Answer --print-abis before any other output, so the caller gets exactly the
|
||||
# ABI directory names on stdout and nothing else. Runs here rather than in the
|
||||
# argument loop because abi_dir_for is not defined yet at that point.
|
||||
if [ "$PRINT_ABIS" = true ]; then
|
||||
for target in "${TARGETS[@]}"; do
|
||||
abi_dir_for "$target"
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Arti Android Build — version $ARTI_VERSION${NC}"
|
||||
|
||||
# --regen-lock only needs git + cargo, not the NDK/cargo-ndk toolchain.
|
||||
|
||||
@@ -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
|
||||
@@ -33,10 +35,14 @@ sha256() {
|
||||
# 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"
|
||||
for arg in ${PASSTHRU[@]+"${PASSTHRU[@]}"}; do
|
||||
[ "$arg" = "--release" ] && ABIS="arm64-v8a"
|
||||
done
|
||||
#
|
||||
# Asked of build-arti.sh with the very flags it is about to receive, rather than
|
||||
# kept as a second copy of the ABI list here. A local copy would drift the moment
|
||||
# an ABI is added: this script would rebuild it twice, hash neither, and still
|
||||
# print ✅ REPRODUCIBLE — the same false pass the paragraph above describes.
|
||||
# Under `set -e` a failing --print-abis (e.g. an unknown triple) aborts here.
|
||||
ABIS="$("$SCRIPT_DIR/build-arti.sh" --print-abis ${PASSTHRU[@]+"${PASSTHRU[@]}"} | tr '\n' ' ')"
|
||||
ABIS="${ABIS% }"
|
||||
|
||||
# sha256 of each built .so, keyed by ABI dir (relative paths → stable keys).
|
||||
hashes() {
|
||||
|
||||
Reference in New Issue
Block a user