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,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// react-native-bspatch Setup Script
|
|
4
|
+
//
|
|
5
|
+
// Automatically injects OTA integration into a React Native project:
|
|
6
|
+
// - iOS: Patches project.pbxproj (Xcode build phase bundle sync)
|
|
7
|
+
// Patches AppDelegate.swift (import + bundleURL override)
|
|
8
|
+
// - Android: Patches MainApplication.kt (import + jsBundleFilePath)
|
|
9
|
+
//
|
|
10
|
+
// Idempotent: safe to run multiple times; skips already-patched files.
|
|
11
|
+
// Usage: node setup_project.js [--root <path>]
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function findFile(baseDir, predicate, maxDepth = 10) {
|
|
20
|
+
const results = [];
|
|
21
|
+
function walk(dir, depth) {
|
|
22
|
+
if (depth > maxDepth) return;
|
|
23
|
+
let entries;
|
|
24
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
const full = path.join(dir, e.name);
|
|
27
|
+
if (e.isDirectory()) { walk(full, depth + 1); }
|
|
28
|
+
else if (predicate(e.name, full)) { results.push(full); }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
walk(baseDir, 0);
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function patchFile(filePath, label, patchFn) {
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
console.log(` ⚠ ${label}: file not found at ${filePath}`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
41
|
+
const patched = patchFn(original);
|
|
42
|
+
if (patched === original) {
|
|
43
|
+
console.log(` ✓ ${label}: already set up (skipped)`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(filePath, patched, 'utf8');
|
|
47
|
+
console.log(` ✓ ${label}: patched`);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const XCODE_BUNDLE_SYNC_SNIPPET = [
|
|
54
|
+
'',
|
|
55
|
+
'# [react-native-bspatch] Sync compiled bundle to output/ for OTA builds',
|
|
56
|
+
'DEST_FILE="$PROJECT_DIR/output/main.jsbundle"',
|
|
57
|
+
'BUNDLE_RESULT="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle"',
|
|
58
|
+
'',
|
|
59
|
+
'if [ -f "$BUNDLE_RESULT" ]; then',
|
|
60
|
+
' mkdir -p "$(dirname \\"$DEST_FILE\\")"',
|
|
61
|
+
' cp "$BUNDLE_RESULT" "$DEST_FILE"',
|
|
62
|
+
' echo "Bundle synced to $DEST_FILE"',
|
|
63
|
+
'else',
|
|
64
|
+
' echo "Bundle not found at $BUNDLE_RESULT (skipping sync)"',
|
|
65
|
+
'fi',
|
|
66
|
+
].join('\\n');
|
|
67
|
+
|
|
68
|
+
// The escaped version as it appears in pbxproj shellScript value
|
|
69
|
+
const XCODE_BUNDLE_SYNC_MARKER = 'react-native-bspatch] Sync compiled bundle to output/';
|
|
70
|
+
|
|
71
|
+
const IOS_IMPORT = 'import react_native_bspatch';
|
|
72
|
+
const IOS_BUNDLE_URL_OVERRIDE = `
|
|
73
|
+
override func bundleURL() -> URL? {
|
|
74
|
+
#if DEBUG
|
|
75
|
+
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
|
76
|
+
#else
|
|
77
|
+
if let otaBundle = OtaBundleResolver.resolve() {
|
|
78
|
+
return otaBundle
|
|
79
|
+
}
|
|
80
|
+
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
81
|
+
#endif
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const ANDROID_IMPORT = 'import vn.reactnativebspatch.OtaBundleResolver';
|
|
86
|
+
const ANDROID_JS_BUNDLE = 'jsBundleFilePath = OtaBundleResolver.resolve(applicationContext)';
|
|
87
|
+
|
|
88
|
+
// ── iOS: project.pbxproj ─────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function patchPbxproj(content) {
|
|
91
|
+
// Already patched?
|
|
92
|
+
if (content.includes(XCODE_BUNDLE_SYNC_MARKER)) return content;
|
|
93
|
+
|
|
94
|
+
// Find the Bundle React Native code and images shellScript value
|
|
95
|
+
// Pattern: name = "Bundle React Native code and images"; ... shellScript = "...";
|
|
96
|
+
// The shellScript ends with \n\";\n — we inject before the last closing \n\";
|
|
97
|
+
const shellScriptRegex = /(00DD1BFF1BD5951E006B06BC[^;]*?shellScript = ")([\s\S]*?)(\s*";)/m;
|
|
98
|
+
const match = content.match(shellScriptRegex);
|
|
99
|
+
if (!match) {
|
|
100
|
+
// Fallback: find by name
|
|
101
|
+
console.log(' ⚠ Could not find "Bundle React Native code and images" build phase by UUID, trying by name...');
|
|
102
|
+
// Simpler approach: find shellScript belonging to the bundle phase by name proximity
|
|
103
|
+
const nameIdx = content.indexOf('name = "Bundle React Native code and images"');
|
|
104
|
+
if (nameIdx === -1) {
|
|
105
|
+
console.log(' ⚠ Could not find build phase. Please add the bundle sync snippet manually.');
|
|
106
|
+
return content;
|
|
107
|
+
}
|
|
108
|
+
// Find the shellScript = "..." after this name line
|
|
109
|
+
const afterName = content.indexOf('shellScript = "', nameIdx - 500);
|
|
110
|
+
if (afterName === -1) return content;
|
|
111
|
+
|
|
112
|
+
// Find the end of this shellScript: look for \n\"; after it
|
|
113
|
+
const scriptEnd = content.indexOf('\\n";', afterName);
|
|
114
|
+
if (scriptEnd === -1) return content;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
content.slice(0, scriptEnd) +
|
|
118
|
+
XCODE_BUNDLE_SYNC_SNIPPET +
|
|
119
|
+
content.slice(scriptEnd)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Insert before the closing \n\";
|
|
124
|
+
const scriptBody = match[2];
|
|
125
|
+
// Remove trailing \n if present before closing
|
|
126
|
+
const insertPoint = match.index + match[1].length + scriptBody.length;
|
|
127
|
+
return (
|
|
128
|
+
content.slice(0, insertPoint) +
|
|
129
|
+
XCODE_BUNDLE_SYNC_SNIPPET +
|
|
130
|
+
content.slice(insertPoint)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── iOS: AppDelegate.swift ────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function patchAppDelegate(content) {
|
|
137
|
+
let result = content;
|
|
138
|
+
let changed = false;
|
|
139
|
+
|
|
140
|
+
// 1. Add import if missing
|
|
141
|
+
if (!result.includes(IOS_IMPORT)) {
|
|
142
|
+
// Insert after the last import line
|
|
143
|
+
const lastImportMatch = [...result.matchAll(/^import .+$/gm)].pop();
|
|
144
|
+
if (lastImportMatch) {
|
|
145
|
+
const idx = lastImportMatch.index + lastImportMatch[0].length;
|
|
146
|
+
result = result.slice(0, idx) + '\n' + IOS_IMPORT + result.slice(idx);
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Add bundleURL override if missing
|
|
152
|
+
if (!result.includes('OtaBundleResolver.resolve()')) {
|
|
153
|
+
// Look for an existing bundleURL() function
|
|
154
|
+
const bundleURLFnRegex = /override func bundleURL\(\)[^{]*\{[\s\S]*?\}/m;
|
|
155
|
+
if (bundleURLFnRegex.test(result)) {
|
|
156
|
+
// Replace existing bundleURL() entirely
|
|
157
|
+
result = result.replace(bundleURLFnRegex, IOS_BUNDLE_URL_OVERRIDE.trim());
|
|
158
|
+
} else {
|
|
159
|
+
// Look for sourceURL override and insert bundleURL after it, or at end of class
|
|
160
|
+
const sourceURLIdx = result.lastIndexOf('override func sourceURL');
|
|
161
|
+
if (sourceURLIdx !== -1) {
|
|
162
|
+
// Find end of that function
|
|
163
|
+
const endBrace = result.indexOf('\n }', sourceURLIdx) + 4;
|
|
164
|
+
result = result.slice(0, endBrace) + '\n' + IOS_BUNDLE_URL_OVERRIDE + result.slice(endBrace);
|
|
165
|
+
} else {
|
|
166
|
+
// Insert before last closing brace of the file
|
|
167
|
+
const lastBrace = result.lastIndexOf('}');
|
|
168
|
+
result = result.slice(0, lastBrace) + IOS_BUNDLE_URL_OVERRIDE + '\n' + result.slice(lastBrace);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return changed ? result : content;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Android: MainApplication.kt ──────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function patchMainApplication(content) {
|
|
180
|
+
let result = content;
|
|
181
|
+
let changed = false;
|
|
182
|
+
|
|
183
|
+
// 1. Add import if missing
|
|
184
|
+
if (!result.includes(ANDROID_IMPORT)) {
|
|
185
|
+
const lastImportMatch = [...result.matchAll(/^import .+$/gm)].pop();
|
|
186
|
+
if (lastImportMatch) {
|
|
187
|
+
const idx = lastImportMatch.index + lastImportMatch[0].length;
|
|
188
|
+
result = result.slice(0, idx) + '\n' + ANDROID_IMPORT + result.slice(idx);
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. Add jsBundleFilePath if missing
|
|
194
|
+
if (!result.includes('OtaBundleResolver.resolve')) {
|
|
195
|
+
// Find getDefaultReactHost(... call and inject jsBundleFilePath param
|
|
196
|
+
// Look for closing paren of getDefaultReactHost block
|
|
197
|
+
const hostCallRegex = /getDefaultReactHost\s*\(\s*([\s\S]*?)\)/m;
|
|
198
|
+
const hostMatch = result.match(hostCallRegex);
|
|
199
|
+
if (hostMatch) {
|
|
200
|
+
// Insert before the closing paren
|
|
201
|
+
const closeIdx = hostMatch.index + hostMatch[0].length - 1;
|
|
202
|
+
result =
|
|
203
|
+
result.slice(0, closeIdx) +
|
|
204
|
+
',\n ' + ANDROID_JS_BUNDLE + '\n ' +
|
|
205
|
+
result.slice(closeIdx);
|
|
206
|
+
changed = true;
|
|
207
|
+
} else {
|
|
208
|
+
console.log(' ⚠ Could not find getDefaultReactHost(...) call. Please add jsBundleFilePath manually.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return changed ? result : content;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function main() {
|
|
218
|
+
// Determine project root
|
|
219
|
+
const args = process.argv.slice(2);
|
|
220
|
+
const rootIdx = args.indexOf('--root');
|
|
221
|
+
const ROOT = rootIdx !== -1 ? path.resolve(args[rootIdx + 1]) : process.cwd();
|
|
222
|
+
|
|
223
|
+
console.log('\n=== react-native-bspatch Setup ===');
|
|
224
|
+
console.log(` Project root: ${ROOT}`);
|
|
225
|
+
|
|
226
|
+
const IOS_DIR = path.join(ROOT, 'ios');
|
|
227
|
+
const ANDROID_DIR = path.join(ROOT, 'android');
|
|
228
|
+
|
|
229
|
+
// ── iOS ──
|
|
230
|
+
console.log('\n[iOS]');
|
|
231
|
+
|
|
232
|
+
// project.pbxproj
|
|
233
|
+
const pbxprojFiles = findFile(IOS_DIR, (name) => name === 'project.pbxproj');
|
|
234
|
+
if (pbxprojFiles.length === 0) {
|
|
235
|
+
console.log(' ⚠ No project.pbxproj found. Is this an iOS project?');
|
|
236
|
+
} else {
|
|
237
|
+
const pbxproj = pbxprojFiles[0];
|
|
238
|
+
patchFile(pbxproj, 'project.pbxproj (Xcode build phase)', patchPbxproj);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// AppDelegate.swift
|
|
242
|
+
const appDelegateFiles = findFile(IOS_DIR, (name) => name === 'AppDelegate.swift');
|
|
243
|
+
if (appDelegateFiles.length === 0) {
|
|
244
|
+
console.log(' ⚠ No AppDelegate.swift found.');
|
|
245
|
+
} else {
|
|
246
|
+
patchFile(appDelegateFiles[0], 'AppDelegate.swift', patchAppDelegate);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Ensure ios/output/ exists
|
|
250
|
+
const outputDir = path.join(IOS_DIR, 'output');
|
|
251
|
+
if (!fs.existsSync(outputDir)) {
|
|
252
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
253
|
+
console.log(' ✓ Created ios/output/ directory');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Android ──
|
|
257
|
+
console.log('\n[Android]');
|
|
258
|
+
const mainAppFiles = findFile(ANDROID_DIR, (name) => name === 'MainApplication.kt');
|
|
259
|
+
if (mainAppFiles.length === 0) {
|
|
260
|
+
console.log(' ⚠ No MainApplication.kt found. Is this an Android project?');
|
|
261
|
+
} else {
|
|
262
|
+
patchFile(mainAppFiles[0], 'MainApplication.kt', patchMainApplication);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('\n=== Setup Complete ===');
|
|
266
|
+
console.log('Next steps:');
|
|
267
|
+
console.log(' 1. iOS: Run Xcode build once to populate ios/output/main.jsbundle');
|
|
268
|
+
console.log(' (or run: yarn ota:init --platform ios)');
|
|
269
|
+
console.log(' 2. Android: Run: yarn ota:init --platform android');
|
|
270
|
+
console.log(' 3. Then: yarn ota:build to start publishing OTA updates\n');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* update_registry.js
|
|
4
|
+
*
|
|
5
|
+
* Called by build_ota.sh after generating patches.
|
|
6
|
+
* 1. Reads : android/output/ota_registry.json (or ios/output/...)
|
|
7
|
+
* 2. Scopes to : appVersion (e.g. "1.0.0")
|
|
8
|
+
* 3. Scopes to : flavor (e.g. "dev", "staging", "production")
|
|
9
|
+
* 4. Updates patches and latestOtaVersion for that specific app+flavor combination.
|
|
10
|
+
* 5. Writes : otaPatches.generated.ts (flattened across all app versions/flavors)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const [
|
|
17
|
+
projectRoot,
|
|
18
|
+
appVersion,
|
|
19
|
+
toOtaVersionStr,
|
|
20
|
+
patchesJson,
|
|
21
|
+
platform = 'android',
|
|
22
|
+
_assetManifestFile = '',
|
|
23
|
+
flavorInput = 'dev',
|
|
24
|
+
] = process.argv.slice(2);
|
|
25
|
+
|
|
26
|
+
const toOtaVersion = parseInt(toOtaVersionStr, 10);
|
|
27
|
+
const newPatches = JSON.parse(patchesJson || '[]');
|
|
28
|
+
|
|
29
|
+
const flavor = flavorInput.toLowerCase();
|
|
30
|
+
|
|
31
|
+
const registryPath = projectRoot.endsWith('.json')
|
|
32
|
+
? projectRoot
|
|
33
|
+
: path.join(projectRoot, 'ota_registry.json');
|
|
34
|
+
|
|
35
|
+
const generatedPath = projectRoot.endsWith('.json')
|
|
36
|
+
? path.join(path.dirname(projectRoot), 'src/ota/otaPatches.generated.ts')
|
|
37
|
+
: path.join(projectRoot, 'src/ota/otaPatches.generated.ts');
|
|
38
|
+
|
|
39
|
+
// ── Load or init registry ───────────────────────────────────────────────────
|
|
40
|
+
let registry = { apps: {} };
|
|
41
|
+
if (fs.existsSync(registryPath)) {
|
|
42
|
+
try {
|
|
43
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn(' ⚠ Failed to parse existing registry, starting fresh');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure nested structure: apps -> appVersion -> flavors -> flavor
|
|
50
|
+
if (!registry.apps) registry.apps = {};
|
|
51
|
+
if (!registry.apps[appVersion]) {
|
|
52
|
+
registry.apps[appVersion] = { flavors: {} };
|
|
53
|
+
}
|
|
54
|
+
if (!registry.apps[appVersion].flavors) {
|
|
55
|
+
registry.apps[appVersion].flavors = {};
|
|
56
|
+
}
|
|
57
|
+
if (!registry.apps[appVersion].flavors[flavor]) {
|
|
58
|
+
registry.apps[appVersion].flavors[flavor] = { latestOtaVersion: 0, patches: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const flavorGroup = registry.apps[appVersion].flavors[flavor];
|
|
62
|
+
flavorGroup.latestOtaVersion = toOtaVersion;
|
|
63
|
+
|
|
64
|
+
// ── Update patches ──────────────────────────────────────────────────────────
|
|
65
|
+
if (newPatches.length > 0) {
|
|
66
|
+
// We merge patches by platform to avoid overwriting between Android and iOS
|
|
67
|
+
const otherPlatformPatches = flavorGroup.patches.filter(p => p.platform !== platform);
|
|
68
|
+
|
|
69
|
+
const mappedNewPatches = newPatches.map(p => {
|
|
70
|
+
// Naming convention from build_ota.sh
|
|
71
|
+
const fullBundleName = `bundle_${appVersion}_v${toOtaVersion}_${platform}_${flavor}.hbc`;
|
|
72
|
+
const manifestName = path.basename(_assetManifestFile);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: `v${p.from}-to-v${p.to}-${flavor}-${platform}`,
|
|
76
|
+
label: `V${p.from} → V${p.to} (${flavor} ${platform})`,
|
|
77
|
+
appVersion,
|
|
78
|
+
filename: p.file,
|
|
79
|
+
fromVersion: p.from,
|
|
80
|
+
toVersion: p.to,
|
|
81
|
+
platform,
|
|
82
|
+
baseHash: p.baseHash || '',
|
|
83
|
+
bundleHash: p.bundleHash || '',
|
|
84
|
+
fullBundleUrl: fullBundleName,
|
|
85
|
+
assetManifestUrl: manifestName,
|
|
86
|
+
flavor: flavor,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
flavorGroup.patches = [...otherPlatformPatches, ...mappedNewPatches];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Write updated registry ───────────────────────────────────────────────────
|
|
94
|
+
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
95
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
96
|
+
console.log(
|
|
97
|
+
` ✓ Registry updated | App: ${appVersion} | Flavor: ${flavor} | latestOta: ${toOtaVersion}`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// ── Regenerate otaPatches.generated.ts ──────────────────────────────────────
|
|
101
|
+
// Flatten all patches from all apps and flavors into the TS file
|
|
102
|
+
const allPatches = [];
|
|
103
|
+
for (const ver in registry.apps) {
|
|
104
|
+
const app = registry.apps[ver];
|
|
105
|
+
if (app.flavors) {
|
|
106
|
+
for (const f in app.flavors) {
|
|
107
|
+
allPatches.push(...app.flavors[f].patches);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const patchLines = allPatches
|
|
113
|
+
.map(p => {
|
|
114
|
+
const fields = [
|
|
115
|
+
`id: '${p.id}'`,
|
|
116
|
+
`label: '${p.label}'`,
|
|
117
|
+
`appVersion: '${p.appVersion}'`,
|
|
118
|
+
`filename: '${p.filename}'`,
|
|
119
|
+
`fromVersion: ${p.fromVersion}`,
|
|
120
|
+
`toVersion: ${p.toVersion}`,
|
|
121
|
+
`platform: '${p.platform || 'android'}'`,
|
|
122
|
+
];
|
|
123
|
+
if (p.baseHash) fields.push(`baseHash: '${p.baseHash}'`);
|
|
124
|
+
if (p.bundleHash) fields.push(`bundleHash: '${p.bundleHash}'`);
|
|
125
|
+
if (p.fullBundleUrl) fields.push(`fullBundleUrl: '${p.fullBundleUrl}'`);
|
|
126
|
+
if (p.assetManifestUrl) fields.push(`assetManifestUrl: '${p.assetManifestUrl}'`);
|
|
127
|
+
// Add flavor for runtime filtering if needed
|
|
128
|
+
fields.push(`flavor: '${p.flavor || ''}'`);
|
|
129
|
+
|
|
130
|
+
return ` { ${fields.join(', ')} },`;
|
|
131
|
+
})
|
|
132
|
+
.join('\n');
|
|
133
|
+
|
|
134
|
+
const tsContent = `// AUTO-GENERATED by build_ota.sh — do not edit manually
|
|
135
|
+
import type { OtaPatch } from 'react-native-bspatch';
|
|
136
|
+
|
|
137
|
+
export const OTA_PATCHES: OtaPatch[] = [
|
|
138
|
+
${patchLines}
|
|
139
|
+
];
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(generatedPath, tsContent);
|
|
143
|
+
console.log(
|
|
144
|
+
` ✓ otaPatches.generated.ts updated (${allPatches.length} total patches across all flavors)`,
|
|
145
|
+
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Custom OTA Upload Script for Sync Server
|
|
5
|
+
# Custom server is a drop-in replacement for GitHub releases.
|
|
6
|
+
# Uploads: 1) All .patch files → {baseFolder}/{appVersion}/
|
|
7
|
+
# 2) ota_registry.json → {baseFolder}/ota_registry.json
|
|
8
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
set -e
|
|
11
|
+
|
|
12
|
+
BASE_URL="https://sync-server.osp.com.vn"
|
|
13
|
+
UPCODE_USER="callapi_up"
|
|
14
|
+
UPCODE_PASS="41221312fsdfdsf2fsdgsdf1AA1"
|
|
15
|
+
|
|
16
|
+
APP_VERSION="$1"
|
|
17
|
+
CHANNEL="$2"
|
|
18
|
+
OTA_VERSION="$3"
|
|
19
|
+
BUNDLE_PATH="$4" # unused — we upload .patch files, not the bundle
|
|
20
|
+
PLATFORM="$5"
|
|
21
|
+
BASE_FOLDER="${6:-congplqg}"
|
|
22
|
+
|
|
23
|
+
ROOT_DIR=$(pwd)
|
|
24
|
+
REGISTRY_FILE="$ROOT_DIR/ota_registry.json"
|
|
25
|
+
OUTPUT_DIR="$ROOT_DIR/android/output"
|
|
26
|
+
if [[ "$PLATFORM" == "ios" ]]; then
|
|
27
|
+
OUTPUT_DIR="$ROOT_DIR/ios/output"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo ""
|
|
31
|
+
echo "=== Custom OTA Upload | App: $APP_VERSION | Channel: $CHANNEL | V$OTA_VERSION ==="
|
|
32
|
+
echo " Output dir: $OUTPUT_DIR"
|
|
33
|
+
|
|
34
|
+
# Validate
|
|
35
|
+
if [[ -z "$APP_VERSION" || -z "$OTA_VERSION" ]]; then
|
|
36
|
+
echo "Usage: $0 <app_version> <channel> <ota_version> <bundle_path> [platform] [base_folder]"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
if [[ ! -f "$REGISTRY_FILE" ]]; then
|
|
40
|
+
echo "ERROR: ota_registry.json not found: $REGISTRY_FILE"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# 1. Login
|
|
45
|
+
echo "Logging in..."
|
|
46
|
+
LOGIN_RES=$(curl -s -X "POST" "$BASE_URL/public/authen/login" \
|
|
47
|
+
-H 'Content-Type: application/json; charset=utf-8' \
|
|
48
|
+
-d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
|
|
49
|
+
|
|
50
|
+
TOKEN=$(echo "$LOGIN_RES" | node -e "
|
|
51
|
+
try {
|
|
52
|
+
const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
53
|
+
console.log(r.data?.accessTokenInfo?.accessToken || '');
|
|
54
|
+
} catch(e) { console.log(''); }
|
|
55
|
+
")
|
|
56
|
+
if [[ -z "$TOKEN" ]]; then
|
|
57
|
+
echo "ERROR: Login failed. Response: $LOGIN_RES"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo " ✓ Login successful"
|
|
61
|
+
|
|
62
|
+
# ── Helpers ─────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
remote_delete() {
|
|
65
|
+
local path=$1
|
|
66
|
+
local encoded
|
|
67
|
+
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$path'))")
|
|
68
|
+
curl -s -X "DELETE" "$BASE_URL/api/minio/delete?path=$encoded" \
|
|
69
|
+
-H "Authorization: Bearer $TOKEN" > /dev/null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
remote_upload() {
|
|
73
|
+
local local_file=$1
|
|
74
|
+
local remote_path=$2
|
|
75
|
+
echo " uploading: $remote_path"
|
|
76
|
+
local RES
|
|
77
|
+
RES=$(curl -s -X "POST" "$BASE_URL/api/minio/upload" \
|
|
78
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
79
|
+
-H 'Content-Type: multipart/form-data' \
|
|
80
|
+
-F "file=@$local_file" \
|
|
81
|
+
-F "path=$remote_path")
|
|
82
|
+
if [[ "$RES" != *"\"success\":true"* ]] && [[ "$RES" != *"\"code\":1"* ]] && [[ "$RES" != *"\"status\":200"* ]] && [[ "$RES" != *"\"code\":200"* ]]; then
|
|
83
|
+
echo " ⚠ Upload potential issue: $RES"
|
|
84
|
+
else
|
|
85
|
+
echo " ✓ Upload success"
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# 2. Upload all patch files, full bundles, and asset manifests
|
|
90
|
+
echo "Uploading files for channel: $CHANNEL..."
|
|
91
|
+
FILE_COUNT=0
|
|
92
|
+
for FILE in "$OUTPUT_DIR"/*.{patch,hbc,json}; do
|
|
93
|
+
[[ -f "$FILE" ]] || continue
|
|
94
|
+
FNAME=$(basename "$FILE")
|
|
95
|
+
# Only upload files belonging to the current APP_VERSION
|
|
96
|
+
if [[ "$FNAME" == "patch_"* ]] || [[ "$FNAME" == "bundle_"* ]]; then
|
|
97
|
+
if [[ "$FNAME" != *"_${APP_VERSION}_"* ]]; then
|
|
98
|
+
continue
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Further general filtering
|
|
103
|
+
if [[ "$FNAME" != *"_${APP_VERSION}_"* ]] && [[ "$FNAME" != *"_${CHANNEL}_"* ]] && [[ "$FNAME" != *"_${PLATFORM}_"* ]] && [[ "$FNAME" != "asset-manifest"* ]] && [[ "$FNAME" != "base-assets"* ]]; then
|
|
104
|
+
continue
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
REMOTE_PATH="$BASE_FOLDER/$APP_VERSION/$CHANNEL/$FNAME"
|
|
108
|
+
remote_delete "$REMOTE_PATH"
|
|
109
|
+
remote_upload "$FILE" "$REMOTE_PATH"
|
|
110
|
+
FILE_COUNT=$((FILE_COUNT + 1))
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# 3. Upload hashed asset store
|
|
114
|
+
if [[ -d "$OUTPUT_DIR/asset-store" ]]; then
|
|
115
|
+
echo "Uploading hashed assets..."
|
|
116
|
+
for FILE in "$OUTPUT_DIR/asset-store"/*; do
|
|
117
|
+
[[ -f "$FILE" ]] || continue
|
|
118
|
+
FNAME=$(basename "$FILE")
|
|
119
|
+
# Assets are global to the base folder (content-addressed)
|
|
120
|
+
REMOTE_PATH="$BASE_FOLDER/assets/$FNAME"
|
|
121
|
+
# Optional: skip if already exists to save time, but for now we re-upload for safety
|
|
122
|
+
remote_upload "$FILE" "$REMOTE_PATH"
|
|
123
|
+
FILE_COUNT=$((FILE_COUNT + 1))
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
echo " ✓ $FILE_COUNT file(s) uploaded (patches + bundles + assets)"
|
|
127
|
+
|
|
128
|
+
# 3. Upload ota_registry.json
|
|
129
|
+
echo "Uploading ota_registry.json..."
|
|
130
|
+
REMOTE_REGISTRY="$BASE_FOLDER/ota_registry.json"
|
|
131
|
+
remote_delete "$REMOTE_REGISTRY"
|
|
132
|
+
remote_upload "$REGISTRY_FILE" "$REMOTE_REGISTRY"
|
|
133
|
+
echo " ✓ Registry uploaded"
|
|
134
|
+
|
|
135
|
+
echo ""
|
|
136
|
+
echo "=== Upload Complete ==="
|
|
137
|
+
echo " Registry : $REMOTE_REGISTRY"
|
|
138
|
+
echo " Patches : $BASE_FOLDER/$APP_VERSION/patch_*.patch ($PATCH_COUNT files)"
|