@sigit / siti / commits / 13893a8

Fix Android manifest

paydii committed Aug 3, 2026 at 19:29 UTC 13893a8d688a24edd59b360e84d8c985a5dd6840
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