react-native-bspatch 1.1.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +47 -52
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +15 -102
  3. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +21 -1
  4. package/cpp/bsdiff/bspatch.c +1 -0
  5. package/ios/OtaBundleResolver.swift +16 -1
  6. package/ios/RNBsPatch.mm +61 -154
  7. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  8. package/lib/commonjs/index.js +4 -4
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/ota/OtaProvider.js +26 -22
  11. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  12. package/lib/commonjs/ota/otaAssetResolver.js +8 -2
  13. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
  14. package/lib/commonjs/ota/otaCore.js +89 -141
  15. package/lib/commonjs/ota/otaCore.js.map +1 -1
  16. package/lib/commonjs/ota/otaLogger.js +0 -2
  17. package/lib/commonjs/ota/otaLogger.js.map +1 -1
  18. package/lib/commonjs/ota/otaMetadata.js +18 -7
  19. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  20. package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -1
  21. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  22. package/lib/commonjs/ota/types.js +1 -10
  23. package/lib/commonjs/ota/types.js.map +1 -1
  24. package/lib/module/NativeBsPatch.js.map +1 -1
  25. package/lib/module/index.js +1 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ota/OtaProvider.js +26 -22
  28. package/lib/module/ota/OtaProvider.js.map +1 -1
  29. package/lib/module/ota/otaAssetResolver.js +8 -2
  30. package/lib/module/ota/otaAssetResolver.js.map +1 -1
  31. package/lib/module/ota/otaCore.js +88 -139
  32. package/lib/module/ota/otaCore.js.map +1 -1
  33. package/lib/module/ota/otaLogger.js +0 -2
  34. package/lib/module/ota/otaLogger.js.map +1 -1
  35. package/lib/module/ota/otaMetadata.js +18 -7
  36. package/lib/module/ota/otaMetadata.js.map +1 -1
  37. package/lib/module/ota/storage/GitHubRawStorage.js +3 -1
  38. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  39. package/lib/module/ota/types.js +0 -9
  40. package/lib/module/ota/types.js.map +1 -1
  41. package/lib/typescript/NativeBsPatch.d.ts +3 -15
  42. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  43. package/lib/typescript/index.d.ts +1 -1
  44. package/lib/typescript/index.d.ts.map +1 -1
  45. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  46. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
  47. package/lib/typescript/ota/otaCore.d.ts +5 -42
  48. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  49. package/lib/typescript/ota/otaLogger.d.ts +1 -3
  50. package/lib/typescript/ota/otaLogger.d.ts.map +1 -1
  51. package/lib/typescript/ota/otaMetadata.d.ts +2 -2
  52. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  53. package/lib/typescript/ota/types.d.ts +18 -54
  54. package/lib/typescript/ota/types.d.ts.map +1 -1
  55. package/package.json +3 -6
  56. package/react-native-bspatch.podspec +0 -1
  57. package/src/NativeBsPatch.ts +3 -16
  58. package/src/index.ts +1 -1
  59. package/src/ota/OtaProvider.tsx +24 -23
  60. package/src/ota/otaAssetResolver.ts +5 -2
  61. package/src/ota/otaCore.ts +118 -176
  62. package/src/ota/otaLogger.ts +0 -2
  63. package/src/ota/otaMetadata.ts +18 -8
  64. package/src/ota/storage/GitHubRawStorage.ts +1 -1
  65. package/src/ota/types.ts +18 -57
  66. package/tools/build_ota.sh +356 -177
  67. package/tools/setup_project.js +95 -11
  68. package/tools/update_registry.js +24 -28
  69. package/tools/{upload_ota_custom.sh → upload_ota.sh} +33 -114
  70. package/lib/commonjs/crypto-js.d.js +0 -2
  71. package/lib/commonjs/crypto-js.d.js.map +0 -1
  72. package/lib/commonjs/ota/storage/CustomStorage.js +0 -160
  73. package/lib/commonjs/ota/storage/CustomStorage.js.map +0 -1
  74. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +0 -99
  75. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +0 -1
  76. package/lib/module/crypto-js.d.js +0 -2
  77. package/lib/module/crypto-js.d.js.map +0 -1
  78. package/lib/module/ota/storage/CustomStorage.js +0 -154
  79. package/lib/module/ota/storage/CustomStorage.js.map +0 -1
  80. package/lib/module/ota/storage/GitHubReleaseStorage.js +0 -93
  81. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +0 -1
  82. package/lib/typescript/ota/storage/CustomStorage.d.ts +0 -12
  83. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +0 -1
  84. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +0 -20
  85. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +0 -1
  86. package/src/crypto-js.d.ts +0 -1
  87. package/src/ota/storage/CustomStorage.ts +0 -166
  88. package/src/ota/storage/GitHubReleaseStorage.ts +0 -103
  89. package/tools/bsdiff +0 -0
@@ -30,10 +30,7 @@ fi
30
30
 
31
31
  if [ -n "$ENV_FILE" ]; then
32
32
  echo "Loading environment variables from $(basename "$ENV_FILE")..."
33
- export $(grep -v '^#' "$ENV_FILE" | grep -v '^$' | xargs)
34
- # Debug: verify loaded variables
35
- echo " Loaded OTA_UPLOAD_USER=${OTA_UPLOAD_USER:+[SET]}"
36
- echo " Loaded OTA_UPLOAD_PASSWORD=${OTA_UPLOAD_PASSWORD:+[SET]}"
33
+ set -a; . "$ENV_FILE"; set +a
37
34
  else
38
35
  echo " No .env file found in $ROOT or $MODULE_DIR"
39
36
  fi
@@ -45,20 +42,41 @@ PLATFORM_ARG=""
45
42
  APP_VERSION_ARG=""
46
43
  NON_INTERACTIVE=false
47
44
 
48
- PUSH_REMOTE=false
45
+ SEED_APK=""
46
+ SEED_IPA=""
47
+ SEED_BUNDLE=""
48
+ FORCE_BASE=false
49
+
49
50
  # Arg parsing
50
51
  while [[ "$#" -gt 0 ]]; do
51
52
  case $1 in
52
53
  --platform) PLATFORM_ARG="$2"; shift ;;
53
54
  --flavor) FLAVOR="$2"; shift ;;
54
55
  --app-version) APP_VERSION_ARG="$2"; shift ;;
56
+ --apk) SEED_APK="$2"; ACTION="seed"; shift ;;
57
+ --ipa) SEED_IPA="$2"; ACTION="seed"; shift ;;
58
+ --bundle) SEED_BUNDLE="$2"; ACTION="seed"; shift ;;
59
+ --force) FORCE_BASE=true ;;
55
60
  -y|--yes|--non-interactive) NON_INTERACTIVE=true ;;
56
61
  --push) PUSH_REMOTE=true ;;
57
62
  reset|base) ACTION="$1" ;;
58
63
  setup) ACTION="$1" ;;
59
- apk|ipa|seed) ACTION="base" ;; # Compatibility for legacy/seed actions
64
+ seed) ACTION="seed" ;;
65
+ apk|ipa) ACTION="base" ;; # Compatibility for legacy actions
60
66
  dev|staging|production) FLAVOR="$1" ;; # Support positional flavor
61
67
  [0-9]*) ;; # Ignore version if passed as $1 (pos 1 usually)
68
+ *)
69
+ if [[ "$1" == *.apk ]] || [[ "$1" == *.aab ]]; then
70
+ SEED_APK="$1"
71
+ ACTION="seed"
72
+ elif [[ "$1" == *.ipa ]]; then
73
+ SEED_IPA="$1"
74
+ ACTION="seed"
75
+ elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]] || [[ "$1" == *.jsbundle ]]; then
76
+ SEED_BUNDLE="$1"
77
+ ACTION="seed"
78
+ fi
79
+ ;;
62
80
  esac
63
81
  shift
64
82
  done
@@ -86,156 +104,65 @@ mkdir -p "$ROOT/ios/output"
86
104
  REGISTRY_SHARED="$ROOT/ota_registry.json"
87
105
 
88
106
  # ── Registry Sync ──────────────────────────────────────────────────────────
89
- # Skip registry sync for reset/clean actions
90
- if [ "$ACTION" != "reset" ]; then
107
+ # Reset syncs too: it edits the latest remote registry instead of overwriting it with a stale local copy
108
+ if [ "$ACTION" != "setup" ]; then
91
109
  echo "Syncing remote registry..."
92
110
 
93
- BASE_URL="${OTA_BASE_URL:-https://sync-server.osp.com.vn}"
94
- UPCODE_USER="${OTA_UPLOAD_USER:-}"
95
- UPCODE_PASS="${OTA_UPLOAD_PASSWORD:-}"
111
+ BASE_URL="${OTA_BASE_URL:-}"
96
112
  BASE_FOLDER="${OTA_FOLDER:-ota}"
97
113
 
98
114
  echo " Using URL: $BASE_URL"
99
115
  echo " Using Folder: $BASE_FOLDER"
100
116
 
101
- if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"* ]]; then
102
- echo " Downloading remote registry from GitHub..."
103
-
104
- # Parse OWNER/REPO/BRANCH from the raw.githubusercontent URL or github.com URL
105
- # e.g. https://raw.githubusercontent.com/owner/repo/branch OR
106
- # https://github.com/owner/repo/tree/branch
107
- CLEAN_URL=$(echo "$BASE_URL" | sed 's#https://raw.githubusercontent.com/##' | sed 's#https://github.com/##' | sed 's#/tree/##')
108
- GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
109
- GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
110
- GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
111
- if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
112
-
113
- # Prefer GitHub CLI token for authenticated requests (higher rate limit, no CDN cache)
114
- GH_CLI_TOKEN=$(gh auth token 2>/dev/null || true)
115
- GH_API_TOKEN="${GH_CLI_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
116
-
117
- # Use GitHub Contents API — NOT raw CDN — to bypass the 5-min CDN cache.
118
- # The API always returns the latest committed content.
119
- REGISTRY_API_URL="https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}"
120
-
121
- API_HTTP_FILE=$(mktemp)
122
- API_BODY_FILE=$(mktemp)
123
-
124
- if [[ -n "$GH_API_TOKEN" ]]; then
125
- curl -s -o "$API_BODY_FILE" -w "%{http_code}" \
126
- -H "Authorization: Bearer $GH_API_TOKEN" \
127
- -H "Accept: application/vnd.github.v3+json" \
128
- "$REGISTRY_API_URL" > "$API_HTTP_FILE" 2>/dev/null || true
129
- else
130
- curl -s -o "$API_BODY_FILE" -w "%{http_code}" \
131
- -H "Accept: application/vnd.github.v3+json" \
132
- "$REGISTRY_API_URL" > "$API_HTTP_FILE" 2>/dev/null || true
133
- fi
134
-
135
- API_HTTP=$(cat "$API_HTTP_FILE" 2>/dev/null || echo "0")
136
- rm -f "$API_HTTP_FILE"
137
-
138
- if [[ "$API_HTTP" = "200" ]]; then
139
- # Decode base64 content returned by the GitHub Contents API in one python3 pass
140
- python3 - "$API_BODY_FILE" "$REGISTRY_SHARED.tmp" << 'PYEOF'
141
- import sys, json, base64
142
- body_file, out_file = sys.argv[1], sys.argv[2]
143
- try:
144
- with open(body_file, 'r') as f:
145
- d = json.load(f)
146
- content = d.get('content', '')
147
- decoded = base64.b64decode(content.replace('\n', ''))
148
- with open(out_file, 'wb') as f:
149
- f.write(decoded)
150
- except Exception as e:
151
- sys.exit(1)
152
- PYEOF
153
- rm -f "$API_BODY_FILE"
154
-
155
- if [ -s "$REGISTRY_SHARED.tmp" ]; then
156
- mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
157
- echo " ✓ Remote registry synced (via GitHub API)"
158
- else
159
- rm -f "$REGISTRY_SHARED.tmp"
160
- echo " Notice: Remote registry empty or decode failed (starting fresh with local state)."
161
- fi
162
- else
163
- rm -f "$API_BODY_FILE" "$REGISTRY_SHARED.tmp"
164
- echo " Notice: Remote registry not found on GitHub API (HTTP $API_HTTP — starting fresh with local state)."
165
- fi
166
-
117
+ # GitHub is the only storage backend for now
118
+ if [[ "$BASE_URL" != "https://raw.githubusercontent.com/"* ]]; then
119
+ echo " ❌ OTA_BASE_URL must be https://raw.githubusercontent.com/<owner>/<repo>/<branch>"
120
+ exit 1
121
+ fi
122
+ echo " Downloading remote registry from GitHub..."
123
+
124
+ CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
125
+ GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
126
+ GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
127
+ GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
128
+ if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
129
+
130
+ # Contents API (not the raw CDN) always returns the latest commit; the raw
131
+ # media type has no 1 MB limit and needs no base64 decoding.
132
+ GH_API_TOKEN="$(gh auth token 2>/dev/null || true)"
133
+ GH_API_TOKEN="${GH_API_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
134
+ GH_API_AUTH=()
135
+ if [[ -n "$GH_API_TOKEN" ]]; then GH_API_AUTH=(-H "Authorization: Bearer $GH_API_TOKEN"); fi
136
+
137
+ API_HTTP=$(curl -s -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "${GH_API_AUTH[@]}" \
138
+ -H "Accept: application/vnd.github.raw" \
139
+ "https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}" || echo 0)
140
+
141
+ if [[ "$API_HTTP" == "200" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$REGISTRY_SHARED.tmp" 2>/dev/null; then
142
+ mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
143
+ echo " ✓ Remote registry synced (via GitHub API)"
144
+ elif [[ "$API_HTTP" == "404" ]]; then
145
+ rm -f "$REGISTRY_SHARED.tmp"
146
+ echo " Notice: No remote registry yet (HTTP 404) — starting fresh with local state."
167
147
  else
168
- if [[ -n "$UPCODE_USER" ]] && [[ -n "$UPCODE_PASS" ]]; then
169
- LOGIN_RES=$(curl -s -X "POST" "$BASE_URL/public/authen/login" \
170
- -H 'Content-Type: application/json; charset=utf-8' \
171
- -w "\n---HTTP_STATUS:%{http_code}" \
172
- -d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
173
- HTTP_STATUS=$(echo "$LOGIN_RES" | grep -o 'HTTP_STATUS:[0-9]*' | cut -d: -f2)
174
- LOGIN_BODY=$(echo "$LOGIN_RES" | sed 's/---HTTP_STATUS:.*//')
175
-
176
- TOKEN=$(echo "$LOGIN_BODY" | node -e "
177
- try {
178
- const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
179
- console.log(r.data?.accessTokenInfo?.accessToken || '');
180
- } catch(e) { console.log(''); }
181
- ")
182
-
183
- if [[ -n "$TOKEN" ]]; then
184
- REGISTRY_REMOTE_PATH="$BASE_FOLDER/ota_registry.json"
185
- ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REGISTRY_REMOTE_PATH', safe=''))")
186
- GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
187
- LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
188
- REGISTRY_URL=$(echo "$LINK_RES" | node -e "
189
- try {
190
- const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
191
- console.log(r.data || '');
192
- } catch(e) { console.log(''); }
193
- ")
194
- if [[ -n "$REGISTRY_URL" ]]; then
195
- curl -s -o "$REGISTRY_SHARED" "$REGISTRY_URL"
196
- echo " ✓ Registry synced"
197
- fi
198
- else
199
- echo " ⚠ MinIO login skipped. Proceeding with local state."
200
- fi
201
- else
202
- echo " ⚠ No MinIO credentials. Proceeding with local state."
203
- fi
148
+ # Continuing would later push a stale local registry over the remote one
149
+ rm -f "$REGISTRY_SHARED.tmp"
150
+ echo " ❌ Registry sync failed (HTTP $API_HTTP). Check network / gh auth and retry."
151
+ exit 1
204
152
  fi
205
153
  fi
206
154
 
207
- # ── Helper: Download from Server ──────────────────────────────────────────────
208
- download_from_minio() {
155
+ # ── Helper: Download from remote storage ───────────────────────────────────────
156
+ download_remote() {
209
157
  local REMOTE_PATH="$1"
210
158
  local DEST="$2"
211
- local CB="?cb=$(date +%s)"
212
-
213
- if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"* ]]; then
214
- # Use Cache-Control: no-cache + timestamp query to bypass GitHub CDN 5-min cache
215
- curl -s -f -H "Cache-Control: no-cache" -o "$DEST" "$BASE_URL/$REMOTE_PATH${CB}" 2>/dev/null || \
216
- curl -s -f -H "Cache-Control: no-cache" -o "$DEST" "$BASE_URL/$(basename "$REMOTE_PATH")${CB}" 2>/dev/null
217
- return $?
218
- fi
219
-
220
- if [[ -z "$TOKEN" ]]; then return 1; fi
221
-
222
- local ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REMOTE_PATH', safe=''))")
223
- local GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
224
- local LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
225
- local FILE_URL=$(echo "$LINK_RES" | node -e "
226
- try {
227
- const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
228
- console.log(r.data || '');
229
- } catch(e) { console.log(''); }
230
- ")
231
- if [[ -n "$FILE_URL" ]]; then
232
- # Use -f to return non-zero exit code on HTTP errors (like 404 Not Found)
233
- curl -s -f -o "$DEST" "$FILE_URL"
234
- if [ $? -eq 0 ]; then
235
- return 0
236
- fi
159
+ # no-cache + timestamp query bypasses the GitHub CDN 5-min cache
160
+ if curl -s -f -H "Cache-Control: no-cache" -o "$DEST.tmp" "$BASE_URL/$REMOTE_PATH?cb=$(date +%s)"; then
161
+ mv "$DEST.tmp" "$DEST"
162
+ else
163
+ rm -f "$DEST.tmp"
164
+ return 1
237
165
  fi
238
- return 1
239
166
  }
240
167
 
241
168
  # ── Helper: compute SHA-256 hash ──────────────────────────────────────────────
@@ -368,17 +295,19 @@ if [ "$ACTION" = "reset" ]; then
368
295
  echo '{"apps":{}}' > "$REGISTRY_SHARED"
369
296
  fi
370
297
 
371
- # Delete only output files belonging to this specific flavor
372
- find "$ROOT/android/output" -mindepth 1 -name "*_${FLAVOR_LOWER}.*" -not -name 'base-assets_*' -delete 2>/dev/null || true
373
- find "$ROOT/android/output" -mindepth 1 -name "*_${FLAVOR_LOWER}_*" -not -name 'base-assets_*' -delete 2>/dev/null || true
374
- find "$ROOT/ios/output" -mindepth 1 -name "*_${FLAVOR_LOWER}.*" -not -name 'base-assets_*' -delete 2>/dev/null || true
375
- find "$ROOT/ios/output" -mindepth 1 -name "*_${FLAVOR_LOWER}_*" -not -name 'base-assets_*' -delete 2>/dev/null || true
298
+ # Delete only this app version + flavor. Base bundles (v1 = store binary) are kept:
299
+ # the next build re-registers them automatically. Delete them by hand to re-seed.
300
+ for D in "$ROOT/android/output" "$ROOT/ios/output"; do
301
+ find "$D" -maxdepth 1 -type f -name "*_${APP_VERSION}_*" -name "*_${FLAVOR_LOWER}[._]*" \
302
+ -not -name "bundle_${APP_VERSION}_v1_*" -delete 2>/dev/null || true
303
+ find "$D" -maxdepth 1 -type f -name "asset-manifest-v*_${FLAVOR_LOWER}.json" -delete 2>/dev/null || true # legacy names
304
+ done
376
305
 
377
- echo " ✓ Registry and output files cleared for flavor '$FLAVOR_LOWER' (base-assets preserved)"
306
+ echo " ✓ Registry and output cleared for $APP_VERSION/$FLAVOR_LOWER (base bundle v1 + base-assets kept)"
378
307
 
379
308
  if [ "$PUSH_REMOTE" = true ]; then
380
309
  echo " Syncing reset state for flavor '$FLAVOR_LOWER' to remote storage..."
381
- bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "0" "" "ios" "--clean-remote"
310
+ bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "0" "" "ios" "--clean-remote"
382
311
  fi
383
312
  exit 0
384
313
  fi
@@ -397,23 +326,44 @@ if [ ! -f "$BSDIFF" ]; then
397
326
  echo " ✓ bsdiff compiled"
398
327
  fi
399
328
 
329
+ find_hermesc() {
330
+ HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
331
+ if [ ! -f "$HERMESC_PATH" ]; then
332
+ HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
333
+ fi
334
+ if [ ! -f "$HERMESC_PATH" ]; then
335
+ HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/linux64-bin/hermesc"
336
+ fi
337
+ if [ ! -f "$HERMESC_PATH" ]; then
338
+ HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/linux64-bin/hermesc"
339
+ fi
340
+ if [ ! -f "$HERMESC_PATH" ]; then
341
+ HERMESC_PATH="$(which hermesc 2>/dev/null || echo "")"
342
+ fi
343
+ }
344
+
400
345
  build_platform() {
401
346
  local PLATFORM="$1"
402
347
  local FLAVOR="$2"
403
348
  local CURRENT="$3"
404
349
  local NEXT="$4"
405
- local FLAVOR_CAP=$(echo "$FLAVOR" | perl -ne 'print ucfirst')
406
350
  local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
407
-
351
+ if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
352
+ if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
353
+ local FLAVOR_CAP=$(echo "$FLAVOR_LOWER" | perl -ne 'print ucfirst')
354
+
408
355
  local OUTPUT="$ROOT/android/output"
409
356
  if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
410
357
  local REGISTRY="$REGISTRY_SHARED"
411
358
 
412
359
  echo ""
413
360
  echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
414
-
415
- # ── Bundle ──
416
- # ... (rest of the function omitted for space, but I'll make sure to replace correctly)
361
+ if [ "$PLATFORM" == "ios" ]; then
362
+ local IOS_VER=$(grep -ho 'MARKETING_VERSION = [^;]*' "$ROOT"/ios/*.xcodeproj/project.pbxproj 2>/dev/null | head -n 1 | sed 's/.*= //')
363
+ if [ -n "$IOS_VER" ] && [ "$IOS_VER" != "$APP_VERSION" ]; then
364
+ echo " ⚠ iOS MARKETING_VERSION is $IOS_VER but building for $APP_VERSION — iOS clients on $IOS_VER won't see this OTA"
365
+ fi
366
+ fi
417
367
 
418
368
  # ── Bundle ──
419
369
  # Workspace for building
@@ -427,7 +377,15 @@ build_platform() {
427
377
  cd "$ROOT"
428
378
  BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
429
379
  else
430
- if [ -f "$BUNDLE_XCODE_IOS" ] && [ "$ACTION" = "base" ] && [ $CURRENT -eq 0 ]; then
380
+ local XCODE_STALE=""
381
+ if [ -f "$BUNDLE_XCODE_IOS" ]; then
382
+ # ponytail: mtime check, any source edit/checkout after the last Xcode build marks it stale
383
+ XCODE_STALE=$(find "$ROOT/src" "$ROOT/App.tsx" "$ROOT/index.js" "$ROOT/package.json" -type f -newer "$BUNDLE_XCODE_IOS" -print -quit 2>/dev/null)
384
+ if [ -n "$XCODE_STALE" ] && [ "$ACTION" = "base" ]; then
385
+ echo " ⚠ Xcode bundle ($(stat -f '%Sm' "$BUNDLE_XCODE_IOS")) is older than $XCODE_STALE — ignoring it"
386
+ fi
387
+ fi
388
+ if [ -f "$BUNDLE_XCODE_IOS" ] && [ -z "$XCODE_STALE" ] && [ "$ACTION" = "base" ] && [ $CURRENT -eq 0 ]; then
431
389
  # Base action: use Xcode-compiled bundle (already Hermes-compiled, matches IPA exactly)
432
390
  echo " Using Xcode bundle as base source..."
433
391
  cp "$BUNDLE_XCODE_IOS" "$WORKSPACE/source.bundle"
@@ -446,10 +404,7 @@ build_platform() {
446
404
  fi
447
405
 
448
406
  # Ensure Hermes bytecode format (for both iOS and Android OTA bundles)
449
- HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
450
- if [ ! -f "$HERMESC_PATH" ]; then
451
- HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
452
- fi
407
+ find_hermesc
453
408
 
454
409
  IS_HBC=false
455
410
  if [ -f "$BUNDLE_SOURCE" ]; then
@@ -461,7 +416,11 @@ build_platform() {
461
416
  echo " Bundle is already in Hermes bytecode format."
462
417
  else
463
418
  echo " Compiling to Hermes bytecode..."
464
- "$HERMESC_PATH" -emit-binary -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
419
+ if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
420
+ echo " ❌ ERROR: hermesc not found"; exit 1
421
+ fi
422
+ # -O matches what Xcode/Gradle use for release, keeping patches against the store bundle small
423
+ "$HERMESC_PATH" -emit-binary -O -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
465
424
  mv "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
466
425
  fi
467
426
 
@@ -486,7 +445,7 @@ build_platform() {
486
445
  fi
487
446
 
488
447
  # ── Assets ──
489
- local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest-v${NEXT}_${FLAVOR_LOWER}.json"
448
+ local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
490
449
  local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
491
450
  local ASSET_STORE_DIR="$OUTPUT/asset-store"
492
451
  mkdir -p "$ASSET_STORE_DIR"
@@ -494,20 +453,16 @@ build_platform() {
494
453
  if [ "$PLATFORM" == "ios" ]; then ASSETS_SRC_DIR="$OUTPUT/assets"; fi
495
454
 
496
455
  # Load base assets — try server if missing locally (e.g. after ota:clean)
497
- if [ ! -f "$BASE_ASSET_MANIFEST" ] && [ -n "$TOKEN" ]; then
456
+ if [ ! -f "$BASE_ASSET_MANIFEST" ]; then
498
457
  local REMOTE_BASE_ASSET_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$BASE_ASSET_MANIFEST")"
499
458
  echo " Base asset manifest missing locally, downloading from server..."
500
- download_from_minio "$REMOTE_BASE_ASSET_PATH" "$BASE_ASSET_MANIFEST" || true
459
+ download_remote "$REMOTE_BASE_ASSET_PATH" "$BASE_ASSET_MANIFEST" || true
501
460
  if [ -f "$BASE_ASSET_MANIFEST" ]; then
502
461
  echo " ✓ base-assets manifest restored from server"
503
462
  else
504
463
  echo " ⚠ base-assets manifest not found on server — all assets will be treated as new"
505
464
  fi
506
465
  fi
507
- local BASE_ASSETS_JSON="{}"
508
- if [ -f "$BASE_ASSET_MANIFEST" ]; then
509
- BASE_ASSETS_JSON=$(cat "$BASE_ASSET_MANIFEST")
510
- fi
511
466
 
512
467
  local ASSET_JSON_LIST="["
513
468
  local FIRST=true
@@ -526,12 +481,10 @@ build_platform() {
526
481
  NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
527
482
 
528
483
  # Optimization: Skip if hash matches the base APK version (client already has it)
529
- local IS_IN_BASE=$(node -e "
530
- try {
531
- const base = JSON.parse(require('fs').readFileSync('$BASE_ASSET_MANIFEST', 'utf8'));
532
- console.log(base['$REL_PATH'] === '$HASH' ? 'true' : 'false');
533
- } catch(e) { console.log('false'); }
534
- ")
484
+ local IS_IN_BASE=false
485
+ if [ -f "$BASE_ASSET_MANIFEST" ] && grep -qF "\"$REL_PATH\": \"$HASH\"" "$BASE_ASSET_MANIFEST"; then
486
+ IS_IN_BASE=true
487
+ fi
535
488
  # Always add ALL assets to the manifest so the resolver can find them at runtime.
536
489
  # Only skip the file copy if the asset is unchanged from base (avoids redundant uploads).
537
490
  local ASSET_IS_BASE="false"
@@ -563,13 +516,16 @@ EOF
563
516
  local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
564
517
  if [ $CURRENT -gt 0 ]; then
565
518
  PATCHES_JSON="["
519
+ # Patches from v1 (fresh store installs) + the last 5 versions. Older clients
520
+ # fall back to the full bundle instead of bsdiff-ing every version each build.
566
521
  for (( i=1; i<$NEXT; i++ )); do
522
+ if [ $i -gt 1 ] && [ $i -lt $((NEXT - 5)) ]; then continue; fi
567
523
  local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
568
524
  local OLD_P="$OUTPUT/$OLD_B"
569
525
  if [ ! -f "$OLD_P" ]; then
570
526
  echo " Missing local bundle for v$i. Attempting to download from server..."
571
527
  local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
572
- download_from_minio "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
528
+ download_remote "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
573
529
  fi
574
530
  if [ ! -f "$OLD_P" ]; then
575
531
  echo " ⚠ Warning: Could not find or download bundle v$i. Skipping patch generation from v$i."
@@ -585,14 +541,164 @@ EOF
585
541
 
586
542
  # ── Finalize ──
587
543
  node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
588
- bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
544
+ bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
545
+ }
546
+
547
+ seed_platform() {
548
+ local PLATFORM="$1"
549
+ local FLAVOR="$2"
550
+ local BINARY_PATH="$3"
551
+ local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
552
+ if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
553
+ if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
554
+
555
+ local OUTPUT="$ROOT/android/output"
556
+ if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
557
+ mkdir -p "$OUTPUT"
558
+
559
+ echo ""
560
+ echo ">>> Seeding Base Bundle (v1) | Platform: $PLATFORM | App: $APP_VERSION | Flavor: $FLAVOR_LOWER"
561
+ echo " Source binary: $BINARY_PATH"
562
+
563
+ if [ ! -f "$BINARY_PATH" ]; then
564
+ echo " ❌ ERROR: Source binary file not found: $BINARY_PATH"
565
+ exit 1
566
+ fi
567
+
568
+ local TARGET_BUNDLE="$OUTPUT/bundle_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.hbc"
569
+ local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
570
+ local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.json"
571
+ local TMP_DIR=$(mktemp -d)
572
+
573
+ if [ "$PLATFORM" == "android" ]; then
574
+ echo " Extracting index.android.bundle from Android binary..."
575
+ if unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "assets/index.android.bundle"; then
576
+ unzip -p "$BINARY_PATH" assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
577
+ elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "base/assets/index.android.bundle"; then
578
+ unzip -p "$BINARY_PATH" base/assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
579
+ elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
580
+ cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
581
+ else
582
+ echo " ❌ ERROR: Could not find 'assets/index.android.bundle' inside $BINARY_PATH"
583
+ rm -rf "$TMP_DIR"
584
+ exit 1
585
+ fi
586
+ else
587
+ echo " Extracting main.jsbundle from iOS binary..."
588
+ local IPA_ENTRY=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o 'Payload/[^/]*\.app/main\.jsbundle' | head -n 1)
589
+ if [ -n "$IPA_ENTRY" ]; then
590
+ unzip -p "$BINARY_PATH" "$IPA_ENTRY" > "$TMP_DIR/extracted.bundle"
591
+ elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "main.jsbundle"; then
592
+ local ANY_BUNDLE=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o '[^ ]*main\.jsbundle' | head -n 1)
593
+ unzip -p "$BINARY_PATH" "$ANY_BUNDLE" > "$TMP_DIR/extracted.bundle"
594
+ elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]] || [[ "$BINARY_PATH" == *.jsbundle ]]; then
595
+ cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
596
+ else
597
+ echo " ❌ ERROR: Could not find 'main.jsbundle' inside $BINARY_PATH"
598
+ rm -rf "$TMP_DIR"
599
+ exit 1
600
+ fi
601
+ fi
602
+
603
+ # Check Hermes bytecode format (0xc6 magic number)
604
+ FIRST_BYTE=$(od -An -N1 -t x1 "$TMP_DIR/extracted.bundle" | tr -d ' ' || echo "")
605
+ if [ "$FIRST_BYTE" == "c6" ]; then
606
+ echo " ✓ Bundle is verified Hermes bytecode format."
607
+ cp "$TMP_DIR/extracted.bundle" "$TARGET_BUNDLE"
608
+ else
609
+ echo " Bundle is JavaScript text. Compiling to Hermes bytecode..."
610
+ find_hermesc
611
+ if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
612
+ echo " ❌ ERROR: hermesc compiler not found. Cannot compile bundle to bytecode."
613
+ rm -rf "$TMP_DIR"
614
+ exit 1
615
+ fi
616
+ "$HERMESC_PATH" -emit-binary -O -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
617
+ fi
618
+
619
+ # Extract base assets manifest
620
+ echo " Scanning assets from binary for base manifest..."
621
+ local NEW_BASE_ASSETS="{"
622
+ local ASSET_JSON_LIST="["
623
+ local FIRST=true
624
+
625
+ if [ "$PLATFORM" == "android" ]; then
626
+ unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" "base/res/drawable*/*" "base/res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
627
+ else
628
+ unzip -q -o "$BINARY_PATH" "Payload/*.app/assets/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
629
+ fi
630
+
631
+ if [ -d "$TMP_DIR/assets_unzipped" ]; then
632
+ while IFS= read -r -d '' ASSET_FILE; do
633
+ local HASH=$(sha256 "$ASSET_FILE")
634
+ local FNAME=$(basename "$ASSET_FILE")
635
+ local EX="${FNAME##*.}"
636
+ local REL_PATH="${ASSET_FILE#$TMP_DIR/assets_unzipped/}"
637
+ # Same keys as build_platform: iOS "assets/...", Android "drawable-*/..." / "raw/..."
638
+ REL_PATH=$(echo "$REL_PATH" | sed -E 's#^Payload/[^/]+\.app/##; s#^(base/)?res/##')
639
+
640
+ if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
641
+ NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
642
+
643
+ if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
644
+ ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\", \"isBase\": true}"
645
+ done < <(find "$TMP_DIR/assets_unzipped" -type f -print0 2>/dev/null)
646
+ fi
647
+
648
+ NEW_BASE_ASSETS+="}"
649
+ ASSET_JSON_LIST+="]"
650
+
651
+ echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
652
+ cat > "$ASSET_MANIFEST_FILE" << EOF
653
+ { "version": 1, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
654
+ EOF
655
+
656
+ rm -rf "$TMP_DIR"
657
+
658
+ local BUNDLE_HASH=$(sha256 "$TARGET_BUNDLE")
659
+ echo " ✓ Base bundle v1 successfully created: $(basename "$TARGET_BUNDLE")"
660
+ echo " SHA-256: $BUNDLE_HASH"
661
+
662
+ # Update registry for v1
663
+ node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
664
+
665
+ # Push if requested
666
+ if [ "$PUSH_REMOTE" = true ]; then
667
+ echo " Pushing seeded base bundle to remote..."
668
+ bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "1" "$TARGET_BUNDLE" "$PLATFORM"
669
+ fi
670
+
671
+ echo " ✓ Seeding complete for $PLATFORM!"
589
672
  }
590
673
 
591
674
  # ── Main ───────────────────────────────────────────────────────────────────
592
- echo "=== OTA Dual Build | App: $APP_VERSION | Flavor: $FLAVOR ==="
675
+ echo "=== OTA Build Pipeline | App: $APP_VERSION | Flavor: $FLAVOR ==="
593
676
 
594
- # Read current version once to coordinate across platforms
595
677
  FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
678
+ if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
679
+ if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
680
+
681
+ # ── Seed Action ────────────────────────────────────────────────────────────
682
+ if [ "$ACTION" = "seed" ]; then
683
+ if [ -n "$SEED_APK" ]; then
684
+ seed_platform "android" "$FLAVOR_LOWER" "$SEED_APK"
685
+ elif [ -n "$SEED_IPA" ]; then
686
+ seed_platform "ios" "$FLAVOR_LOWER" "$SEED_IPA"
687
+ elif [ -n "$SEED_BUNDLE" ]; then
688
+ PLATFORM="${PLATFORM_ARG:-android}"
689
+ seed_platform "$PLATFORM" "$FLAVOR_LOWER" "$SEED_BUNDLE"
690
+ else
691
+ echo "❌ Please provide a binary file to seed:"
692
+ echo " ./build_ota.sh seed --apk <path-to-apk>"
693
+ echo " ./build_ota.sh seed --ipa <path-to-ipa>"
694
+ exit 1
695
+ fi
696
+ echo ""
697
+ echo "=== Base Seeding Complete ==="
698
+ exit 0
699
+ fi
700
+
701
+ # Read current version once to coordinate across platforms
596
702
  CURRENT_TOTAL=0
597
703
  if [ -f "$REGISTRY_SHARED" ]; then
598
704
  CURRENT_TOTAL=$(node -e "
@@ -602,6 +708,79 @@ if [ -f "$REGISTRY_SHARED" ]; then
602
708
  } catch(e) { console.log(0); }
603
709
  ")
604
710
  fi
711
+
712
+ # ── Fail-Safe Guard for OTA Updates ────────────────────────────────────────
713
+ # If building an OTA patch but no base version (v1) exists in the registry:
714
+ if [ "$ACTION" = "ota" ] && [ $CURRENT_TOTAL -eq 0 ]; then
715
+ echo "Checking for pre-existing Base Bundle v1..."
716
+
717
+ CHECK_PLATFORMS=()
718
+ if [ -n "$PLATFORM_ARG" ]; then
719
+ CHECK_PLATFORMS=("$PLATFORM_ARG")
720
+ else
721
+ CHECK_PLATFORMS=("android" "ios")
722
+ fi
723
+
724
+ ALL_FOUND=true
725
+ MISSING_PLATFORM=""
726
+ for P in "${CHECK_PLATFORMS[@]}"; do
727
+ P_OUTPUT="$ROOT/$P/output"
728
+ P_BASE_BUNDLE="$P_OUTPUT/bundle_${APP_VERSION}_v1_${P}_${FLAVOR_LOWER}.hbc"
729
+
730
+ # Check if local exists
731
+ if [ ! -f "$P_BASE_BUNDLE" ]; then
732
+ # Try downloading from remote if available
733
+ REMOTE_BASE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$P_BASE_BUNDLE")"
734
+ download_remote "$REMOTE_BASE_PATH" "$P_BASE_BUNDLE" || true
735
+ fi
736
+
737
+ if [ -f "$P_BASE_BUNDLE" ]; then
738
+ echo " ✓ Found Base Bundle v1 for $P: $(basename "$P_BASE_BUNDLE")"
739
+ # Auto-adopt base bundle in registry
740
+ node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$P" "" "$FLAVOR_LOWER"
741
+ else
742
+ ALL_FOUND=false
743
+ MISSING_PLATFORM="$P"
744
+ fi
745
+ done
746
+
747
+ if [ "$ALL_FOUND" = true ]; then
748
+ echo " ✓ Base bundle v1 verified and registered. Proceeding with incremental OTA update..."
749
+ CURRENT_TOTAL=1
750
+ else
751
+ if [ "$FORCE_BASE" = true ]; then
752
+ echo " ⚠ Warning: --force flag detected. Creating base bundle v1 from current working tree..."
753
+ ACTION="base"
754
+ else
755
+ echo ""
756
+ echo "╔══════════════════════════════════════════════════════════════════════════════════════════╗"
757
+ echo "║ ❌ CANNOT BUILD OTA: MISSING BASE BUNDLE (v1) ║"
758
+ echo "╠══════════════════════════════════════════════════════════════════════════════════════════╣"
759
+ echo "║ App Version: $APP_VERSION | Flavor: $FLAVOR_LOWER | Missing: $MISSING_PLATFORM"
760
+ echo "║"
761
+ echo "║ No base bundle (v1) was found for this app version (neither locally nor on remote). ║"
762
+ echo "║ If an OTA update is built now, it would generate a base bundle from your current ║"
763
+ echo "║ working tree, which will MISMATCH the version installed on user devices and force a ║"
764
+ echo "║ 100% full-bundle download (wasting bandwidth and bypassing patch benefits). ║"
765
+ echo "║"
766
+ echo "║ How to resolve: ║"
767
+ echo "║ 1. If you already have the release APK/IPA submitted to Google Play / App Store: ║"
768
+ echo "║ Seed the exact base bundle from your release binary: ║"
769
+ echo "║ ./build_ota.sh seed --apk <path-to-apk> ║"
770
+ echo "║ ./build_ota.sh seed --ipa <path-to-ipa> ║"
771
+ echo "║"
772
+ echo "║ 2. If you are preparing a brand new Store release right now from this commit: ║"
773
+ echo "║ ./build_ota.sh base ║"
774
+ echo "║"
775
+ echo "║ 3. If you really want to force create base v1 from current code: ║"
776
+ echo "║ ./build_ota.sh --force ║"
777
+ echo "╚══════════════════════════════════════════════════════════════════════════════════════════╝"
778
+ echo ""
779
+ exit 1
780
+ fi
781
+ fi
782
+ fi
783
+
605
784
  TARGET_OTA=$((CURRENT_TOTAL + 1))
606
785
 
607
786
  if [ -n "$PLATFORM_ARG" ]; then