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.
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/android/build.gradle +100 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +139 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchPackage.kt +16 -0
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +227 -0
- package/cpp/CMakeLists.txt +43 -0
- package/cpp/bsdiff/bspatch.c +194 -0
- package/cpp/bsdiff/bspatch.h +41 -0
- package/cpp/bsdiff/bspatch_wrapper.c +150 -0
- package/cpp/bsdiff/bspatch_wrapper.h +6 -0
- package/cpp/bspatch_jni.cpp +41 -0
- package/cpp/bzip2/LICENSE +42 -0
- package/cpp/bzip2/blocksort.c +1094 -0
- package/cpp/bzip2/bzlib.c +1572 -0
- package/cpp/bzip2/bzlib.h +282 -0
- package/cpp/bzip2/bzlib_private.h +509 -0
- package/cpp/bzip2/compress.c +672 -0
- package/cpp/bzip2/crctable.c +104 -0
- package/cpp/bzip2/decompress.c +652 -0
- package/cpp/bzip2/huffman.c +205 -0
- package/cpp/bzip2/randtable.c +84 -0
- package/ios/OtaBundleResolver.swift +241 -0
- package/ios/RNBsPatch.h +8 -0
- package/ios/RNBsPatch.mm +198 -0
- package/lib/commonjs/NativeBsPatch.js +9 -0
- package/lib/commonjs/NativeBsPatch.js.map +1 -0
- package/lib/commonjs/crypto-js.d.js +2 -0
- package/lib/commonjs/crypto-js.d.js.map +1 -0
- package/lib/commonjs/index.js +90 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/ota/OtaProvider.js +306 -0
- package/lib/commonjs/ota/OtaProvider.js.map +1 -0
- package/lib/commonjs/ota/otaAssetResolver.js +145 -0
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +736 -0
- package/lib/commonjs/ota/otaCore.js.map +1 -0
- package/lib/commonjs/ota/otaLogger.js +64 -0
- package/lib/commonjs/ota/otaLogger.js.map +1 -0
- package/lib/commonjs/ota/otaMetadata.js +138 -0
- package/lib/commonjs/ota/otaMetadata.js.map +1 -0
- package/lib/commonjs/ota/storage/CustomStorage.js +140 -0
- package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +96 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -0
- package/lib/commonjs/ota/types.js +39 -0
- package/lib/commonjs/ota/types.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativeBsPatch.js +5 -0
- package/lib/module/NativeBsPatch.js.map +1 -0
- package/lib/module/crypto-js.d.js +2 -0
- package/lib/module/crypto-js.d.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/ota/OtaProvider.js +299 -0
- package/lib/module/ota/OtaProvider.js.map +1 -0
- package/lib/module/ota/otaAssetResolver.js +138 -0
- package/lib/module/ota/otaAssetResolver.js.map +1 -0
- package/lib/module/ota/otaCore.js +725 -0
- package/lib/module/ota/otaCore.js.map +1 -0
- package/lib/module/ota/otaLogger.js +59 -0
- package/lib/module/ota/otaLogger.js.map +1 -0
- package/lib/module/ota/otaMetadata.js +127 -0
- package/lib/module/ota/otaMetadata.js.map +1 -0
- package/lib/module/ota/storage/CustomStorage.js +134 -0
- package/lib/module/ota/storage/CustomStorage.js.map +1 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js +90 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -0
- package/lib/module/ota/types.js +38 -0
- package/lib/module/ota/types.js.map +1 -0
- package/lib/typescript/NativeBsPatch.d.ts +39 -0
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +15 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/ota/OtaProvider.d.ts +10 -0
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -0
- package/lib/typescript/ota/otaAssetResolver.d.ts +15 -0
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts +109 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -0
- package/lib/typescript/ota/otaLogger.d.ts +44 -0
- package/lib/typescript/ota/otaLogger.d.ts.map +1 -0
- package/lib/typescript/ota/otaMetadata.d.ts +46 -0
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -0
- package/lib/typescript/ota/storage/CustomStorage.d.ts +12 -0
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -0
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +20 -0
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +191 -0
- package/lib/typescript/ota/types.d.ts.map +1 -0
- package/package.json +89 -0
- package/react-native-bspatch.podspec +46 -0
- package/src/NativeBsPatch.ts +52 -0
- package/src/crypto-js.d.ts +1 -0
- package/src/index.ts +29 -0
- package/src/ota/OtaProvider.tsx +399 -0
- package/src/ota/otaAssetResolver.ts +182 -0
- package/src/ota/otaCore.ts +963 -0
- package/src/ota/otaLogger.ts +72 -0
- package/src/ota/otaMetadata.ts +149 -0
- package/src/ota/storage/CustomStorage.ts +143 -0
- package/src/ota/storage/GitHubReleaseStorage.ts +98 -0
- package/src/ota/types.ts +211 -0
- package/tools/bsdiff +0 -0
- package/tools/bsdiff.c +445 -0
- package/tools/bsdiff.h +45 -0
- package/tools/build_ota.sh +457 -0
- package/tools/setup_project.js +273 -0
- package/tools/update_registry.js +145 -0
- 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
|
+
|