react-native-bspatch 1.1.4 → 2.0.1
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/README.md +22 -32
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +15 -102
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +21 -1
- package/cpp/bsdiff/bspatch.c +1 -0
- package/ios/OtaBundleResolver.swift +16 -1
- package/ios/RNBsPatch.mm +61 -154
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +4 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +26 -22
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +8 -2
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +89 -141
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaLogger.js +0 -2
- package/lib/commonjs/ota/otaLogger.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +18 -7
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js +1 -10
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +26 -22
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +8 -2
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +88 -139
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaLogger.js +0 -2
- package/lib/module/ota/otaLogger.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +18 -7
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js +0 -9
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +3 -15
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +5 -42
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaLogger.d.ts +1 -3
- package/lib/typescript/ota/otaLogger.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +2 -2
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +18 -54
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -6
- package/react-native-bspatch.podspec +0 -1
- package/src/NativeBsPatch.ts +3 -16
- package/src/index.ts +1 -1
- package/src/ota/OtaProvider.tsx +24 -23
- package/src/ota/otaAssetResolver.ts +5 -2
- package/src/ota/otaCore.ts +118 -176
- package/src/ota/otaLogger.ts +0 -2
- package/src/ota/otaMetadata.ts +18 -8
- package/src/ota/storage/GitHubRawStorage.ts +1 -1
- package/src/ota/types.ts +18 -57
- package/tools/build_ota.sh +117 -183
- package/tools/setup_project.js +33 -14
- package/tools/update_registry.js +24 -28
- package/tools/{upload_ota_custom.sh → upload_ota.sh} +33 -114
- package/lib/commonjs/crypto-js.d.js +0 -2
- package/lib/commonjs/crypto-js.d.js.map +0 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +0 -160
- package/lib/commonjs/ota/storage/CustomStorage.js.map +0 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +0 -99
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/module/crypto-js.d.js +0 -2
- package/lib/module/crypto-js.d.js.map +0 -1
- package/lib/module/ota/storage/CustomStorage.js +0 -154
- package/lib/module/ota/storage/CustomStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +0 -93
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts +0 -12
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +0 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +0 -20
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +0 -1
- package/src/crypto-js.d.ts +0 -1
- package/src/ota/storage/CustomStorage.ts +0 -166
- package/src/ota/storage/GitHubReleaseStorage.ts +0 -103
- package/tools/bsdiff +0 -0
package/src/ota/types.ts
CHANGED
|
@@ -28,15 +28,6 @@ export enum OtaUpdateMode {
|
|
|
28
28
|
BACKGROUND_DOWNLOAD = 'BACKGROUND_DOWNLOAD',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export enum OtaStorageType {
|
|
32
|
-
/** Custom storage server (primary) */
|
|
33
|
-
CUSTOM = 'CUSTOM',
|
|
34
|
-
/** GitHub Releases — public or token-authenticated */
|
|
35
|
-
GITHUB = 'GITHUB',
|
|
36
|
-
/** GitHub Raw repository storage */
|
|
37
|
-
GITHUB_RAW = 'GITHUB_RAW',
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
export type OtaPatch = {
|
|
41
32
|
/** Unique ID: `v{from}-to-v{to}` */
|
|
42
33
|
id: string;
|
|
@@ -60,10 +51,10 @@ export type OtaPatch = {
|
|
|
60
51
|
minAppVersion?: string;
|
|
61
52
|
/** URL for full bundle download (fallback when patch fails) */
|
|
62
53
|
fullBundleUrl?: string;
|
|
63
|
-
/**
|
|
64
|
-
compressed?: boolean;
|
|
65
|
-
/** HMAC-SHA256 signature for security verification */
|
|
54
|
+
/** RSA/ECDSA SHA-256 signature (base64) of the patch file */
|
|
66
55
|
signature?: string;
|
|
56
|
+
/** RSA/ECDSA SHA-256 signature (base64) of the resulting bundle */
|
|
57
|
+
bundleSignature?: string;
|
|
67
58
|
/** Whether this update is mandatory */
|
|
68
59
|
mandatory?: boolean;
|
|
69
60
|
/** URL to the asset manifest JSON for this version */
|
|
@@ -100,6 +91,10 @@ export type OtaMetadata = {
|
|
|
100
91
|
previousVersions: number[];
|
|
101
92
|
rollbackCount: number;
|
|
102
93
|
lastLaunchSuccessful: boolean;
|
|
94
|
+
/** Native app version the OTA bundles were built for; native resolver drops OTA state when it changes */
|
|
95
|
+
appVersion?: string;
|
|
96
|
+
/** Versions that crashed on boot and were rolled back; never re-applied */
|
|
97
|
+
failedVersions?: number[];
|
|
103
98
|
};
|
|
104
99
|
|
|
105
100
|
export type OtaMetrics = {
|
|
@@ -136,59 +131,25 @@ export type OtaConfig = {
|
|
|
136
131
|
|
|
137
132
|
/** Telemetry: structured event callback for analytics */
|
|
138
133
|
onEvent?: OtaEventCallback;
|
|
139
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* SPKI public key (PEM or base64) for RSA/ECDSA signature verification.
|
|
136
|
+
* When set, patches and bundles without a valid signature are rejected.
|
|
137
|
+
*/
|
|
140
138
|
publicKey?: string;
|
|
141
|
-
/** Base URL for the shared hashed asset store (e.g. a GitHub release URL) */
|
|
142
|
-
assetBaseUrl?: string;
|
|
143
|
-
|
|
144
|
-
/** Storage strategy */
|
|
145
|
-
storageType?: OtaStorageType;
|
|
146
|
-
|
|
147
|
-
/** GitHub Releases configuration (used when storageType === GITHUB) */
|
|
148
|
-
githubRelease?: {
|
|
149
|
-
/** Owner / repo in the format "owner/repo" */
|
|
150
|
-
repo: string;
|
|
151
|
-
/** Tag name of the release to fetch updates from */
|
|
152
|
-
tag: string;
|
|
153
|
-
/**
|
|
154
|
-
* Path inside the release where ota_registry.json lives.
|
|
155
|
-
* If your release contains the file at root, set to "ota_registry.json".
|
|
156
|
-
* If it's inside a folder "ota/", set to "ota/ota_registry.json".
|
|
157
|
-
*/
|
|
158
|
-
registryPath?: string;
|
|
159
|
-
/**
|
|
160
|
-
* Optional GitHub personal access token for private repos or higher rate limits.
|
|
161
|
-
* For public repos this is optional.
|
|
162
|
-
*/
|
|
163
|
-
token?: string;
|
|
164
|
-
/**
|
|
165
|
-
* Asset base URL for content-addressed assets.
|
|
166
|
-
* Supports CDN URLs (jsDelivr, GitHub raw) or a custom base.
|
|
167
|
-
* Example: "https://cdn.jsdelivr.net/gh/owner/repo@tag/assets"
|
|
168
|
-
*/
|
|
169
|
-
assetBaseUrl?: string;
|
|
170
|
-
};
|
|
171
139
|
|
|
172
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Static storage layout read by the default provider: `${baseUrl}/${baseFolder}/...`.
|
|
142
|
+
* Any static host/CDN serving the same layout works (e.g. a public bucket).
|
|
143
|
+
* Read-only and public: never put upload credentials here, they ship inside the app.
|
|
144
|
+
*/
|
|
173
145
|
customServer?: {
|
|
146
|
+
/** e.g. https://raw.githubusercontent.com/<owner>/<repo>/<branch> */
|
|
174
147
|
baseUrl: string;
|
|
175
|
-
username: string;
|
|
176
|
-
password: string;
|
|
177
148
|
channel: string;
|
|
178
149
|
baseFolder?: string;
|
|
179
|
-
useMinio?: boolean;
|
|
180
|
-
/** Override default API endpoints for non-standard servers */
|
|
181
|
-
endpoints?: {
|
|
182
|
-
/** Auth login endpoint (default: /public/authen/login) */
|
|
183
|
-
login?: string;
|
|
184
|
-
/** Generate download link endpoint (default: /api/minio/gen-download-link) */
|
|
185
|
-
genDownloadLink?: string;
|
|
186
|
-
/** Direct file download endpoint (default: /files/downloadStreamFile) */
|
|
187
|
-
downloadFile?: string;
|
|
188
|
-
};
|
|
189
150
|
};
|
|
190
151
|
|
|
191
|
-
/** Storage provider instance (optional, defaults to
|
|
152
|
+
/** Storage provider instance (optional, defaults to GitHubRawStorageProvider) */
|
|
192
153
|
storage?: OtaStorageProvider;
|
|
193
154
|
};
|
|
194
155
|
|
package/tools/build_ota.sh
CHANGED
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
set -e
|
|
16
|
-
|
|
16
|
+
# Resolve symlinks: npx runs us via node_modules/.bin/react-native-bspatch
|
|
17
|
+
SOURCE="$0"
|
|
18
|
+
while [ -L "$SOURCE" ]; do
|
|
19
|
+
LINK_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
20
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
21
|
+
case "$SOURCE" in /*) ;; *) SOURCE="$LINK_DIR/$SOURCE" ;; esac
|
|
22
|
+
done
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
|
|
17
24
|
MODULE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
18
25
|
ROOT="$PWD"
|
|
19
26
|
BUNDLE_XCODE_IOS="$ROOT/ios/output/main.jsbundle"
|
|
@@ -30,10 +37,7 @@ fi
|
|
|
30
37
|
|
|
31
38
|
if [ -n "$ENV_FILE" ]; then
|
|
32
39
|
echo "Loading environment variables from $(basename "$ENV_FILE")..."
|
|
33
|
-
|
|
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]}"
|
|
40
|
+
set -a; . "$ENV_FILE"; set +a
|
|
37
41
|
else
|
|
38
42
|
echo " No .env file found in $ROOT or $MODULE_DIR"
|
|
39
43
|
fi
|
|
@@ -75,7 +79,7 @@ while [[ "$#" -gt 0 ]]; do
|
|
|
75
79
|
elif [[ "$1" == *.ipa ]]; then
|
|
76
80
|
SEED_IPA="$1"
|
|
77
81
|
ACTION="seed"
|
|
78
|
-
elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]]; then
|
|
82
|
+
elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]] || [[ "$1" == *.jsbundle ]]; then
|
|
79
83
|
SEED_BUNDLE="$1"
|
|
80
84
|
ACTION="seed"
|
|
81
85
|
fi
|
|
@@ -88,7 +92,14 @@ FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
|
88
92
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
89
93
|
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
90
94
|
|
|
91
|
-
# ── Setup
|
|
95
|
+
# ── Setup (project wiring only, no build env needed) ─────────────────────────
|
|
96
|
+
if [ "$ACTION" = "setup" ]; then
|
|
97
|
+
echo "Setting up react-native-bspatch in this project..."
|
|
98
|
+
node "$SCRIPT_DIR/setup_project.js" --root "$ROOT"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# ── Build environment ──────────────────────────────────────────────────────
|
|
92
103
|
echo "Preparing build environment..."
|
|
93
104
|
# Kill any existing Metro processes to prevent file locks
|
|
94
105
|
pkill -f "react-native/local-cli/cli.js" || true
|
|
@@ -107,156 +118,65 @@ mkdir -p "$ROOT/ios/output"
|
|
|
107
118
|
REGISTRY_SHARED="$ROOT/ota_registry.json"
|
|
108
119
|
|
|
109
120
|
# ── Registry Sync ──────────────────────────────────────────────────────────
|
|
110
|
-
#
|
|
111
|
-
if [ "$ACTION" != "
|
|
121
|
+
# Reset syncs too: it edits the latest remote registry instead of overwriting it with a stale local copy
|
|
122
|
+
if [ "$ACTION" != "setup" ]; then
|
|
112
123
|
echo "Syncing remote registry..."
|
|
113
124
|
|
|
114
|
-
BASE_URL="${OTA_BASE_URL:-
|
|
115
|
-
UPCODE_USER="${OTA_UPLOAD_USER:-}"
|
|
116
|
-
UPCODE_PASS="${OTA_UPLOAD_PASSWORD:-}"
|
|
125
|
+
BASE_URL="${OTA_BASE_URL:-}"
|
|
117
126
|
BASE_FOLDER="${OTA_FOLDER:-ota}"
|
|
118
127
|
|
|
119
128
|
echo " Using URL: $BASE_URL"
|
|
120
129
|
echo " Using Folder: $BASE_FOLDER"
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
-H "Accept: application/vnd.github.v3+json" \
|
|
153
|
-
"$REGISTRY_API_URL" > "$API_HTTP_FILE" 2>/dev/null || true
|
|
154
|
-
fi
|
|
155
|
-
|
|
156
|
-
API_HTTP=$(cat "$API_HTTP_FILE" 2>/dev/null || echo "0")
|
|
157
|
-
rm -f "$API_HTTP_FILE"
|
|
158
|
-
|
|
159
|
-
if [[ "$API_HTTP" = "200" ]]; then
|
|
160
|
-
# Decode base64 content returned by the GitHub Contents API in one python3 pass
|
|
161
|
-
python3 - "$API_BODY_FILE" "$REGISTRY_SHARED.tmp" << 'PYEOF'
|
|
162
|
-
import sys, json, base64
|
|
163
|
-
body_file, out_file = sys.argv[1], sys.argv[2]
|
|
164
|
-
try:
|
|
165
|
-
with open(body_file, 'r') as f:
|
|
166
|
-
d = json.load(f)
|
|
167
|
-
content = d.get('content', '')
|
|
168
|
-
decoded = base64.b64decode(content.replace('\n', ''))
|
|
169
|
-
with open(out_file, 'wb') as f:
|
|
170
|
-
f.write(decoded)
|
|
171
|
-
except Exception as e:
|
|
172
|
-
sys.exit(1)
|
|
173
|
-
PYEOF
|
|
174
|
-
rm -f "$API_BODY_FILE"
|
|
175
|
-
|
|
176
|
-
if [ -s "$REGISTRY_SHARED.tmp" ]; then
|
|
177
|
-
mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
|
|
178
|
-
echo " ✓ Remote registry synced (via GitHub API)"
|
|
179
|
-
else
|
|
180
|
-
rm -f "$REGISTRY_SHARED.tmp"
|
|
181
|
-
echo " Notice: Remote registry empty or decode failed (starting fresh with local state)."
|
|
182
|
-
fi
|
|
183
|
-
else
|
|
184
|
-
rm -f "$API_BODY_FILE" "$REGISTRY_SHARED.tmp"
|
|
185
|
-
echo " Notice: Remote registry not found on GitHub API (HTTP $API_HTTP — starting fresh with local state)."
|
|
186
|
-
fi
|
|
187
|
-
|
|
131
|
+
# GitHub is the only storage backend for now
|
|
132
|
+
if [[ "$BASE_URL" != "https://raw.githubusercontent.com/"* ]]; then
|
|
133
|
+
echo " ❌ OTA_BASE_URL must be https://raw.githubusercontent.com/<owner>/<repo>/<branch>"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
echo " Downloading remote registry from GitHub..."
|
|
137
|
+
|
|
138
|
+
CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
|
|
139
|
+
GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
|
|
140
|
+
GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
|
|
141
|
+
GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
|
|
142
|
+
if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
|
|
143
|
+
|
|
144
|
+
# Contents API (not the raw CDN) always returns the latest commit; the raw
|
|
145
|
+
# media type has no 1 MB limit and needs no base64 decoding.
|
|
146
|
+
GH_API_TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
147
|
+
GH_API_TOKEN="${GH_API_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
|
|
148
|
+
GH_API_AUTH=()
|
|
149
|
+
if [[ -n "$GH_API_TOKEN" ]]; then GH_API_AUTH=(-H "Authorization: Bearer $GH_API_TOKEN"); fi
|
|
150
|
+
|
|
151
|
+
API_HTTP=$(curl -s -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "${GH_API_AUTH[@]}" \
|
|
152
|
+
-H "Accept: application/vnd.github.raw" \
|
|
153
|
+
"https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}" || echo 0)
|
|
154
|
+
|
|
155
|
+
if [[ "$API_HTTP" == "200" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$REGISTRY_SHARED.tmp" 2>/dev/null; then
|
|
156
|
+
mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
|
|
157
|
+
echo " ✓ Remote registry synced (via GitHub API)"
|
|
158
|
+
elif [[ "$API_HTTP" == "404" ]]; then
|
|
159
|
+
rm -f "$REGISTRY_SHARED.tmp"
|
|
160
|
+
echo " Notice: No remote registry yet (HTTP 404) — starting fresh with local state."
|
|
188
161
|
else
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
-d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
|
|
194
|
-
HTTP_STATUS=$(echo "$LOGIN_RES" | grep -o 'HTTP_STATUS:[0-9]*' | cut -d: -f2)
|
|
195
|
-
LOGIN_BODY=$(echo "$LOGIN_RES" | sed 's/---HTTP_STATUS:.*//')
|
|
196
|
-
|
|
197
|
-
TOKEN=$(echo "$LOGIN_BODY" | node -e "
|
|
198
|
-
try {
|
|
199
|
-
const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
200
|
-
console.log(r.data?.accessTokenInfo?.accessToken || '');
|
|
201
|
-
} catch(e) { console.log(''); }
|
|
202
|
-
")
|
|
203
|
-
|
|
204
|
-
if [[ -n "$TOKEN" ]]; then
|
|
205
|
-
REGISTRY_REMOTE_PATH="$BASE_FOLDER/ota_registry.json"
|
|
206
|
-
ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REGISTRY_REMOTE_PATH', safe=''))")
|
|
207
|
-
GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
|
|
208
|
-
LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
|
|
209
|
-
REGISTRY_URL=$(echo "$LINK_RES" | node -e "
|
|
210
|
-
try {
|
|
211
|
-
const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
212
|
-
console.log(r.data || '');
|
|
213
|
-
} catch(e) { console.log(''); }
|
|
214
|
-
")
|
|
215
|
-
if [[ -n "$REGISTRY_URL" ]]; then
|
|
216
|
-
curl -s -o "$REGISTRY_SHARED" "$REGISTRY_URL"
|
|
217
|
-
echo " ✓ Registry synced"
|
|
218
|
-
fi
|
|
219
|
-
else
|
|
220
|
-
echo " ⚠ MinIO login skipped. Proceeding with local state."
|
|
221
|
-
fi
|
|
222
|
-
else
|
|
223
|
-
echo " ⚠ No MinIO credentials. Proceeding with local state."
|
|
224
|
-
fi
|
|
162
|
+
# Continuing would later push a stale local registry over the remote one
|
|
163
|
+
rm -f "$REGISTRY_SHARED.tmp"
|
|
164
|
+
echo " ❌ Registry sync failed (HTTP $API_HTTP). Check network / gh auth and retry."
|
|
165
|
+
exit 1
|
|
225
166
|
fi
|
|
226
167
|
fi
|
|
227
168
|
|
|
228
|
-
# ── Helper: Download from
|
|
229
|
-
|
|
169
|
+
# ── Helper: Download from remote storage ───────────────────────────────────────
|
|
170
|
+
download_remote() {
|
|
230
171
|
local REMOTE_PATH="$1"
|
|
231
172
|
local DEST="$2"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return $?
|
|
239
|
-
fi
|
|
240
|
-
|
|
241
|
-
if [[ -z "$TOKEN" ]]; then return 1; fi
|
|
242
|
-
|
|
243
|
-
local ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REMOTE_PATH', safe=''))")
|
|
244
|
-
local GEN_LINK_URL="$BASE_URL/api/minio/gen-download-link?pathFile=$ENCODED_PATH"
|
|
245
|
-
local LINK_RES=$(curl -s -X "GET" "$GEN_LINK_URL" -H "Authorization: Bearer $TOKEN")
|
|
246
|
-
local FILE_URL=$(echo "$LINK_RES" | node -e "
|
|
247
|
-
try {
|
|
248
|
-
const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
249
|
-
console.log(r.data || '');
|
|
250
|
-
} catch(e) { console.log(''); }
|
|
251
|
-
")
|
|
252
|
-
if [[ -n "$FILE_URL" ]]; then
|
|
253
|
-
# Use -f to return non-zero exit code on HTTP errors (like 404 Not Found)
|
|
254
|
-
curl -s -f -o "$DEST" "$FILE_URL"
|
|
255
|
-
if [ $? -eq 0 ]; then
|
|
256
|
-
return 0
|
|
257
|
-
fi
|
|
173
|
+
# no-cache + timestamp query bypasses the GitHub CDN 5-min cache
|
|
174
|
+
if curl -s -f -H "Cache-Control: no-cache" -o "$DEST.tmp" "$BASE_URL/$REMOTE_PATH?cb=$(date +%s)"; then
|
|
175
|
+
mv "$DEST.tmp" "$DEST"
|
|
176
|
+
else
|
|
177
|
+
rm -f "$DEST.tmp"
|
|
178
|
+
return 1
|
|
258
179
|
fi
|
|
259
|
-
return 1
|
|
260
180
|
}
|
|
261
181
|
|
|
262
182
|
# ── Helper: compute SHA-256 hash ──────────────────────────────────────────────
|
|
@@ -389,28 +309,23 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
389
309
|
echo '{"apps":{}}' > "$REGISTRY_SHARED"
|
|
390
310
|
fi
|
|
391
311
|
|
|
392
|
-
# Delete only
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
312
|
+
# Delete only this app version + flavor. Base bundles (v1 = store binary) are kept:
|
|
313
|
+
# the next build re-registers them automatically. Delete them by hand to re-seed.
|
|
314
|
+
for D in "$ROOT/android/output" "$ROOT/ios/output"; do
|
|
315
|
+
find "$D" -maxdepth 1 -type f -name "*_${APP_VERSION}_*" -name "*_${FLAVOR_LOWER}[._]*" \
|
|
316
|
+
-not -name "bundle_${APP_VERSION}_v1_*" -delete 2>/dev/null || true
|
|
317
|
+
find "$D" -maxdepth 1 -type f -name "asset-manifest-v*_${FLAVOR_LOWER}.json" -delete 2>/dev/null || true # legacy names
|
|
318
|
+
done
|
|
397
319
|
|
|
398
|
-
echo " ✓ Registry and output
|
|
320
|
+
echo " ✓ Registry and output cleared for $APP_VERSION/$FLAVOR_LOWER (base bundle v1 + base-assets kept)"
|
|
399
321
|
|
|
400
322
|
if [ "$PUSH_REMOTE" = true ]; then
|
|
401
323
|
echo " Syncing reset state for flavor '$FLAVOR_LOWER' to remote storage..."
|
|
402
|
-
bash "$SCRIPT_DIR/
|
|
324
|
+
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "0" "" "ios" "--clean-remote"
|
|
403
325
|
fi
|
|
404
326
|
exit 0
|
|
405
327
|
fi
|
|
406
328
|
|
|
407
|
-
# ── Setup ─────────────────────────────────────────────────────────────────────
|
|
408
|
-
if [ "$ACTION" = "setup" ]; then
|
|
409
|
-
echo "Setting up react-native-bspatch in this project..."
|
|
410
|
-
node "$SCRIPT_DIR/setup_project.js" --root "$ROOT"
|
|
411
|
-
exit 0
|
|
412
|
-
fi
|
|
413
|
-
|
|
414
329
|
# ── Ensure bsdiff compiled ───────────────────────────────────────────────────
|
|
415
330
|
if [ ! -f "$BSDIFF" ]; then
|
|
416
331
|
echo "Compiling bsdiff..."
|
|
@@ -439,15 +354,23 @@ build_platform() {
|
|
|
439
354
|
local FLAVOR="$2"
|
|
440
355
|
local CURRENT="$3"
|
|
441
356
|
local NEXT="$4"
|
|
442
|
-
local FLAVOR_CAP=$(echo "$FLAVOR" | perl -ne 'print ucfirst')
|
|
443
357
|
local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
444
|
-
|
|
358
|
+
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
359
|
+
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
360
|
+
local FLAVOR_CAP=$(echo "$FLAVOR_LOWER" | perl -ne 'print ucfirst')
|
|
361
|
+
|
|
445
362
|
local OUTPUT="$ROOT/android/output"
|
|
446
363
|
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
447
364
|
local REGISTRY="$REGISTRY_SHARED"
|
|
448
365
|
|
|
449
366
|
echo ""
|
|
450
367
|
echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
|
|
368
|
+
if [ "$PLATFORM" == "ios" ]; then
|
|
369
|
+
local IOS_VER=$(grep -ho 'MARKETING_VERSION = [^;]*' "$ROOT"/ios/*.xcodeproj/project.pbxproj 2>/dev/null | head -n 1 | sed 's/.*= //')
|
|
370
|
+
if [ -n "$IOS_VER" ] && [ "$IOS_VER" != "$APP_VERSION" ]; then
|
|
371
|
+
echo " ⚠ iOS MARKETING_VERSION is $IOS_VER but building for $APP_VERSION — iOS clients on $IOS_VER won't see this OTA"
|
|
372
|
+
fi
|
|
373
|
+
fi
|
|
451
374
|
|
|
452
375
|
# ── Bundle ──
|
|
453
376
|
# Workspace for building
|
|
@@ -461,7 +384,15 @@ build_platform() {
|
|
|
461
384
|
cd "$ROOT"
|
|
462
385
|
BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
|
|
463
386
|
else
|
|
464
|
-
|
|
387
|
+
local XCODE_STALE=""
|
|
388
|
+
if [ -f "$BUNDLE_XCODE_IOS" ]; then
|
|
389
|
+
# ponytail: mtime check, any source edit/checkout after the last Xcode build marks it stale
|
|
390
|
+
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)
|
|
391
|
+
if [ -n "$XCODE_STALE" ] && [ "$ACTION" = "base" ]; then
|
|
392
|
+
echo " ⚠ Xcode bundle ($(stat -f '%Sm' "$BUNDLE_XCODE_IOS")) is older than $XCODE_STALE — ignoring it"
|
|
393
|
+
fi
|
|
394
|
+
fi
|
|
395
|
+
if [ -f "$BUNDLE_XCODE_IOS" ] && [ -z "$XCODE_STALE" ] && [ "$ACTION" = "base" ] && [ $CURRENT -eq 0 ]; then
|
|
465
396
|
# Base action: use Xcode-compiled bundle (already Hermes-compiled, matches IPA exactly)
|
|
466
397
|
echo " Using Xcode bundle as base source..."
|
|
467
398
|
cp "$BUNDLE_XCODE_IOS" "$WORKSPACE/source.bundle"
|
|
@@ -492,7 +423,11 @@ build_platform() {
|
|
|
492
423
|
echo " Bundle is already in Hermes bytecode format."
|
|
493
424
|
else
|
|
494
425
|
echo " Compiling to Hermes bytecode..."
|
|
495
|
-
"$HERMESC_PATH"
|
|
426
|
+
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
427
|
+
echo " ❌ ERROR: hermesc not found"; exit 1
|
|
428
|
+
fi
|
|
429
|
+
# -O matches what Xcode/Gradle use for release, keeping patches against the store bundle small
|
|
430
|
+
"$HERMESC_PATH" -emit-binary -O -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
496
431
|
mv "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
497
432
|
fi
|
|
498
433
|
|
|
@@ -517,7 +452,7 @@ build_platform() {
|
|
|
517
452
|
fi
|
|
518
453
|
|
|
519
454
|
# ── Assets ──
|
|
520
|
-
local ASSET_MANIFEST_FILE="$OUTPUT/asset-
|
|
455
|
+
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
521
456
|
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
522
457
|
local ASSET_STORE_DIR="$OUTPUT/asset-store"
|
|
523
458
|
mkdir -p "$ASSET_STORE_DIR"
|
|
@@ -525,20 +460,16 @@ build_platform() {
|
|
|
525
460
|
if [ "$PLATFORM" == "ios" ]; then ASSETS_SRC_DIR="$OUTPUT/assets"; fi
|
|
526
461
|
|
|
527
462
|
# Load base assets — try server if missing locally (e.g. after ota:clean)
|
|
528
|
-
if [ ! -f "$BASE_ASSET_MANIFEST" ]
|
|
463
|
+
if [ ! -f "$BASE_ASSET_MANIFEST" ]; then
|
|
529
464
|
local REMOTE_BASE_ASSET_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$BASE_ASSET_MANIFEST")"
|
|
530
465
|
echo " Base asset manifest missing locally, downloading from server..."
|
|
531
|
-
|
|
466
|
+
download_remote "$REMOTE_BASE_ASSET_PATH" "$BASE_ASSET_MANIFEST" || true
|
|
532
467
|
if [ -f "$BASE_ASSET_MANIFEST" ]; then
|
|
533
468
|
echo " ✓ base-assets manifest restored from server"
|
|
534
469
|
else
|
|
535
470
|
echo " ⚠ base-assets manifest not found on server — all assets will be treated as new"
|
|
536
471
|
fi
|
|
537
472
|
fi
|
|
538
|
-
local BASE_ASSETS_JSON="{}"
|
|
539
|
-
if [ -f "$BASE_ASSET_MANIFEST" ]; then
|
|
540
|
-
BASE_ASSETS_JSON=$(cat "$BASE_ASSET_MANIFEST")
|
|
541
|
-
fi
|
|
542
473
|
|
|
543
474
|
local ASSET_JSON_LIST="["
|
|
544
475
|
local FIRST=true
|
|
@@ -557,12 +488,10 @@ build_platform() {
|
|
|
557
488
|
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
558
489
|
|
|
559
490
|
# Optimization: Skip if hash matches the base APK version (client already has it)
|
|
560
|
-
local IS_IN_BASE
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
} catch(e) { console.log('false'); }
|
|
565
|
-
")
|
|
491
|
+
local IS_IN_BASE=false
|
|
492
|
+
if [ -f "$BASE_ASSET_MANIFEST" ] && grep -qF "\"$REL_PATH\": \"$HASH\"" "$BASE_ASSET_MANIFEST"; then
|
|
493
|
+
IS_IN_BASE=true
|
|
494
|
+
fi
|
|
566
495
|
# Always add ALL assets to the manifest so the resolver can find them at runtime.
|
|
567
496
|
# Only skip the file copy if the asset is unchanged from base (avoids redundant uploads).
|
|
568
497
|
local ASSET_IS_BASE="false"
|
|
@@ -594,13 +523,16 @@ EOF
|
|
|
594
523
|
local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
|
|
595
524
|
if [ $CURRENT -gt 0 ]; then
|
|
596
525
|
PATCHES_JSON="["
|
|
526
|
+
# Patches from v1 (fresh store installs) + the last 5 versions. Older clients
|
|
527
|
+
# fall back to the full bundle instead of bsdiff-ing every version each build.
|
|
597
528
|
for (( i=1; i<$NEXT; i++ )); do
|
|
529
|
+
if [ $i -gt 1 ] && [ $i -lt $((NEXT - 5)) ]; then continue; fi
|
|
598
530
|
local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
599
531
|
local OLD_P="$OUTPUT/$OLD_B"
|
|
600
532
|
if [ ! -f "$OLD_P" ]; then
|
|
601
533
|
echo " Missing local bundle for v$i. Attempting to download from server..."
|
|
602
534
|
local REMOTE_BUNDLE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B"
|
|
603
|
-
|
|
535
|
+
download_remote "$REMOTE_BUNDLE_PATH" "$OLD_P" || true
|
|
604
536
|
fi
|
|
605
537
|
if [ ! -f "$OLD_P" ]; then
|
|
606
538
|
echo " ⚠ Warning: Could not find or download bundle v$i. Skipping patch generation from v$i."
|
|
@@ -616,7 +548,7 @@ EOF
|
|
|
616
548
|
|
|
617
549
|
# ── Finalize ──
|
|
618
550
|
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
619
|
-
bash "$SCRIPT_DIR/
|
|
551
|
+
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
|
|
620
552
|
}
|
|
621
553
|
|
|
622
554
|
seed_platform() {
|
|
@@ -642,7 +574,7 @@ seed_platform() {
|
|
|
642
574
|
|
|
643
575
|
local TARGET_BUNDLE="$OUTPUT/bundle_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
644
576
|
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
645
|
-
local ASSET_MANIFEST_FILE="$OUTPUT/asset-
|
|
577
|
+
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
646
578
|
local TMP_DIR=$(mktemp -d)
|
|
647
579
|
|
|
648
580
|
if [ "$PLATFORM" == "android" ]; then
|
|
@@ -666,7 +598,7 @@ seed_platform() {
|
|
|
666
598
|
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "main.jsbundle"; then
|
|
667
599
|
local ANY_BUNDLE=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o '[^ ]*main\.jsbundle' | head -n 1)
|
|
668
600
|
unzip -p "$BINARY_PATH" "$ANY_BUNDLE" > "$TMP_DIR/extracted.bundle"
|
|
669
|
-
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
601
|
+
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]] || [[ "$BINARY_PATH" == *.jsbundle ]]; then
|
|
670
602
|
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
671
603
|
else
|
|
672
604
|
echo " ❌ ERROR: Could not find 'main.jsbundle' inside $BINARY_PATH"
|
|
@@ -688,7 +620,7 @@ seed_platform() {
|
|
|
688
620
|
rm -rf "$TMP_DIR"
|
|
689
621
|
exit 1
|
|
690
622
|
fi
|
|
691
|
-
"$HERMESC_PATH" -emit-binary -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
|
|
623
|
+
"$HERMESC_PATH" -emit-binary -O -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
|
|
692
624
|
fi
|
|
693
625
|
|
|
694
626
|
# Extract base assets manifest
|
|
@@ -698,7 +630,7 @@ seed_platform() {
|
|
|
698
630
|
local FIRST=true
|
|
699
631
|
|
|
700
632
|
if [ "$PLATFORM" == "android" ]; then
|
|
701
|
-
unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
633
|
+
unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" "base/res/drawable*/*" "base/res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
702
634
|
else
|
|
703
635
|
unzip -q -o "$BINARY_PATH" "Payload/*.app/assets/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
704
636
|
fi
|
|
@@ -709,7 +641,9 @@ seed_platform() {
|
|
|
709
641
|
local FNAME=$(basename "$ASSET_FILE")
|
|
710
642
|
local EX="${FNAME##*.}"
|
|
711
643
|
local REL_PATH="${ASSET_FILE#$TMP_DIR/assets_unzipped/}"
|
|
712
|
-
|
|
644
|
+
# Same keys as build_platform: iOS "assets/...", Android "drawable-*/..." / "raw/..."
|
|
645
|
+
REL_PATH=$(echo "$REL_PATH" | sed -E 's#^Payload/[^/]+\.app/##; s#^(base/)?res/##')
|
|
646
|
+
|
|
713
647
|
if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
|
|
714
648
|
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
715
649
|
|
|
@@ -738,7 +672,7 @@ EOF
|
|
|
738
672
|
# Push if requested
|
|
739
673
|
if [ "$PUSH_REMOTE" = true ]; then
|
|
740
674
|
echo " Pushing seeded base bundle to remote..."
|
|
741
|
-
bash "$SCRIPT_DIR/
|
|
675
|
+
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "1" "$TARGET_BUNDLE" "$PLATFORM"
|
|
742
676
|
fi
|
|
743
677
|
|
|
744
678
|
echo " ✓ Seeding complete for $PLATFORM!"
|
|
@@ -804,7 +738,7 @@ if [ "$ACTION" = "ota" ] && [ $CURRENT_TOTAL -eq 0 ]; then
|
|
|
804
738
|
if [ ! -f "$P_BASE_BUNDLE" ]; then
|
|
805
739
|
# Try downloading from remote if available
|
|
806
740
|
REMOTE_BASE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$P_BASE_BUNDLE")"
|
|
807
|
-
|
|
741
|
+
download_remote "$REMOTE_BASE_PATH" "$P_BASE_BUNDLE" || true
|
|
808
742
|
fi
|
|
809
743
|
|
|
810
744
|
if [ -f "$P_BASE_BUNDLE" ]; then
|