🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 22m51s
🏗️ Build and Test JellyTau / Supply Chain (pull_request) Failing after 25s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 14s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m19s
The plugin versions could not be matched upward without this: both
tauri-plugin-log 2.9.0 and tauri-plugin-updater 2.10.1 require tauri
^2.10, and the tree was on 2.9.5. So the framework moves with them --
tauri 2.9.5 -> 2.11.5, tauri-build 2.5.3 -> 2.6.3, wry 0.53.5 -> 0.55.1
-- and every plugin's Rust crate and npm package is now pinned to the
same version on both sides.
That upgrade broke Android outright, and the breakage is the interesting
part.
Seven call sites in this crate reach JNI through
ndk_context::android_context(), which reads a process-global pair of
pointers. Nothing here ever set that global. `tao` did -- the windowing
layer under wry, three levels below anything this project names in
Cargo.toml. tao 0.34.5 called initialize_android_context() while starting
the activity and our code read what it left behind. tao 0.35.3 keeps the
same two pointers in a private struct and no longer publishes them.
The result, on every launch, was:
PANIC at ndk-context/src/lib.rs:72: android context was not initialized
8: ndk_context::android_context
9: jellytau_lib::run::{{closure}}
Not a crash in our code, and not a change to our code: an undocumented
side effect of a transitive dependency disappeared. Relying on someone
else to populate a global is a dependency that does not appear in
Cargo.toml and gives no warning when it goes.
src-tauri/src/android_context.rs now owns that invariant instead of
assuming it. JNI_OnLoad captures the JavaVM as the shared library loads
-- the earliest moment available, and nothing in tao, wry or tauri
defines one to collide with. The Context is resolved lazily via
ActivityThread.currentApplication() and pinned as a global reference for
the process lifetime, since ndk_context stores a bare pointer and does
not own it. It publishes the Application rather than the Activity:
SecureStorage.initialize() immediately reduces its argument to
applicationContext anyway, and an Application cannot outlive itself the
way a retained Activity would.
Restoring the global keeps all seven callers untouched. Threading a VM
and Context handle through five credential call sites would have been a
larger change with more risk, on the credential path.
Failure now degrades instead of aborting: it is logged and credentials
fall back to the encrypted-file path, which the app already supports.
Verified on a device, R8-minified, not merely compiled:
[INIT] Android JavaVM and Application published to ndk_context
Android SecureStorage initialized successfully
Android Keystore available via SecureStorage
[INIT] Using system keyring for credential storage
[CodecDetection] Detected 7 video codecs: av1,h263,h264,hevc,...
-- the real keystore path, not the fallback, and the app stays up. None
of this is reachable by CI: nothing there runs the app.
Also fixed here, both found the same way:
- `tauri android build --apk true` is now `--apk`. The CLI took a value
until 2.10; from 2.11 the stray `true` is a positional and the build
fails before starting. Three call sites in build-android.sh and one
in build-release.yml -- the latter builds the signed APK, by far the
most-downloaded artifact.
- scripts/build-android.sh ran `npm install` on its clean-build path in
a bun project, ignoring bun.lock and re-resolving the tree. That is
exactly how the plugin crate/package versions drift apart again.
scripts/check-tooling.sh now fails on any npm/yarn/pnpm invocation or
foreign lockfile, and runs in CI.
DR-222, DR-223.
138 lines
5.4 KiB
Bash
Executable File
138 lines
5.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Build Android APK
|
|
|
|
set -e
|
|
|
|
# Source Rust environment
|
|
source "$HOME/.cargo/env.fish" 2>/dev/null || source "$HOME/.cargo/env" 2>/dev/null || true
|
|
|
|
# Set Android environment variables
|
|
export ANDROID_HOME="$HOME/Android/Sdk"
|
|
export NDK_HOME="$ANDROID_HOME/ndk/$(ls "$ANDROID_HOME/ndk" | head -1)"
|
|
|
|
echo "🤖 Building Android APK..."
|
|
echo "Android SDK: $ANDROID_HOME"
|
|
echo "NDK: $NDK_HOME"
|
|
echo ""
|
|
|
|
# Parse args: build type (debug/release) and optional --clean flag.
|
|
# By default the build is INCREMENTAL — Cargo and Vite reuse their caches.
|
|
# Pass --clean (or CLEAN=1) to wipe all caches for a from-scratch build.
|
|
#
|
|
# ABI selection: by default Tauri builds all four ABIs (arm64/arm/x86/x86_64),
|
|
# which is what a distributable universal APK needs — but for an on-device test
|
|
# it means three wasted Rust compiles. Pass --device (or ABI=aarch64) to build
|
|
# only the connected device's architecture; --abi <t> targets one explicitly.
|
|
#
|
|
# Side-by-side: the `debug` build type always installs as
|
|
# com.dtourolle.jellytau.debug ("JellyTau Debug"), so it never collides with a
|
|
# real install. `release --debug` puts a *release* build — R8-minified, exactly
|
|
# what ships — into that same slot, signed with the local debug keystore. That
|
|
# is how you validate minification (R8 stripping JNI-loaded classes has broken
|
|
# release APKs here before) without the real signing key and without
|
|
# uninstalling the app you actually use.
|
|
BUILD_TYPE="debug"
|
|
CLEAN="${CLEAN:-0}"
|
|
ABI="${ABI:-}"
|
|
SIDE_BY_SIDE="${SIDE_BY_SIDE:-0}"
|
|
next_is_abi=0
|
|
for arg in "$@"; do
|
|
if [ "$next_is_abi" = "1" ]; then
|
|
ABI="$arg"
|
|
next_is_abi=0
|
|
continue
|
|
fi
|
|
case "$arg" in
|
|
--clean) CLEAN=1 ;;
|
|
--abi) next_is_abi=1 ;;
|
|
--device) ABI="device" ;;
|
|
--debug|--side-by-side) SIDE_BY_SIDE=1 ;;
|
|
debug|release) BUILD_TYPE="$arg" ;;
|
|
esac
|
|
done
|
|
|
|
# The debug build type is side-by-side unconditionally; the flag only means
|
|
# something for a release build.
|
|
if [ "$BUILD_TYPE" = "debug" ]; then
|
|
SIDE_BY_SIDE=1
|
|
fi
|
|
|
|
# Resolve --device to the attached device's Rust target triple.
|
|
if [ "$ABI" = "device" ]; then
|
|
device_abi="$(adb shell getprop ro.product.cpu.abi 2>/dev/null | tr -d '\r\n')"
|
|
case "$device_abi" in
|
|
arm64-v8a) ABI="aarch64" ;;
|
|
armeabi-v7a) ABI="armv7" ;;
|
|
x86_64) ABI="x86_64" ;;
|
|
x86) ABI="i686" ;;
|
|
*)
|
|
echo "⚠️ Could not detect device ABI (got '${device_abi:-none}') — building all targets."
|
|
ABI=""
|
|
;;
|
|
esac
|
|
[ -n "$ABI" ] && echo "🎯 Device ABI $device_abi → building only '$ABI'"
|
|
fi
|
|
|
|
TARGET_ARGS=()
|
|
if [ -n "$ABI" ]; then
|
|
TARGET_ARGS=(--target "$ABI")
|
|
fi
|
|
|
|
# Step 0: Optionally clear build caches for a fully fresh build.
|
|
if [ "$CLEAN" = "1" ]; then
|
|
echo "🧹 Clearing build caches (clean build)..."
|
|
rm -rf node_modules/.vite dist .svelte-kit .next build target src-tauri/target 2>/dev/null || true
|
|
# `bun install`, NOT `npm install`. This is a bun project (see packageManager
|
|
# in package.json) and bun.lock is the lockfile that is committed; npm
|
|
# ignores it, re-resolves the tree from package.json alone, and writes a
|
|
# package-lock.json that .gitignore then hides.
|
|
#
|
|
# That is not cosmetic. The Tauri CLI refuses to build when a plugin's Rust
|
|
# crate and npm package differ by minor version, so the JS side is pinned
|
|
# exactly to match Cargo.lock; a re-resolve is precisely how those halves
|
|
# drift apart again. A clean build must not be able to change what gets
|
|
# installed.
|
|
bun install > /dev/null 2>&1
|
|
fi
|
|
|
|
# Step 1: Sync Android source files
|
|
echo "🔄 Syncing Android sources..."
|
|
./scripts/sync-android-sources.sh
|
|
|
|
# Step 2: Build the frontend first to avoid dev server issues
|
|
echo "🎨 Building frontend..."
|
|
bun run build
|
|
|
|
# Step 2: Build Android APK
|
|
# `--apk` is a boolean flag, NOT `--apk true`.
|
|
#
|
|
# tauri-cli took a value here until 2.10; from 2.11 it is a plain flag and the
|
|
# stray `true` is parsed as a positional argument, failing with
|
|
# "error: unexpected argument 'true' found" before the build starts. Found by
|
|
# deploying to a device after the Tauri 2.9.5 -> 2.11.5 upgrade.
|
|
if [ "$BUILD_TYPE" = "release" ] && [ "$SIDE_BY_SIDE" = "1" ]; then
|
|
# A release build in the debug slot: R8 still runs, but the applicationId is
|
|
# suffixed and the debug keystore signs it (read by build.gradle.kts from
|
|
# JT_SIDE_BY_SIDE), so the real key is not needed and it replaces any other
|
|
# .debug install cleanly. Deliberately does NOT write keystore.properties.
|
|
echo "📦 Building side-by-side release APK (com.dtourolle.jellytau.debug)..."
|
|
JT_SIDE_BY_SIDE=1 bun run tauri android build --apk "${TARGET_ARGS[@]}"
|
|
elif [ "$BUILD_TYPE" = "release" ]; then
|
|
# Configure release signing from .env (single source of truth). Must run
|
|
# after sync-android-sources.sh, since gen/android is (re)generated there.
|
|
./scripts/write-keystore-properties.sh
|
|
echo "📦 Building release APK..."
|
|
bun run tauri android build --apk "${TARGET_ARGS[@]}"
|
|
else
|
|
echo "📦 Building debug APK..."
|
|
bun run tauri android build --apk --debug "${TARGET_ARGS[@]}"
|
|
fi
|
|
|
|
echo ""
|
|
echo "✅ APK build complete!"
|
|
echo "📱 APK location: src-tauri/gen/android/app/build/outputs/apk/"
|
|
|
|
# Containerised builds run as root against a bind-mounted tree; hand the
|
|
# artifacts back to the host user. No-op when not root. See DR-213.
|
|
"$(dirname "$0")/restore-ownership.sh"
|