|
1
|
#!/usr/bin/env bash |
|
2
|
# build-android.sh |
|
3
|
# |
|
4
|
# Full Play Store pipeline for Siti AI (Android): |
|
5
|
# 1. Clean — rm -rf src-tauri/gen/android |
|
6
|
# 2. Init — cargo tauri android init (prod Android config) |
|
7
|
# 3. Icons — pnpm gim |
|
8
|
# 4. Override — copy src-tauri/gen-override/android/* into gen/android/ |
|
9
|
# 5. Keystore — patch storeFile path in gen/android/keystore.properties |
|
10
|
# 6. Build — cargo tauri android build --aab |
|
11
|
# 7. Verify — assert the merged manifest still launches and reaches the net |
|
12
|
# |
|
13
|
# The build produces a signed universal .aab ready to upload to the Play |
|
14
|
# Console. versionCode is injected at init/build time as YYYYMMDDhh (UTC) via a |
|
15
|
# second -c flag, so tauri.prod-android.conf.json is never mutated. |
|
16
|
# |
|
17
|
# Every configurable value has a hardcoded default that can be overridden by |
|
18
|
# setting the corresponding environment variable before invoking the script. |
|
19
|
# KEYSTORE_PATH has no default and is read from an untracked .env.playstore if |
|
20
|
# present — copy .env.playstore.example to get started. |
|
21
|
# |
|
22
|
# Usage: |
|
23
|
# ./scripts/build-android.sh # full pipeline |
|
24
|
# ./scripts/build-android.sh clean # rm gen/android only |
|
25
|
# ./scripts/build-android.sh init # android init only |
|
26
|
# ./scripts/build-android.sh icons # generate icons only |
|
27
|
# ./scripts/build-android.sh override # copy gen-override files only |
|
28
|
# ./scripts/build-android.sh keystore # patch keystore.properties only |
|
29
|
# ./scripts/build-android.sh build # android build only |
|
30
|
# ./scripts/build-android.sh verify # check the merged manifest only |
|
31
|
# |
|
32
|
# make psmobile # full pipeline via Makefile |
|
33
|
# make psmobile-build |
|
34
|
|
|
35
|
set -euo pipefail |
|
36
|
|
|
37
|
# ── Paths ───────────────────────────────────────────────────────────────────── |
|
38
|
|
|
39
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|
40
|
ANDROID_CONFIG="${REPO_ROOT}/src-tauri/tauri.prod-android.conf.json" |
|
41
|
GEN_ANDROID_DIR="${REPO_ROOT}/src-tauri/gen/android" |
|
42
|
GEN_OVERRIDE_DIR="${REPO_ROOT}/src-tauri/gen-override/android" |
|
43
|
KEYSTORE_PROPERTIES="${GEN_ANDROID_DIR}/keystore.properties" |
|
44
|
|
|
45
|
# Auto-load the untracked .env.playstore (upload keystore path) so |
|
46
|
# `make psmobile` works without the caller exporting it first. Mirrors how |
|
47
|
# build-ios.sh / build-macos.sh / build-visionos.sh load .env.appstore. See |
|
48
|
# .env.playstore.example for the expected keys. |
|
49
|
# |
|
50
|
# Sourcing assigns unconditionally, which would let the file quietly beat an |
|
51
|
# inline `KEYSTORE_PATH=… make psmobile`. Stash the caller's value first and put |
|
52
|
# it back afterwards, so an explicit override always wins over the file. |
|
53
|
KEYSTORE_PATH_FROM_CALLER="${KEYSTORE_PATH:-}" |
|
54
|
|
|
55
|
if [[ -f "${REPO_ROOT}/.env.playstore" ]]; then |
|
56
|
set -a |
|
57
|
# shellcheck disable=SC1091 |
|
58
|
. "${REPO_ROOT}/.env.playstore" |
|
59
|
set +a |
|
60
|
fi |
|
61
|
|
|
62
|
if [[ -n "$KEYSTORE_PATH_FROM_CALLER" ]]; then |
|
63
|
KEYSTORE_PATH="$KEYSTORE_PATH_FROM_CALLER" |
|
64
|
fi |
|
65
|
|
|
66
|
# ── Overridable config ──────────────────────────────────────────────────────── |
|
67
|
|
|
68
|
# Absolute path to the upload keystore (.jks). Required — no default, so that no |
|
69
|
# personal path is baked into the public repo. Set it in .env.playstore (copy |
|
70
|
# .env.playstore.example), or export it before running this script. |
|
71
|
# |
|
72
|
# Note this is the *only* way to point the build at a keystore: do_keystore |
|
73
|
# rewrites storeFile= in gen/android/keystore.properties from this value, so |
|
74
|
# editing that file by hand has no effect. |
|
75
|
KEYSTORE_PATH="${KEYSTORE_PATH:?set KEYSTORE_PATH in .env.playstore (see .env.playstore.example) or export it}" |
|
76
|
|
|
77
|
# versionCode: YYYYMMDDhh (UTC) — same UTC timestamp base as the iOS bundleVersion |
|
78
|
# Integer form of the date+hour; fits the Play Store 32-bit limit (≤ 2,100,000,000) through 2099 |
|
79
|
# Monotonically increasing → satisfies Play Store requirement |
|
80
|
VERSION_CODE="$(date -u +"%Y%m%d%H")" |
|
81
|
VERSION_CODE_JSON="{\"bundle\":{\"android\":{\"versionCode\":${VERSION_CODE}}}}" |
|
82
|
|
|
83
|
# ── Helpers ─────────────────────────────────────────────────────────────────── |
|
84
|
|
|
85
|
step() { echo ""; echo "── $* ──────────────────────────────────────────────"; } |
|
86
|
ok() { echo "✅ $*"; } |
|
87
|
fail() { echo "❌ $*" >&2; exit 1; } |
|
88
|
|
|
89
|
# ── Steps ───────────────────────────────────────────────────────────────────── |
|
90
|
|
|
91
|
do_clean() { |
|
92
|
step "1/7 Clean gen/android" |
|
93
|
|
|
94
|
echo "🗑 rm -rf ${GEN_ANDROID_DIR}" |
|
95
|
rm -rf "${GEN_ANDROID_DIR}" |
|
96
|
ok "Cleaned" |
|
97
|
} |
|
98
|
|
|
99
|
do_init() { |
|
100
|
step "2/7 Android init" |
|
101
|
|
|
102
|
[[ -f "$ANDROID_CONFIG" ]] || fail "Android config not found: $ANDROID_CONFIG" |
|
103
|
|
|
104
|
echo "🏷 versionCode → ${VERSION_CODE}" |
|
105
|
echo "📂 config → ${ANDROID_CONFIG}" |
|
106
|
echo "" |
|
107
|
|
|
108
|
cargo tauri android init \ |
|
109
|
-c "${ANDROID_CONFIG}" \ |
|
110
|
-c "${VERSION_CODE_JSON}" |
|
111
|
|
|
112
|
ok "Android project initialised → ${GEN_ANDROID_DIR}" |
|
113
|
} |
|
114
|
|
|
115
|
do_icons() { |
|
116
|
step "3/7 Generate icons" |
|
117
|
|
|
118
|
pnpm gim |
|
119
|
ok "Icons generated" |
|
120
|
} |
|
121
|
|
|
122
|
do_override() { |
|
123
|
step "4/7 Apply gen-override/android" |
|
124
|
|
|
125
|
# Nothing to do if the override directory contains only .keep files. |
|
126
|
OVERRIDE_COUNT="$(find "${GEN_OVERRIDE_DIR}" -not -name '.keep' -not -type d | wc -l | tr -d ' ')" |
|
127
|
|
|
128
|
if [[ "$OVERRIDE_COUNT" -eq 0 ]]; then |
|
129
|
echo "ℹ️ No override files — skipping." |
|
130
|
return |
|
131
|
fi |
|
132
|
|
|
133
|
echo "📋 Copying ${OVERRIDE_COUNT} file(s) from gen-override/android → gen/android" |
|
134
|
|
|
135
|
rsync -av \ |
|
136
|
--exclude='.keep' \ |
|
137
|
"${GEN_OVERRIDE_DIR}/" \ |
|
138
|
"${GEN_ANDROID_DIR}/" |
|
139
|
|
|
140
|
ok "Overrides applied" |
|
141
|
} |
|
142
|
|
|
143
|
do_keystore() { |
|
144
|
step "5/7 Patch keystore.properties" |
|
145
|
|
|
146
|
[[ -f "$KEYSTORE_PROPERTIES" ]] || fail "keystore.properties not found: ${KEYSTORE_PROPERTIES}\n Run 'override' step first." |
|
147
|
[[ -f "$KEYSTORE_PATH" ]] || fail "Keystore file not found: ${KEYSTORE_PATH}\n Set KEYSTORE_PATH env var to the correct .jks path." |
|
148
|
|
|
149
|
echo "🔑 storeFile → ${KEYSTORE_PATH}" |
|
150
|
|
|
151
|
# Replace the storeFile= line in-place, leaving all other keys untouched. |
|
152
|
sed -i '' "s|^storeFile=.*|storeFile=${KEYSTORE_PATH}|" "${KEYSTORE_PROPERTIES}" |
|
153
|
|
|
154
|
ok "Patched → ${KEYSTORE_PROPERTIES}" |
|
155
|
} |
|
156
|
|
|
157
|
do_build() { |
|
158
|
step "6/7 Build" |
|
159
|
|
|
160
|
[[ -f "$ANDROID_CONFIG" ]] || fail "Android config not found: $ANDROID_CONFIG" |
|
161
|
|
|
162
|
echo "🏷 versionCode → ${VERSION_CODE}" |
|
163
|
echo "📦 config → ${ANDROID_CONFIG}" |
|
164
|
echo "" |
|
165
|
|
|
166
|
cargo tauri android build \ |
|
167
|
--aab \ |
|
168
|
-c "${ANDROID_CONFIG}" \ |
|
169
|
-c "${VERSION_CODE_JSON}" |
|
170
|
|
|
171
|
AAB_PATH="${GEN_ANDROID_DIR}/app/build/outputs/bundle/universalRelease/app-universal-release.aab" |
|
172
|
if [[ -f "$AAB_PATH" ]]; then |
|
173
|
ok "Build complete → ${AAB_PATH}" |
|
174
|
else |
|
175
|
ok "Build complete (locate the .aab under ${GEN_ANDROID_DIR}/app/build/outputs/bundle/)" |
|
176
|
fi |
|
177
|
} |
|
178
|
|
|
179
|
do_verify() { |
|
180
|
step "7/7 Verify merged manifest" |
|
181
|
|
|
182
|
# Guards the failure mode that shipped in 1.0.1: an override file copied over |
|
183
|
# app/src/main/AndroidManifest.xml replaced Tauri's generated manifest instead |
|
184
|
# of merging with it, so the released .aab had no MAIN/LAUNCHER filter (the |
|
185
|
# Play Store offered Install but no Open, and no icon appeared in the drawer) |
|
186
|
# and no INTERNET permission (no model could ever download). Both are silent |
|
187
|
# at build time — nothing fails, the .aab just cannot be launched — so assert |
|
188
|
# on the post-merge manifest that actually goes into the bundle. |
|
189
|
MERGED_MANIFEST="$(find "${GEN_ANDROID_DIR}/app/build/intermediates" \ |
|
190
|
-path '*bundle_manifest*universalRelease*' -name 'AndroidManifest.xml' \ |
|
191
|
-print -quit 2>/dev/null || true)" |
|
192
|
|
|
193
|
[[ -n "$MERGED_MANIFEST" ]] || fail "No merged release manifest found — run the 'build' step first." |
|
194
|
|
|
195
|
echo "🔍 ${MERGED_MANIFEST#"${REPO_ROOT}/"}" |
|
196
|
|
|
197
|
# "<what to look for>|<what its absence breaks>" |
|
198
|
REQUIRED=( |
|
199
|
'android.intent.action.MAIN|no launcher entry — Play Store shows Install but no Open' |
|
200
|
'android.intent.category.LAUNCHER|no icon in the app drawer' |
|
201
|
'android.permission.INTERNET|model downloads fail' |
|
202
|
'android:icon=|no launcher icon' |
|
203
|
'android:label=|no app name under the icon' |
|
204
|
) |
|
205
|
|
|
206
|
MISSING=0 |
|
207
|
for entry in "${REQUIRED[@]}"; do |
|
208
|
needle="${entry%%|*}" |
|
209
|
consequence="${entry#*|}" |
|
210
|
if grep -qF "$needle" "$MERGED_MANIFEST"; then |
|
211
|
echo " ✓ ${needle}" |
|
212
|
else |
|
213
|
echo " ✗ ${needle} — ${consequence}" >&2 |
|
214
|
MISSING=1 |
|
215
|
fi |
|
216
|
done |
|
217
|
|
|
218
|
[[ "$MISSING" -eq 0 ]] || fail "Merged manifest is missing required entries (see above). |
|
219
|
Almost always caused by a file in gen-override/android/app/src/main/ |
|
220
|
replacing a generated file rather than adding to it. Manifest additions |
|
221
|
belong in a build-type source set such as app/src/release/, which AGP's |
|
222
|
manifest merger folds into the generated one." |
|
223
|
|
|
224
|
ok "Manifest is launchable" |
|
225
|
} |
|
226
|
|
|
227
|
# ── Entry point ─────────────────────────────────────────────────────────────── |
|
228
|
|
|
229
|
STEP="${1:-all}" |
|
230
|
|
|
231
|
case "$STEP" in |
|
232
|
clean) do_clean ;; |
|
233
|
init) do_init ;; |
|
234
|
icons) do_icons ;; |
|
235
|
override) do_override ;; |
|
236
|
keystore) do_keystore ;; |
|
237
|
build) do_build ;; |
|
238
|
verify) do_verify ;; |
|
239
|
all) |
|
240
|
do_clean |
|
241
|
do_init |
|
242
|
do_icons |
|
243
|
do_override |
|
244
|
do_keystore |
|
245
|
do_build |
|
246
|
do_verify |
|
247
|
echo "" |
|
248
|
echo "🎉 Pipeline complete: Siti AI Android .aab is ready for Play Store upload." |
|
249
|
;; |
|
250
|
*) |
|
251
|
echo "Usage: $0 [clean|init|icons|override|keystore|build|verify|all]" >&2 |
|
252
|
exit 1 |
|
253
|
;; |
|
254
|
esac |