-
Notifications
You must be signed in to change notification settings - Fork 0
225 lines (213 loc) · 10.7 KB
/
Copy pathandroid.yml
File metadata and controls
225 lines (213 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# Phase 0 / Task 0.3 — Android CI: Kotlin unit tests + emulator E2E (Maestro).
# Incorporates v2 audit fixes: caching, deterministic APK path, failure artifacts,
# regeneration guard, and a note that ARM64 performance (Gate G2) cannot run on hosted x86_64 runners.
name: android
on:
pull_request:
paths:
- "src/android/**"
- "src-tauri/gen/android/**"
- "src-tauri/**"
- "maestro/**"
- "scripts/check-android-so-alignment.ts"
- ".github/workflows/android.yml"
workflow_dispatch:
env:
NDK_VERSION: "28.2.13676358" # r28 — 16 KB-aligned by default (Gate G1)
SHERPA_ONNX_VERSION: "1.13.3"
jobs:
# Guard: a regeneration (`tauri android init`) must not silently delete committed custom native code.
regen-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Assert custom native files present
shell: bash
run: |
set -e
base="src-tauri/gen/android/app/src/main/java/com/galaxyruler/verbatim"
for f in MainActivity.kt FloatingBubbleService.kt VerbatimAccessibilityService.kt AndroidSpeechSupport.kt; do
test -f "$base/$f" || { echo "::error::missing committed native file $base/$f"; exit 1; }
done
test -f "src-tauri/gen/android/app/src/test/java/com/galaxyruler/verbatim/SmokeTest.kt" || { echo "::error::missing SmokeTest.kt"; exit 1; }
test -f "src-tauri/gen/android/debug.keystore" || { echo "::error::missing deterministic debug keystore"; exit 1; }
grep -q "storeFile = debugKeystoreFile" "src-tauri/gen/android/app/build.gradle.kts" || { echo "::error::debug signing config is not wired to debug.keystore"; exit 1; }
e2e:
runs-on: ubuntu-latest # HW-accelerated emulation is Linux-only
timeout-minutes: 60 # backstop: a dead-device `adb` once hung this job for the full 6h
steps:
- uses: actions/checkout@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules && sudo udevadm trigger --name-match=kvm
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "21" }
- uses: oven-sh/setup-bun@v2
- uses: dtolnay/rust-toolchain@stable
with: { targets: x86_64-linux-android }
- uses: android-actions/setup-android@v3
- name: Install NDK
run: sdkmanager "ndk;${{ env.NDK_VERSION }}"
- uses: Swatinem/rust-cache@v2
with: { workspaces: "src-tauri -> target" }
- uses: actions/cache@v4 # bun deps
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('bun.lock') }}
- run: bun install --frozen-lockfile
- name: Download official sherpa-onnx Android runtime
shell: bash
run: |
set -euo pipefail
archive="$RUNNER_TEMP/sherpa-onnx-v${SHERPA_ONNX_VERSION}-android.tar.bz2"
extract_dir="$RUNNER_TEMP/sherpa-onnx-android"
curl -L -o "$archive" "https://github.com/k2-fsa/sherpa-onnx/releases/download/v${SHERPA_ONNX_VERSION}/sherpa-onnx-v${SHERPA_ONNX_VERSION}-android.tar.bz2"
mkdir -p "$extract_dir"
tar -xjf "$archive" -C "$extract_dir"
lib_path="$(find "$extract_dir" -type f -path '*/x86_64/*' -name 'libsherpa-onnx-c-api.so' -print -quit)"
if [[ -z "$lib_path" ]]; then
echo "Unable to find x86_64 libsherpa-onnx-c-api.so in official sherpa-onnx Android archive" >&2
exit 1
fi
echo "SHERPA_ONNX_LIB_DIR=$(dirname "$lib_path")" >> "$GITHUB_ENV"
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Build debug APK (x86_64 for emulator)
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_NDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_E2E_FEATURES: android-asr android-e2e-bundled-frontend
TAURI_CONFIG: '{"build":{"devUrl":null}}'
run: |
bun run tauri android build --debug --target x86_64 --apk --features "$ANDROID_E2E_FEATURES" \
--config "$TAURI_CONFIG"
- name: Stage bundled frontend assets in debug APK
working-directory: src-tauri/gen/android
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_NDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_E2E_FEATURES: android-asr android-e2e-bundled-frontend
CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android26-clang
shell: bash
run: |
set -euo pipefail
assets_dir="app/src/main/assets"
gradle_file="app/build.gradle.kts"
test -f "../../../dist/index.html" || {
echo "::error::frontend build missing dist/index.html"
exit 1
}
# Tauri CLI regenerates this Gradle field with only android-asr for
# the final Rust task; patch the CI copy so the packaged library uses
# tauri/custom-protocol and loads the bundled assets directly.
grep -Fq 'features = "android-asr"' "$gradle_file" || {
echo "::error::expected generated Android Rust feature list not found in $gradle_file"
exit 1
}
perl -0pi -e 's/features = "android-asr"/features = "$ENV{ANDROID_E2E_FEATURES}"/' "$gradle_file"
grep -Fq "features = \"$ANDROID_E2E_FEATURES\"" "$gradle_file" || {
echo "::error::failed to enable bundled-frontend Rust feature for Android e2e debug APK"
exit 1
}
mkdir -p "$assets_dir"
cp -R ../../../dist/. "$assets_dir/"
bash ./gradlew :app:assembleUniversalDebug \
-PtargetList=x86_64 \
-ParchList=x86_64 \
-PabiList=x86_64
# Run here, not as a standalone job: the build above generates gen/android's
# tauri.settings.gradle (machine-specific, not committed), which gradle needs.
- name: Kotlin unit tests
working-directory: src-tauri/gen/android
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_NDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
# via bash so it runs regardless of the committed wrapper exec bit
run: bash ./gradlew :app:testUniversalDebugUnitTest
- name: Resolve APK path (deterministic; tauri merges a single --target to "universal")
id: apk
run: |
set -e
base="src-tauri/gen/android/app/build/outputs/apk"
apk="$base/universal/debug/app-universal-debug.apk"
test -f "$apk" || apk="$(find "$base" -name 'app-*-debug.apk' | head -1)"
test -f "$apk" || { echo "::error::debug APK not found under $base"; exit 1; }
echo "path=$apk" >> "$GITHUB_OUTPUT"
- name: Guard bundled frontend APK config
shell: bash
run: |
set -euo pipefail
apk="${{ steps.apk.outputs.path }}"
conf="$RUNNER_TEMP/tauri-conf.json"
unzip -p "$apk" assets/tauri.conf.json > "$conf" || {
echo "::error::debug APK missing embedded assets/tauri.conf.json"
exit 1
}
if grep -q "localhost:1420" "$conf"; then
echo "::error::debug APK embedded assets/tauri.conf.json still references localhost:1420; CI e2e must load bundled frontend assets"
exit 1
fi
unzip -l "$apk" | grep -q 'assets/index.html' || {
echo "::error::debug APK missing bundled frontend assets/index.html"
exit 1
}
- name: Guard 16 KB Android .so alignment
env:
ANDROID_NDK_HOME: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
ANDROID_NDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}
LLVM_OBJDUMP: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objdump
run: >
bun scripts/check-android-so-alignment.ts
"${{ steps.apk.outputs.path }}"
"$SHERPA_ONNX_LIB_DIR"
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-34
- name: Create AVD snapshot (cache miss)
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot
script: echo "avd warmed"
- name: E2E (Maestro)
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot
# Capture logcat + screenshot INSIDE the script, while the emulator is still alive.
# A standalone post-step cannot: the action kills the emulator on script failure,
# after which `adb` blocks on "- waiting for device -" forever (this once hung the
# job for 6h). `timeout` guards each adb call. Kept to single logical lines — the
# action's `sh` mis-parses a multi-line `if/fi` block here.
script: |
adb uninstall com.galaxyruler.verbatim || true
adb install -r "${{ steps.apk.outputs.path }}"
maestro test maestro/ || { code=$?; echo "Maestro failed ($code); capturing diagnostics while the device is alive"; timeout 60 adb logcat -d > logcat.txt 2>/dev/null || true; timeout 60 adb exec-out screencap -p > screen.png 2>/dev/null || true; exit $code; }
- name: Upload failure artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-failure
path: |
logcat.txt
screen.png
~/.maestro/tests/**
# NOTE (Gate G2): x86_64 emulators cannot establish ARM64 ASR latency/RAM/thermal behavior.
# Physical-device benchmarks (Snapdragon 6xx/7xx + a flagship) must run on a self-hosted/device-farm
# lane, scheduled separately — they are NOT part of this PR gate.
# NOTE (Maestro): standard runtime permission dialogs are automatable, but Accessibility/overlay
# Settings and OEM permission managers are not — those flows need Appium/UiAutomator (added with Phase 2).