@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.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.
Files changed (105) hide show
  1. package/CodePush.podspec +35 -3
  2. package/README.md +115 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +19 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -2
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +18 -1
  94. package/ios/CodePush/CodePush.mm +16 -1
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/package.json +10 -3
  100. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  101. package/scripts/export-embedded-bundle.sh +76 -0
  102. package/src/CodePush.js +46 -3
  103. package/src/CodePush.test.js +382 -0
  104. package/src/package-mixins.js +20 -2
  105. package/typings/react-native-code-push.d.ts +77 -0
@@ -1,4 +1,5 @@
1
1
  #import "CodePush.h"
2
+ #import "CodePushBinaryPatch.h"
2
3
  #if __has_include(<SSZipArchive/SSZipArchive.h>)
3
4
  #import <SSZipArchive/SSZipArchive.h>
4
5
  #else
@@ -9,8 +10,18 @@
9
10
 
10
11
  #pragma mark - Private constants
11
12
 
13
+ static NSString *const BinaryPatchDownloadUrlKey = @"binaryPatchDownloadUrl";
14
+ static NSString *const BinaryPatchFolderName = @"binary-patch";
15
+ // The fields of what a download that tried a patch reports back with. They are read by the
16
+ // app that asked for the download and by nothing else, so they never reach a file.
17
+ static NSString *const BinaryPatchResultApplyDurationMsKey = @"applyDurationMs";
18
+ static NSString *const BinaryPatchResultFallbackReasonKey = @"fallbackReason";
19
+ static NSString *const BinaryPatchResultStatusKey = @"status";
20
+ static NSString *const BinaryPatchResultStatusApplied = @"applied";
21
+ static NSString *const BinaryPatchResultStatusFallback = @"fallback";
12
22
  static NSString *const DiffManifestFileName = @"hotcodepush.json";
13
23
  static NSString *const DownloadFileName = @"download.zip";
24
+ static NSString *const DownloadUrlKey = @"downloadUrl";
14
25
  static NSString *const RelativeBundlePathKey = @"bundlePath";
15
26
  static NSString *const StatusFile = @"codepush.json";
16
27
  static NSString *const UpdateBundleFileName = @"app.jsbundle";
@@ -47,8 +58,136 @@ static NSString *const UnzippedFolderName = @"unzipped";
47
58
  expectedBundleFileName:(NSString *)expectedBundleFileName
48
59
  operationQueue:(dispatch_queue_t)operationQueue
49
60
  progressCallback:(void (^)(long long, long long))progressCallback
50
- doneCallback:(void (^)())doneCallback
61
+ doneCallback:(void (^)(NSDictionary *binaryPatchResult))doneCallback
51
62
  failCallback:(void (^)(NSError *err))failCallback
63
+ {
64
+ void (^downloadFullPackage)(NSDictionary *) = ^(NSDictionary *binaryPatchResult) {
65
+ [self downloadAndInstallPackage:updatePackage
66
+ expectedBundleFileName:expectedBundleFileName
67
+ operationQueue:operationQueue
68
+ downloadUrl:updatePackage[DownloadUrlKey]
69
+ isBinaryPatchUpdate:NO
70
+ progressCallback:progressCallback
71
+ doneCallback:^{
72
+ doneCallback(binaryPatchResult);
73
+ }
74
+ failCallback:failCallback
75
+ patchAppliedCallback:nil
76
+ patchFallbackCallback:nil];
77
+ };
78
+
79
+ // A release that was published with a binary patch offers two archives of the same
80
+ // update. The patch is worth trying because it is a fraction of the size, and the
81
+ // full archive is always there when it does not work out.
82
+ NSString *binaryPatchDownloadUrl = updatePackage[BinaryPatchDownloadUrlKey];
83
+ if (![binaryPatchDownloadUrl isKindOfClass:[NSString class]] || [binaryPatchDownloadUrl length] == 0) {
84
+ // A download that never had a patch to try has nothing to report about one.
85
+ return downloadFullPackage(nil);
86
+ }
87
+
88
+ // Every way the patch can fail ends the same way, with the update being downloaded in
89
+ // full instead, so none of them reaches the caller as an error. The fallback happens
90
+ // exactly once without anything having to count it: the full archive is downloaded by
91
+ // a call that is not allowed to take the patch path, so it has no failure of its own
92
+ // to fall back from.
93
+ NSDate *patchAttemptStartTime = [NSDate date];
94
+ // Set once the applier has restored the bundle, which is also what tells a failure
95
+ // that follows apart from one that came before.
96
+ __block NSNumber *applyDurationMs = nil;
97
+ [self downloadAndInstallPackage:updatePackage
98
+ expectedBundleFileName:expectedBundleFileName
99
+ operationQueue:operationQueue
100
+ downloadUrl:binaryPatchDownloadUrl
101
+ isBinaryPatchUpdate:YES
102
+ progressCallback:progressCallback
103
+ doneCallback:^{
104
+ [self deleteBinaryPatchFolder];
105
+ doneCallback([self appliedBinaryPatchResult:applyDurationMs]);
106
+ }
107
+ failCallback:^(NSError *err) {
108
+ [self deleteBinaryPatchFolder];
109
+ CPLog(@"The binary patch update could not be completed (%@). Downloading the full update instead.", err.localizedDescription);
110
+ // An error raised after the bundle was restored is the restored
111
+ // update failing the checks every update passes before it is
112
+ // installed. Before that point the appliers have no word for what
113
+ // happened - the patch archive not being downloadable, say - and
114
+ // inventing one here would put a value on the wire that no
115
+ // platform reports, so the fallback is reported without a reason.
116
+ NSString *failureReason = applyDurationMs == nil
117
+ ? nil
118
+ : CodePushBinaryPatchReasonPackageVerificationFailed;
119
+ downloadFullPackage([self binaryPatchFallbackResult:failureReason
120
+ attemptStartTime:patchAttemptStartTime]);
121
+ }
122
+ patchAppliedCallback:^(double durationMs) {
123
+ applyDurationMs = @(durationMs);
124
+ }
125
+ patchFallbackCallback:^(NSString *failureReason) {
126
+ [self deleteBinaryPatchFolder];
127
+ CPLog(@"Binary patch update failed (%@). Downloading the full update instead.", failureReason);
128
+ downloadFullPackage([self binaryPatchFallbackResult:failureReason
129
+ attemptStartTime:patchAttemptStartTime]);
130
+ }];
131
+ }
132
+
133
+ /*
134
+ * The result of a patch attempt the update was installed from, timed over the apply itself.
135
+ */
136
+ + (NSDictionary *)appliedBinaryPatchResult:(NSNumber *)applyDurationMs
137
+ {
138
+ return @{
139
+ BinaryPatchResultStatusKey: BinaryPatchResultStatusApplied,
140
+ BinaryPatchResultApplyDurationMsKey: applyDurationMs ?: @0,
141
+ };
142
+ }
143
+
144
+ /*
145
+ * The result of a patch attempt the update had to be downloaded in full after.
146
+ *
147
+ * There is no completed apply to time here, so the attempt itself is what is timed: from the
148
+ * patch archive being asked for to the moment the attempt was given up on. The reason is left
149
+ * out when the attempt ended in an error none of the appliers has a word for.
150
+ */
151
+ + (NSDictionary *)binaryPatchFallbackResult:(NSString *)failureReason
152
+ attemptStartTime:(NSDate *)attemptStartTime
153
+ {
154
+ NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
155
+ BinaryPatchResultStatusKey: BinaryPatchResultStatusFallback,
156
+ BinaryPatchResultApplyDurationMsKey: @(round([[NSDate date] timeIntervalSinceDate:attemptStartTime] * 1000)),
157
+ }];
158
+ if (failureReason) {
159
+ result[BinaryPatchResultFallbackReasonKey] = failureReason;
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ /*
166
+ * Downloads an update from one of its archives and installs it.
167
+ *
168
+ * isBinaryPatchUpdate says whether the archive holds a binary patch of the JS bundle,
169
+ * which has to be applied before the contents are the update. Only an archive downloaded
170
+ * from the binary patch URL is treated that way, so an update being downloaded in full
171
+ * can never end up on the patch path.
172
+ *
173
+ * patchAppliedCallback is called with how long the apply took the moment the bundle has
174
+ * been restored, which is before the update is known to install. It is nil for a full
175
+ * download, which has no patch to apply.
176
+ *
177
+ * patchFallbackCallback is called instead of doneCallback when the patch cannot be
178
+ * applied: the update was not installed, and the caller has to download the full archive.
179
+ * It is nil for a full download, which has nothing to fall back to.
180
+ */
181
+ + (void)downloadAndInstallPackage:(NSDictionary *)updatePackage
182
+ expectedBundleFileName:(NSString *)expectedBundleFileName
183
+ operationQueue:(dispatch_queue_t)operationQueue
184
+ downloadUrl:(NSString *)downloadUrl
185
+ isBinaryPatchUpdate:(BOOL)isBinaryPatchUpdate
186
+ progressCallback:(void (^)(long long, long long))progressCallback
187
+ doneCallback:(void (^)(void))doneCallback
188
+ failCallback:(void (^)(NSError *err))failCallback
189
+ patchAppliedCallback:(void (^)(double applyDurationMs))patchAppliedCallback
190
+ patchFallbackCallback:(void (^)(NSString *failureReason))patchFallbackCallback
52
191
  {
53
192
  NSString *newUpdateHash = updatePackage[@"packageHash"];
54
193
  NSString *newUpdateFolderPath = [self getPackageFolderPath:newUpdateHash];
@@ -109,6 +248,24 @@ static NSString *const UnzippedFolderName = @"unzipped";
109
248
  nonFailingError = nil;
110
249
  }
111
250
 
251
+ // Rebuild the JS bundle the archive only carries a patch of, which leaves the
252
+ // contents identical to the ones the full archive would have delivered.
253
+ if (isBinaryPatchUpdate) {
254
+ NSDate *patchStartTime = [NSDate date];
255
+ NSString *patchFailureReason = nil;
256
+ if (![CodePushBinaryPatch restoreBundleInUnzippedFolder:unzippedFolderPath
257
+ workingFolder:[self getBinaryPatchFolderPath]
258
+ baseBundleURL:[CodePush binaryBundleURL]
259
+ failureReason:&patchFailureReason]) {
260
+ patchFallbackCallback(patchFailureReason);
261
+ return;
262
+ }
263
+
264
+ double applyDurationMs = round([[NSDate date] timeIntervalSinceDate:patchStartTime] * 1000);
265
+ CPLog(@"Restored the update from its binary patch in %.0f ms.", applyDurationMs);
266
+ patchAppliedCallback(applyDurationMs);
267
+ }
268
+
112
269
  NSString *diffManifestFilePath = [unzippedFolderPath stringByAppendingPathComponent:DiffManifestFileName];
113
270
  BOOL isDiffUpdate = [[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath];
114
271
 
@@ -252,6 +409,15 @@ static NSString *const UnzippedFolderName = @"unzipped";
252
409
  }
253
410
 
254
411
  } else {
412
+ if (isBinaryPatchUpdate) {
413
+ // Whatever the patch URL served, it is not a patch archive - an error page
414
+ // answered with a 200 looks like this too. Moving it into place would
415
+ // install bytes no hash has ever been checked against, so the full archive
416
+ // is downloaded instead.
417
+ patchFallbackCallback(CodePushBinaryPatchReasonInvalidManifest);
418
+ return;
419
+ }
420
+
255
421
  [[NSFileManager defaultManager] createDirectoryAtPath:newUpdateFolderPath
256
422
  withIntermediateDirectories:YES
257
423
  attributes:nil
@@ -284,7 +450,22 @@ static NSString *const UnzippedFolderName = @"unzipped";
284
450
 
285
451
  failCallback:failCallback];
286
452
 
287
- [downloadHandler download:updatePackage[@"downloadUrl"]];
453
+ [downloadHandler download:downloadUrl];
454
+ }
455
+
456
+ /*
457
+ * Removes what a patch attempt works in, whichever way the attempt ended - and whatever
458
+ * an attempt that was killed halfway through left there.
459
+ */
460
+ + (void)deleteBinaryPatchFolder
461
+ {
462
+ [[NSFileManager defaultManager] removeItemAtPath:[self getBinaryPatchFolderPath]
463
+ error:nil];
464
+ }
465
+
466
+ + (NSString *)getBinaryPatchFolderPath
467
+ {
468
+ return [[self getCodePushPath] stringByAppendingPathComponent:BinaryPatchFolderName];
288
469
  }
289
470
 
290
471
  + (NSString *)getCodePushPath
@@ -7,6 +7,9 @@ NSString * const AssetsFolderName = @"assets";
7
7
  NSString * const BinaryHashKey = @"CodePushBinaryHash";
8
8
  NSString * const ManifestFolderPrefix = @"CodePush";
9
9
 
10
+ /* How much of a file is read at a time while hashing it. */
11
+ static const NSUInteger HashChunkSize = 1024 * 8;
12
+
10
13
  /*
11
14
  Ignore list for hashing
12
15
  */
@@ -95,11 +98,56 @@ NSString * const IgnoreDSStore = @".DS_Store";
95
98
  {
96
99
  uint8_t digest[CC_SHA256_DIGEST_LENGTH];
97
100
  CC_SHA256(inputData.bytes, (CC_LONG)inputData.length, digest);
101
+ return [self hashStringForDigest:digest];
102
+ }
103
+
104
+ /*
105
+ A file is hashed a chunk at a time, so that hashing one costs no more memory than the
106
+ chunk however large the file is.
107
+ */
108
+ + (NSString *)computeHashForFileAtPath:(NSString *)filePath
109
+ {
110
+ NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
111
+ if (!fileHandle) {
112
+ CPLog(@"Unable to open %@ to hash it.", filePath);
113
+ return nil;
114
+ }
115
+
116
+ CC_SHA256_CTX context;
117
+ CC_SHA256_Init(&context);
118
+
119
+ while (YES) {
120
+ @autoreleasepool {
121
+ NSError *error = nil;
122
+ NSData *chunk = [fileHandle readDataUpToLength:HashChunkSize error:&error];
123
+ if (!chunk) {
124
+ CPLog(@"Unable to read %@ to hash it: %@", filePath, error);
125
+ [fileHandle closeAndReturnError:nil];
126
+ return nil;
127
+ }
128
+
129
+ if (chunk.length == 0) {
130
+ break;
131
+ }
132
+
133
+ CC_SHA256_Update(&context, chunk.bytes, (CC_LONG)chunk.length);
134
+ }
135
+ }
136
+
137
+ [fileHandle closeAndReturnError:nil];
138
+
139
+ uint8_t digest[CC_SHA256_DIGEST_LENGTH];
140
+ CC_SHA256_Final(digest, &context);
141
+ return [self hashStringForDigest:digest];
142
+ }
143
+
144
+ + (NSString *)hashStringForDigest:(const uint8_t *)digest
145
+ {
98
146
  NSMutableString* inputHash = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
99
147
  for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
100
148
  [inputHash appendFormat:@"%02x", digest[i]];
101
149
  }
102
-
150
+
103
151
  return inputHash;
104
152
  }
105
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.0.0",
3
+ "version": "13.1.0-beta.0",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
@@ -42,15 +42,19 @@
42
42
  "bin/code-push.js",
43
43
  "android",
44
44
  "ios",
45
+ "cpp",
45
46
  "cli/dist",
46
47
  "cli/**/*.ts",
47
48
  "!cli/node_modules",
49
+ "scripts/export-embedded-bundle.sh",
50
+ "scripts/binary-patch/build-hdiffpatch.sh",
48
51
  "expo",
49
52
  "typings",
50
53
  "*.podspec",
51
54
  "react-native.config.js",
52
55
  "app.plugin.js",
53
56
  "package.json",
57
+ "THIRD-PARTY-NOTICES",
54
58
  "!ios/build",
55
59
  "!android/build",
56
60
  "!android/app/build",
@@ -58,6 +62,7 @@
58
62
  "!android/gradlew",
59
63
  "!android/gradlew.bat",
60
64
  "!android/local.properties",
65
+ "!cpp/binarypatch/host",
61
66
  "!**/__tests__",
62
67
  "!**/__fixtures__",
63
68
  "!**/__mocks__",
@@ -74,11 +79,13 @@
74
79
  "typecheck": "npm run type:src && npm run type:cli && npm run type:scripts && npm run type:e2e",
75
80
  "build:cli": "npm run --workspace cli clean && npm run --workspace cli build",
76
81
  "prepack": "npm run build:cli",
77
- "publish": "npm publish --access=public",
78
82
  "eslint": "eslint --quiet .",
79
- "jest": "jest src/versioning/* expo/* && npm run --workspace cli test",
83
+ "jest": "jest src/CodePush.test.js src/versioning/* expo/* && npm run --workspace cli test",
80
84
  "e2e": "ts-node --project e2e/tsconfig.json e2e/run.ts"
81
85
  },
86
+ "publishConfig": {
87
+ "access": "public"
88
+ },
82
89
  "repository": {
83
90
  "type": "git",
84
91
  "url": "git+https://github.com/Soomgo-Mobile/react-native-code-push.git"
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Builds the `hdiffz` / `hpatchz` command line tools that the CodePush CLI uses to
4
+ # generate and verify binary patches, and installs them into `.hdiffpatch-tools/`
5
+ # (git-ignored) at the repository root.
6
+ #
7
+ # The tools are built from source instead of being vendored as prebuilt binaries so
8
+ # that the exact upstream revision is reproducible on every platform and no opaque
9
+ # executable is committed to the repository.
10
+ #
11
+ # The `make` flags below disable every codec except zstd. That keeps the build to a
12
+ # single extra dependency (sisong/zstd, which must sit next to the HDiffPatch clone)
13
+ # and matches the patch format the appliers support: `hdiffz -f -m-6 -c-zstd-21-24`.
14
+ #
15
+ # Usage:
16
+ # scripts/binary-patch/build-hdiffpatch.sh [--force]
17
+ #
18
+ # Environment:
19
+ # HDIFFPATCH_TOOLS_DIR install directory (default: <repo>/.hdiffpatch-tools)
20
+ #
21
+ set -euo pipefail
22
+
23
+ HDIFFPATCH_REPO="https://github.com/sisong/HDiffPatch.git"
24
+ HDIFFPATCH_TAG="v5.1.3"
25
+ # sisong/zstd is a fork of facebook/zstd that HDiffPatch's makefile expects as a
26
+ # sibling directory. It publishes no release tags, so it is pinned by commit: an
27
+ # unpinned default branch would silently change the generator on any upstream push,
28
+ # and this SHA is the snapshot the vendored decompressor sources came from.
29
+ ZSTD_REPO="https://github.com/sisong/zstd.git"
30
+ ZSTD_COMMIT="68c88c7c7ad22b5e6882a5296ef96d27dc8750c4"
31
+
32
+ force=0
33
+ for arg in "$@"; do
34
+ case "$arg" in
35
+ --force) force=1 ;;
36
+ *)
37
+ echo "error: unknown argument: $arg" >&2
38
+ echo "usage: $0 [--force]" >&2
39
+ exit 2
40
+ ;;
41
+ esac
42
+ done
43
+
44
+ script_dir=$(cd "$(dirname "$0")" && pwd)
45
+ repo_root=$(cd "$script_dir/../.." && pwd)
46
+ tools_dir="${HDIFFPATCH_TOOLS_DIR:-$repo_root/.hdiffpatch-tools}"
47
+
48
+ if [ "$force" -eq 0 ] && [ -x "$tools_dir/hdiffz" ] && [ -x "$tools_dir/hpatchz" ]; then
49
+ echo "hdiffz/hpatchz already present in $tools_dir (pass --force to rebuild)"
50
+ exit 0
51
+ fi
52
+
53
+ for tool in git make cc c++; do
54
+ if ! command -v "$tool" >/dev/null 2>&1; then
55
+ echo "error: '$tool' is required to build hdiffz/hpatchz" >&2
56
+ exit 1
57
+ fi
58
+ done
59
+
60
+ work_dir=$(mktemp -d)
61
+ cleanup() { rm -rf "$work_dir"; }
62
+ trap cleanup EXIT
63
+
64
+ echo "cloning $HDIFFPATCH_REPO @ $HDIFFPATCH_TAG"
65
+ git clone --quiet --depth 1 --branch "$HDIFFPATCH_TAG" "$HDIFFPATCH_REPO" "$work_dir/HDiffPatch"
66
+ echo "cloning $ZSTD_REPO @ $ZSTD_COMMIT"
67
+ # `git clone --branch` does not take a commit, so the pinned commit is fetched into an
68
+ # empty repository instead - still a single-commit download.
69
+ git init --quiet "$work_dir/zstd"
70
+ git -C "$work_dir/zstd" remote add origin "$ZSTD_REPO"
71
+ git -C "$work_dir/zstd" fetch --quiet --depth 1 origin "$ZSTD_COMMIT"
72
+ git -C "$work_dir/zstd" checkout --quiet FETCH_HEAD
73
+
74
+ jobs=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
75
+ echo "building hdiffz/hpatchz (zstd only, $jobs jobs)"
76
+ make -C "$work_dir/HDiffPatch" \
77
+ BSD=0 LDEF=0 LZMA=0 MD5=0 VCD=0 BZIP2=0 ZLIB=0 ZSTD=1 DIR_DIFF=0 XXH=0 \
78
+ -j"$jobs"
79
+
80
+ mkdir -p "$tools_dir"
81
+ cp "$work_dir/HDiffPatch/hdiffz" "$work_dir/HDiffPatch/hpatchz" "$tools_dir/"
82
+ chmod +x "$tools_dir/hdiffz" "$tools_dir/hpatchz"
83
+
84
+ echo "installed:"
85
+ echo " $tools_dir/hdiffz"
86
+ echo " $tools_dir/hpatchz"
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ #
3
+ # Exports the JS bundle an Xcode build embeds in the app, so a later CodePush release can
4
+ # compute a binary patch against exactly the bytes the store binary ships.
5
+ #
6
+ # Call it at the end of the "Bundle React Native code and images" build phase:
7
+ #
8
+ # "$SRCROOT/../node_modules/@bravemobile/react-native-code-push/scripts/export-embedded-bundle.sh"
9
+ #
10
+ # The bundle and a `binary-patch-base.json` record describing it land in
11
+ # `$BUILD_DIR/codepush/embedded-bundle/$CONFIGURATION-$PLATFORM_NAME/`. Set
12
+ # CODEPUSH_EXPORT_DIR to export somewhere else; the `$CONFIGURATION-$PLATFORM_NAME`
13
+ # directory is appended to it either way, so builds of different configurations never
14
+ # overwrite each other.
15
+ #
16
+ # Builds that embed no bundle - Debug for the simulator, or any build run with
17
+ # SKIP_BUNDLING - have nothing to export, and the script exits without doing anything.
18
+
19
+ set -euo pipefail
20
+
21
+ if [[ -z "${CONFIGURATION_BUILD_DIR:-}" || -z "${UNLOCALIZED_RESOURCES_FOLDER_PATH:-}" || -z "${BUILD_DIR:-}" || -z "${CONFIGURATION:-}" || -z "${PLATFORM_NAME:-}" ]]; then
22
+ echo "error: export-embedded-bundle.sh must run as an Xcode build phase; the build settings it needs are not set." >&2
23
+ exit 1
24
+ fi
25
+
26
+ # The name React Native gives the bundle it writes into the app, which BUNDLE_NAME renames.
27
+ embedded_bundle="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/${BUNDLE_NAME:-main}.jsbundle"
28
+
29
+ if [[ ! -f "$embedded_bundle" ]]; then
30
+ exit 0
31
+ fi
32
+
33
+ export_dir="${CODEPUSH_EXPORT_DIR:-$BUILD_DIR/codepush/embedded-bundle}/$CONFIGURATION-$PLATFORM_NAME"
34
+ mkdir -p "$export_dir"
35
+ cp "$embedded_bundle" "$export_dir/"
36
+
37
+ exported_bundle="$export_dir/$(basename "$embedded_bundle")"
38
+ bundle_hash="$(shasum -a 256 "$exported_bundle" | awk '{ print $1 }')"
39
+
40
+ # Read from the built product rather than the source Info.plist, where the versions are
41
+ # still unexpanded build settings.
42
+ binary_version=""
43
+ build_number=""
44
+ info_plist="$CONFIGURATION_BUILD_DIR/${INFOPLIST_PATH:-}"
45
+ if [[ -n "${INFOPLIST_PATH:-}" && -f "$info_plist" ]]; then
46
+ binary_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$info_plist" 2>/dev/null || true)"
47
+ build_number="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$info_plist" 2>/dev/null || true)"
48
+ fi
49
+
50
+ # A build outside a git checkout still exports a usable record, just without the commit.
51
+ git_sha="$(git -C "${PROJECT_DIR:-$PWD}" rev-parse HEAD 2>/dev/null || true)"
52
+
53
+ # Record name and field names are a contract shared with the CodePush CLI
54
+ # (cli/functions/makeBinaryPatchBundle.ts) and the Android export script
55
+ # (android/codepush-export.gradle). Keep the three in step.
56
+ record_json=""
57
+
58
+ append_field() {
59
+ local name="$1" value="$2"
60
+ if [[ -z "$value" ]]; then
61
+ return 0
62
+ fi
63
+ if [[ -n "$record_json" ]]; then
64
+ record_json+=","$'\n'
65
+ fi
66
+ record_json+=" \"$name\": \"$value\""
67
+ }
68
+
69
+ append_field baseBundleHash "$bundle_hash"
70
+ append_field binaryVersion "$binary_version"
71
+ append_field buildNumber "$build_number"
72
+ append_field gitSha "$git_sha"
73
+
74
+ printf '{\n%s\n}\n' "$record_json" > "$export_dir/binary-patch-base.json"
75
+
76
+ echo "CodePush: exported the embedded bundle to $exported_bundle"
package/src/CodePush.js CHANGED
@@ -181,6 +181,11 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
181
181
  */
182
182
  const updateInfo = {
183
183
  download_url: latestReleaseInfo.downloadUrl,
184
+ /**
185
+ * Only released updates that were published with a binary patch carry this.
186
+ * When it is missing, the update is downloaded in full from `download_url`.
187
+ */
188
+ binary_patch_download_url: latestReleaseInfo.binaryPatchDownloadUrl,
184
189
  // (`enabled` will always be true in the release information obtained from the previous process.)
185
190
  is_available: latestReleaseInfo.enabled,
186
191
  package_hash: latestReleaseInfo.packageHash,
@@ -234,8 +239,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
234
239
  * 4) The server said there is an update, but the update's hash is the same as that
235
240
  * of the binary's currently running version. This should only happen in Android -
236
241
  * unlike iOS, we don't attach the binary's hash to the updateCheck request
237
- * because we want to avoid having to install diff updates against the binary's
238
- * version, which we can't do yet on Android.
242
+ * because an update built against the binary's version does not need one: a
243
+ * release published with a binary patch carries the patch archive next to the
244
+ * full one, and the client decides for itself which of the two to install.
239
245
  */
240
246
  if (!update || update.updateAppVersion ||
241
247
  localPackage && (update.packageHash === localPackage.packageHash) ||
@@ -281,6 +287,11 @@ function mapToRemotePackageMetadata(updateInfo) {
281
287
  packageHash: updateInfo.package_hash ?? '',
282
288
  packageSize: updateInfo.package_size ?? 0,
283
289
  downloadUrl: updateInfo.download_url ?? '',
290
+ // The field stays out of the package unless the update really has a binary patch,
291
+ // so that the native side sees exactly what it saw before patches existed.
292
+ ...(updateInfo.binary_patch_download_url
293
+ ? { binaryPatchDownloadUrl: updateInfo.binary_patch_download_url }
294
+ : {}),
284
295
  };
285
296
  }
286
297
 
@@ -532,9 +543,28 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
532
543
  mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
533
544
  minimumBackgroundDuration: 0,
534
545
  updateDialog: null,
546
+ // The callback registered on the decorator is the default for every sync, including a
547
+ // manual one; a callback passed to this call overrides it for this call.
548
+ onBinaryPatchResult: sharedCodePushOptions.onBinaryPatchResult ?? null,
535
549
  ...options,
536
550
  };
537
551
 
552
+ /*
553
+ * A callback an app registered to observe how a binary patch went must not be able to
554
+ * cost it an update, so it is isolated the way the other sync callbacks are: whatever it
555
+ * throws is logged and the install carries on. An app that registered none leaves the
556
+ * download exactly as it was before this option existed.
557
+ */
558
+ const onBinaryPatchResult = typeof syncOptions.onBinaryPatchResult === "function"
559
+ ? (label, result) => {
560
+ try {
561
+ syncOptions.onBinaryPatchResult(label, result);
562
+ } catch (error) {
563
+ log(`An error has occurred : ${error.stack}`);
564
+ }
565
+ }
566
+ : null;
567
+
538
568
  syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
539
569
  ? syncStatusChangeCallback
540
570
  : (syncStatus) => {
@@ -586,7 +616,10 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
586
616
  syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
587
617
  sharedCodePushOptions.onDownloadStart?.(remotePackageLabel);
588
618
 
589
- const localPackage = await remotePackage.download(downloadProgressCallback);
619
+ const localPackage = await remotePackage.download(
620
+ downloadProgressCallback,
621
+ onBinaryPatchResult && ((result) => onBinaryPatchResult(remotePackageLabel, result)),
622
+ );
590
623
 
591
624
  sharedCodePushOptions.onDownloadSuccess?.(remotePackageLabel);
592
625
 
@@ -720,6 +753,9 @@ let CodePush;
720
753
  *
721
754
  * onRolloutSkipped: (label: string, error: Error) => void | undefined,
722
755
  * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
756
+ *
757
+ * onBinaryPatchResult: (label: string, result: object) => void | undefined,
758
+ * setOnBinaryPatchResult(onBinaryPatchResultFunction: (label: string, result: object) => void | undefined): void,
723
759
  * }}
724
760
  */
725
761
  const sharedCodePushOptions = {
@@ -770,6 +806,12 @@ const sharedCodePushOptions = {
770
806
  if (typeof onRolloutSkippedFunction !== 'function') throw new Error('Please pass a function to onRolloutSkipped');
771
807
  this.onRolloutSkipped = onRolloutSkippedFunction;
772
808
  },
809
+ onBinaryPatchResult: undefined,
810
+ setOnBinaryPatchResult(onBinaryPatchResultFunction) {
811
+ if (!onBinaryPatchResultFunction) return;
812
+ if (typeof onBinaryPatchResultFunction !== 'function') throw new Error('Please pass a function to onBinaryPatchResult');
813
+ this.onBinaryPatchResult = onBinaryPatchResultFunction;
814
+ },
773
815
  }
774
816
 
775
817
  function codePushify(options = {}) {
@@ -806,6 +848,7 @@ function codePushify(options = {}) {
806
848
  sharedCodePushOptions.setOnDownloadSuccess(options.onDownloadSuccess);
807
849
  sharedCodePushOptions.setOnSyncError(options.onSyncError);
808
850
  sharedCodePushOptions.setOnRolloutSkipped(options.onRolloutSkipped);
851
+ sharedCodePushOptions.setOnBinaryPatchResult(options.onBinaryPatchResult);
809
852
 
810
853
  const decorator = (RootComponent) => {
811
854
  class CodePushComponent extends React.Component {