mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
We were shipping a QR decoder we could not verify. io.github.zxing-cpp:android ships four .so files built by a third party on toolchains we cannot see, and nothing in this tree could check them -- while tools/arti-build holds libarti_android.so to a pinned-NDK, canonical-path, byte-for-byte reproducible standard. A QR scanner is a thing you point at a stranger's phone; there was no principled reason for the binary that parses the result to be the exempt one. tools/zxing-cpp-build mirrors tools/arti-build: the NDK revision is pinned (and is deliberately the same revision arti pins, so one install serves both), the upstream tag is pinned and cloned at that tag only, absolute paths are remapped, SOURCE_DATE_EPOCH comes from the tag's commit rather than from build time, and everyone builds at the same canonical path. Verified, not asserted: two clean builds of arm64-v8a produced identical bytes (dec4397c3e2f1e482b1905119dd4ea9e285f3420ffe4e66f38b2d22f54639dbf) and verify-reproducible.sh confirms they match what is committed. NDK discovery reads each candidate's source.properties and refuses anything but the pinned revision -- no wildcards, borrowing arti's r25b-vs-r27 lesson rather than re-learning it. After each build the script decodes the library's own .note.android.ident and fails unless the min SDK and NDK build number are what was asked for: the gate checks the input toolchain, the stamp checks the output, and only the second catches a stale CMake cache slipping a different compiler past the first. All four ABIs, unlike arti's two. Tor is optional and can be absent; a scanner that fails to load is a broken core feature, and what this replaced was pure Java that worked everywhere. Dropping the AAR means carrying the two things it supplied besides the binary: - Its Kotlin half, vendored verbatim at src/main/java/zxingcpp. The package and class name are load-bearing -- the library exports Java_zxingcpp_BarcodeReader_readYBuffer -- so it keeps both, its upstream Apache-2.0 header, and an exclusion from spotless so our MIT header is never stamped onto someone else's file. - Its consumer ProGuard rule. Without -keep class zxingcpp.**, R8 renames the class and the scanner fails to start in release builds only, with no build error and no warning. Not a size change: the published AAR's libraries are already stripped, and ours come out only marginally smaller. This buys verifiability. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134jvyriixNTHST4WRbbqbX
236 lines
9.4 KiB
Bash
Executable File
236 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Build libzxingcpp_android.so from zxing-cpp source, reproducibly.
|
|
#
|
|
# Replaces the prebuilt io.github.zxing-cpp:android AAR, whose four .so files
|
|
# were built by a third party on unknown toolchains and which nothing in this
|
|
# tree could verify. Same standard tools/arti-build holds libarti_android.so
|
|
# to: pinned NDK, pinned source, canonical build path, verifiable bytes.
|
|
#
|
|
# Usage:
|
|
# ./build-zxingcpp.sh # every ABI
|
|
# ./build-zxingcpp.sh --abi arm64-v8a # one ABI (faster)
|
|
# ./build-zxingcpp.sh --out DIR # write .so somewhere else (verify uses this)
|
|
#
|
|
# Prerequisites: git, cmake, ninja, and the exact NDK revision in
|
|
# ANDROID_NDK_VERSION. Any other revision is refused — it would change the
|
|
# output bytes, which is the whole point.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
ZXING_VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ZXING_CPP_VERSION")"
|
|
NDK_VERSION="$(tr -d '[:space:]' < "$SCRIPT_DIR/ANDROID_NDK_VERSION")"
|
|
|
|
# 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
|
|
# nobody's bytes match. Overriding it changes the output; only do that if you
|
|
# do not care about matching the published .so.
|
|
BUILD_ROOT="${ZXING_REPRO_DIR:-/tmp/amethyst-zxingcpp-build}"
|
|
SOURCE_DIR="$BUILD_ROOT/.zxing-cpp-source"
|
|
OUTPUT_DIR="$PROJECT_ROOT/amethyst/src/main/jniLibs"
|
|
LIB_NAME="libzxingcpp_android.so"
|
|
MIN_SDK_VERSION=26
|
|
|
|
# Every ABI the app splits on. Unlike Tor — an optional feature that ships on
|
|
# two ABIs — a QR scanner that does not load is a broken core feature, and the
|
|
# decoder this replaced was pure Java and worked everywhere.
|
|
ABIS=(arm64-v8a armeabi-v7a x86 x86_64)
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--abi) ABIS=("$2"); shift 2 ;;
|
|
--out) OUTPUT_DIR="$2"; shift 2 ;;
|
|
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
info() { printf '\033[0;34m==>\033[0m %s\n' "$1"; }
|
|
ok() { printf '\033[0;32m ok\033[0m %s\n' "$1"; }
|
|
fail() { printf '\033[0;31merror:\033[0m %s\n' "$1" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NDK: find the pinned revision, refuse anything else
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ndk_revision() {
|
|
sed -n 's/^Pkg\.Revision *= *//p' "$1/source.properties" 2>/dev/null | tr -d '[:space:]' || true
|
|
}
|
|
|
|
find_ndk() {
|
|
# No wildcards. An exported ANDROID_NDK_HOME is only a hint: CI images and
|
|
# IDE installs routinely point it at a bundled NDK that is not ours, so each
|
|
# candidate is checked against source.properties and skipped when it does
|
|
# not match. Picking "some NDK" is exactly how arti's committed binaries
|
|
# ended up built by r25b while its docs asked for r27.
|
|
local candidate revision rejected=""
|
|
for candidate in \
|
|
"${ANDROID_NDK_HOME:-}" \
|
|
"${ANDROID_NDK_ROOT:-}" \
|
|
"${ANDROID_HOME:-}/ndk/$NDK_VERSION" \
|
|
"${ANDROID_SDK_ROOT:-}/ndk/$NDK_VERSION" \
|
|
"${HOME:-}/Android/Sdk/ndk/$NDK_VERSION" \
|
|
"${HOME:-}/Library/Android/sdk/ndk/$NDK_VERSION" \
|
|
"/usr/local/lib/android/sdk/ndk/$NDK_VERSION"; do
|
|
[ -n "$candidate" ] || continue
|
|
[ -d "$candidate" ] || continue
|
|
revision="$(ndk_revision "$candidate")"
|
|
if [ "$revision" = "$NDK_VERSION" ]; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
[ -n "$revision" ] && rejected="$rejected\n $candidate (r$revision)"
|
|
done
|
|
|
|
printf 'error: NDK %s not found.\n' "$NDK_VERSION" >&2
|
|
[ -n "$rejected" ] && printf ' rejected:%b\n' "$rejected" >&2
|
|
printf ' install it with: sdkmanager "ndk;%s"\n' "$NDK_VERSION" >&2
|
|
return 1
|
|
}
|
|
|
|
# An ELF tool, preferring the pinned NDK's own llvm-* copy so the post-build
|
|
# checks work on hosts without binutils (notably macOS).
|
|
ndk_tool() {
|
|
local name="$1" candidate
|
|
for candidate in "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/"llvm-$name"; do
|
|
[ -x "$candidate" ] && { echo "$candidate"; return 0; }
|
|
done
|
|
command -v "$name" 2>/dev/null && return 0
|
|
return 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Source
|
|
# ---------------------------------------------------------------------------
|
|
|
|
fetch_source() {
|
|
if [ ! -d "$SOURCE_DIR/.git" ]; then
|
|
info "Cloning zxing-cpp $ZXING_VERSION"
|
|
rm -rf "$SOURCE_DIR"
|
|
git clone --depth 1 --branch "$ZXING_VERSION" \
|
|
https://github.com/zxing-cpp/zxing-cpp.git "$SOURCE_DIR" >/dev/null 2>&1 \
|
|
|| fail "could not clone zxing-cpp at $ZXING_VERSION"
|
|
else
|
|
info "Updating clone to $ZXING_VERSION"
|
|
git -C "$SOURCE_DIR" fetch --depth 1 origin tag "$ZXING_VERSION" >/dev/null 2>&1 || true
|
|
git -C "$SOURCE_DIR" checkout -q "$ZXING_VERSION"
|
|
fi
|
|
|
|
local head
|
|
head="$(git -C "$SOURCE_DIR" rev-parse HEAD)"
|
|
ok "zxing-cpp $ZXING_VERSION at $head"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build
|
|
# ---------------------------------------------------------------------------
|
|
|
|
build_abi() {
|
|
local abi="$1"
|
|
local build_dir="$BUILD_ROOT/build/$abi"
|
|
|
|
info "Building $abi"
|
|
rm -rf "$build_dir"
|
|
mkdir -p "$build_dir"
|
|
|
|
# The wrapper's own CMakeLists pulls in core/ and sets the flags upstream
|
|
# cares about (readers only, hidden visibility, --exclude-libs). Everything
|
|
# added here is either reproducibility or the release/strip settings the
|
|
# published AAR applies at packaging time rather than in CMake.
|
|
cmake -S "$SOURCE_DIR/wrappers/android/zxingcpp/src/main/cpp" -B "$build_dir" -G Ninja \
|
|
-DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
|
|
-DANDROID_ABI="$abi" \
|
|
-DANDROID_PLATFORM="android-$MIN_SDK_VERSION" \
|
|
-DANDROID_ARM_NEON=ON \
|
|
-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DZXING_WRITERS=OFF \
|
|
-DZXING_READERS=ON \
|
|
-DZXING_UNIT_TESTS=OFF \
|
|
-DCMAKE_C_FLAGS="$ZXING_REPRO_CFLAGS" \
|
|
-DCMAKE_CXX_FLAGS="$ZXING_REPRO_CFLAGS" \
|
|
-DCMAKE_SHARED_LINKER_FLAGS="$ZXING_REPRO_LDFLAGS" \
|
|
>/dev/null || fail "cmake configure failed for $abi"
|
|
|
|
cmake --build "$build_dir" --target zxingcpp_android >/dev/null \
|
|
|| fail "build failed for $abi"
|
|
|
|
local built="$build_dir/$LIB_NAME"
|
|
[ -f "$built" ] || fail "$LIB_NAME not produced for $abi"
|
|
|
|
# Strip: the published AAR ships stripped libraries (AGP strips at packaging
|
|
# time), so an unstripped one would differ from what the app used to carry
|
|
# for no benefit — debug info in a shipped .so helps nobody here.
|
|
local strip_tool
|
|
strip_tool="$(ndk_tool strip)" || fail "no strip tool found"
|
|
"$strip_tool" --strip-unneeded "$built"
|
|
|
|
mkdir -p "$OUTPUT_DIR/$abi"
|
|
cp "$built" "$OUTPUT_DIR/$abi/$LIB_NAME"
|
|
|
|
verify_abi "$abi" "$OUTPUT_DIR/$abi/$LIB_NAME"
|
|
}
|
|
|
|
# Re-check the NDK stamp in the output rather than trusting that the right NDK
|
|
# was selected: this is what catches a toolchain that was picked up despite the
|
|
# revision gate (a stale CMake cache, an overriding environment variable).
|
|
verify_abi() {
|
|
local abi="$1" lib="$2"
|
|
local readelf
|
|
readelf="$(ndk_tool readelf)" || { ok "$abi (no readelf; stamp not checked)"; return 0; }
|
|
|
|
# .note.android.ident records the min SDK as a little-endian word followed by the NDK's
|
|
# release name and build number as NUL-padded strings, e.g. `1a 00 00 00 r30 16248370`.
|
|
# readelf prints it as raw hex, so the strings are recovered from that rather than grepped
|
|
# for directly -- min SDK 26 is the byte 0x1a and never appears as the text "26".
|
|
local hex ascii
|
|
hex="$("$readelf" --notes "$lib" 2>/dev/null | sed -n 's/.*description data: *//p' | head -1)"
|
|
[ -n "$hex" ] || fail "$abi: no .note.android.ident -- not an Android library?"
|
|
|
|
ascii="$(printf '%s' "$hex" | tr -d ' ' | sed 's/../\\x&/g' | xargs -0 printf 2>/dev/null | tr -c '[:print:]' ' ')"
|
|
|
|
# The build number is everything after the last dot of the pinned revision.
|
|
local ndk_build_number="${NDK_VERSION##*.}"
|
|
printf '%s' "$ascii" | grep -q "$ndk_build_number" \
|
|
|| fail "$abi: built by the wrong NDK -- .note.android.ident has no build number $ndk_build_number (got: $ascii)"
|
|
|
|
# First word, little-endian, is the min SDK the library targets.
|
|
local min_sdk_hex min_sdk
|
|
min_sdk_hex="$(printf '%s' "$hex" | awk '{printf "%s%s%s%s", $4, $3, $2, $1}')"
|
|
min_sdk=$((16#$min_sdk_hex))
|
|
[ "$min_sdk" = "$MIN_SDK_VERSION" ] \
|
|
|| fail "$abi: targets minSdk $min_sdk, expected $MIN_SDK_VERSION"
|
|
|
|
local size
|
|
size="$(wc -c < "$lib" | tr -d '[:space:]')"
|
|
ok "$abi minSdk $min_sdk NDK $ndk_build_number ${size} bytes"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main() {
|
|
command -v cmake >/dev/null || fail "cmake not found"
|
|
command -v ninja >/dev/null || fail "ninja not found"
|
|
command -v git >/dev/null || fail "git not found"
|
|
|
|
ANDROID_NDK_HOME="$(find_ndk)" || exit 1
|
|
export ANDROID_NDK_HOME
|
|
ok "NDK $NDK_VERSION at $ANDROID_NDK_HOME"
|
|
|
|
mkdir -p "$BUILD_ROOT"
|
|
fetch_source
|
|
|
|
# shellcheck source=repro-env.sh
|
|
. "$SCRIPT_DIR/repro-env.sh"
|
|
ok "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-unset}"
|
|
|
|
for abi in "${ABIS[@]}"; do
|
|
build_abi "$abi"
|
|
done
|
|
|
|
info "Done. Libraries in $OUTPUT_DIR"
|
|
}
|
|
|
|
main "$@"
|