Files
amethyst/tools/zxing-cpp-build/verify-reproducible.sh
T
Claude ba46499ab1 build(qr): build libzxingcpp_android.so ourselves, reproducibly
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
2026-09-17 15:57:09 +00:00

77 lines
2.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Verify that libzxingcpp_android.so builds reproducibly.
#
# Builds the native library twice from a clean state into scratch directories
# and confirms the two outputs are byte-for-byte identical, then compares them
# against what is committed under jniLibs. Both builds compile in the canonical
# path, so a match here means any checkout -- ours, F-Droid's, an auditor's --
# produces the same bytes. See README.md -> "Reproducible builds".
#
# Usage:
# ./verify-reproducible.sh # every ABI
# ./verify-reproducible.sh --abi arm64-v8a # one ABI (faster)
#
# Exit 0 = reproducible and matching the commit, exit 1 = they differ.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
JNILIBS="$PROJECT_ROOT/amethyst/src/main/jniLibs"
LIB_NAME="libzxingcpp_android.so"
PASSTHRU=("$@")
# Only the ABIs this run actually rebuilds. Hashing the whole tree would let an
# untouched ABI hash identically in both runs and report the lot reproducible --
# the same trap tools/arti-build hit and documents.
ABIS=(arm64-v8a armeabi-v7a x86 x86_64)
for ((i = 0; i < ${#PASSTHRU[@]}; i++)); do
[ "${PASSTHRU[$i]}" = "--abi" ] && ABIS=("${PASSTHRU[$((i + 1))]}")
done
sha256() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi
}
hashes_in() {
local dir="$1" abi
( cd "$dir" && for abi in "${ABIS[@]}"; do
[ -f "$abi/$LIB_NAME" ] && sha256 "$abi/$LIB_NAME"
done )
}
RUN_A="$(mktemp -d -t zxingcpp-verify-a.XXXXXX)"
RUN_B="$(mktemp -d -t zxingcpp-verify-b.XXXXXX)"
trap 'rm -rf "$RUN_A" "$RUN_B"' EXIT
printf '==> Build 1 of 2\n'
"$SCRIPT_DIR/build-zxingcpp.sh" --out "$RUN_A" ${PASSTHRU[@]+"${PASSTHRU[@]}"} >/dev/null
printf '==> Build 2 of 2\n'
"$SCRIPT_DIR/build-zxingcpp.sh" --out "$RUN_B" ${PASSTHRU[@]+"${PASSTHRU[@]}"} >/dev/null
A="$(hashes_in "$RUN_A")"
B="$(hashes_in "$RUN_B")"
if [ "$A" != "$B" ]; then
printf '\nNOT REPRODUCIBLE -- the two builds differ:\n'
diff <(printf '%s\n' "$A") <(printf '%s\n' "$B") || true
exit 1
fi
printf '\nReproducible: two builds produced identical bytes.\n'
printf '%s\n' "$A" | sed 's/^/ /'
# A reproducible build that does not match the commit is still a problem: it
# means the shipped library came from something other than this tag.
COMMITTED="$(hashes_in "$JNILIBS")"
if [ "$COMMITTED" != "$A" ]; then
printf '\nWARNING: the committed libraries do NOT match this build.\n'
diff <(printf '%s\n' "$COMMITTED") <(printf '%s\n' "$A") || true
printf '\nRun build-zxingcpp.sh and commit the result.\n'
exit 1
fi
printf '\nCommitted libraries match.\n'