react-native-bspatch 1.1.2 → 1.1.4
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 +145 -42
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +117 -17
- package/cpp/CMakeLists.txt +9 -1
- package/cpp/bsdiff/bspatch.c +14 -3
- package/cpp/bsdiff/bspatch_wrapper.c +124 -40
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +61 -12
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +43 -20
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +41 -5
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +23 -3
- package/lib/commonjs/ota/storage/CustomStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +61 -12
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaCore.js +43 -20
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +38 -3
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/CustomStorage.js +23 -3
- package/lib/module/ota/storage/CustomStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -0
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +3 -0
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +11 -0
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +3 -1
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +6 -0
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +6 -2
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/react-native-bspatch.podspec +6 -1
- package/src/NativeBsPatch.ts +17 -0
- package/src/ota/OtaProvider.tsx +80 -23
- package/src/ota/otaCore.ts +54 -24
- package/src/ota/otaMetadata.ts +46 -4
- package/src/ota/storage/CustomStorage.ts +26 -3
- package/src/ota/storage/GitHubRawStorage.ts +5 -0
- package/src/ota/storage/GitHubReleaseStorage.ts +5 -0
- package/src/ota/types.ts +6 -2
- package/tools/build_ota.sh +366 -18
- package/tools/bump_version.js +133 -0
- package/tools/generate_keys.js +75 -0
- package/tools/setup_project.js +69 -4
- package/tools/update_registry.js +51 -1
package/src/ota/otaMetadata.ts
CHANGED
|
@@ -106,9 +106,51 @@ export const registerNewVersion = (
|
|
|
106
106
|
};
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Scans the OTA directory and removes old version directories and leftover temporary files
|
|
111
|
+
* that are no longer referenced in the active metadata.
|
|
112
|
+
*/
|
|
113
|
+
export const cleanupOldVersions = async (
|
|
114
|
+
metadata: OtaMetadata,
|
|
115
|
+
baseVersion: number,
|
|
116
|
+
): Promise<void> => {
|
|
117
|
+
try {
|
|
118
|
+
if (!(await ReactNativeBlobUtil.fs.exists(OTA_DIR))) return;
|
|
119
|
+
const items = await ReactNativeBlobUtil.fs.ls(OTA_DIR);
|
|
120
|
+
|
|
121
|
+
// Retain current active version and rollback versions
|
|
122
|
+
const retainedVersions = new Set<number>([
|
|
123
|
+
metadata.currentVersion,
|
|
124
|
+
...(metadata.previousVersions || []),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
// Check if directory matches "v<number>"
|
|
129
|
+
const versionMatch = item.match(/^v(\d+)$/);
|
|
130
|
+
if (versionMatch) {
|
|
131
|
+
const ver = parseInt(versionMatch[1], 10);
|
|
132
|
+
if (!retainedVersions.has(ver) && ver !== baseVersion) {
|
|
133
|
+
const dirPath = `${OTA_DIR}/${item}`;
|
|
134
|
+
await ReactNativeBlobUtil.fs.unlink(dirPath).catch(() => {});
|
|
135
|
+
if (__DEV__) {
|
|
136
|
+
console.log(`[otaMetadata] Cleaned up obsolete OTA version: ${item}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Clean up leftover temporary bundle files (.tmp)
|
|
141
|
+
if (item.endsWith('.tmp')) {
|
|
142
|
+
await ReactNativeBlobUtil.fs.unlink(`${OTA_DIR}/${item}`).catch(() => {});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.warn('[otaMetadata] Cleanup old versions failed:', e);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
109
150
|
/**
|
|
110
151
|
* Marks the current session as healthy, indicating a successful boot.
|
|
111
152
|
* This prevents the native bridge from rolling back the version on the next launch.
|
|
153
|
+
* Also cleans up obsolete version directories from disk.
|
|
112
154
|
*
|
|
113
155
|
* @param defaultVersion - Base version for metadata lookup fallback.
|
|
114
156
|
* @returns The updated metadata.
|
|
@@ -117,16 +159,16 @@ export const markHealthy = async (
|
|
|
117
159
|
defaultVersion: number,
|
|
118
160
|
): Promise<OtaMetadata> => {
|
|
119
161
|
const metadata = await loadMetadata(defaultVersion);
|
|
120
|
-
if (metadata.lastLaunchSuccessful && metadata.rollbackCount === 0) {
|
|
121
|
-
return metadata;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
162
|
const updated: OtaMetadata = {
|
|
125
163
|
...metadata,
|
|
126
164
|
lastLaunchSuccessful: true,
|
|
127
165
|
rollbackCount: 0,
|
|
128
166
|
};
|
|
129
167
|
await saveMetadata(updated);
|
|
168
|
+
|
|
169
|
+
// Garbage collect older versions on disk to free storage
|
|
170
|
+
await cleanupOldVersions(updated, defaultVersion);
|
|
171
|
+
|
|
130
172
|
return updated;
|
|
131
173
|
};
|
|
132
174
|
|
|
@@ -7,8 +7,15 @@ interface TokenCache {
|
|
|
7
7
|
expiresAt: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface RegistryCache {
|
|
11
|
+
etag?: string;
|
|
12
|
+
patches: OtaPatch[];
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/** Cache tokens per server endpoint so switching configs invalidates stale tokens. */
|
|
11
16
|
const tokenCache = new Map<string, TokenCache>();
|
|
17
|
+
/** Cache registry ETag and parsed patches to avoid redownloading unmodified manifests. */
|
|
18
|
+
const registryCache = new Map<string, RegistryCache>();
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Implementation for the custom MinIO-based storage server.
|
|
@@ -60,6 +67,8 @@ export class CustomStorageProvider implements OtaStorageProvider {
|
|
|
60
67
|
|
|
61
68
|
const { baseUrl, channel, baseFolder = 'bspatch' } = config.customServer;
|
|
62
69
|
const registryPath = `${baseFolder}/ota_registry.json`;
|
|
70
|
+
const regCacheKey = `${baseUrl}|${registryPath}|${config.appVersion}|${channel}`;
|
|
71
|
+
const cachedReg = registryCache.get(regCacheKey);
|
|
63
72
|
|
|
64
73
|
try {
|
|
65
74
|
const token = await this.getAccessToken(config);
|
|
@@ -74,11 +83,23 @@ export class CustomStorageProvider implements OtaStorageProvider {
|
|
|
74
83
|
const registryUrl = linkJson.data;
|
|
75
84
|
if (!registryUrl) throw new Error('Failed to generate registry download link');
|
|
76
85
|
|
|
77
|
-
const
|
|
86
|
+
const reqHeaders: Record<string, string> = {};
|
|
87
|
+
if (cachedReg?.etag) {
|
|
88
|
+
reqHeaders['If-None-Match'] = cachedReg.etag;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const registryRes = await ReactNativeBlobUtil.fetch('GET', registryUrl, reqHeaders);
|
|
92
|
+
if (registryRes.respInfo.status === 304 && cachedReg) {
|
|
93
|
+
return cachedReg.patches;
|
|
94
|
+
}
|
|
78
95
|
if (registryRes.respInfo.status !== 200) {
|
|
79
96
|
throw new Error(`Registry fetch failed with status ${registryRes.respInfo.status}`);
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
const etag = registryRes.respInfo.headers?.['etag'] || registryRes.respInfo.headers?.['ETag'];
|
|
100
|
+
if (!config.appVersion) {
|
|
101
|
+
throw new Error('[CustomStorage] config.appVersion is required to query OTA registry');
|
|
102
|
+
}
|
|
82
103
|
const registry = JSON.parse(registryRes.data);
|
|
83
104
|
const appGroup = registry?.apps?.[config.appVersion];
|
|
84
105
|
const flavorGroup = appGroup?.flavors?.[channel.toLowerCase()];
|
|
@@ -87,10 +108,12 @@ export class CustomStorageProvider implements OtaStorageProvider {
|
|
|
87
108
|
return config.bundledPatches || [];
|
|
88
109
|
}
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
const matchedPatches = (flavorGroup.patches || []).filter((p: OtaPatch) => p.platform === Platform.OS);
|
|
112
|
+
registryCache.set(regCacheKey, { etag, patches: matchedPatches });
|
|
113
|
+
return matchedPatches;
|
|
91
114
|
} catch (error) {
|
|
92
115
|
console.warn('[CustomStorage] Manifest fetch error:', (error as Error).message);
|
|
93
|
-
return config.bundledPatches || [];
|
|
116
|
+
return cachedReg?.patches || config.bundledPatches || [];
|
|
94
117
|
}
|
|
95
118
|
}
|
|
96
119
|
|
|
@@ -61,6 +61,11 @@ export class GitHubRawStorageProvider implements OtaStorageProvider {
|
|
|
61
61
|
? JSON.parse(res.data)
|
|
62
62
|
: res.data;
|
|
63
63
|
|
|
64
|
+
if (!config.appVersion) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'[GitHubRawStorage] config.appVersion is required to query OTA registry',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
64
69
|
const appGroup = registry?.apps?.[config.appVersion];
|
|
65
70
|
const flavorGroup = appGroup?.flavors?.[channelName];
|
|
66
71
|
|
|
@@ -59,6 +59,11 @@ export class GitHubReleaseStorageProvider implements OtaStorageProvider {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Structured: apps["1.0.0"].channels["production"].patches[]
|
|
62
|
+
if (!config.appVersion) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'[GitHubReleaseStorage] config.appVersion is required to query OTA registry',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
62
67
|
const appGroup = registry?.apps?.[config.appVersion];
|
|
63
68
|
const channelGroup = appGroup?.channels?.[config.githubRelease.repo] // use repo as channel fallback
|
|
64
69
|
?? appGroup?.channels?.production
|
package/src/ota/types.ts
CHANGED
|
@@ -117,8 +117,8 @@ export type OtaNetworkStrategy = {
|
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
export type OtaConfig = {
|
|
120
|
-
/** Current app versionName (e.g. "1.0.0") */
|
|
121
|
-
appVersion
|
|
120
|
+
/** Current app versionName (e.g. "1.0.0"). If omitted, auto-detected from native app. */
|
|
121
|
+
appVersion?: string;
|
|
122
122
|
/** Initial version baked into the APK (usually 1) */
|
|
123
123
|
baseAppVersion: number;
|
|
124
124
|
/** Default patches if manifest fetch fails */
|
|
@@ -213,8 +213,12 @@ export type OtaState = {
|
|
|
213
213
|
pendingCount: number;
|
|
214
214
|
isChecking: boolean;
|
|
215
215
|
metrics: OtaMetrics | null;
|
|
216
|
+
/** True if a background update has been downloaded and prepared silently */
|
|
217
|
+
isUpdateReady: boolean;
|
|
216
218
|
applyUpdate: () => Promise<void>;
|
|
217
219
|
resetToFactory: () => Promise<void>;
|
|
218
220
|
markAsHealthy: () => Promise<void>;
|
|
219
221
|
checkUpdate: () => Promise<void>;
|
|
222
|
+
/** Reload the React Native JS bundle immediately to apply downloaded update */
|
|
223
|
+
reloadBundle: () => void;
|
|
220
224
|
};
|
package/tools/build_ota.sh
CHANGED
|
@@ -42,19 +42,44 @@ fi
|
|
|
42
42
|
FLAVOR="dev"
|
|
43
43
|
ACTION="ota"
|
|
44
44
|
PLATFORM_ARG=""
|
|
45
|
+
APP_VERSION_ARG=""
|
|
46
|
+
NON_INTERACTIVE=false
|
|
47
|
+
|
|
48
|
+
SEED_APK=""
|
|
49
|
+
SEED_IPA=""
|
|
50
|
+
SEED_BUNDLE=""
|
|
51
|
+
FORCE_BASE=false
|
|
45
52
|
|
|
46
|
-
PUSH_REMOTE=false
|
|
47
53
|
# Arg parsing
|
|
48
54
|
while [[ "$#" -gt 0 ]]; do
|
|
49
55
|
case $1 in
|
|
50
56
|
--platform) PLATFORM_ARG="$2"; shift ;;
|
|
51
57
|
--flavor) FLAVOR="$2"; shift ;;
|
|
58
|
+
--app-version) APP_VERSION_ARG="$2"; shift ;;
|
|
59
|
+
--apk) SEED_APK="$2"; ACTION="seed"; shift ;;
|
|
60
|
+
--ipa) SEED_IPA="$2"; ACTION="seed"; shift ;;
|
|
61
|
+
--bundle) SEED_BUNDLE="$2"; ACTION="seed"; shift ;;
|
|
62
|
+
--force) FORCE_BASE=true ;;
|
|
63
|
+
-y|--yes|--non-interactive) NON_INTERACTIVE=true ;;
|
|
52
64
|
--push) PUSH_REMOTE=true ;;
|
|
53
65
|
reset|base) ACTION="$1" ;;
|
|
54
66
|
setup) ACTION="$1" ;;
|
|
55
|
-
|
|
67
|
+
seed) ACTION="seed" ;;
|
|
68
|
+
apk|ipa) ACTION="base" ;; # Compatibility for legacy actions
|
|
56
69
|
dev|staging|production) FLAVOR="$1" ;; # Support positional flavor
|
|
57
70
|
[0-9]*) ;; # Ignore version if passed as $1 (pos 1 usually)
|
|
71
|
+
*)
|
|
72
|
+
if [[ "$1" == *.apk ]] || [[ "$1" == *.aab ]]; then
|
|
73
|
+
SEED_APK="$1"
|
|
74
|
+
ACTION="seed"
|
|
75
|
+
elif [[ "$1" == *.ipa ]]; then
|
|
76
|
+
SEED_IPA="$1"
|
|
77
|
+
ACTION="seed"
|
|
78
|
+
elif [[ "$1" == *.hbc ]] || [[ "$1" == *.bundle ]]; then
|
|
79
|
+
SEED_BUNDLE="$1"
|
|
80
|
+
ACTION="seed"
|
|
81
|
+
fi
|
|
82
|
+
;;
|
|
58
83
|
esac
|
|
59
84
|
shift
|
|
60
85
|
done
|
|
@@ -239,19 +264,111 @@ sha256() {
|
|
|
239
264
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
240
265
|
}
|
|
241
266
|
|
|
242
|
-
# ──
|
|
243
|
-
|
|
267
|
+
# ── Auto-Detect APP_VERSION ──────────────────────────────────────────────────
|
|
268
|
+
APP_VERSION_INFO=$(node -e "
|
|
244
269
|
const fs = require('fs');
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
270
|
+
const path = require('path');
|
|
271
|
+
const root = '$ROOT';
|
|
272
|
+
const cliOverride = '$APP_VERSION_ARG';
|
|
273
|
+
|
|
274
|
+
if (cliOverride && cliOverride !== '') {
|
|
275
|
+
console.log(JSON.stringify({ version: cliOverride, source: 'CLI --app-version' }));
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 1. Try android/app/build.gradle (most reliable for Android release)
|
|
280
|
+
try {
|
|
281
|
+
const gradlePath = path.join(root, 'android/app/build.gradle');
|
|
282
|
+
if (fs.existsSync(gradlePath)) {
|
|
283
|
+
const content = fs.readFileSync(gradlePath, 'utf8');
|
|
284
|
+
const match = content.match(/versionName\\s+[\"']([^\"']+)[\"']/);
|
|
285
|
+
if (match && match[1] !== '0.0.0' && match[1] !== '0.0.1') {
|
|
286
|
+
console.log(JSON.stringify({ version: match[1], source: 'android/app/build.gradle' }));
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {}
|
|
291
|
+
|
|
292
|
+
// 2. Try ios project.pbxproj (most reliable for iOS release)
|
|
293
|
+
try {
|
|
294
|
+
const iosDir = path.join(root, 'ios');
|
|
295
|
+
if (fs.existsSync(iosDir)) {
|
|
296
|
+
const files = fs.readdirSync(iosDir);
|
|
297
|
+
for (const f of files) {
|
|
298
|
+
if (f.endsWith('.xcodeproj')) {
|
|
299
|
+
const pbxPath = path.join(iosDir, f, 'project.pbxproj');
|
|
300
|
+
if (fs.existsSync(pbxPath)) {
|
|
301
|
+
const content = fs.readFileSync(pbxPath, 'utf8');
|
|
302
|
+
const match = content.match(/MARKETING_VERSION\\s*=\\s*([^;]+);/);
|
|
303
|
+
if (match && match[1].trim() !== '0.0.0' && match[1].trim() !== '0.0.1') {
|
|
304
|
+
console.log(JSON.stringify({ version: match[1].trim(), source: 'ios/' + f }));
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {}
|
|
312
|
+
|
|
313
|
+
// 3. Try package.json
|
|
314
|
+
try {
|
|
315
|
+
const pkgPath = path.join(root, 'package.json');
|
|
316
|
+
if (fs.existsSync(pkgPath)) {
|
|
317
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
318
|
+
if (pkg.version && pkg.version !== '0.0.0' && pkg.version !== '0.0.1') {
|
|
319
|
+
console.log(JSON.stringify({ version: pkg.version, source: 'package.json' }));
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {}
|
|
324
|
+
|
|
325
|
+
// 4. Fallback: src/ota/otaConfig.ts
|
|
326
|
+
try {
|
|
327
|
+
const otaConfigPath = path.join(root, 'src/ota/otaConfig.ts');
|
|
328
|
+
if (fs.existsSync(otaConfigPath)) {
|
|
329
|
+
const content = fs.readFileSync(otaConfigPath, 'utf8');
|
|
330
|
+
const match = content.match(/export const APP_VERSION = ['\"](.+?)['\"]/);
|
|
331
|
+
if (match) {
|
|
332
|
+
console.log(JSON.stringify({ version: match[1], source: 'src/ota/otaConfig.ts' }));
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {}
|
|
337
|
+
|
|
338
|
+
console.log(JSON.stringify({ version: 'unknown', source: 'none' }));
|
|
248
339
|
")
|
|
249
340
|
|
|
250
|
-
|
|
251
|
-
|
|
341
|
+
APP_VERSION=$(node -e "try { console.log(JSON.parse('$APP_VERSION_INFO').version); } catch(e) { console.log('unknown'); }")
|
|
342
|
+
APP_VERSION_SOURCE=$(node -e "try { console.log(JSON.parse('$APP_VERSION_INFO').source); } catch(e) { console.log('none'); }")
|
|
343
|
+
|
|
344
|
+
# Interactive confirmation / prompt:
|
|
345
|
+
# Prompt if running in an interactive terminal, not in reset mode, and no explicit flags passed
|
|
346
|
+
if [ -t 0 ] && [ -z "$APP_VERSION_ARG" ] && [ "$NON_INTERACTIVE" = false ] && [ "$ACTION" != "reset" ]; then
|
|
347
|
+
echo ""
|
|
348
|
+
echo "────────────────────────────────────────────────────────"
|
|
349
|
+
if [ "$APP_VERSION" != "unknown" ]; then
|
|
350
|
+
echo " Detected Native App Version: $APP_VERSION ($APP_VERSION_SOURCE)"
|
|
351
|
+
read -p " Enter App Version to build [Press Enter for '$APP_VERSION']: " USER_INPUT_VER
|
|
352
|
+
else
|
|
353
|
+
read -p " Enter Native App Version to build (e.g. 1.0.0): " USER_INPUT_VER
|
|
354
|
+
fi
|
|
355
|
+
echo "────────────────────────────────────────────────────────"
|
|
356
|
+
echo ""
|
|
357
|
+
|
|
358
|
+
if [ -n "$USER_INPUT_VER" ]; then
|
|
359
|
+
APP_VERSION=$(echo "$USER_INPUT_VER" | tr -d ' ')
|
|
360
|
+
APP_VERSION_SOURCE="User Input"
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
if [ "$APP_VERSION" == "unknown" ] || [ -z "$APP_VERSION" ]; then
|
|
365
|
+
echo "ERROR: Native App Version is required to build OTA updates."
|
|
366
|
+
echo " Please enter a valid version or use: ./build_ota.sh --app-version <version>"
|
|
252
367
|
exit 1
|
|
253
368
|
fi
|
|
254
369
|
|
|
370
|
+
echo " ✓ Building OTA for APP_VERSION=$APP_VERSION (Source: $APP_VERSION_SOURCE)"
|
|
371
|
+
|
|
255
372
|
# ── Reset ─────────────────────────────────────────────────────────────────────
|
|
256
373
|
if [ "$ACTION" = "reset" ]; then
|
|
257
374
|
echo "Resetting OTA registry and output for flavor '$FLAVOR_LOWER'..."
|
|
@@ -301,6 +418,22 @@ if [ ! -f "$BSDIFF" ]; then
|
|
|
301
418
|
echo " ✓ bsdiff compiled"
|
|
302
419
|
fi
|
|
303
420
|
|
|
421
|
+
find_hermesc() {
|
|
422
|
+
HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/osx-bin/hermesc"
|
|
423
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
424
|
+
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
|
|
425
|
+
fi
|
|
426
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
427
|
+
HERMESC_PATH="$ROOT/node_modules/hermes-compiler/hermesc/linux64-bin/hermesc"
|
|
428
|
+
fi
|
|
429
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
430
|
+
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/linux64-bin/hermesc"
|
|
431
|
+
fi
|
|
432
|
+
if [ ! -f "$HERMESC_PATH" ]; then
|
|
433
|
+
HERMESC_PATH="$(which hermesc 2>/dev/null || echo "")"
|
|
434
|
+
fi
|
|
435
|
+
}
|
|
436
|
+
|
|
304
437
|
build_platform() {
|
|
305
438
|
local PLATFORM="$1"
|
|
306
439
|
local FLAVOR="$2"
|
|
@@ -316,9 +449,6 @@ build_platform() {
|
|
|
316
449
|
echo ""
|
|
317
450
|
echo ">>> Building $PLATFORM | Version: $APP_VERSION | OTA: v$CURRENT → v$NEXT"
|
|
318
451
|
|
|
319
|
-
# ── Bundle ──
|
|
320
|
-
# ... (rest of the function omitted for space, but I'll make sure to replace correctly)
|
|
321
|
-
|
|
322
452
|
# ── Bundle ──
|
|
323
453
|
# Workspace for building
|
|
324
454
|
local WORKSPACE="$OUTPUT/workspace"
|
|
@@ -350,10 +480,7 @@ build_platform() {
|
|
|
350
480
|
fi
|
|
351
481
|
|
|
352
482
|
# Ensure Hermes bytecode format (for both iOS and Android OTA bundles)
|
|
353
|
-
|
|
354
|
-
if [ ! -f "$HERMESC_PATH" ]; then
|
|
355
|
-
HERMESC_PATH="$ROOT/node_modules/react-native/sdks/hermesc/osx-bin/hermesc"
|
|
356
|
-
fi
|
|
483
|
+
find_hermesc
|
|
357
484
|
|
|
358
485
|
IS_HBC=false
|
|
359
486
|
if [ -f "$BUNDLE_SOURCE" ]; then
|
|
@@ -492,11 +619,159 @@ EOF
|
|
|
492
619
|
bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM"
|
|
493
620
|
}
|
|
494
621
|
|
|
622
|
+
seed_platform() {
|
|
623
|
+
local PLATFORM="$1"
|
|
624
|
+
local FLAVOR="$2"
|
|
625
|
+
local BINARY_PATH="$3"
|
|
626
|
+
local FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
627
|
+
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
628
|
+
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
629
|
+
|
|
630
|
+
local OUTPUT="$ROOT/android/output"
|
|
631
|
+
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
632
|
+
mkdir -p "$OUTPUT"
|
|
633
|
+
|
|
634
|
+
echo ""
|
|
635
|
+
echo ">>> Seeding Base Bundle (v1) | Platform: $PLATFORM | App: $APP_VERSION | Flavor: $FLAVOR_LOWER"
|
|
636
|
+
echo " Source binary: $BINARY_PATH"
|
|
637
|
+
|
|
638
|
+
if [ ! -f "$BINARY_PATH" ]; then
|
|
639
|
+
echo " ❌ ERROR: Source binary file not found: $BINARY_PATH"
|
|
640
|
+
exit 1
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
local TARGET_BUNDLE="$OUTPUT/bundle_${APP_VERSION}_v1_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
644
|
+
local BASE_ASSET_MANIFEST="$OUTPUT/base-assets_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
645
|
+
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest-v1_${FLAVOR_LOWER}.json"
|
|
646
|
+
local TMP_DIR=$(mktemp -d)
|
|
647
|
+
|
|
648
|
+
if [ "$PLATFORM" == "android" ]; then
|
|
649
|
+
echo " Extracting index.android.bundle from Android binary..."
|
|
650
|
+
if unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "assets/index.android.bundle"; then
|
|
651
|
+
unzip -p "$BINARY_PATH" assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
652
|
+
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "base/assets/index.android.bundle"; then
|
|
653
|
+
unzip -p "$BINARY_PATH" base/assets/index.android.bundle > "$TMP_DIR/extracted.bundle"
|
|
654
|
+
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
655
|
+
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
656
|
+
else
|
|
657
|
+
echo " ❌ ERROR: Could not find 'assets/index.android.bundle' inside $BINARY_PATH"
|
|
658
|
+
rm -rf "$TMP_DIR"
|
|
659
|
+
exit 1
|
|
660
|
+
fi
|
|
661
|
+
else
|
|
662
|
+
echo " Extracting main.jsbundle from iOS binary..."
|
|
663
|
+
local IPA_ENTRY=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o 'Payload/[^/]*\.app/main\.jsbundle' | head -n 1)
|
|
664
|
+
if [ -n "$IPA_ENTRY" ]; then
|
|
665
|
+
unzip -p "$BINARY_PATH" "$IPA_ENTRY" > "$TMP_DIR/extracted.bundle"
|
|
666
|
+
elif unzip -l "$BINARY_PATH" 2>/dev/null | grep -q "main.jsbundle"; then
|
|
667
|
+
local ANY_BUNDLE=$(unzip -l "$BINARY_PATH" 2>/dev/null | grep -o '[^ ]*main\.jsbundle' | head -n 1)
|
|
668
|
+
unzip -p "$BINARY_PATH" "$ANY_BUNDLE" > "$TMP_DIR/extracted.bundle"
|
|
669
|
+
elif [[ "$BINARY_PATH" == *.bundle ]] || [[ "$BINARY_PATH" == *.hbc ]]; then
|
|
670
|
+
cp "$BINARY_PATH" "$TMP_DIR/extracted.bundle"
|
|
671
|
+
else
|
|
672
|
+
echo " ❌ ERROR: Could not find 'main.jsbundle' inside $BINARY_PATH"
|
|
673
|
+
rm -rf "$TMP_DIR"
|
|
674
|
+
exit 1
|
|
675
|
+
fi
|
|
676
|
+
fi
|
|
677
|
+
|
|
678
|
+
# Check Hermes bytecode format (0xc6 magic number)
|
|
679
|
+
FIRST_BYTE=$(od -An -N1 -t x1 "$TMP_DIR/extracted.bundle" | tr -d ' ' || echo "")
|
|
680
|
+
if [ "$FIRST_BYTE" == "c6" ]; then
|
|
681
|
+
echo " ✓ Bundle is verified Hermes bytecode format."
|
|
682
|
+
cp "$TMP_DIR/extracted.bundle" "$TARGET_BUNDLE"
|
|
683
|
+
else
|
|
684
|
+
echo " Bundle is JavaScript text. Compiling to Hermes bytecode..."
|
|
685
|
+
find_hermesc
|
|
686
|
+
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
687
|
+
echo " ❌ ERROR: hermesc compiler not found. Cannot compile bundle to bytecode."
|
|
688
|
+
rm -rf "$TMP_DIR"
|
|
689
|
+
exit 1
|
|
690
|
+
fi
|
|
691
|
+
"$HERMESC_PATH" -emit-binary -out "$TARGET_BUNDLE" "$TMP_DIR/extracted.bundle"
|
|
692
|
+
fi
|
|
693
|
+
|
|
694
|
+
# Extract base assets manifest
|
|
695
|
+
echo " Scanning assets from binary for base manifest..."
|
|
696
|
+
local NEW_BASE_ASSETS="{"
|
|
697
|
+
local ASSET_JSON_LIST="["
|
|
698
|
+
local FIRST=true
|
|
699
|
+
|
|
700
|
+
if [ "$PLATFORM" == "android" ]; then
|
|
701
|
+
unzip -q -o "$BINARY_PATH" "res/drawable*/*" "res/raw/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
702
|
+
else
|
|
703
|
+
unzip -q -o "$BINARY_PATH" "Payload/*.app/assets/*" -d "$TMP_DIR/assets_unzipped" 2>/dev/null || true
|
|
704
|
+
fi
|
|
705
|
+
|
|
706
|
+
if [ -d "$TMP_DIR/assets_unzipped" ]; then
|
|
707
|
+
while IFS= read -r -d '' ASSET_FILE; do
|
|
708
|
+
local HASH=$(sha256 "$ASSET_FILE")
|
|
709
|
+
local FNAME=$(basename "$ASSET_FILE")
|
|
710
|
+
local EX="${FNAME##*.}"
|
|
711
|
+
local REL_PATH="${ASSET_FILE#$TMP_DIR/assets_unzipped/}"
|
|
712
|
+
|
|
713
|
+
if [ "$NEW_BASE_ASSETS" != "{" ]; then NEW_BASE_ASSETS+=", "; fi
|
|
714
|
+
NEW_BASE_ASSETS+="\"$REL_PATH\": \"$HASH\""
|
|
715
|
+
|
|
716
|
+
if [ "$FIRST" = true ]; then FIRST=false; else ASSET_JSON_LIST+=", "; fi
|
|
717
|
+
ASSET_JSON_LIST+="{\"hash\": \"$HASH\", \"ext\": \"$EX\", \"originalPath\": \"$REL_PATH\", \"isBase\": true}"
|
|
718
|
+
done < <(find "$TMP_DIR/assets_unzipped" -type f -print0 2>/dev/null)
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
NEW_BASE_ASSETS+="}"
|
|
722
|
+
ASSET_JSON_LIST+="]"
|
|
723
|
+
|
|
724
|
+
echo "$NEW_BASE_ASSETS" > "$BASE_ASSET_MANIFEST"
|
|
725
|
+
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
726
|
+
{ "version": 1, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
727
|
+
EOF
|
|
728
|
+
|
|
729
|
+
rm -rf "$TMP_DIR"
|
|
730
|
+
|
|
731
|
+
local BUNDLE_HASH=$(sha256 "$TARGET_BUNDLE")
|
|
732
|
+
echo " ✓ Base bundle v1 successfully created: $(basename "$TARGET_BUNDLE")"
|
|
733
|
+
echo " SHA-256: $BUNDLE_HASH"
|
|
734
|
+
|
|
735
|
+
# Update registry for v1
|
|
736
|
+
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
737
|
+
|
|
738
|
+
# Push if requested
|
|
739
|
+
if [ "$PUSH_REMOTE" = true ]; then
|
|
740
|
+
echo " Pushing seeded base bundle to remote..."
|
|
741
|
+
bash "$SCRIPT_DIR/upload_ota_custom.sh" "$APP_VERSION" "$FLAVOR_LOWER" "1" "$TARGET_BUNDLE" "$PLATFORM"
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
echo " ✓ Seeding complete for $PLATFORM!"
|
|
745
|
+
}
|
|
746
|
+
|
|
495
747
|
# ── Main ───────────────────────────────────────────────────────────────────
|
|
496
|
-
echo "=== OTA
|
|
748
|
+
echo "=== OTA Build Pipeline | App: $APP_VERSION | Flavor: $FLAVOR ==="
|
|
497
749
|
|
|
498
|
-
# Read current version once to coordinate across platforms
|
|
499
750
|
FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
751
|
+
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
752
|
+
if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
753
|
+
|
|
754
|
+
# ── Seed Action ────────────────────────────────────────────────────────────
|
|
755
|
+
if [ "$ACTION" = "seed" ]; then
|
|
756
|
+
if [ -n "$SEED_APK" ]; then
|
|
757
|
+
seed_platform "android" "$FLAVOR_LOWER" "$SEED_APK"
|
|
758
|
+
elif [ -n "$SEED_IPA" ]; then
|
|
759
|
+
seed_platform "ios" "$FLAVOR_LOWER" "$SEED_IPA"
|
|
760
|
+
elif [ -n "$SEED_BUNDLE" ]; then
|
|
761
|
+
PLATFORM="${PLATFORM_ARG:-android}"
|
|
762
|
+
seed_platform "$PLATFORM" "$FLAVOR_LOWER" "$SEED_BUNDLE"
|
|
763
|
+
else
|
|
764
|
+
echo "❌ Please provide a binary file to seed:"
|
|
765
|
+
echo " ./build_ota.sh seed --apk <path-to-apk>"
|
|
766
|
+
echo " ./build_ota.sh seed --ipa <path-to-ipa>"
|
|
767
|
+
exit 1
|
|
768
|
+
fi
|
|
769
|
+
echo ""
|
|
770
|
+
echo "=== Base Seeding Complete ==="
|
|
771
|
+
exit 0
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
# Read current version once to coordinate across platforms
|
|
500
775
|
CURRENT_TOTAL=0
|
|
501
776
|
if [ -f "$REGISTRY_SHARED" ]; then
|
|
502
777
|
CURRENT_TOTAL=$(node -e "
|
|
@@ -506,6 +781,79 @@ if [ -f "$REGISTRY_SHARED" ]; then
|
|
|
506
781
|
} catch(e) { console.log(0); }
|
|
507
782
|
")
|
|
508
783
|
fi
|
|
784
|
+
|
|
785
|
+
# ── Fail-Safe Guard for OTA Updates ────────────────────────────────────────
|
|
786
|
+
# If building an OTA patch but no base version (v1) exists in the registry:
|
|
787
|
+
if [ "$ACTION" = "ota" ] && [ $CURRENT_TOTAL -eq 0 ]; then
|
|
788
|
+
echo "Checking for pre-existing Base Bundle v1..."
|
|
789
|
+
|
|
790
|
+
CHECK_PLATFORMS=()
|
|
791
|
+
if [ -n "$PLATFORM_ARG" ]; then
|
|
792
|
+
CHECK_PLATFORMS=("$PLATFORM_ARG")
|
|
793
|
+
else
|
|
794
|
+
CHECK_PLATFORMS=("android" "ios")
|
|
795
|
+
fi
|
|
796
|
+
|
|
797
|
+
ALL_FOUND=true
|
|
798
|
+
MISSING_PLATFORM=""
|
|
799
|
+
for P in "${CHECK_PLATFORMS[@]}"; do
|
|
800
|
+
P_OUTPUT="$ROOT/$P/output"
|
|
801
|
+
P_BASE_BUNDLE="$P_OUTPUT/bundle_${APP_VERSION}_v1_${P}_${FLAVOR_LOWER}.hbc"
|
|
802
|
+
|
|
803
|
+
# Check if local exists
|
|
804
|
+
if [ ! -f "$P_BASE_BUNDLE" ]; then
|
|
805
|
+
# Try downloading from remote if available
|
|
806
|
+
REMOTE_BASE_PATH="$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$(basename "$P_BASE_BUNDLE")"
|
|
807
|
+
download_from_minio "$REMOTE_BASE_PATH" "$P_BASE_BUNDLE" || true
|
|
808
|
+
fi
|
|
809
|
+
|
|
810
|
+
if [ -f "$P_BASE_BUNDLE" ]; then
|
|
811
|
+
echo " ✓ Found Base Bundle v1 for $P: $(basename "$P_BASE_BUNDLE")"
|
|
812
|
+
# Auto-adopt base bundle in registry
|
|
813
|
+
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "1" "[]" "$P" "" "$FLAVOR_LOWER"
|
|
814
|
+
else
|
|
815
|
+
ALL_FOUND=false
|
|
816
|
+
MISSING_PLATFORM="$P"
|
|
817
|
+
fi
|
|
818
|
+
done
|
|
819
|
+
|
|
820
|
+
if [ "$ALL_FOUND" = true ]; then
|
|
821
|
+
echo " ✓ Base bundle v1 verified and registered. Proceeding with incremental OTA update..."
|
|
822
|
+
CURRENT_TOTAL=1
|
|
823
|
+
else
|
|
824
|
+
if [ "$FORCE_BASE" = true ]; then
|
|
825
|
+
echo " ⚠ Warning: --force flag detected. Creating base bundle v1 from current working tree..."
|
|
826
|
+
ACTION="base"
|
|
827
|
+
else
|
|
828
|
+
echo ""
|
|
829
|
+
echo "╔══════════════════════════════════════════════════════════════════════════════════════════╗"
|
|
830
|
+
echo "║ ❌ CANNOT BUILD OTA: MISSING BASE BUNDLE (v1) ║"
|
|
831
|
+
echo "╠══════════════════════════════════════════════════════════════════════════════════════════╣"
|
|
832
|
+
echo "║ App Version: $APP_VERSION | Flavor: $FLAVOR_LOWER | Missing: $MISSING_PLATFORM"
|
|
833
|
+
echo "║"
|
|
834
|
+
echo "║ No base bundle (v1) was found for this app version (neither locally nor on remote). ║"
|
|
835
|
+
echo "║ If an OTA update is built now, it would generate a base bundle from your current ║"
|
|
836
|
+
echo "║ working tree, which will MISMATCH the version installed on user devices and force a ║"
|
|
837
|
+
echo "║ 100% full-bundle download (wasting bandwidth and bypassing patch benefits). ║"
|
|
838
|
+
echo "║"
|
|
839
|
+
echo "║ How to resolve: ║"
|
|
840
|
+
echo "║ 1. If you already have the release APK/IPA submitted to Google Play / App Store: ║"
|
|
841
|
+
echo "║ Seed the exact base bundle from your release binary: ║"
|
|
842
|
+
echo "║ ./build_ota.sh seed --apk <path-to-apk> ║"
|
|
843
|
+
echo "║ ./build_ota.sh seed --ipa <path-to-ipa> ║"
|
|
844
|
+
echo "║"
|
|
845
|
+
echo "║ 2. If you are preparing a brand new Store release right now from this commit: ║"
|
|
846
|
+
echo "║ ./build_ota.sh base ║"
|
|
847
|
+
echo "║"
|
|
848
|
+
echo "║ 3. If you really want to force create base v1 from current code: ║"
|
|
849
|
+
echo "║ ./build_ota.sh --force ║"
|
|
850
|
+
echo "╚══════════════════════════════════════════════════════════════════════════════════════════╝"
|
|
851
|
+
echo ""
|
|
852
|
+
exit 1
|
|
853
|
+
fi
|
|
854
|
+
fi
|
|
855
|
+
fi
|
|
856
|
+
|
|
509
857
|
TARGET_OTA=$((CURRENT_TOTAL + 1))
|
|
510
858
|
|
|
511
859
|
if [ -n "$PLATFORM_ARG" ]; then
|