6 files changed
+167
-32
.env.playstore.example
new
+29
@@ -0,0 +1,29 @@
1
+# Play Store build configuration for Siti AI (Android).
2
+#
3
+# No credential or personal path is committed. Copy this file to an untracked
4
+# `.env.playstore`, fill in the real value, and scripts/build-android.sh
5
+# auto-loads it:
6
+#
7
+# cp .env.playstore.example .env.playstore # then edit
8
+# make psmobile
9
+#
10
+# This is the Android counterpart to .env.appstore (macOS / iOS / visionOS).
11
+# Root .gitignore ignores `.env.*`, so the filled-in copy stays untracked.
12
+
13
+# ── Signing (make psmobile) ────────────────────────────────────────────────
14
+# Absolute path to the upload keystore (.jks). Required — the build aborts
15
+# without it.
16
+#
17
+# This is the only way to point the build at a keystore: the `keystore` step
18
+# rewrites `storeFile=` in src-tauri/gen/android/keystore.properties from this
19
+# value on every run, so editing that file by hand has no effect.
20
+#
21
+# The matching `password` and `keyAlias` live in
22
+# src-tauri/gen-override/android/keystore.properties — copy the .example beside
23
+# it and fill those in once. Neither file is committed.
24
+#
25
+# An inline override still wins over this file, for a one-off build against a
26
+# different keystore:
27
+#
28
+# KEYSTORE_PATH=/other/upload.jks make psmobile
29
+KEYSTORE_PATH=/absolute/path/to/your/upload-keystore.jks
.gitignore
+1
@@ -49,6 +49,7 @@ keystore.properties
49
.env.*
50
!.env.example
51
!.env.appstore.example
52
+!.env.playstore.example
53
54
# Local Apple signing configs — contain cert SHA-1 identities and local
55
# provisioning-profile paths. Copy the .example files, fill in your own, and
scripts/build-android.sh
+89
-11
@@ -8,6 +8,7 @@
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,6 +16,8 @@
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,6 +27,7 @@
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
@@ -38,13 +42,37 @@ 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
44
-# personal path is baked into the public repo. Copy
45
-# src-tauri/gen-override/android/keystore.properties.example to keystore.properties
46
-# and set storeFile, or export KEYSTORE_PATH before running this script.
47
-KEYSTORE_PATH="${KEYSTORE_PATH:?set KEYSTORE_PATH to the absolute path of your upload-keystore.jks}"
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
@@ -61,7 +89,7 @@ fail() { echo "❌ $*" >&2; exit 1; }
89
# ── Steps ─────────────────────────────────────────────────────────────────────
90
91
do_clean() {
64
- step "1/6 Clean gen/android"
92
+ step "1/7 Clean gen/android"
93
94
echo "🗑 rm -rf ${GEN_ANDROID_DIR}"
95
rm -rf "${GEN_ANDROID_DIR}"
@@ -69,7 +97,7 @@ do_clean() {
97
}
98
99
do_init() {
72
- step "2/6 Android init"
100
+ step "2/7 Android init"
101
102
[[ -f "$ANDROID_CONFIG" ]] || fail "Android config not found: $ANDROID_CONFIG"
103
@@ -85,14 +113,14 @@ do_init() {
113
}
114
115
do_icons() {
88
- step "3/6 Generate icons"
116
+ step "3/7 Generate icons"
117
118
pnpm gim
119
ok "Icons generated"
120
}
121
122
do_override() {
95
- step "4/6 Apply gen-override/android"
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 ' ')"
@@ -113,7 +141,7 @@ do_override() {
141
}
142
143
do_keystore() {
116
- step "5/6 Patch keystore.properties"
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."
@@ -127,7 +155,7 @@ do_keystore() {
155
}
156
157
do_build() {
130
- step "6/6 Build"
158
+ step "6/7 Build"
159
160
[[ -f "$ANDROID_CONFIG" ]] || fail "Android config not found: $ANDROID_CONFIG"
161
@@ -148,6 +176,54 @@ do_build() {
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}"
@@ -159,6 +235,7 @@ case "$STEP" in
235
override) do_override ;;
236
keystore) do_keystore ;;
237
build) do_build ;;
238
+ verify) do_verify ;;
239
all)
240
do_clean
241
do_init
@@ -166,11 +243,12 @@ case "$STEP" in
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
*)
173
- echo "Usage: $0 [clean|init|icons|override|keystore|build|all]" >&2
251
+ echo "Usage: $0 [clean|init|icons|override|keystore|build|verify|all]" >&2
252
exit 1
253
;;
254
esac
src-tauri/gen-override/android/app/src/main/AndroidManifest.xml
deleted
-18
@@ -1,18 +0,0 @@
1
-<?xml version="1.0" encoding="utf-8"?>
2
-<!-- Merged with Tauri's generated manifest to add App Links verification. -->
3
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
4
- <application>
5
- <activity
6
- android:name=".MainActivity"
7
- android:exported="true">
8
- <intent-filter android:autoVerify="true">
9
- <action android:name="android.intent.action.VIEW" />
10
- <category android:name="android.intent.category.DEFAULT" />
11
- <category android:name="android.intent.category.BROWSABLE" />
12
- <data
13
- android:scheme="https"
14
- android:host="getsiti.5mb.app" />
15
- </intent-filter>
16
- </activity>
17
- </application>
18
-</manifest>
src-tauri/gen-override/android/app/src/release/AndroidManifest.xml
new
+41
@@ -0,0 +1,41 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<!--
3
+ App Links overlay — release builds only.
4
+
5
+ This is NOT the app manifest. It is a build-type source set that AGP's
6
+ manifest merger folds into the generated app/src/main/AndroidManifest.xml,
7
+ adding one intent-filter to the existing MainActivity and changing nothing
8
+ else.
9
+
10
+ It lives here, and not in src/main/, because scripts/build-android.sh applies
11
+ gen-override with `rsync` — a file copy, not an XML merge. An override placed
12
+ at app/src/main/AndroidManifest.xml *replaces* the manifest Tauri generates,
13
+ silently dropping MAIN/LAUNCHER, INTERNET, the launcher icon, and the
14
+ FileProvider. That shipped once: 1.0.1 installed from the Play Store with no
15
+ launcher entry, so the Store showed no "Open" button. Keep additions in a
16
+ source set the merger owns, and let Tauri's generated manifest stay untouched.
17
+
18
+ The App Links filter must stay in its own <intent-filter>. Android evaluates a
19
+ filter as a unit — action AND category AND data must all match — so a filter
20
+ carrying a <data> element can never match the launcher's ACTION_MAIN intent,
21
+ which has no URI. Merging the two sets of tags into one filter reproduces the
22
+ same "no Open button" bug.
23
+
24
+ Verification also needs https://getsiti.5mb.app/.well-known/assetlinks.json to
25
+ serve this app's package name and signing-certificate SHA-256 fingerprint,
26
+ otherwise autoVerify fails and links fall back to a disambiguation dialog.
27
+-->
28
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
29
+ <application>
30
+ <activity android:name=".MainActivity">
31
+ <intent-filter android:autoVerify="true">
32
+ <action android:name="android.intent.action.VIEW" />
33
+ <category android:name="android.intent.category.DEFAULT" />
34
+ <category android:name="android.intent.category.BROWSABLE" />
35
+ <data
36
+ android:scheme="https"
37
+ android:host="getsiti.5mb.app" />
38
+ </intent-filter>
39
+ </activity>
40
+ </application>
41
+</manifest>
src-tauri/gen-override/android/keystore.properties.example
+7
-3
@@ -1,11 +1,15 @@
1
# Android upload-keystore config for release (.aab) signing.
2
#
3
-# Copy this file to `keystore.properties` (which is gitignored) and fill in your
4
-# own values, or export KEYSTORE_PATH before running scripts/build-android.sh —
5
-# the script patches `storeFile` at build time.
3
+# Copy this file to `keystore.properties` (which is gitignored) and fill in
4
+# `password` and `keyAlias`.
5
+#
6
+# Leave `storeFile` as-is: scripts/build-android.sh rewrites that line on every
7
+# run from KEYSTORE_PATH, so a hand-edited value here is always discarded. Set
8
+# the keystore location in .env.playstore instead (see .env.playstore.example).
9
#
10
# NEVER commit the real keystore.properties or the .jks file.
11
12
password=CHANGE_ME
13
keyAlias=upload
14
+# Overwritten by scripts/build-android.sh from KEYSTORE_PATH — see above.
15
storeFile=/absolute/path/to/your/upload-keystore.jks