@hot-updater/react-native 1.0.5 → 1.0.7
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 +129 -34
- package/android/src/oldarch/HotUpdaterModule.kt +130 -34
- package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +69 -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 +90 -90
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +154 -52
- 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 +17 -2
- 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 +58 -5
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/native.js +42 -7
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js +5 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +18 -3
- 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 +61 -7
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/native.js +41 -7
- package/lib/module/native.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js +7 -0
- 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 +26 -2
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +17 -3
- 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 +37 -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 +26 -2
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +17 -3
- 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 +37 -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 +19 -3
- package/src/fetchUpdateResponse.ts +4 -1
- package/src/httpClient.ts +88 -9
- package/src/native.ts +53 -9
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +39 -1
|
@@ -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,11 +532,61 @@ RCT_EXPORT_MODULE();
|
|
|
590
532
|
if (params.selection()) {
|
|
591
533
|
paramDict[@"selection"] = params.selection();
|
|
592
534
|
}
|
|
535
|
+
// Legacy: what JavaScript from the first artifact protocol v2 SDK sends.
|
|
536
|
+
if (params.filesUrl()) {
|
|
537
|
+
paramDict[@"filesUrl"] = params.filesUrl();
|
|
538
|
+
}
|
|
539
|
+
if (params.requestHeaders()) {
|
|
540
|
+
paramDict[@"requestHeaders"] = params.requestHeaders();
|
|
541
|
+
}
|
|
542
|
+
if (params.requestTimeout().has_value()) {
|
|
543
|
+
paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
|
|
544
|
+
}
|
|
593
545
|
|
|
594
546
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
595
547
|
[impl updateBundle:paramDict resolver:resolve rejecter:reject];
|
|
596
548
|
}
|
|
597
549
|
|
|
550
|
+
- (void)installArtifact:(JS::NativeHotUpdater::InstallArtifactParams &)params
|
|
551
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
552
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
553
|
+
NSLog(@"[HotUpdater.mm] installArtifact called.");
|
|
554
|
+
NSMutableDictionary *paramDict = [NSMutableDictionary dictionary];
|
|
555
|
+
if (params.bundleId()) {
|
|
556
|
+
paramDict[@"bundleId"] = params.bundleId();
|
|
557
|
+
}
|
|
558
|
+
if (params.manifestUrl()) {
|
|
559
|
+
paramDict[@"manifestUrl"] = params.manifestUrl();
|
|
560
|
+
}
|
|
561
|
+
if (params.manifestFileHash()) {
|
|
562
|
+
paramDict[@"manifestFileHash"] = params.manifestFileHash();
|
|
563
|
+
}
|
|
564
|
+
if (params.archiveUrl()) {
|
|
565
|
+
paramDict[@"archiveUrl"] = params.archiveUrl();
|
|
566
|
+
}
|
|
567
|
+
if (params.patch()) {
|
|
568
|
+
paramDict[@"patch"] = params.patch();
|
|
569
|
+
}
|
|
570
|
+
if (params.filesUrl()) {
|
|
571
|
+
paramDict[@"filesUrl"] = params.filesUrl();
|
|
572
|
+
}
|
|
573
|
+
if (params.requestHeaders()) {
|
|
574
|
+
paramDict[@"requestHeaders"] = params.requestHeaders();
|
|
575
|
+
}
|
|
576
|
+
if (params.requestTimeout().has_value()) {
|
|
577
|
+
paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
|
|
578
|
+
}
|
|
579
|
+
if (params.channel()) {
|
|
580
|
+
paramDict[@"channel"] = params.channel();
|
|
581
|
+
}
|
|
582
|
+
if (params.selection()) {
|
|
583
|
+
paramDict[@"selection"] = params.selection();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
587
|
+
[impl installArtifact:paramDict resolver:resolve rejecter:reject];
|
|
588
|
+
}
|
|
589
|
+
|
|
598
590
|
- (NSNumber *)acceptReleaseCatalog:(NSDictionary *)params {
|
|
599
591
|
return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
|
|
600
592
|
}
|
|
@@ -710,7 +702,7 @@ RCT_EXPORT_MODULE();
|
|
|
710
702
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
711
703
|
return facebook::react::typedConstants<JS::NativeHotUpdater::Constants::Builder>({
|
|
712
704
|
.MIN_BUNDLE_ID = [self getMinBundleId],
|
|
713
|
-
.APP_VERSION =
|
|
705
|
+
.APP_VERSION = HotUpdaterAppVersion(),
|
|
714
706
|
.CHANNEL = [impl getChannel],
|
|
715
707
|
.DEFAULT_CHANNEL = [impl getDefaultChannel],
|
|
716
708
|
.FINGERPRINT_HASH = [impl getFingerprintHash],
|
|
@@ -760,6 +752,14 @@ RCT_EXPORT_METHOD(updateBundle:(NSDictionary *)params
|
|
|
760
752
|
[impl updateBundle:params resolver:resolve rejecter:reject];
|
|
761
753
|
}
|
|
762
754
|
|
|
755
|
+
RCT_EXPORT_METHOD(installArtifact:(NSDictionary *)params
|
|
756
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
757
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
758
|
+
NSLog(@"[HotUpdater.mm] installArtifact called.");
|
|
759
|
+
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
760
|
+
[impl installArtifact:params resolver:resolve rejecter:reject];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
763
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(acceptReleaseCatalog:(NSDictionary *)params) {
|
|
764
764
|
return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
|
|
765
765
|
}
|
|
@@ -260,10 +260,52 @@ 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
|
+
// Legacy: artifact protocol v1, where every file comes with its URL.
|
|
267
|
+
// JavaScript from the first protocol v2 SDK also sends `filesUrl`
|
|
268
|
+
// here, with only the Hermes patch in `assets`; its OTA Bundles must
|
|
269
|
+
// still install.
|
|
270
|
+
stage(params, resolver: resolve, rejecter: reject) { data, reject in
|
|
271
|
+
switch Self.parseFileResolver(data, required: false) {
|
|
272
|
+
case .failure(let error):
|
|
273
|
+
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
274
|
+
return nil
|
|
275
|
+
case .success(let fileResolver):
|
|
276
|
+
return Self.parseAssets(data, requireFileHash: fileResolver == nil, rejecter: reject)
|
|
277
|
+
.map { ($0, fileResolver) }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Installs a Bundle over artifact protocol v2: native asks `filesUrl` for
|
|
284
|
+
* the files it downloads one by one, after reusing local files and
|
|
285
|
+
* choosing the archive.
|
|
286
|
+
* @param params Dictionary with bundleId, manifest, `filesUrl`, and the
|
|
287
|
+
* optional Hermes `patch` with its `assetPath`
|
|
288
|
+
* @param resolve Promise resolve callback, called with the staged bundle's
|
|
289
|
+
* `{ delivery, patchFallback }`
|
|
290
|
+
* @param reject Promise reject callback, as for `updateBundle`
|
|
291
|
+
*/
|
|
292
|
+
public func installArtifact(_ params: [AnyHashable: Any]?,
|
|
293
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
294
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
295
|
+
stage(params, resolver: resolve, rejecter: reject) { data, reject in
|
|
296
|
+
Self.parseArtifactInstall(data, rejecter: reject)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private typealias StagePlan = (assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver?)
|
|
301
|
+
|
|
302
|
+
/// Validates the common parameters, then stages the Bundle with the
|
|
303
|
+
/// assets and file resolver `plan` returns; `plan` rejects and returns nil
|
|
304
|
+
/// when its own parameters are invalid.
|
|
305
|
+
private func stage(_ params: [AnyHashable: Any]?,
|
|
306
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
307
|
+
rejecter reject: @escaping RCTPromiseRejectBlock,
|
|
308
|
+
plan: ([AnyHashable: Any], RCTPromiseRejectBlock) -> StagePlan?) {
|
|
267
309
|
do {
|
|
268
310
|
// Validate parameters (this runs on calling thread - typically JS thread)
|
|
269
311
|
guard let data = params else {
|
|
@@ -309,54 +351,9 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
309
351
|
} else {
|
|
310
352
|
archiveUrl = nil
|
|
311
353
|
}
|
|
312
|
-
guard let
|
|
313
|
-
let error = UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
314
|
-
.error(description: "Missing manifest assets")
|
|
315
|
-
reject("INVALID_MANIFEST", error.localizedDescription, error)
|
|
354
|
+
guard let planned = plan(data, reject) else {
|
|
316
355
|
return
|
|
317
356
|
}
|
|
318
|
-
let assets = assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
|
|
319
|
-
guard let fileHash = entry.value["fileHash"] as? String,
|
|
320
|
-
!fileHash.isEmpty
|
|
321
|
-
else {
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let patchPayload = entry.value["patch"] as? [String: Any]
|
|
326
|
-
let patch = patchPayload.flatMap { payload -> BsdiffPatchDescriptor? in
|
|
327
|
-
guard let algorithm = payload["algorithm"] as? String,
|
|
328
|
-
let baseBundleId = payload["baseBundleId"] as? String,
|
|
329
|
-
let baseFileHash = payload["baseFileHash"] as? String,
|
|
330
|
-
let patchFileHash = payload["patchFileHash"] as? String,
|
|
331
|
-
let patchUrlString = payload["patchUrl"] as? String,
|
|
332
|
-
let patchUrl = URL(string: patchUrlString)
|
|
333
|
-
else {
|
|
334
|
-
return nil
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return BsdiffPatchDescriptor(
|
|
338
|
-
algorithm: algorithm,
|
|
339
|
-
baseBundleId: baseBundleId,
|
|
340
|
-
baseFileHash: baseFileHash,
|
|
341
|
-
patchFileHash: patchFileHash,
|
|
342
|
-
patchUrl: patchUrl,
|
|
343
|
-
byteSize: Self.parseSafeByteSize(payload["byteSize"])
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
let filePayload = entry.value["file"] as? [String: Any]
|
|
347
|
-
let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
|
|
348
|
-
let fileCompression = filePayload?["compression"] as? String
|
|
349
|
-
guard fileUrl != nil || patch != nil else {
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
partialResult[entry.key] = ChangedAssetDescriptor(
|
|
354
|
-
fileUrl: fileUrl,
|
|
355
|
-
fileHash: fileHash,
|
|
356
|
-
fileCompression: fileCompression,
|
|
357
|
-
patch: patch
|
|
358
|
-
)
|
|
359
|
-
}
|
|
360
357
|
let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
|
|
361
358
|
if let selection, !bundleStorage.stageReleaseSelection(selection) {
|
|
362
359
|
// Not an update failure, so the rejection carries no stage or reason.
|
|
@@ -371,7 +368,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
371
368
|
NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
|
|
372
369
|
|
|
373
370
|
// 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
|
|
371
|
+
bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: planned.assets, fileResolver: planned.fileResolver, progressHandler: { payload in
|
|
375
372
|
DispatchQueue.main.async {
|
|
376
373
|
NotificationCenter.default.post(
|
|
377
374
|
name: .updateProgressDidChange,
|
|
@@ -429,6 +426,111 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
429
426
|
}
|
|
430
427
|
}
|
|
431
428
|
|
|
429
|
+
/// Every changed file with its hash and URL or Hermes patch; without
|
|
430
|
+
/// `requireFileHash`, also entries that hold only the Hermes patch.
|
|
431
|
+
private static func parseAssets(_ data: [AnyHashable: Any],
|
|
432
|
+
requireFileHash: Bool,
|
|
433
|
+
rejecter reject: RCTPromiseRejectBlock) -> [String: ChangedAssetDescriptor]? {
|
|
434
|
+
guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
|
|
435
|
+
let error = UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
436
|
+
.error(description: "Missing manifest assets")
|
|
437
|
+
reject("INVALID_MANIFEST", error.localizedDescription, error)
|
|
438
|
+
return nil
|
|
439
|
+
}
|
|
440
|
+
return assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
|
|
441
|
+
let fileHash = entry.value["fileHash"] as? String ?? ""
|
|
442
|
+
guard !fileHash.isEmpty || !requireFileHash else {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let patch = (entry.value["patch"] as? [String: Any]).flatMap(parsePatch)
|
|
447
|
+
let filePayload = entry.value["file"] as? [String: Any]
|
|
448
|
+
let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
|
|
449
|
+
let fileCompression = filePayload?["compression"] as? String
|
|
450
|
+
guard fileUrl != nil || patch != nil,
|
|
451
|
+
!fileHash.isEmpty || fileUrl == nil else {
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
partialResult[entry.key] = ChangedAssetDescriptor(
|
|
456
|
+
fileUrl: fileUrl,
|
|
457
|
+
fileHash: fileHash,
|
|
458
|
+
fileCompression: fileCompression,
|
|
459
|
+
patch: patch
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// The resolver that asks `filesUrl` for the files native downloads one
|
|
465
|
+
/// by one, with the update check's headers and timeout; nil when
|
|
466
|
+
/// `filesUrl` is missing and not `required`.
|
|
467
|
+
private static func parseFileResolver(_ data: [AnyHashable: Any],
|
|
468
|
+
required: Bool) -> Result<ArtifactFileResolver?, NSError> {
|
|
469
|
+
let filesUrlString = data["filesUrl"] as? String ?? ""
|
|
470
|
+
if filesUrlString.isEmpty && !required {
|
|
471
|
+
return .success(nil)
|
|
472
|
+
}
|
|
473
|
+
guard !filesUrlString.isEmpty, let filesUrl = URL(string: filesUrlString) else {
|
|
474
|
+
return .failure(UpdateFailure.invalidResponse.with(resource: .file).error(
|
|
475
|
+
description: "Invalid 'filesUrl' provided: \(filesUrlString)"
|
|
476
|
+
))
|
|
477
|
+
}
|
|
478
|
+
// The update check's headers, which the artifact files request repeats.
|
|
479
|
+
let headers = (data["requestHeaders"] as? [String: Any] ?? [:])
|
|
480
|
+
.compactMapValues { $0 as? String }
|
|
481
|
+
let timeout = (data["requestTimeout"] as? NSNumber).map { $0.doubleValue / 1000 }
|
|
482
|
+
.flatMap { $0 > 0 ? $0 : nil } ?? HTTPArtifactFileResolver.defaultTimeout
|
|
483
|
+
return .success(HTTPArtifactFileResolver(filesUrl: filesUrl, headers: headers, timeout: timeout))
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Artifact protocol v2: the Hermes patch as the only planned asset, and
|
|
487
|
+
/// the resolver that asks `filesUrl` for the files native downloads one
|
|
488
|
+
/// by one.
|
|
489
|
+
private static func parseArtifactInstall(_ data: [AnyHashable: Any],
|
|
490
|
+
rejecter reject: RCTPromiseRejectBlock) -> StagePlan? {
|
|
491
|
+
let fileResolver: ArtifactFileResolver?
|
|
492
|
+
switch parseFileResolver(data, required: true) {
|
|
493
|
+
case .failure(let error):
|
|
494
|
+
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
495
|
+
return nil
|
|
496
|
+
case .success(let resolver):
|
|
497
|
+
fileResolver = resolver
|
|
498
|
+
}
|
|
499
|
+
var assets: [String: ChangedAssetDescriptor] = [:]
|
|
500
|
+
if let patchPayload = data["patch"] as? [String: Any],
|
|
501
|
+
let assetPath = patchPayload["assetPath"] as? String,
|
|
502
|
+
let patch = parsePatch(patchPayload) {
|
|
503
|
+
assets[assetPath] = ChangedAssetDescriptor(
|
|
504
|
+
fileUrl: nil,
|
|
505
|
+
fileHash: "",
|
|
506
|
+
fileCompression: nil,
|
|
507
|
+
patch: patch
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
return (assets, fileResolver)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private static func parsePatch(_ payload: [String: Any]) -> BsdiffPatchDescriptor? {
|
|
514
|
+
guard let algorithm = payload["algorithm"] as? String,
|
|
515
|
+
let baseBundleId = payload["baseBundleId"] as? String,
|
|
516
|
+
let baseFileHash = payload["baseFileHash"] as? String,
|
|
517
|
+
let patchFileHash = payload["patchFileHash"] as? String,
|
|
518
|
+
let patchUrlString = payload["patchUrl"] as? String,
|
|
519
|
+
let patchUrl = URL(string: patchUrlString)
|
|
520
|
+
else {
|
|
521
|
+
return nil
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return BsdiffPatchDescriptor(
|
|
525
|
+
algorithm: algorithm,
|
|
526
|
+
baseBundleId: baseBundleId,
|
|
527
|
+
baseFileHash: baseFileHash,
|
|
528
|
+
patchFileHash: patchFileHash,
|
|
529
|
+
patchUrl: patchUrl,
|
|
530
|
+
byteSize: parseSafeByteSize(payload["byteSize"])
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
432
534
|
private static func parseSelection(_ data: [String: Any]) -> PersistedSelection? {
|
|
433
535
|
guard let kind = data["kind"] as? String,
|
|
434
536
|
let bundleId = data["bundleId"] as? String,
|
|
@@ -448,7 +550,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
448
550
|
)
|
|
449
551
|
}
|
|
450
552
|
|
|
451
|
-
public func acceptReleaseCatalog(_ params:
|
|
553
|
+
public func acceptReleaseCatalog(_ params: [AnyHashable: Any]?) -> Bool {
|
|
452
554
|
guard let data = params as? [String: Any],
|
|
453
555
|
let catalogId = data["catalogId"] as? String,
|
|
454
556
|
let scopeKey = data["scopeKey"] as? String,
|
|
@@ -507,7 +609,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
507
609
|
}
|
|
508
610
|
}
|
|
509
611
|
|
|
510
|
-
public func isReleaseSelectionCurrent(_ params:
|
|
612
|
+
public func isReleaseSelectionCurrent(_ params: [AnyHashable: Any]?) -> Bool {
|
|
511
613
|
guard let data = params as? [String: Any],
|
|
512
614
|
let catalogId = data["catalogId"] as? String,
|
|
513
615
|
let scopeKey = data["scopeKey"] as? String,
|
|
@@ -528,7 +630,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
528
630
|
}
|
|
529
631
|
|
|
530
632
|
public func commitReleaseSelection(
|
|
531
|
-
_ params:
|
|
633
|
+
_ params: [AnyHashable: Any]?,
|
|
532
634
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
533
635
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
534
636
|
) {
|
|
@@ -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
|
+
}
|