react-native-bspatch 1.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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +471 -0
  3. package/android/build.gradle +100 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +139 -0
  6. package/android/src/main/java/vn/reactnativebspatch/BsPatchPackage.kt +16 -0
  7. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +227 -0
  8. package/cpp/CMakeLists.txt +43 -0
  9. package/cpp/bsdiff/bspatch.c +194 -0
  10. package/cpp/bsdiff/bspatch.h +41 -0
  11. package/cpp/bsdiff/bspatch_wrapper.c +150 -0
  12. package/cpp/bsdiff/bspatch_wrapper.h +6 -0
  13. package/cpp/bspatch_jni.cpp +41 -0
  14. package/cpp/bzip2/LICENSE +42 -0
  15. package/cpp/bzip2/blocksort.c +1094 -0
  16. package/cpp/bzip2/bzlib.c +1572 -0
  17. package/cpp/bzip2/bzlib.h +282 -0
  18. package/cpp/bzip2/bzlib_private.h +509 -0
  19. package/cpp/bzip2/compress.c +672 -0
  20. package/cpp/bzip2/crctable.c +104 -0
  21. package/cpp/bzip2/decompress.c +652 -0
  22. package/cpp/bzip2/huffman.c +205 -0
  23. package/cpp/bzip2/randtable.c +84 -0
  24. package/ios/OtaBundleResolver.swift +241 -0
  25. package/ios/RNBsPatch.h +8 -0
  26. package/ios/RNBsPatch.mm +198 -0
  27. package/lib/commonjs/NativeBsPatch.js +9 -0
  28. package/lib/commonjs/NativeBsPatch.js.map +1 -0
  29. package/lib/commonjs/crypto-js.d.js +2 -0
  30. package/lib/commonjs/crypto-js.d.js.map +1 -0
  31. package/lib/commonjs/index.js +90 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/ota/OtaProvider.js +306 -0
  34. package/lib/commonjs/ota/OtaProvider.js.map +1 -0
  35. package/lib/commonjs/ota/otaAssetResolver.js +145 -0
  36. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -0
  37. package/lib/commonjs/ota/otaCore.js +736 -0
  38. package/lib/commonjs/ota/otaCore.js.map +1 -0
  39. package/lib/commonjs/ota/otaLogger.js +64 -0
  40. package/lib/commonjs/ota/otaLogger.js.map +1 -0
  41. package/lib/commonjs/ota/otaMetadata.js +138 -0
  42. package/lib/commonjs/ota/otaMetadata.js.map +1 -0
  43. package/lib/commonjs/ota/storage/CustomStorage.js +140 -0
  44. package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -0
  45. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +96 -0
  46. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -0
  47. package/lib/commonjs/ota/types.js +39 -0
  48. package/lib/commonjs/ota/types.js.map +1 -0
  49. package/lib/commonjs/package.json +1 -0
  50. package/lib/module/NativeBsPatch.js +5 -0
  51. package/lib/module/NativeBsPatch.js.map +1 -0
  52. package/lib/module/crypto-js.d.js +2 -0
  53. package/lib/module/crypto-js.d.js.map +1 -0
  54. package/lib/module/index.js +23 -0
  55. package/lib/module/index.js.map +1 -0
  56. package/lib/module/ota/OtaProvider.js +299 -0
  57. package/lib/module/ota/OtaProvider.js.map +1 -0
  58. package/lib/module/ota/otaAssetResolver.js +138 -0
  59. package/lib/module/ota/otaAssetResolver.js.map +1 -0
  60. package/lib/module/ota/otaCore.js +725 -0
  61. package/lib/module/ota/otaCore.js.map +1 -0
  62. package/lib/module/ota/otaLogger.js +59 -0
  63. package/lib/module/ota/otaLogger.js.map +1 -0
  64. package/lib/module/ota/otaMetadata.js +127 -0
  65. package/lib/module/ota/otaMetadata.js.map +1 -0
  66. package/lib/module/ota/storage/CustomStorage.js +134 -0
  67. package/lib/module/ota/storage/CustomStorage.js.map +1 -0
  68. package/lib/module/ota/storage/GitHubReleaseStorage.js +90 -0
  69. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -0
  70. package/lib/module/ota/types.js +38 -0
  71. package/lib/module/ota/types.js.map +1 -0
  72. package/lib/typescript/NativeBsPatch.d.ts +39 -0
  73. package/lib/typescript/NativeBsPatch.d.ts.map +1 -0
  74. package/lib/typescript/index.d.ts +15 -0
  75. package/lib/typescript/index.d.ts.map +1 -0
  76. package/lib/typescript/ota/OtaProvider.d.ts +10 -0
  77. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -0
  78. package/lib/typescript/ota/otaAssetResolver.d.ts +15 -0
  79. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -0
  80. package/lib/typescript/ota/otaCore.d.ts +109 -0
  81. package/lib/typescript/ota/otaCore.d.ts.map +1 -0
  82. package/lib/typescript/ota/otaLogger.d.ts +44 -0
  83. package/lib/typescript/ota/otaLogger.d.ts.map +1 -0
  84. package/lib/typescript/ota/otaMetadata.d.ts +46 -0
  85. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -0
  86. package/lib/typescript/ota/storage/CustomStorage.d.ts +12 -0
  87. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -0
  88. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +20 -0
  89. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -0
  90. package/lib/typescript/ota/types.d.ts +191 -0
  91. package/lib/typescript/ota/types.d.ts.map +1 -0
  92. package/package.json +89 -0
  93. package/react-native-bspatch.podspec +46 -0
  94. package/src/NativeBsPatch.ts +52 -0
  95. package/src/crypto-js.d.ts +1 -0
  96. package/src/index.ts +29 -0
  97. package/src/ota/OtaProvider.tsx +399 -0
  98. package/src/ota/otaAssetResolver.ts +182 -0
  99. package/src/ota/otaCore.ts +963 -0
  100. package/src/ota/otaLogger.ts +72 -0
  101. package/src/ota/otaMetadata.ts +149 -0
  102. package/src/ota/storage/CustomStorage.ts +143 -0
  103. package/src/ota/storage/GitHubReleaseStorage.ts +98 -0
  104. package/src/ota/types.ts +211 -0
  105. package/tools/bsdiff +0 -0
  106. package/tools/bsdiff.c +445 -0
  107. package/tools/bsdiff.h +45 -0
  108. package/tools/build_ota.sh +457 -0
  109. package/tools/setup_project.js +273 -0
  110. package/tools/update_registry.js +145 -0
  111. package/tools/upload_ota_custom.sh +138 -0
@@ -0,0 +1,457 @@
1
+ #!/bin/bash
2
+
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # OTA Build Script — incremental, one version per run
5
+ #
6
+ # Supports single-step updates (v1 -> v3 direct) and version grouping.
7
+ # Generates SHA-256 hashes for integrity verification.
8
+ #
9
+ # Usage:
10
+ # ./build_ota.sh → build next OTA version for current App.tsx
11
+ # ./build_ota.sh base → first build: create base APK/IPA + save base bundle
12
+ # ./build_ota.sh reset → wipe registry and start fresh
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ set -e
16
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+ MODULE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ ROOT="$PWD"
19
+ BUNDLE_XCODE_IOS="$ROOT/ios/output/main.jsbundle"
20
+ BSDIFF="$SCRIPT_DIR/bsdiff"
21
+ GENERATED_TS="$ROOT/src/ota/otaPatches.generated.ts"
22
+
23
+ # ── Load .env file if exists ─────────────────────────────────────────────────
24
+ # Look in project root first, then module directory (fallback)
25
+ ENV_FILE=""
26
+ if [ -f "$ROOT/.env" ]; then
27
+ ENV_FILE="$ROOT/.env"
28
+ elif [ -f "$MODULE_DIR/.env" ]; then
29
+ ENV_FILE="$MODULE_DIR/.env"
30
+ fi
31
+
32
+ if [ -n "$ENV_FILE" ]; then
33
+ echo "Loading environment variables from $(basename "$ENV_FILE")..."
34
+ export $(grep -v '^#' "$ENV_FILE" | grep -v '^$' | xargs)
35
+ # Debug: verify loaded variables
36
+ echo " Loaded OTA_UPLOAD_USER=${OTA_UPLOAD_USER:+[SET]}"
37
+ echo " Loaded OTA_UPLOAD_PASSWORD=${OTA_UPLOAD_PASSWORD:+[SET]}"
38
+ else
39
+ echo " No .env file found in $ROOT or $MODULE_DIR"
40
+ fi
41
+
42
+ # ── Parameters ─────────────────────────────────────────────────────────────
43
+ FLAVOR="dev"
44
+ ACTION="ota"
45
+ PLATFORM_ARG=""
46
+
47
+ # Arg parsing
48
+ while [[ "$#" -gt 0 ]]; do
49
+ case $1 in
50
+ --platform) PLATFORM_ARG="$2"; shift ;;
51
+ --flavor) FLAVOR="$2"; shift ;;
52
+ reset|base) ACTION="$1" ;;
53
+ setup) ACTION="$1" ;;
54
+ apk|ipa|seed) ACTION="base" ;; # Compatibility for legacy/seed actions
55
+ dev|staging|production) FLAVOR="$1" ;; # Support positional flavor
56
+ [0-9]*) ;; # Ignore version if passed as $1 (pos 1 usually)
57
+ esac
58
+ shift
59
+ done
60
+ # Configuration
61
+ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
62
+
63
+ # ── Setup ──────────────────────────────────────────────────────────────────
64
+ echo "Preparing build environment..."
65
+ # Kill any existing Metro processes to prevent file locks
66
+ pkill -f "react-native/local-cli/cli.js" || true
67
+ pkill -f "metro-config/src/index.js" || true
68
+
69
+ # Manually clear metro cache if it exists (robust vs ENOTEMPTY error)
70
+ METRO_CACHE_DIR=$(node -e "console.log(require('os').tmpdir())")/metro-cache
71
+ if [ -d "$METRO_CACHE_DIR" ]; then
72
+ echo " Cleaning Metro cache..."
73
+ rm -rf "$METRO_CACHE_DIR"
74
+ fi
75
+
76
+ mkdir -p "$ROOT/android/output"
77
+ mkdir -p "$ROOT/ios/output"
78
+ # Shared registry file for dual-platform builds
79
+ REGISTRY_SHARED="$ROOT/ota_registry.json"
80
+
81
+ # ── Registry Sync ──────────────────────────────────────────────────────────
82
+ # Skip registry sync for reset/clean actions
83
+ if [ "$ACTION" != "reset" ]; then
84
+ # Download the latest ota_registry.json from server so we don't clobber other platforms
85
+ echo "Syncing remote registry..."
86
+
87
+ # Load credentials from environment variables (required for security)
88
+ # Upload credentials are used by the build script
89
+ BASE_URL="${OTA_BASE_URL:-https://sync-server.osp.com.vn}"
90
+ UPCODE_USER="${OTA_UPLOAD_USER:-}"
91
+ UPCODE_PASS="${OTA_UPLOAD_PASSWORD:-}"
92
+ BASE_FOLDER="${OTA_FOLDER:-congplqg}"
93
+
94
+ # Debug: show credentials (without exposing password)
95
+ echo " Using URL: $BASE_URL"
96
+ echo " Using User: $UPCODE_USER"
97
+ echo " Using Folder: $BASE_FOLDER"
98
+
99
+ # Validate required credentials
100
+ if [[ -z "$UPCODE_USER" ]] || [[ -z "$UPCODE_PASS" ]]; then
101
+ echo "ERROR: OTA upload credentials not set. Please set OTA_UPLOAD_USER and OTA_UPLOAD_PASSWORD in .env file."
102
+ echo " OTA_UPLOAD_USER=\"your-username\""
103
+ echo " OTA_UPLOAD_PASSWORD=\"your-password\""
104
+ echo " Optional: OTA_BASE_URL OTA_FOLDER"
105
+ exit 1
106
+ fi
107
+
108
+ # Debug: show curl command for manual testing
109
+ echo " Login URL: ${BASE_URL}/public/authen/login"
110
+
111
+ LOGIN_RES=$(curl -s -X "POST" "$BASE_URL/public/authen/login" \
112
+ -H 'Content-Type: application/json; charset=utf-8' \
113
+ -w "\n---HTTP_STATUS:%{http_code}" \
114
+ -d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
115
+
116
+ # Extract HTTP status and response body
117
+ HTTP_STATUS=$(echo "$LOGIN_RES" | grep -o 'HTTP_STATUS:[0-9]*' | cut -d: -f2)
118
+ LOGIN_BODY=$(echo "$LOGIN_RES" | sed 's/---HTTP_STATUS:.*//')
119
+
120
+ # Debug: show login response on error or empty
121
+ if [ -z "$LOGIN_BODY" ] || [ "$HTTP_STATUS" != "200" ]; then
122
+ echo " ⚠ Login failed - HTTP $HTTP_STATUS"
123
+ echo " ⚠ Response: $LOGIN_BODY"
124
+ fi
125
+
126
+ TOKEN=$(echo "$LOGIN_BODY" | node -e "
127
+ try {
128
+ const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
129
+ console.log(r.data?.accessTokenInfo?.accessToken || '');
130
+ } catch(e) { console.log(''); }
131
+ ")
132
+
133
+ if [[ -n "$TOKEN" ]]; then
134
+ REGISTRY_REMOTE_PATH="$BASE_FOLDER/ota_registry.json"
135
+ # Encode slash as %2F for MinIO API
136
+ ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REGISTRY_REMOTE_PATH', safe=''))")
137
+ GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
138
+
139
+ LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
140
+ REGISTRY_URL=$(echo "$LINK_RES" | node -e "
141
+ try {
142
+ const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
143
+ console.log(r.data || '');
144
+ } catch(e) { console.log(''); }
145
+ ")
146
+
147
+ if [[ -n "$REGISTRY_URL" ]]; then
148
+ echo " Downloading remote registry..."
149
+ curl -s -o "$REGISTRY_SHARED" "$REGISTRY_URL"
150
+ echo " ✓ Registry synced"
151
+ else
152
+ echo " ⚠ Remote registry not found (starting fresh)."
153
+ # Do not delete existing local registry if sync fails, just proceed
154
+ fi
155
+ else
156
+ echo " ⚠ Login failed during registry sync. Proceeding with local state."
157
+ fi
158
+ fi
159
+
160
+ # ── Helper: Download from Server ──────────────────────────────────────────────
161
+ download_from_minio() {
162
+ local REMOTE_PATH="$1"
163
+ local DEST="$2"
164
+ if [[ -z "$TOKEN" ]]; then return 1; fi
165
+
166
+ local ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REMOTE_PATH', safe=''))")
167
+ local GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
168
+ local LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
169
+ local FILE_URL=$(echo "$LINK_RES" | node -e "
170
+ try {
171
+ const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
172
+ console.log(r.data || '');
173
+ } catch(e) { console.log(''); }
174
+ ")
175
+ if [[ -n "$FILE_URL" ]]; then
176
+ # Use -f to return non-zero exit code on HTTP errors (like 404 Not Found)
177
+ curl -s -f -o "$DEST" "$FILE_URL"
178
+ if [ $? -eq 0 ]; then
179
+ return 0
180
+ fi
181
+ fi
182
+ return 1
183
+ }
184
+
185
+ # ── Helper: compute SHA-256 hash ──────────────────────────────────────────────
186
+ sha256() {
187
+ shasum -a 256 "$1" | awk '{print $1}'
188
+ }
189
+
190
+ # ── Extract APP_VERSION from otaConfig.ts ─────────────────────────────────────
191
+ APP_VERSION=$(node -e "
192
+ const fs = require('fs');
193
+ const content = fs.readFileSync('$ROOT/src/ota/otaConfig.ts', 'utf8');
194
+ const match = content.match(/export const APP_VERSION = ['\"](.+?)['\"]/);
195
+ console.log(match ? match[1] : 'unknown');
196
+ ")
197
+
198
+ if [ "$APP_VERSION" == "unknown" ]; then
199
+ echo "ERROR: Could not find APP_VERSION in otaConfig.ts"
200
+ exit 1
201
+ fi
202
+
203
+ # ── Reset ─────────────────────────────────────────────────────────────────────
204
+ if [ "$ACTION" = "reset" ]; then
205
+ echo "Resetting OTA registry and clearing output..."
206
+ rm -f "$REGISTRY_SHARED"
207
+ # Preserve base-assets manifests so the build after reset can still
208
+ # correctly identify which assets are already bundled in the base app.
209
+ # Only delete bundles, patches, manifests and workspace — not base-assets.
210
+ find "$ROOT/android/output" -mindepth 1 -not -name 'base-assets_*' -delete 2>/dev/null || true
211
+ find "$ROOT/ios/output" -mindepth 1 -not -name 'base-assets_*' -delete 2>/dev/null || true
212
+ cat > "$GENERATED_TS" << 'TSEEOF'
213
+ // AUTO-GENERATED by build_ota.sh — do not edit manually
214
+ import type { OtaPatch } from 'react-native-bspatch';
215
+
216
+ export const OTA_PATCHES: OtaPatch[] = [];
217
+ TSEEOF
218
+ echo " ✓ Registry and output files cleared (base-assets preserved)"
219
+ exit 0
220
+ fi
221
+
222
+ # ── Setup ─────────────────────────────────────────────────────────────────────
223
+ if [ "$ACTION" = "setup" ]; then
224
+ echo "Setting up react-native-bspatch in this project..."
225
+ node "$SCRIPT_DIR/setup_project.js" --root "$ROOT"
226
+ exit 0
227
+ fi
228
+
229
+ # ── Ensure bsdiff compiled ───────────────────────────────────────────────────
230
+ if [ ! -f "$BSDIFF" ]; then
231
+ echo "Compiling bsdiff..."
232
+ clang -O2 -D BSDIFF_EXECUTABLE -o "$BSDIFF" "$SCRIPT_DIR/bsdiff.c" -lbz2
233
+ echo " ✓ bsdiff compiled"
234
+ fi
235
+
236
+ build_platform() {
237
+ local PLATFORM="$1"
238
+ local FLAVOR="$2"
239
+ local CURRENT="$3"
240
+ local NEXT="$4"
241
+ local FLAVOR_CAP=$(echo "$FLAVOR" | perl -ne 'print ucfirst')
242
+ local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
243
+
244
+ local OUTPUT="$ROOT/android/output"
245
+ if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
246
+ local REGISTRY="$REGISTRY_SHARED"
247
+
248
+ echo ""
249
+ echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
250
+
251
+ # ── Bundle ──
252
+ # ... (rest of the function omitted for space, but I'll make sure to replace correctly)
253
+
254
+ # ── Bundle ──
255
+ # Workspace for building
256
+ local WORKSPACE="$OUTPUT/workspace"
257
+ mkdir -p "$WORKSPACE"
258
+
259
+ # Determine source bundle
260
+ if [ "$PLATFORM" == "android" ]; then
261
+ cd "$ROOT/android"
262
+ ./gradlew ":app:createBundle${FLAVOR_CAP}ReleaseJsAndAssets" -Pkotlin.incremental=false --quiet
263
+ cd "$ROOT"
264
+ BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
265
+ else
266
+ if [ -f "$BUNDLE_XCODE_IOS" ] && [ "$ACTION" = "base" ] && [ $CURRENT -eq 0 ]; then
267
+ # Base action: use Xcode-compiled bundle (already Hermes-compiled, matches IPA exactly)
268
+ echo " Using Xcode bundle as base source..."
269
+ cp "$BUNDLE_XCODE_IOS" "$WORKSPACE/source.bundle"
270
+ # Also discover assets for base manifest
271
+ echo " Discovering assets from project (shadow bundle)..."
272
+ npx react-native bundle --platform ios --dev false --entry-file index.js \
273
+ --bundle-output "$WORKSPACE/shadow.bundle.tmp" --assets-dest "$OUTPUT/assets"
274
+ rm -f "$WORKSPACE/shadow.bundle.tmp"
275
+ else
276
+ # OTA update: always generate fresh bundle from current code + discover assets
277
+ echo " Generating fresh iOS bundle from current code..."
278
+ npx react-native bundle --platform ios --dev false --entry-file index.js \
279
+ --bundle-output "$WORKSPACE/source.bundle" --assets-dest "$OUTPUT/assets"
280
+ fi
281
+ BUNDLE_SOURCE="$WORKSPACE/source.bundle"
282
+ fi
283
+
284
+ # Ensure Hermes bytecode format (for both iOS and Android OTA bundles)
285
+ HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
286
+ if [ ! -f "$HERMESC_PATH" ]; then
287
+ HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
288
+ fi
289
+
290
+ IS_HBC=false
291
+ if [ -f "$BUNDLE_SOURCE" ]; then
292
+ FIRST_BYTE=$(od -An -N1 -t x1 "$BUNDLE_SOURCE" | tr -d ' ' || echo "")
293
+ if [ "$FIRST_BYTE" == "c6" ]; then IS_HBC=true; fi
294
+ fi
295
+
296
+ if [ "$IS_HBC" = true ]; then
297
+ echo " Bundle is already in Hermes bytecode format."
298
+ else
299
+ echo " Compiling to Hermes bytecode..."
300
+ "$HERMESC_PATH" -emit-binary -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
301
+ mv "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
302
+ fi
303
+
304
+ local BUNDLE_NAME="bundle_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
305
+ local NEW_BUNDLE_PATH="$OUTPUT/$BUNDLE_NAME"
306
+
307
+ if [ $CURRENT -eq 0 ] && [ "$ACTION" = "base" ] && [ "$PLATFORM" == "android" ]; then
308
+ echo "Building base Release APK for $FLAVOR_CAP..."
309
+ cd "$ROOT/android"
310
+ ./gradlew "assemble${FLAVOR_CAP}Release" -Pkotlin.incremental=false --quiet
311
+ cd "$ROOT"
312
+ # Locate the APK dynamically as flavor names can vary in path
313
+ APK_PATH=$(find "$ROOT/android/app/build/outputs/apk" -name "app-${FLAVOR_LOWER}-release.apk" | head -n 1)
314
+ if [ -f "$APK_PATH" ]; then
315
+ unzip -p "$APK_PATH" assets/index.android.bundle > "$NEW_BUNDLE_PATH"
316
+ else
317
+ echo " ⚠ Warning: Could not find base APK at $APK_PATH. Using generated bundle directly."
318
+ cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
319
+ fi
320
+ else
321
+ cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
322
+ fi
323
+
324
+ # ── Assets ──
325
+ local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest-v${NEXT}_${FLAVOR_LOWER}.json"
326
+ local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
327
+ local ASSET_STORE_DIR="$OUTPUT/asset-store"
328
+ mkdir -p "$ASSET_STORE_DIR"
329
+ local ASSETS_SRC_DIR="$ROOT/android/app/build/generated/res/react/${FLAVOR_LOWER}Release"
330
+ if [ "$PLATFORM" == "ios" ]; then ASSETS_SRC_DIR="$OUTPUT/assets"; fi
331
+
332
+ # Load base assets — try server if missing locally (e.g. after ota:clean)
333
+ if [ ! -f "$BASE_ASSET_MANIFEST" ] && [ -n "$TOKEN" ]; then
334
+ local REMOTE_BASE_ASSET_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$BASE_ASSET_MANIFEST")"
335
+ echo " Base asset manifest missing locally, downloading from server..."
336
+ download_from_minio "$REMOTE_BASE_ASSET_PATH" "$BASE_ASSET_MANIFEST" || true
337
+ if [ -f "$BASE_ASSET_MANIFEST" ]; then
338
+ echo " ✓ base-assets manifest restored from server"
339
+ else
340
+ echo " ⚠ base-assets manifest not found on server — all assets will be treated as new"
341
+ fi
342
+ fi
343
+ local BASE_ASSETS_JSON="{}"
344
+ if [ -f "$BASE_ASSET_MANIFEST" ]; then
345
+ BASE_ASSETS_JSON=$(cat "$BASE_ASSET_MANIFEST")
346
+ fi
347
+
348
+ local ASSET_JSON_LIST="["
349
+ local FIRST=true
350
+ local NEW_BASE_ASSETS="{"
351
+
352
+ if [ -d "$ASSETS_SRC_DIR" ]; then
353
+ while IFS= read -r -d '' ASSET_FILE; do
354
+ local HASH=$(sha256 "$ASSET_FILE")
355
+ local FNAME=$(basename "$ASSET_FILE")
356
+ local EX="${FNAME##*.}"
357
+ # Capture relative path for resolver matching
358
+ local REL_PATH="${ASSET_FILE#$ASSETS_SRC_DIR/}"
359
+
360
+ # Track for the new base manifest
361
+ if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
362
+ NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
363
+
364
+ # Optimization: Skip if hash matches the base APK version (client already has it)
365
+ local IS_IN_BASE=$(node -e "
366
+ try {
367
+ const base = JSON.parse(require('fs').readFileSync('$BASE_ASSET_MANIFEST', 'utf8'));
368
+ console.log(base['$REL_PATH'] === '$HASH' ? 'true' : 'false');
369
+ } catch(e) { console.log('false'); }
370
+ ")
371
+ # Always add ALL assets to the manifest so the resolver can find them at runtime.
372
+ # Only skip the file copy if the asset is unchanged from base (avoids redundant uploads).
373
+ local ASSET_IS_BASE="false"
374
+ if [ "$IS_IN_BASE" != "true" ] || [ "$ACTION" = "base" ]; then
375
+ cp "$ASSET_FILE" "$ASSET_STORE_DIR/${HASH}.${EX}"
376
+ else
377
+ ASSET_IS_BASE="true"
378
+ fi
379
+
380
+ if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
381
+ ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\", \"isBase\": $ASSET_IS_BASE}"
382
+ done < <(find "$ASSETS_SRC_DIR" -type f -print0 2>/dev/null)
383
+ fi
384
+ ASSET_JSON_LIST+="]"
385
+ NEW_BASE_ASSETS+="}"
386
+
387
+ # If base action, save the manifest to local disk for future comparisons
388
+ if [ "$ACTION" = "base" ]; then
389
+ echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
390
+ echo " ✓ Base asset manifest saved locally"
391
+ fi
392
+
393
+ cat > "$ASSET_MANIFEST_FILE" << EOF
394
+ { "version": $NEXT, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
395
+ EOF
396
+
397
+ # ── Patches ──
398
+ local PATCHES_JSON="[]"
399
+ local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
400
+ if [ $CURRENT -gt 0 ]; then
401
+ PATCHES_JSON="["
402
+ for (( i=1; i<$NEXT; i++ )); do
403
+ local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
404
+ local OLD_P="$OUTPUT/$OLD_B"
405
+ if [ ! -f "$OLD_P" ]; then
406
+ echo " Missing local bundle for v$i. Attempting to download from server..."
407
+ local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
408
+ download_from_minio "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
409
+ fi
410
+ if [ ! -f "$OLD_P" ]; then
411
+ echo " ⚠ Warning: Could not find or download bundle v$i. Skipping patch generation from v$i."
412
+ continue
413
+ fi
414
+ local PNAME="patch_${APP_VERSION}_v${i}_to_v${NEXT}_${FLAVOR_LOWER}_${PLATFORM}.patch"
415
+ "$BSDIFF" "$OLD_P" "$NEW_BUNDLE_PATH" "$OUTPUT/$PNAME"
416
+ if [ "$PATCHES_JSON" != "[" ]; then PATCHES_JSON+=", "; fi
417
+ PATCHES_JSON+="{\"from\": $i, \"to\": $NEXT, \"file\": \"$PNAME\", \"baseHash\": \"$(sha256 "$OLD_P")\", \"bundleHash\": \"$NEW_HASH\"}"
418
+ done
419
+ PATCHES_JSON+="]"
420
+ fi
421
+
422
+ # ── Finalize ──
423
+ node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
424
+ bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
425
+ }
426
+
427
+ # ── Main ───────────────────────────────────────────────────────────────────
428
+ echo "=== OTA Dual Build | App: $APP_VERSION | Flavor: $FLAVOR ==="
429
+
430
+ # Read current version once to coordinate across platforms
431
+ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
432
+ CURRENT_TOTAL=0
433
+ if [ -f "$REGISTRY_SHARED" ]; then
434
+ CURRENT_TOTAL=$(node -e "
435
+ try {
436
+ const reg = JSON.parse(require('fs').readFileSync('$REGISTRY_SHARED','utf8'));
437
+ console.log(reg.apps['$APP_VERSION']?.flavors?.['$FLAVOR_LOWER']?.latestOtaVersion || 0);
438
+ } catch(e) { console.log(0); }
439
+ ")
440
+ fi
441
+ TARGET_OTA=$((CURRENT_TOTAL + 1))
442
+
443
+ if [ -n "$PLATFORM_ARG" ]; then
444
+ build_platform "$PLATFORM_ARG" "$FLAVOR" "$CURRENT_TOTAL" "$TARGET_OTA"
445
+ else
446
+ # Build both
447
+ build_platform "android" "$FLAVOR" "$CURRENT_TOTAL" "$TARGET_OTA"
448
+ build_platform "ios" "$FLAVOR" "$CURRENT_TOTAL" "$TARGET_OTA"
449
+ fi
450
+
451
+ echo ""
452
+ echo "=== All Builds Complete ==="
453
+
454
+
455
+
456
+
457
+