Files
hash-wallet/tools/demo/build-apk.sh
T
jwinterm c396e94b77 Add store capture harness: record real wallet flows into screenshots + video
Drives the real app through real flows on a device while recording the screen,
then cuts ONE recording into both deliverables — store screenshots and
marketing video — so a listing can't drift out of sync with its own trailer.

integration_test/demo/
  DemoSession adds what a passing test lacks: pacing (beat/hold/typeSlowly, so
  footage is watchable rather than a strobe), timing marks reported through
  binding.reportData, and seed guards. Scenarios reuse the existing 25 page
  robots rather than reimplementing navigation:
    core_store_set     dashboard, receive, send (composed, NEVER broadcast),
                       wallets, security — the store screenshot set
    onboarding_create  welcome -> PIN -> seed -> dashboard
    restore_from_seed  restore -> syncing
    swap_and_buy       Trocador quote, stopping BEFORE creating a trade; Buy
                       skipped when no provider key, matching what reviewers see

tools/demo/
  capture.sh   orchestrates boot/record/cut/shots/reel
  marks.py     maps timing marks onto the recording; cuts, extracts frames,
               builds manifests
  reel.py      branded master; reuses such_graphics video_transitions/video_qa
               when ~/src/such-graphics is present, degrades cleanly when not
               (this repo is public and must build without it)
  build-apk.sh local APK via the CI container image

Seed safety — the demo wallet is a real wallet with real funds:
  - seed comes from Vaultwarden at capture time, never the repo
  - guardNoSeedOnScreen() fails the run on a visible mnemonic, catching both
    the known seed and the generic shape of one (so freshly generated seeds
    are caught too)
  - unavoidable seed screens are wrapped in blindfold(); those spans are
    excised from the video with 0.6s padding, and screenshots honour the same
    padding
  - marks.py refuses to build a publishable master if any clip was cut without
    marks, since blindfold spans could not have been excised

Apple App Preview gets no touch indicators, captions, cards or music, and
reel.py enforces the 15-30s window rather than emitting an out-of-spec file.
Everything else keeps taps on, drawn by SurfaceFlinger via show_touches rather
than composited, so they look real.

Play sizing: the harness builds its own 1080x1920 AVD. The stock Medium Phone
AVD is 1080x2400 = 2.22:1 and Play rejects phone screenshots above 2:1.

Also gitignores android/app/upload-keystore.jks (and *.jks/*.keystore): the
Android workflow decodes ANDROID_KEYSTORE_BASE64 to exactly that path, so the
Play upload signing key was one `git add -A` away from being committed.
2026-07-31 15:25:29 -04:00

223 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build a Hash Bags APK locally using the SAME container image CI uses.
#
# Why a container: this repo pins Flutter 3.32.0. A newer host Flutter (3.38+)
# cannot resolve the dependency set at all -- hive_generator wants an analyzer
# that still ships the `macros` package, which newer Dart SDKs removed. Rather
# than fight that, we build inside
# ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly
# which is exactly what .github/workflows/build-android.yml uses.
#
# Usage:
# tools/demo/build-apk.sh # debug, x86_64 (emulator capture)
# BUILD_MODE=release ABI=arm64 tools/demo/build-apk.sh
# SKIP_DEPS=1 tools/demo/build-apk.sh # re-build only, deps already fetched
#
# The container runs as root (FLUTTER_ROOT lives under /root in the image), so
# the script chowns everything back to the invoking user on exit -- otherwise
# you get root-owned build artifacts scattered through your worktree.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
IMAGE="${CI_IMAGE:-ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly}"
BUILD_MODE="${BUILD_MODE:-debug}"
ABI="${ABI:-x86_64}"
SKIP_DEPS="${SKIP_DEPS:-0}"
HOST_UID="$(id -u)"
HOST_GID="$(id -g)"
case "$ABI" in
x86_64) TARGET_PLATFORM="android-x64" ;;
arm64) TARGET_PLATFORM="android-arm64" ;;
all) TARGET_PLATFORM="" ;;
*) echo "ABI must be x86_64 | arm64 | all" >&2; exit 1 ;;
esac
echo "==> image $IMAGE"
echo "==> mode $BUILD_MODE"
echo "==> abi $ABI (${TARGET_PLATFORM:-universal})"
echo "==> repo $REPO_ROOT"
docker run --rm \
--network host \
-v "$REPO_ROOT":/work \
-w /work \
-e SKIP_DEPS="$SKIP_DEPS" \
-e BUILD_MODE="$BUILD_MODE" \
-e TARGET_PLATFORM="$TARGET_PLATFORM" \
-e HOST_UID="$HOST_UID" \
-e HOST_GID="$HOST_GID" \
"$IMAGE" bash -c '
set -euo pipefail
# Chown back on ANY exit path so a failed build does not leave a root-owned
# worktree behind.
trap "chown -R ${HOST_UID}:${HOST_GID} /work 2>/dev/null || true" EXIT
git config --global --add safe.directory "*"
step() { echo; echo "########## $* ##########"; }
if [[ "$SKIP_DEPS" != "1" ]]; then
step "torch_dart prebuilt"
pushd scripts >/dev/null
if [[ ! -d torch_dart ]]; then
wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz
mkdir -p torch_dart && tar -xzf torch_dart.tar.gz -C torch_dart && rm torch_dart.tar.gz
else echo "already present"; fi
popd >/dev/null
step "reown_flutter prebuilt"
pushd scripts >/dev/null
if [[ ! -d reown_flutter ]]; then
wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz
mkdir -p reown_flutter && tar -xzf reown_flutter.tar.gz -C reown_flutter && rm reown_flutter.tar.gz
else echo "already present"; fi
popd >/dev/null
step "bitbox flutter"
# Guard on the ARTIFACT, not the directory. A half-finished checkout with no
# api.aar looks "done" by directory alone, and gradle then fails deep into
# assembleDebug with an opaque "Could not find :api:".
pushd scripts >/dev/null
if [[ -e bitbox_flutter/go/api/api.aar ]]; then
echo "api.aar already built"
else
./build_bitbox_flutter.sh
fi
popd >/dev/null
step "monero_c prebuilt .so bundle"
./scripts/prepare_moneroc.sh
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
echo "monero_c TAG: $MONERO_C_TAG"
BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG"
mkdir -p "$BUNDLE_DIR"
if [[ ! -d "$BUNDLE_DIR/x86_64-linux-android" ]]; then
pushd "$BUNDLE_DIR" >/dev/null
wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
unzip -q release-bundle.zip && rm release-bundle.zip
popd >/dev/null
else echo "bundle already extracted"; fi
step "stage jniLibs"
declare -A ABI_MAP=(
[aarch64-linux-android]=arm64-v8a
[armv7a-linux-androideabi]=armeabi-v7a
[x86_64-linux-android]=x86_64
)
for target in "${!ABI_MAP[@]}"; do
abi="${ABI_MAP[$target]}"
[[ -d "$BUNDLE_DIR/$target" ]] || { echo "SKIP $target"; continue; }
mkdir -p "android/app/src/main/jniLibs/$abi"
for coin in monero wownero; do
src="$BUNDLE_DIR/$target/lib${coin}_wallet2_api_c.so"
[[ -f "$src" ]] && cp -f "$src" "android/app/src/main/jniLibs/$abi/"
done
done
find android/app/src/main/jniLibs -name "*.so" | sort
step "reown yttrium uniffi bindings"
# The prebuilt reown_flutter tarball that build-android.yml fetches does NOT
# contain the generated Kotlin uniffi bindings. Without them, gradle dies
# deep inside assembleDebug with a wall of
# e: ...Extensions.kt: Unresolved reference "uniffi"
# which says nothing about the real cause.
#
# Upstream generates them in a SEPARATE image layer
# (scripts/android/docker/Dockerfile.reown runs build_reown_deps.sh), which
# the GitHub workflow never runs -- it assumes the runner already has them.
# That is why the Gitea mac-mini runner builds green while a clean
# replication of build-android.yml does not.
if find scripts/reown_flutter -type d -name uniffi 2>/dev/null | grep -q .; then
echo "uniffi bindings present"
elif [[ -x scripts/android/build_reown_deps.sh ]]; then
echo "uniffi bindings missing -- generating via build_reown_deps.sh"
echo "(this needs the Rust/uniffi toolchain and is slow on a cold run)"
./scripts/android/build_reown_deps.sh || {
echo
echo "FATAL: could not generate the reown uniffi bindings."
echo "Options:"
echo " 1. Build the upstream deps image: scripts/android/docker/build.sh"
echo " 2. Use an APK from the Gitea runner, which already has them:"
echo " https://git.such.software/Builds/hash-wallet/actions"
exit 1
}
else
echo "FATAL: no uniffi bindings and no build_reown_deps.sh to generate them." >&2
exit 1
fi
step "android configure (generates pubspec.yaml, manifest, app_properties)"
pushd scripts/android >/dev/null
source ./app_env.sh cakewallet
./app_config.sh
popd >/dev/null
fi
step "local throwaway signing key"
# android/app/build.gradle wires the DEBUG build type to signingConfigs.release
# (see `debug { signingConfig signingConfigs.release }`). With no key.properties
# present, `file(null)` blows up gradle configuration before a single source
# file compiles -- so even a debug build needs a keystore.
#
# CI supplies the real Play upload key from secrets. Locally we mint a
# throwaway instead: a capture/demo APK must never be signed with the upload
# key, and this keeps that impossible by construction.
if [[ ! -f android/key.properties ]]; then
keytool -genkeypair -v \
-keystore android/app/debug-capture.jks \
-storepass hashbags -keypass hashbags \
-alias hashbags-debug \
-keyalg RSA -keysize 2048 -validity 3650 \
-dname "CN=Hash Bags Capture, OU=Demo, O=Such Software LLC, C=US" 2>/dev/null
cat > android/key.properties <<EOF
storePassword=hashbags
keyPassword=hashbags
keyAlias=hashbags-debug
storeFile=debug-capture.jks
EOF
echo "minted throwaway keystore android/app/debug-capture.jks (NOT the upload key)"
else
echo "android/key.properties already present — leaving it alone"
fi
step "flutter pub get"
flutter pub get
step "generate secrets (empty defaults)"
# Real keys are injected by CI only. Locally these stay empty, which is the
# correct posture for a capture build: no live Trocador/MoonPay credentials
# end up in a demo APK.
[[ -f lib/.secrets.g.dart ]] && echo "secrets already generated" || dart run tool/generate_new_secrets.dart
step "codegen (mobx + hive adapters) -- this is the slow one"
bash model_generator.sh
step "localization"
dart run tool/generate_localization.dart
step "compile svg assets"
./compile_graphics.sh
step "build apk ($BUILD_MODE / ${TARGET_PLATFORM:-universal})"
EXTRA=""
[[ -n "$TARGET_PLATFORM" ]] && EXTRA="--target-platform=$TARGET_PLATFORM"
flutter build apk --dart-define-from-file=env.json --$BUILD_MODE $EXTRA
echo
echo "==> artifacts:"
ls -la build/app/outputs/flutter-apk/*.apk
'
# scripts/android/app_icon.sh points assets/images/app_logo.png at an ABSOLUTE
# path built from `pwd`, which inside the container is /work. That leaves a
# tracked file replaced by a symlink that dangles on the host and shows up as a
# typechange in `git status`. Put it back.
if [[ -L "$REPO_ROOT/assets/images/app_logo.png" ]]; then
git -C "$REPO_ROOT" checkout -- assets/images/app_logo.png 2>/dev/null \
&& echo "restored assets/images/app_logo.png (container symlink removed)"
fi
echo
echo "Done. APK(s) under $REPO_ROOT/build/app/outputs/flutter-apk/"