@hot-updater/react-native 1.0.4 → 1.0.6
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/HotUpdater.podspec +24 -3
- package/Package.swift +100 -0
- package/android/build.gradle +3 -4
- package/android/hot-updater-builtin-assets.gradle +67 -0
- package/android/hot-updater-min-bundle-id.gradle +69 -0
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +85 -0
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +116 -11
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +117 -12
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +5 -52
- package/android/src/newarch/HotUpdaterModule.kt +46 -2
- package/android/src/oldarch/HotUpdaterModule.kt +46 -2
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +66 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.m +3 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +87 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +88 -13
- package/ios/HotUpdater/Internal/HotUpdater.mm +41 -90
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +25 -11
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +26 -0
- package/ios/HotUpdater/Internal/HotUpdaterMinBundleId.m +10 -0
- package/ios/HotUpdater/Package.swift +88 -0
- package/ios/Plugins/HotUpdaterMinBundleIdPlugin/HotUpdaterMinBundleIdPlugin.swift +28 -0
- package/ios/scripts/generate-min-bundle-id.zsh +49 -0
- package/lib/commonjs/checkForUpdate.js +8 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/fetchUpdateResponse.js +5 -2
- package/lib/commonjs/fetchUpdateResponse.js.map +1 -1
- package/lib/commonjs/httpClient.js +64 -5
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/native.js +9 -0
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +8 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/fetchUpdateResponse.js +4 -1
- package/lib/module/fetchUpdateResponse.js.map +1 -1
- package/lib/module/httpClient.js +67 -7
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/native.js +9 -0
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +2 -0
- package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +18 -2
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +13 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/fetchUpdateResponse.d.ts +2 -0
- package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +18 -2
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +13 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +5 -4
- package/react-native.config.js +7 -0
- package/src/checkForUpdate.ts +11 -0
- package/src/fetchUpdateResponse.ts +4 -1
- package/src/httpClient.ts +84 -9
- package/src/native.ts +7 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +13 -1
|
@@ -285,7 +285,7 @@ public protocol BundleStorageService {
|
|
|
285
285
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
286
286
|
|
|
287
287
|
// Bundle update
|
|
288
|
-
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
|
|
288
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
|
|
289
289
|
func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
290
290
|
func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
291
291
|
func getActiveUpdateState() -> [String: Any]
|
|
@@ -336,6 +336,12 @@ public extension BundleStorageService {
|
|
|
336
336
|
|
|
337
337
|
class BundleFileStorageService: BundleStorageService {
|
|
338
338
|
private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
|
|
339
|
+
/// Files downloaded at once when the archive is not used.
|
|
340
|
+
static let parallelFileDownloads = 16
|
|
341
|
+
/// What one request costs beyond the first `parallelFileDownloads`, in
|
|
342
|
+
/// bytes: each waits a round trip for a free slot, about 100 ms, which at
|
|
343
|
+
/// 5 MB/s over 16 slots is the time of 32 KiB.
|
|
344
|
+
private static let requestOverheadByteSize: Int64 = 32 * 1024
|
|
339
345
|
|
|
340
346
|
private struct ActiveBundleMetadataSnapshot {
|
|
341
347
|
let activeBundleId: String
|
|
@@ -1168,7 +1174,15 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1168
1174
|
guard !overheadUnderflowed,
|
|
1169
1175
|
tarOverhead >= 0 else { return false }
|
|
1170
1176
|
|
|
1171
|
-
|
|
1177
|
+
// Many small files cost more in requests than in bytes.
|
|
1178
|
+
let requestOverhead = Int64(max(0, downloads.count - Self.parallelFileDownloads))
|
|
1179
|
+
* Self.requestOverheadByteSize
|
|
1180
|
+
let (individualCost, costOverflowed) = individualLowerBound
|
|
1181
|
+
.addingReportingOverflow(requestOverhead)
|
|
1182
|
+
guard !costOverflowed,
|
|
1183
|
+
individualCost <= Self.maximumSafeByteSize else { return false }
|
|
1184
|
+
|
|
1185
|
+
if archive.downloadByteSize <= individualCost {
|
|
1172
1186
|
return true
|
|
1173
1187
|
}
|
|
1174
1188
|
|
|
@@ -1176,7 +1190,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1176
1190
|
!hasOfferedPatch else {
|
|
1177
1191
|
return false
|
|
1178
1192
|
}
|
|
1179
|
-
let (archiveBound, boundOverflowed) =
|
|
1193
|
+
let (archiveBound, boundOverflowed) = individualCost
|
|
1180
1194
|
.addingReportingOverflow(tarOverhead)
|
|
1181
1195
|
guard !boundOverflowed,
|
|
1182
1196
|
archiveBound <= Self.maximumSafeByteSize else { return false }
|
|
@@ -2015,7 +2029,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2015
2029
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
2016
2030
|
* @param completion Callback with result of the operation
|
|
2017
2031
|
*/
|
|
2018
|
-
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
2032
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver? = nil, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
2019
2033
|
// Check if bundle is in crashed history
|
|
2020
2034
|
if getCrashHistory().contains(bundleId) {
|
|
2021
2035
|
NSLog("[BundleStorage] Bundle '\(bundleId)' is in crashed history, rejecting update")
|
|
@@ -2038,6 +2052,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2038
2052
|
manifestFileHash: manifestFileHash,
|
|
2039
2053
|
archiveUrl: archiveUrl,
|
|
2040
2054
|
changedAssets: assets,
|
|
2055
|
+
fileResolver: fileResolver,
|
|
2041
2056
|
storeDir: storeDir,
|
|
2042
2057
|
progressHandler: progressHandler,
|
|
2043
2058
|
completion: completion
|
|
@@ -2045,6 +2060,35 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2045
2060
|
}
|
|
2046
2061
|
}
|
|
2047
2062
|
|
|
2063
|
+
/// Asks `fileResolver` for the URLs of `downloads`, in requests of at
|
|
2064
|
+
/// most `artifactFilesRequestLimit` paths.
|
|
2065
|
+
private func resolveFileUrls(
|
|
2066
|
+
fileResolver: ArtifactFileResolver,
|
|
2067
|
+
downloads: inout [String: ChangedAssetDescriptor]
|
|
2068
|
+
) throws {
|
|
2069
|
+
let paths = downloads.keys.sorted()
|
|
2070
|
+
for start in stride(from: 0, to: paths.count, by: artifactFilesRequestLimit) {
|
|
2071
|
+
let chunk = Array(paths[start..<min(start + artifactFilesRequestLimit, paths.count)])
|
|
2072
|
+
let files: [String: ResolvedArtifactFile]
|
|
2073
|
+
do {
|
|
2074
|
+
files = try fileResolver.resolve(paths: chunk)
|
|
2075
|
+
} catch {
|
|
2076
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2077
|
+
}
|
|
2078
|
+
for path in chunk {
|
|
2079
|
+
guard let file = files[path], let descriptor = downloads[path] else {
|
|
2080
|
+
throw BundleStorageError.invalidBundle
|
|
2081
|
+
}
|
|
2082
|
+
downloads[path] = ChangedAssetDescriptor(
|
|
2083
|
+
fileUrl: file.url,
|
|
2084
|
+
fileHash: descriptor.fileHash,
|
|
2085
|
+
fileCompression: file.compression,
|
|
2086
|
+
patch: descriptor.patch
|
|
2087
|
+
)
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2048
2092
|
/// How the per-file install produced one file.
|
|
2049
2093
|
private enum AssetInstallOutcome {
|
|
2050
2094
|
case patched
|
|
@@ -2490,6 +2534,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2490
2534
|
manifestFileHash: String,
|
|
2491
2535
|
archiveUrl: URL?,
|
|
2492
2536
|
changedAssets: [String: ChangedAssetDescriptor],
|
|
2537
|
+
fileResolver: ArtifactFileResolver?,
|
|
2493
2538
|
storeDir: String,
|
|
2494
2539
|
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2495
2540
|
completion: @escaping (Result<UpdateDelivery, Error>) -> Void
|
|
@@ -2576,20 +2621,43 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2576
2621
|
else {
|
|
2577
2622
|
throw BundleStorageError.invalidBundle
|
|
2578
2623
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2624
|
+
if fileResolver == nil {
|
|
2625
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
2626
|
+
// Validate the entire wire contract even when every byte can be reused.
|
|
2627
|
+
guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
|
|
2628
|
+
throw BundleStorageError.invalidBundle
|
|
2629
|
+
}
|
|
2630
|
+
} else {
|
|
2631
|
+
// In v2, `assets` holds only the Hermes patch.
|
|
2632
|
+
guard Set(changedAssets.keys).isSubset(of: Set(targetManifest.assets.keys)) else {
|
|
2633
|
+
throw BundleStorageError.invalidBundle
|
|
2634
|
+
}
|
|
2582
2635
|
}
|
|
2583
2636
|
for (path, asset) in targetManifest.assets {
|
|
2584
2637
|
guard FileUtilities.normalizedRelativePath(from: path) == path,
|
|
2585
|
-
path != "manifest.json"
|
|
2586
|
-
let descriptor = changedAssets[path],
|
|
2587
|
-
descriptor.fileUrl != nil,
|
|
2588
|
-
descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
2638
|
+
path != "manifest.json" else {
|
|
2589
2639
|
throw BundleStorageError.invalidBundle
|
|
2590
2640
|
}
|
|
2641
|
+
if fileResolver == nil {
|
|
2642
|
+
guard let descriptor = changedAssets[path],
|
|
2643
|
+
descriptor.fileUrl != nil,
|
|
2644
|
+
descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
2645
|
+
throw BundleStorageError.invalidBundle
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2591
2648
|
_ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
|
|
2592
2649
|
}
|
|
2650
|
+
// In v2, the manifest names every file; URLs come once the install
|
|
2651
|
+
// knows which files it downloads.
|
|
2652
|
+
let plannedAssets: [String: ChangedAssetDescriptor] = fileResolver == nil
|
|
2653
|
+
? changedAssets
|
|
2654
|
+
: targetManifest.assets.reduce(into: [:]) { planned, entry in
|
|
2655
|
+
planned[entry.key] = ChangedAssetDescriptor(
|
|
2656
|
+
fileUrl: nil,
|
|
2657
|
+
fileHash: entry.value.fileHash,
|
|
2658
|
+
patch: changedAssets[entry.key]?.patch
|
|
2659
|
+
)
|
|
2660
|
+
}
|
|
2593
2661
|
|
|
2594
2662
|
// Downloads, patches, and extraction below classify their own errors.
|
|
2595
2663
|
failurePhase = .storage
|
|
@@ -2642,7 +2710,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2642
2710
|
}
|
|
2643
2711
|
continue
|
|
2644
2712
|
}
|
|
2645
|
-
downloads[assetPath] =
|
|
2713
|
+
downloads[assetPath] = plannedAssets[assetPath]
|
|
2646
2714
|
}
|
|
2647
2715
|
|
|
2648
2716
|
var archiveInstalled = false
|
|
@@ -2677,11 +2745,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2677
2745
|
}
|
|
2678
2746
|
|
|
2679
2747
|
if !archiveInstalled {
|
|
2748
|
+
if let fileResolver {
|
|
2749
|
+
failurePhase = .transfer
|
|
2750
|
+
failureResource = .file
|
|
2751
|
+
try resolveFileUrls(fileResolver: fileResolver, downloads: &downloads)
|
|
2752
|
+
failurePhase = .storage
|
|
2753
|
+
failureResource = nil
|
|
2754
|
+
}
|
|
2680
2755
|
diffFiles = createDiffProgressFiles(changedAssets: downloads)
|
|
2681
2756
|
emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2682
2757
|
let progressLock = NSLock()
|
|
2683
2758
|
let workers = OperationQueue()
|
|
2684
|
-
workers.maxConcurrentOperationCount =
|
|
2759
|
+
workers.maxConcurrentOperationCount = Self.parallelFileDownloads
|
|
2685
2760
|
var installError: Error?
|
|
2686
2761
|
var installOutcomes: [AssetInstallOutcome] = []
|
|
2687
2762
|
for progressFile in diffFiles {
|
|
@@ -9,17 +9,41 @@
|
|
|
9
9
|
#include <signal.h>
|
|
10
10
|
#include <unistd.h>
|
|
11
11
|
|
|
12
|
+
#if defined(SWIFT_PACKAGE)
|
|
13
|
+
// Swift Package Manager: the Swift classes, through the protocols they conform
|
|
14
|
+
// to (see HotUpdaterInterface.h).
|
|
15
|
+
#import "HotUpdaterInterface.h"
|
|
16
|
+
typedef NSObject<HotUpdaterImplInterface> HotUpdaterImpl;
|
|
17
|
+
typedef NSObject<HotUpdaterConfigInterface> HotUpdaterConfig;
|
|
18
|
+
#else
|
|
12
19
|
#if __has_include("HotUpdater/HotUpdater-Swift.h")
|
|
13
20
|
#import "HotUpdater/HotUpdater-Swift.h"
|
|
14
21
|
#else
|
|
15
22
|
#import "HotUpdater-Swift.h"
|
|
16
23
|
#endif
|
|
17
24
|
|
|
25
|
+
static HotUpdaterImpl *HotUpdaterMakeImpl(void)
|
|
26
|
+
{
|
|
27
|
+
return [[HotUpdaterImpl alloc] init];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static NSString *HotUpdaterAppVersion(void)
|
|
31
|
+
{
|
|
32
|
+
return [HotUpdaterImpl appVersion];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static HotUpdaterConfig *HotUpdaterSharedConfig(void)
|
|
36
|
+
{
|
|
37
|
+
return [HotUpdaterConfig shared];
|
|
38
|
+
}
|
|
39
|
+
#endif
|
|
40
|
+
|
|
18
41
|
@interface HotUpdater (InternalSharedImpl)
|
|
19
42
|
+ (HotUpdaterImpl *)sharedImpl;
|
|
20
|
-
+ (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs;
|
|
21
43
|
@end
|
|
22
44
|
|
|
45
|
+
extern "C" NSString *HotUpdaterBuildMinBundleId(void);
|
|
46
|
+
|
|
23
47
|
namespace {
|
|
24
48
|
constexpr size_t kHotUpdaterMaxBundleIdLength = 128;
|
|
25
49
|
const int kHotUpdaterSignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
|
|
@@ -207,61 +231,11 @@ extern "C" BOOL HotUpdaterPerformRecoveryReload(void)
|
|
|
207
231
|
|
|
208
232
|
extern "C" NSString *HotUpdaterGetMinBundleId(void)
|
|
209
233
|
{
|
|
210
|
-
static NSString *uuid = nil;
|
|
211
|
-
static dispatch_once_t onceToken;
|
|
212
|
-
dispatch_once(&onceToken, ^{
|
|
213
234
|
#if DEBUG
|
|
214
|
-
|
|
235
|
+
return @"00000000-0000-0000-0000-000000000000";
|
|
215
236
|
#else
|
|
216
|
-
|
|
217
|
-
// hot-updater CLI passes to xcodebuild and the Info.plist slot it adds
|
|
218
|
-
// (`$(HOT_UPDATER_MIN_BUNDLE_ID)`) carries into the app, as Android
|
|
219
|
-
// takes -PMIN_BUNDLE_ID.
|
|
220
|
-
NSString *customValue = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"HOT_UPDATER_MIN_BUNDLE_ID"];
|
|
221
|
-
if ([customValue isKindOfClass:[NSString class]]) {
|
|
222
|
-
NSString *trimmed = [customValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
223
|
-
// An unset build setting leaves the slot empty or unexpanded; anything
|
|
224
|
-
// but a UUID falls back to the compile time below.
|
|
225
|
-
NSUUID *parsed = trimmed.length > 0 ? [[NSUUID alloc] initWithUUIDString:trimmed] : nil;
|
|
226
|
-
if (parsed) {
|
|
227
|
-
uuid = parsed.UUIDString.lowercaseString;
|
|
228
|
-
RCTLogInfo(@"[HotUpdater.mm] Using MIN_BUNDLE_ID from the HOT_UPDATER_MIN_BUNDLE_ID build setting: %@", uuid);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
if (trimmed.length > 0 && ![trimmed isEqualToString:@"$(HOT_UPDATER_MIN_BUNDLE_ID)"]) {
|
|
232
|
-
RCTLogWarn(@"[HotUpdater.mm] Ignoring HOT_UPDATER_MIN_BUNDLE_ID %@: not a UUID", trimmed);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Step 2: Fall back to the compile time (26-hour subtraction)
|
|
237
|
-
RCTLogInfo(@"[HotUpdater.mm] No HOT_UPDATER_MIN_BUNDLE_ID build setting, using the compile time");
|
|
238
|
-
|
|
239
|
-
NSString *compileDateStr = [NSString stringWithFormat:@"%s %s", __DATE__, __TIME__];
|
|
240
|
-
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
|
241
|
-
|
|
242
|
-
// Parse __DATE__ __TIME__ as UTC to ensure consistent timezone handling across all build environments
|
|
243
|
-
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
|
244
|
-
[formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
|
|
245
|
-
[formatter setDateFormat:@"MMM d yyyy HH:mm:ss"];
|
|
246
|
-
NSDate *buildDate = [formatter dateFromString:compileDateStr];
|
|
247
|
-
if (!buildDate) {
|
|
248
|
-
RCTLogWarn(@"[HotUpdater.mm] Could not parse build date: %@", compileDateStr);
|
|
249
|
-
uuid = @"00000000-0000-0000-0000-000000000000";
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Subtract 26 hours (93600 seconds) to ensure MIN_BUNDLE_ID is always in the past
|
|
254
|
-
// This guarantees that uuidv7-based bundleIds (generated at runtime) will always be newer than MIN_BUNDLE_ID
|
|
255
|
-
// Why 26 hours? Global timezone range spans from UTC-12 to UTC+14 (total 26 hours)
|
|
256
|
-
// By subtracting 26 hours, MIN_BUNDLE_ID becomes a safe "past timestamp" regardless of build timezone
|
|
257
|
-
// Example: Build at 15:00 in any timezone -> parse as 15:00 UTC -> subtract 26h -> 13:00 UTC (previous day)
|
|
258
|
-
NSTimeInterval adjustedTimestamp = [buildDate timeIntervalSince1970] - 93600.0;
|
|
259
|
-
uint64_t buildTimestampMs = (uint64_t)(adjustedTimestamp * 1000.0);
|
|
260
|
-
|
|
261
|
-
uuid = [HotUpdater generateUUIDv7FromTimestamp:buildTimestampMs];
|
|
237
|
+
return HotUpdaterBuildMinBundleId();
|
|
262
238
|
#endif
|
|
263
|
-
});
|
|
264
|
-
return uuid;
|
|
265
239
|
}
|
|
266
240
|
|
|
267
241
|
extern "C" void *HotUpdaterCopyMinBundleId(void)
|
|
@@ -405,54 +379,22 @@ RCT_EXPORT_MODULE();
|
|
|
405
379
|
static HotUpdaterImpl *_sharedImpl = nil;
|
|
406
380
|
static dispatch_once_t onceToken;
|
|
407
381
|
dispatch_once(&onceToken, ^{
|
|
408
|
-
_sharedImpl =
|
|
382
|
+
_sharedImpl = HotUpdaterMakeImpl();
|
|
409
383
|
});
|
|
410
384
|
return _sharedImpl;
|
|
411
385
|
}
|
|
412
386
|
|
|
413
387
|
#pragma mark - React Native Constants (Keep getMinBundleId, delegate others)
|
|
414
388
|
|
|
415
|
-
// Returns the
|
|
389
|
+
// Returns the bundle ID of the bundle this build ships (MIN_BUNDLE_ID)
|
|
416
390
|
- (NSString *)getMinBundleId {
|
|
417
391
|
return HotUpdaterGetMinBundleId();
|
|
418
392
|
}
|
|
419
393
|
|
|
420
|
-
// Helper method: Generate UUID v7 from timestamp (milliseconds)
|
|
421
|
-
+ (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs {
|
|
422
|
-
unsigned char bytes[16];
|
|
423
|
-
|
|
424
|
-
// UUID v7 format: timestamp_ms (48 bits) + ver (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
|
|
425
|
-
bytes[0] = (timestampMs >> 40) & 0xFF;
|
|
426
|
-
bytes[1] = (timestampMs >> 32) & 0xFF;
|
|
427
|
-
bytes[2] = (timestampMs >> 24) & 0xFF;
|
|
428
|
-
bytes[3] = (timestampMs >> 16) & 0xFF;
|
|
429
|
-
bytes[4] = (timestampMs >> 8) & 0xFF;
|
|
430
|
-
bytes[5] = timestampMs & 0xFF;
|
|
431
|
-
|
|
432
|
-
// Version 7
|
|
433
|
-
bytes[6] = 0x70;
|
|
434
|
-
bytes[7] = 0x00;
|
|
435
|
-
|
|
436
|
-
// Variant bits (10xxxxxx)
|
|
437
|
-
bytes[8] = 0x80;
|
|
438
|
-
bytes[9] = 0x00;
|
|
439
|
-
|
|
440
|
-
// Remaining bytes (zeros for deterministic MIN_BUNDLE_ID)
|
|
441
|
-
for (int i = 10; i < 16; i++) {
|
|
442
|
-
bytes[i] = 0x00;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return [NSString stringWithFormat:
|
|
446
|
-
@"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
|
447
|
-
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
|
448
|
-
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
394
|
- (NSDictionary *)_buildConstantsDictionary {
|
|
453
395
|
return @{
|
|
454
396
|
@"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null],
|
|
455
|
-
@"APP_VERSION":
|
|
397
|
+
@"APP_VERSION": HotUpdaterAppVersion() ?: [NSNull null],
|
|
456
398
|
@"CHANNEL": [[HotUpdater sharedImpl] getChannel] ?: [NSNull null],
|
|
457
399
|
@"DEFAULT_CHANNEL": [[HotUpdater sharedImpl] getDefaultChannel] ?: [NSNull null],
|
|
458
400
|
@"FINGERPRINT_HASH": [[HotUpdater sharedImpl] getFingerprintHash] ?: [NSNull null]
|
|
@@ -484,7 +426,7 @@ RCT_EXPORT_MODULE();
|
|
|
484
426
|
publicKey:(NSString *)publicKey
|
|
485
427
|
channel:(NSString *)channel
|
|
486
428
|
isolationKey:(NSString *)isolationKey {
|
|
487
|
-
HotUpdaterConfig *config =
|
|
429
|
+
HotUpdaterConfig *config = HotUpdaterSharedConfig();
|
|
488
430
|
[config configureWithFingerprintHash:fingerprintHash publicKey:publicKey channel:channel isolationKey:isolationKey];
|
|
489
431
|
}
|
|
490
432
|
|
|
@@ -590,6 +532,15 @@ RCT_EXPORT_MODULE();
|
|
|
590
532
|
if (params.selection()) {
|
|
591
533
|
paramDict[@"selection"] = params.selection();
|
|
592
534
|
}
|
|
535
|
+
if (params.filesUrl()) {
|
|
536
|
+
paramDict[@"filesUrl"] = params.filesUrl();
|
|
537
|
+
}
|
|
538
|
+
if (params.requestHeaders()) {
|
|
539
|
+
paramDict[@"requestHeaders"] = params.requestHeaders();
|
|
540
|
+
}
|
|
541
|
+
if (params.requestTimeout().has_value()) {
|
|
542
|
+
paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
|
|
543
|
+
}
|
|
593
544
|
|
|
594
545
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
595
546
|
[impl updateBundle:paramDict resolver:resolve rejecter:reject];
|
|
@@ -710,7 +661,7 @@ RCT_EXPORT_MODULE();
|
|
|
710
661
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
711
662
|
return facebook::react::typedConstants<JS::NativeHotUpdater::Constants::Builder>({
|
|
712
663
|
.MIN_BUNDLE_ID = [self getMinBundleId],
|
|
713
|
-
.APP_VERSION =
|
|
664
|
+
.APP_VERSION = HotUpdaterAppVersion(),
|
|
714
665
|
.CHANNEL = [impl getChannel],
|
|
715
666
|
.DEFAULT_CHANNEL = [impl getDefaultChannel],
|
|
716
667
|
.FINGERPRINT_HASH = [impl getFingerprintHash],
|
|
@@ -260,7 +260,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
260
260
|
* @param reject Promise reject callback; download and install failures
|
|
261
261
|
* carry `stage`, `reason`, and their details in the error's userInfo
|
|
262
262
|
*/
|
|
263
|
-
public func updateBundle(_ params:
|
|
263
|
+
public func updateBundle(_ params: [AnyHashable: Any]?,
|
|
264
264
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
265
265
|
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
266
266
|
|
|
@@ -316,11 +316,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
316
316
|
return
|
|
317
317
|
}
|
|
318
318
|
let assets = assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
else {
|
|
322
|
-
return
|
|
323
|
-
}
|
|
319
|
+
// In artifact protocol v2, an entry holds only the Hermes patch.
|
|
320
|
+
let fileHash = entry.value["fileHash"] as? String ?? ""
|
|
324
321
|
|
|
325
322
|
let patchPayload = entry.value["patch"] as? [String: Any]
|
|
326
323
|
let patch = patchPayload.flatMap { payload -> BsdiffPatchDescriptor? in
|
|
@@ -346,7 +343,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
346
343
|
let filePayload = entry.value["file"] as? [String: Any]
|
|
347
344
|
let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
|
|
348
345
|
let fileCompression = filePayload?["compression"] as? String
|
|
349
|
-
guard fileUrl != nil || patch != nil
|
|
346
|
+
guard fileUrl != nil || patch != nil,
|
|
347
|
+
!fileHash.isEmpty || fileUrl == nil else {
|
|
350
348
|
return
|
|
351
349
|
}
|
|
352
350
|
|
|
@@ -357,6 +355,22 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
357
355
|
patch: patch
|
|
358
356
|
)
|
|
359
357
|
}
|
|
358
|
+
var fileResolver: ArtifactFileResolver?
|
|
359
|
+
if let filesUrlString = data["filesUrl"] as? String {
|
|
360
|
+
guard let filesUrl = URL(string: filesUrlString) else {
|
|
361
|
+
let error = UpdateFailure.invalidResponse.with(resource: .file).error(
|
|
362
|
+
description: "Invalid 'filesUrl' provided: \(filesUrlString)"
|
|
363
|
+
)
|
|
364
|
+
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
// The update check's headers, which the artifact files request repeats.
|
|
368
|
+
let headers = (data["requestHeaders"] as? [String: Any] ?? [:])
|
|
369
|
+
.compactMapValues { $0 as? String }
|
|
370
|
+
let timeout = (data["requestTimeout"] as? NSNumber).map { $0.doubleValue / 1000 }
|
|
371
|
+
.flatMap { $0 > 0 ? $0 : nil } ?? HTTPArtifactFileResolver.defaultTimeout
|
|
372
|
+
fileResolver = HTTPArtifactFileResolver(filesUrl: filesUrl, headers: headers, timeout: timeout)
|
|
373
|
+
}
|
|
360
374
|
let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
|
|
361
375
|
if let selection, !bundleStorage.stageReleaseSelection(selection) {
|
|
362
376
|
// Not an update failure, so the rejection carries no stage or reason.
|
|
@@ -371,7 +385,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
371
385
|
NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
|
|
372
386
|
|
|
373
387
|
// Heavy work is delegated to bundle storage service with safe error handling
|
|
374
|
-
bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, progressHandler: { payload in
|
|
388
|
+
bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, fileResolver: fileResolver, progressHandler: { payload in
|
|
375
389
|
DispatchQueue.main.async {
|
|
376
390
|
NotificationCenter.default.post(
|
|
377
391
|
name: .updateProgressDidChange,
|
|
@@ -448,7 +462,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
448
462
|
)
|
|
449
463
|
}
|
|
450
464
|
|
|
451
|
-
public func acceptReleaseCatalog(_ params:
|
|
465
|
+
public func acceptReleaseCatalog(_ params: [AnyHashable: Any]?) -> Bool {
|
|
452
466
|
guard let data = params as? [String: Any],
|
|
453
467
|
let catalogId = data["catalogId"] as? String,
|
|
454
468
|
let scopeKey = data["scopeKey"] as? String,
|
|
@@ -507,7 +521,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
507
521
|
}
|
|
508
522
|
}
|
|
509
523
|
|
|
510
|
-
public func isReleaseSelectionCurrent(_ params:
|
|
524
|
+
public func isReleaseSelectionCurrent(_ params: [AnyHashable: Any]?) -> Bool {
|
|
511
525
|
guard let data = params as? [String: Any],
|
|
512
526
|
let catalogId = data["catalogId"] as? String,
|
|
513
527
|
let scopeKey = data["scopeKey"] as? String,
|
|
@@ -528,7 +542,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
528
542
|
}
|
|
529
543
|
|
|
530
544
|
public func commitReleaseSelection(
|
|
531
|
-
_ params:
|
|
545
|
+
_ params: [AnyHashable: Any]?,
|
|
532
546
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
533
547
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
534
548
|
) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#if SWIFT_PACKAGE
|
|
2
|
+
import Foundation
|
|
3
|
+
import HotUpdaterInterface
|
|
4
|
+
|
|
5
|
+
// Swift Package Manager only: the Objective-C++ module reaches these classes
|
|
6
|
+
// through the protocols in HotUpdaterInterface.h, and these conformances make
|
|
7
|
+
// the compiler check that the classes still match them.
|
|
8
|
+
extension HotUpdaterImpl: HotUpdaterImplInterface {}
|
|
9
|
+
|
|
10
|
+
extension HotUpdaterConfig: HotUpdaterConfigInterface {}
|
|
11
|
+
|
|
12
|
+
@_cdecl("HotUpdaterMakeImpl")
|
|
13
|
+
public func hotUpdaterMakeImpl() -> HotUpdaterImplInterface {
|
|
14
|
+
HotUpdaterImpl()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@_cdecl("HotUpdaterAppVersion")
|
|
18
|
+
public func hotUpdaterAppVersion() -> NSString? {
|
|
19
|
+
HotUpdaterImpl.appVersion as NSString?
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@_cdecl("HotUpdaterSharedConfig")
|
|
23
|
+
public func hotUpdaterSharedConfig() -> HotUpdaterConfigInterface {
|
|
24
|
+
HotUpdaterConfig.shared
|
|
25
|
+
}
|
|
26
|
+
#endif
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
// Generated before every compile by ios/scripts/generate-min-bundle-id.zsh,
|
|
4
|
+
// so this file is the only one rebuilt when the value changes.
|
|
5
|
+
#import "HotUpdaterBuildMinBundleId.h"
|
|
6
|
+
|
|
7
|
+
NSString *HotUpdaterBuildMinBundleId(void)
|
|
8
|
+
{
|
|
9
|
+
return @HOT_UPDATER_BUILD_MIN_BUNDLE_ID;
|
|
10
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// swift-tools-version: 5.10
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let coreSources = [
|
|
5
|
+
"ArtifactFileResolver.swift",
|
|
6
|
+
"FileUtilities.swift",
|
|
7
|
+
"BrotliFileDecompressor.swift",
|
|
8
|
+
"TarArchiveExtractor.swift",
|
|
9
|
+
"BundleFileStorageService.swift",
|
|
10
|
+
"BuiltInAssetResolver.swift",
|
|
11
|
+
"BundleMetadata.swift",
|
|
12
|
+
"FileManagerService.swift",
|
|
13
|
+
"HashUtils.swift",
|
|
14
|
+
"HotUpdaterConfig.swift",
|
|
15
|
+
"KeyValueStorageService.swift",
|
|
16
|
+
"NotificationExtension.swift",
|
|
17
|
+
"ReleaseCatalogCacheService.swift",
|
|
18
|
+
"SignatureVerifier.swift",
|
|
19
|
+
"UpdateFailure.swift",
|
|
20
|
+
"URLSessionDownloadService.swift",
|
|
21
|
+
"VersionedPreferencesService.swift",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
let coreExcludedFiles = [
|
|
25
|
+
"BsdiffPatchBridge.h",
|
|
26
|
+
"BsdiffPatchBridge.mm",
|
|
27
|
+
"CohortService.swift",
|
|
28
|
+
"HotUpdater-Bridging-Header.h",
|
|
29
|
+
"HotUpdater.mm",
|
|
30
|
+
"HotUpdaterImpl.swift",
|
|
31
|
+
"HotUpdaterInterface.swift",
|
|
32
|
+
"HotUpdaterMinBundleId.m",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
let bsdiffPatchBridgeExcludedFiles =
|
|
36
|
+
coreSources + coreExcludedFiles.filter {
|
|
37
|
+
$0 != "BsdiffPatchBridge.h" && $0 != "BsdiffPatchBridge.mm"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let package = Package(
|
|
41
|
+
name: "HotUpdater",
|
|
42
|
+
platforms: [
|
|
43
|
+
.iOS(.v13),
|
|
44
|
+
.macOS(.v10_15)
|
|
45
|
+
],
|
|
46
|
+
products: [
|
|
47
|
+
.library(
|
|
48
|
+
name: "HotUpdaterCore",
|
|
49
|
+
targets: ["HotUpdaterCore"]
|
|
50
|
+
)
|
|
51
|
+
],
|
|
52
|
+
dependencies: [],
|
|
53
|
+
targets: [
|
|
54
|
+
.target(
|
|
55
|
+
name: "HotUpdaterRecovery",
|
|
56
|
+
path: "Recovery",
|
|
57
|
+
exclude: ["HotUpdaterRecoveryHooks.mm"],
|
|
58
|
+
sources: ["HotUpdaterRecovery.mm"],
|
|
59
|
+
publicHeadersPath: ".",
|
|
60
|
+
cxxSettings: [.define("RCT_NEW_ARCH_ENABLED")]
|
|
61
|
+
),
|
|
62
|
+
.target(
|
|
63
|
+
name: "HotUpdaterBsdiffPatch",
|
|
64
|
+
path: "Internal",
|
|
65
|
+
exclude: bsdiffPatchBridgeExcludedFiles,
|
|
66
|
+
sources: ["BsdiffPatchBridge.mm"],
|
|
67
|
+
publicHeadersPath: ".",
|
|
68
|
+
linkerSettings: [
|
|
69
|
+
.linkedFramework("Foundation"),
|
|
70
|
+
.linkedLibrary("bz2"),
|
|
71
|
+
]
|
|
72
|
+
),
|
|
73
|
+
// React Native's full native module cannot be built through SPM yet,
|
|
74
|
+
// but the pure-Swift update core can be.
|
|
75
|
+
.target(
|
|
76
|
+
name: "HotUpdaterCore",
|
|
77
|
+
dependencies: ["HotUpdaterBsdiffPatch"],
|
|
78
|
+
path: "Internal",
|
|
79
|
+
exclude: coreExcludedFiles,
|
|
80
|
+
sources: coreSources
|
|
81
|
+
),
|
|
82
|
+
.testTarget(
|
|
83
|
+
name: "HotUpdaterTest",
|
|
84
|
+
dependencies: ["HotUpdaterCore", "HotUpdaterRecovery"],
|
|
85
|
+
path: "Test"
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import PackagePlugin
|
|
3
|
+
|
|
4
|
+
// Runs ios/scripts/generate-min-bundle-id.zsh before every build of the target,
|
|
5
|
+
// the SwiftPM counterpart of the podspec's script phase. The script writes a
|
|
6
|
+
// Swift file defining HotUpdaterBuildMinBundleId(), which HotUpdater.mm calls.
|
|
7
|
+
@main
|
|
8
|
+
struct HotUpdaterMinBundleIdPlugin: BuildToolPlugin {
|
|
9
|
+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
|
|
10
|
+
let script = context.package.directoryURL
|
|
11
|
+
.appending(path: "ios/scripts/generate-min-bundle-id.zsh")
|
|
12
|
+
let outputDirectory = context.pluginWorkDirectoryURL
|
|
13
|
+
.appending(path: "MinBundleId")
|
|
14
|
+
var environment: [String: String] = [:]
|
|
15
|
+
if let minBundleId = ProcessInfo.processInfo.environment["HOT_UPDATER_MIN_BUNDLE_ID"] {
|
|
16
|
+
environment["HOT_UPDATER_MIN_BUNDLE_ID"] = minBundleId
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
.prebuildCommand(
|
|
20
|
+
displayName: "[HotUpdater] Generate MIN_BUNDLE_ID",
|
|
21
|
+
executable: URL(filePath: "/bin/zsh"),
|
|
22
|
+
arguments: [script.path(percentEncoded: false), "--swift", outputDirectory.path(percentEncoded: false)],
|
|
23
|
+
environment: environment,
|
|
24
|
+
outputFilesDirectory: outputDirectory
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|