@bravemobile/react-native-code-push 12.4.1 → 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 (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -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 +46 -11
  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 +79 -88
  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 +98 -27
  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 +112 -26
  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 +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  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/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -4,7 +4,13 @@
4
4
  import childProcess from "child_process";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
+ import { createRequire } from "node:module";
7
8
  import shell from "shelljs";
9
+ /**
10
+ * Tells Hermes to lay the compiled bytecode out like an existing bundle's, which keeps
11
+ * the binary patch between the two small. Older compilers do not have the flag.
12
+ */
13
+ const BASE_BYTECODE_FLAG = '-base-bytecode';
8
14
  /**
9
15
  * Run Hermes compile CLI command
10
16
  *
@@ -12,9 +18,10 @@ import shell from "shelljs";
12
18
  * @param outputPath {string} Path to output .hbc file
13
19
  * @param sourcemapOutput {string} Path to output sourcemap file (Warning: if sourcemapOutput points to the outputPath, the sourcemap will be included in the CodePush bundle and increase the deployment size)
14
20
  * @param extraHermesFlags {string[]} Additional options to pass to `hermesc` command
21
+ * @param projectRoot {string} Root directory of the target app project used to resolve the app's React Native module and locate the matching Hermes compiler. Defaults to the current working directory.
15
22
  * @return {Promise<void>}
16
23
  */
17
- export async function runHermesEmitBinaryCommand(bundleName, outputPath, sourcemapOutput, extraHermesFlags = []) {
24
+ export async function runHermesEmitBinaryCommand(bundleName, outputPath, sourcemapOutput, extraHermesFlags = [], projectRoot = process.cwd()) {
18
25
  const hermesArgs = [
19
26
  '-emit-binary',
20
27
  '-out',
@@ -28,9 +35,18 @@ export async function runHermesEmitBinaryCommand(bundleName, outputPath, sourcem
28
35
  console.log('Converting JS bundle to byte code via Hermes, running command:\n');
29
36
  return new Promise((resolve, reject) => {
30
37
  try {
31
- const hermesCommand = getHermesCommand();
38
+ const hermesCommand = getHermesCommand(projectRoot);
32
39
  const disableAllWarningsArg = '-w';
33
- shell.exec(`${hermesCommand} ${hermesArgs.join(' ')} ${disableAllWarningsArg}`);
40
+ const compileResult = shell.exec(`${hermesCommand} ${hermesArgs.join(' ')} ${disableAllWarningsArg}`);
41
+ if (compileResult.code !== 0) {
42
+ // Reported here rather than left to the missing .hbc file, so a rejected
43
+ // flag - a base bundle that is not Hermes bytecode, for instance - fails
44
+ // the release instead of silently producing an unaligned bundle.
45
+ const extraFlagsHint = extraHermesFlags.length > 0
46
+ ? ` Additional options: ${extraHermesFlags.join(' ')}`
47
+ : '';
48
+ throw new Error(`"hermesc" command failed (exitCode=${compileResult.code}).${extraFlagsHint}`);
49
+ }
34
50
  // Copy HBC bundle to overwrite JS bundle
35
51
  const source = path.join(outputPath, bundleName + '.hbc');
36
52
  const destination = path.join(outputPath, bundleName);
@@ -47,7 +63,7 @@ export async function runHermesEmitBinaryCommand(bundleName, outputPath, sourcem
47
63
  return;
48
64
  }
49
65
  // compose-source-maps.js file path
50
- const composeSourceMapsPath = getComposeSourceMapsPath();
66
+ const composeSourceMapsPath = getComposeSourceMapsPath(projectRoot);
51
67
  if (composeSourceMapsPath === null) {
52
68
  throw new Error('react-native compose-source-maps.js scripts is not found');
53
69
  }
@@ -87,7 +103,40 @@ export async function runHermesEmitBinaryCommand(bundleName, outputPath, sourcem
87
103
  });
88
104
  });
89
105
  }
90
- function getHermesCommand() {
106
+ /**
107
+ * Builds the flags that align a compilation with the bundle already inside the app
108
+ * binary, so the binary patch between them stays small.
109
+ *
110
+ * When the app's Hermes compiler predates the flag the release still goes ahead
111
+ * without it: the patch is then computed against unaligned bytecode and is larger, but
112
+ * it is still a valid patch. A compiler that does accept the flag and fails is a
113
+ * different matter and fails the release, because the base input is then wrong.
114
+ *
115
+ * @param baseBundlePath {string} JS bundle from the target binary to align against
116
+ * @param projectRoot {string} Root directory of the target app project, used to locate its Hermes compiler
117
+ * @return {string[]} Flags to pass to `runHermesEmitBinaryCommand` as `extraHermesFlags`
118
+ */
119
+ export function resolveBaseBytecodeHermesFlags(baseBundlePath, projectRoot = process.cwd()) {
120
+ if (!hermesSupportsBaseBytecode(projectRoot)) {
121
+ console.warn(`warn: The Hermes compiler of this app does not support "${BASE_BYTECODE_FLAG}". ` +
122
+ 'Compiling without it, which makes the binary patch larger.');
123
+ return [];
124
+ }
125
+ return [BASE_BYTECODE_FLAG, baseBundlePath];
126
+ }
127
+ /** Whether a `hermesc --help` output advertises the base bytecode flag. */
128
+ export function helpOutputSupportsBaseBytecode(helpOutput) {
129
+ return helpOutput.includes(BASE_BYTECODE_FLAG);
130
+ }
131
+ function hermesSupportsBaseBytecode(projectRoot) {
132
+ const hermesCommand = getHermesCommand(projectRoot);
133
+ const result = childProcess.spawnSync(hermesCommand, ['--help'], { encoding: 'utf8' });
134
+ if (result.error) {
135
+ throw new Error(`failed to run "${hermesCommand} --help": ${result.error.message}`);
136
+ }
137
+ return helpOutputSupportsBaseBytecode(`${result.stdout ?? ''}${result.stderr ?? ''}`);
138
+ }
139
+ function getHermesCommand(projectRoot) {
91
140
  const fileExists = (file) => {
92
141
  try {
93
142
  return fs.statSync(file).isFile();
@@ -96,7 +145,7 @@ function getHermesCommand() {
96
145
  return false;
97
146
  }
98
147
  };
99
- const hermescExecutable = path.join(getHermesCompilerPath(), getHermesOSBin(), getHermesOSExe());
148
+ const hermescExecutable = path.join(getHermesCompilerPath(projectRoot), getHermesOSBin(), getHermesOSExe());
100
149
  if (fileExists(hermescExecutable)) {
101
150
  return hermescExecutable;
102
151
  }
@@ -124,45 +173,67 @@ function getHermesOSExe() {
124
173
  return hermesExecutableName;
125
174
  }
126
175
  }
127
- function getComposeSourceMapsPath() {
176
+ function getComposeSourceMapsPath(projectRoot) {
128
177
  // detect if compose-source-maps.js script exists
129
- const composeSourceMaps = path.join(getReactNativePackagePath(), 'scripts', 'compose-source-maps.js');
178
+ const composeSourceMaps = path.join(getReactNativePackagePath(projectRoot), 'scripts', 'compose-source-maps.js');
130
179
  if (fs.existsSync(composeSourceMaps)) {
131
180
  return composeSourceMaps;
132
181
  }
133
182
  return null;
134
183
  }
135
- function getReactNativePackagePath() {
136
- const result = childProcess.spawnSync('node', [
137
- '--print',
138
- "require.resolve('react-native/package.json')",
139
- ]);
140
- const packagePath = path.dirname(result.stdout.toString());
141
- if (result.status === 0 && directoryExistsSync(packagePath)) {
184
+ function getReactNativePackagePath(projectRoot) {
185
+ const packagePath = resolvePackageRoot(projectRoot, 'react-native');
186
+ if (packagePath !== null) {
142
187
  return packagePath;
143
188
  }
144
- return path.join('node_modules', 'react-native');
189
+ return path.join(projectRoot, 'node_modules', 'react-native');
145
190
  }
146
- function getHermescDirPathInHermesCompilerPackage() {
147
- const result = childProcess.spawnSync('node', [
148
- '--print',
149
- "require.resolve('hermes-compiler/package.json')",
150
- ]);
151
- const packagePath = path.dirname(result.stdout.toString());
152
- const hermescDirPath = path.join(packagePath, 'hermesc');
153
- if (result.status === 0 && directoryExistsSync(hermescDirPath)) {
191
+ function getHermescDirPathInHermesCompilerPackage(projectRoot) {
192
+ const reactNativePackagePath = getReactNativePackagePath(projectRoot);
193
+ const hermescDirPath = path.join(path.dirname(reactNativePackagePath), 'hermes-compiler', 'hermesc');
194
+ if (directoryExistsSync(hermescDirPath)) {
154
195
  return hermescDirPath;
155
196
  }
156
197
  return null;
157
198
  }
158
- function getHermesCompilerPath() {
159
- const hermescDirPath = getHermescDirPathInHermesCompilerPackage();
199
+ function getHermesCompilerPath(projectRoot) {
200
+ const hermescDirPath = getHermescDirPathInHermesCompilerPackage(projectRoot);
160
201
  if (hermescDirPath) {
161
202
  // Since react-native 0.83, Hermes compiler executables are in 'hermes-compiler' package
162
203
  return hermescDirPath;
163
204
  }
164
205
  else {
165
- return path.join(getReactNativePackagePath(), 'sdks', 'hermesc');
206
+ return path.join(getReactNativePackagePath(projectRoot), 'sdks', 'hermesc');
207
+ }
208
+ }
209
+ function resolvePackageRoot(projectRoot, packageName) {
210
+ try {
211
+ const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
212
+ const resolvedPath = projectRequire.resolve(packageName);
213
+ return findPackageRoot(packageName, resolvedPath);
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ function findPackageRoot(packageName, resolvedPath) {
220
+ let currentPath = path.dirname(resolvedPath);
221
+ while (true) {
222
+ const packageJsonPath = path.join(currentPath, 'package.json');
223
+ try {
224
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
225
+ if (packageJson.name === packageName) {
226
+ return currentPath;
227
+ }
228
+ }
229
+ catch {
230
+ // Continue traversing upward until the package root is found.
231
+ }
232
+ const parentPath = path.dirname(currentPath);
233
+ if (parentPath === currentPath) {
234
+ return null;
235
+ }
236
+ currentPath = parentPath;
166
237
  }
167
238
  }
168
239
  function directoryExistsSync(dirname) {
@@ -0,0 +1,100 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
5
+ import { helpOutputSupportsBaseBytecode, resolveBaseBytecodeHermesFlags, } from "./runHermesEmitBinaryCommand.js";
6
+ /**
7
+ * The base bytecode flag only exists in recent Hermes compilers, and the compiler that
8
+ * matters is the one inside the app being released - so the flag is decided by asking
9
+ * that binary, not by a version check.
10
+ */
11
+ /** Excerpt of `hermesc --help` from a compiler that has the flag. */
12
+ const HELP_WITH_BASE_BYTECODE = `OVERVIEW: Hermes driver
13
+
14
+ USAGE: hermesc [options] <input>
15
+
16
+ OPTIONS:
17
+ -Wno-direct-eval - Disable Warning when attempting a direct (local) eval
18
+ -base-bytecode=<string> - input base bytecode for delta optimizing mode
19
+ -bytecode-output-manifest=<string> - Name of the manifest file generated when compiling multiple segments to bytecode
20
+ -commonjs - Use CommonJS modules
21
+ `;
22
+ /** The same excerpt from a compiler that predates the flag. */
23
+ const HELP_WITHOUT_BASE_BYTECODE = `OVERVIEW: Hermes driver
24
+
25
+ USAGE: hermesc [options] <input>
26
+
27
+ OPTIONS:
28
+ -Wno-direct-eval - Disable Warning when attempting a direct (local) eval
29
+ -bytecode-output-manifest=<string> - Name of the manifest file generated when compiling multiple segments to bytecode
30
+ -commonjs - Use CommonJS modules
31
+ `;
32
+ function findRepoRoot() {
33
+ let dir = process.cwd();
34
+ for (;;) {
35
+ if (fs.existsSync(path.join(dir, "node_modules", "react-native"))) {
36
+ return dir;
37
+ }
38
+ const parent = path.dirname(dir);
39
+ if (parent === dir) {
40
+ throw new Error(`cannot locate the repository root from ${process.cwd()}`);
41
+ }
42
+ dir = parent;
43
+ }
44
+ }
45
+ function hermesOSBinDirName() {
46
+ switch (process.platform) {
47
+ case 'win32':
48
+ return 'win64-bin';
49
+ case 'darwin':
50
+ return 'osx-bin';
51
+ default:
52
+ return 'linux64-bin';
53
+ }
54
+ }
55
+ const repoRoot = findRepoRoot();
56
+ const baseBundlePath = path.join(repoRoot, "cli", "fixtures", "binary-patch", "base.bundle");
57
+ let workDir;
58
+ /** A project whose Hermes compiler reports a help text without the flag. */
59
+ function writeProjectWithFakeHermesc(helpOutput) {
60
+ const projectRoot = fs.mkdtempSync(path.join(workDir, "project-"));
61
+ const hermescDir = path.join(projectRoot, "node_modules", "react-native", "sdks", "hermesc", hermesOSBinDirName());
62
+ fs.mkdirSync(hermescDir, { recursive: true });
63
+ const hermescPath = path.join(hermescDir, "hermesc");
64
+ fs.writeFileSync(hermescPath, `#!/bin/sh\ncat <<'HELP'\n${helpOutput}HELP\n`);
65
+ fs.chmodSync(hermescPath, 0o755);
66
+ return projectRoot;
67
+ }
68
+ beforeAll(() => {
69
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-hermes-flags-"));
70
+ });
71
+ afterAll(() => {
72
+ fs.rmSync(workDir, { recursive: true, force: true });
73
+ });
74
+ afterEach(() => {
75
+ jest.restoreAllMocks();
76
+ });
77
+ describe("helpOutputSupportsBaseBytecode", () => {
78
+ it("detects the flag in the help output of a compiler that has it", () => {
79
+ expect(helpOutputSupportsBaseBytecode(HELP_WITH_BASE_BYTECODE)).toBe(true);
80
+ });
81
+ it("reports no support when the help output does not list the flag", () => {
82
+ expect(helpOutputSupportsBaseBytecode(HELP_WITHOUT_BASE_BYTECODE)).toBe(false);
83
+ });
84
+ });
85
+ describe("resolveBaseBytecodeHermesFlags", () => {
86
+ it("aligns the compilation with the base bundle when the compiler supports it", () => {
87
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, repoRoot)).toEqual(['-base-bytecode', baseBundlePath]);
88
+ });
89
+ const itUnlessWindows = process.platform === 'win32' ? it.skip : it;
90
+ itUnlessWindows("warns and compiles without the flag when the compiler does not support it", () => {
91
+ const projectRoot = writeProjectWithFakeHermesc(HELP_WITHOUT_BASE_BYTECODE);
92
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
93
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, projectRoot)).toEqual([]);
94
+ expect(warn.mock.calls.join('\n')).toMatch(/-base-bytecode/);
95
+ });
96
+ itUnlessWindows("uses the flag when the app's own compiler advertises it", () => {
97
+ const projectRoot = writeProjectWithFakeHermesc(HELP_WITH_BASE_BYTECODE);
98
+ expect(resolveBaseBytecodeHermesFlags(baseBundlePath, projectRoot)).toEqual(['-base-bytecode', baseBundlePath]);
99
+ });
100
+ });
@@ -0,0 +1,38 @@
1
+ import { spawnSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ /**
5
+ * Builds hdiffz/hpatchz once, before any test worker starts.
6
+ *
7
+ * Several suites generate and apply real patches, which is the only way to prove the
8
+ * committed patch format is what every applier understands. Provisioning the tools here
9
+ * rather than in each suite keeps a clean checkout - and CI - free of a manual setup
10
+ * step, without letting two workers run the same build at the same time.
11
+ *
12
+ * The build script installs into `HDIFFPATCH_TOOLS_DIR` when it is set and into
13
+ * `<repo>/.hdiffpatch-tools` otherwise, which is where the CLI looks for the tools. It
14
+ * is a no-op when they are already there, so the check below only decides whether to
15
+ * warn that this run has a multi-minute build ahead of it.
16
+ *
17
+ * Note that this module is loaded outside the jest module resolver, so it cannot import
18
+ * from the CLI sources.
19
+ */
20
+ const REPO_ROOT = path.join(__dirname, '..');
21
+ const BUILD_SCRIPT_PATH = path.join(REPO_ROOT, 'scripts', 'binary-patch', 'build-hdiffpatch.sh');
22
+ const DEFAULT_TOOLS_DIR = path.join(REPO_ROOT, '.hdiffpatch-tools');
23
+ export default function ensureBinaryPatchTools() {
24
+ const toolsDir = process.env.HDIFFPATCH_TOOLS_DIR || DEFAULT_TOOLS_DIR;
25
+ const alreadyInstalled = ['hdiffz', 'hpatchz'].every((tool) => fs.existsSync(path.join(toolsDir, tool)));
26
+ if (!alreadyInstalled) {
27
+ // Announced because a first build clones and compiles HDiffPatch, which takes
28
+ // minutes; a silent wait would look like a hang.
29
+ console.log(`Building hdiffz/hpatchz into ${toolsDir} (first run only)`);
30
+ }
31
+ const result = spawnSync(BUILD_SCRIPT_PATH, { encoding: 'utf8' });
32
+ if (result.error) {
33
+ throw new Error(`failed to run ${BUILD_SCRIPT_PATH}: ${result.error.message}`);
34
+ }
35
+ if (result.status !== 0) {
36
+ throw new Error(`${BUILD_SCRIPT_PATH} failed:\n${result.stdout ?? ''}${result.stderr ?? ''}`);
37
+ }
38
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Binary patch codec for CodePush updates.
3
+ *
4
+ * A binary patch update carries only the difference between the bundle that is
5
+ * already inside the app binary and the bundle a release wants to run, instead of
6
+ * the whole bundle. Patches are produced by HDiffPatch's `hdiffz` and applied on
7
+ * device by the native appliers; `hpatchz` is the reference applier used here so a
8
+ * patch can be verified right after it is produced.
9
+ *
10
+ * The tool options below are part of the format contract, not tuning knobs:
11
+ * -m-6 the base bundle is held in memory while patching, which is what
12
+ * the native appliers do, and match score 6 suits bytecode
13
+ * -c-zstd-21-24 zstd is the only decompressor the native appliers link
14
+ * -f overwrite the output file if it already exists
15
+ * Changing them produces patches the native appliers cannot read.
16
+ *
17
+ * A patch contains no checksum of the base data, and its zstd streams carry no
18
+ * content checksums, so a successful apply is not proof of a correct result: a base
19
+ * of the right size but the wrong content, or a corrupted patch body, can both apply
20
+ * "successfully" and yield wrong bytes. Callers must verify the base and target
21
+ * hashes themselves.
22
+ */
23
+ import { spawnSync } from "child_process";
24
+ import fs from "fs";
25
+ import path from "path";
26
+ /**
27
+ * Identifies the patch format so a client can refuse a patch it cannot apply.
28
+ * Bump the format version whenever the tool options above change.
29
+ */
30
+ export const BINARY_PATCH_ALGORITHM = 'hdiffpatch-m-zstd';
31
+ export const BINARY_PATCH_FORMAT_VERSION = 1;
32
+ const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
33
+ const HPATCHZ_OPTIONS = ['-f', '-m'];
34
+ const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
35
+ const TOOLS_DIR_NAME = '.hdiffpatch-tools';
36
+ const BUILD_SCRIPT_PATH = 'scripts/binary-patch/build-hdiffpatch.sh';
37
+ /**
38
+ * Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
39
+ * then at a `.hdiffpatch-tools` directory in the working directory or any directory
40
+ * above it. The tools are built from source rather than installed as a package
41
+ * dependency, so the error explains how to get them.
42
+ */
43
+ export function resolveBinaryPatchTool(tool) {
44
+ const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
45
+ if (configuredDir) {
46
+ const configured = path.join(configuredDir, tool);
47
+ if (fs.existsSync(configured)) {
48
+ return configured;
49
+ }
50
+ throw new Error(`${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
51
+ `Build the tools with '${BUILD_SCRIPT_PATH}'.`);
52
+ }
53
+ let directory = path.resolve(process.cwd());
54
+ for (;;) {
55
+ const candidate = path.join(directory, TOOLS_DIR_NAME, tool);
56
+ if (fs.existsSync(candidate)) {
57
+ return candidate;
58
+ }
59
+ const parent = path.dirname(directory);
60
+ if (parent === directory) {
61
+ break;
62
+ }
63
+ directory = parent;
64
+ }
65
+ throw new Error(`'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
66
+ `Build it with '${BUILD_SCRIPT_PATH}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`);
67
+ }
68
+ /** Writes the patch that turns the bundle at `basePath` into the one at `targetPath`. */
69
+ export function generatePatch(basePath, targetPath, patchPath) {
70
+ runTool('hdiffz', [...HDIFFZ_OPTIONS, basePath, targetPath, patchPath]);
71
+ }
72
+ /** Writes the bundle that `patchPath` produces from the bundle at `basePath`. */
73
+ export function applyPatch(basePath, patchPath, outputPath) {
74
+ runTool('hpatchz', [...HPATCHZ_OPTIONS, basePath, patchPath, outputPath]);
75
+ }
76
+ function runTool(tool, args) {
77
+ const executable = resolveBinaryPatchTool(tool);
78
+ const result = spawnSync(executable, args, { encoding: 'utf8' });
79
+ if (result.error) {
80
+ throw new Error(`failed to run ${tool}: ${result.error.message}`);
81
+ }
82
+ if (result.status !== 0) {
83
+ const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
84
+ throw new Error(`${tool} failed with ${reason}\n${result.stdout ?? ''}${result.stderr ?? ''}`);
85
+ }
86
+ }
@@ -0,0 +1,190 @@
1
+ import { spawnSync } from "child_process";
2
+ import crypto from "crypto";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
7
+ import { applyPatch, generatePatch, resolveBinaryPatchTool } from "./binaryPatch.js";
8
+ /**
9
+ * Locks the binary patch codec contract: which bytes a patch restores, and which
10
+ * kinds of broken input the appliers do and do not catch.
11
+ *
12
+ * Everything here runs against real binaries and real bytes - hdiffz/hpatchz and a
13
+ * host build of the native applier - because the whole point is to prove that the
14
+ * committed patch format really is what every applier understands.
15
+ */
16
+ /** Building the applier from source takes a while on a cold machine. */
17
+ const BUILD_APPLIER_TIMEOUT_MS = 5 * 60 * 1000;
18
+ function findRepoRoot() {
19
+ let dir = process.cwd();
20
+ for (;;) {
21
+ if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
22
+ return dir;
23
+ }
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) {
26
+ throw new Error(`cannot locate the repository root from ${process.cwd()}`);
27
+ }
28
+ dir = parent;
29
+ }
30
+ }
31
+ const repoRoot = findRepoRoot();
32
+ const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
33
+ const baseFixture = path.join(fixtureDir, "base.bundle");
34
+ const targetFixture = path.join(fixtureDir, "target.bundle");
35
+ const patchFixture = path.join(fixtureDir, "update.patch");
36
+ /**
37
+ * One shared tree builds the applier for both platforms: iOS pulls it in through the
38
+ * podspec at the repository root, Android through externalNativeBuild.
39
+ */
40
+ const applierDir = path.join(repoRoot, "cpp", "binarypatch");
41
+ let workDir;
42
+ function sha256(filePath) {
43
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
44
+ }
45
+ function workPath(name) {
46
+ return path.join(workDir, name);
47
+ }
48
+ /** Writes a copy of `sourcePath` with `mutate` applied to its bytes. */
49
+ function writeMutatedCopy(sourcePath, name, mutate) {
50
+ const destination = workPath(name);
51
+ fs.writeFileSync(destination, mutate(fs.readFileSync(sourcePath)));
52
+ return destination;
53
+ }
54
+ function cCompiler() {
55
+ return process.env.CC || "cc";
56
+ }
57
+ function hasCCompiler() {
58
+ return spawnSync(cCompiler(), ["--version"], { stdio: "ignore" }).status === 0;
59
+ }
60
+ beforeAll(() => {
61
+ // hdiffz/hpatchz are provisioned for the whole run by the jest global setup.
62
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-binary-patch-"));
63
+ });
64
+ afterAll(() => {
65
+ fs.rmSync(workDir, { recursive: true, force: true });
66
+ });
67
+ describe("generatePatch/applyPatch", () => {
68
+ it("restores the target bytes from a freshly generated patch", () => {
69
+ const generatedPatch = workPath("generated.patch");
70
+ const restored = workPath("restored-from-generated.bundle");
71
+ generatePatch(baseFixture, targetFixture, generatedPatch);
72
+ applyPatch(baseFixture, generatedPatch, restored);
73
+ expect(fs.statSync(generatedPatch).size).toBeGreaterThan(0);
74
+ expect(sha256(restored)).toBe(sha256(targetFixture));
75
+ });
76
+ it("restores the target bytes from the committed patch fixture", () => {
77
+ const restored = workPath("restored-from-fixture.bundle");
78
+ applyPatch(baseFixture, patchFixture, restored);
79
+ expect(sha256(restored)).toBe(sha256(targetFixture));
80
+ });
81
+ it("reports a missing patch tool with a message that says how to get it", () => {
82
+ const previous = process.env.HDIFFPATCH_TOOLS_DIR;
83
+ process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
84
+ try {
85
+ expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-hdiffpatch\.sh/);
86
+ }
87
+ finally {
88
+ if (previous === undefined) {
89
+ delete process.env.HDIFFPATCH_TOOLS_DIR;
90
+ }
91
+ else {
92
+ process.env.HDIFFPATCH_TOOLS_DIR = previous;
93
+ }
94
+ }
95
+ });
96
+ });
97
+ describe("applyPatch failure modes", () => {
98
+ it("fails on a patch with a corrupted header", () => {
99
+ const corrupted = writeMutatedCopy(patchFixture, "corrupt-header.patch", (bytes) => {
100
+ // Byte 0 is inside the patch type string, which the header parser reads first.
101
+ bytes[0] ^= 0xff;
102
+ return bytes;
103
+ });
104
+ expect(() => applyPatch(baseFixture, corrupted, workPath("out-corrupt-header.bundle"))).toThrow(/hpatchz/);
105
+ });
106
+ it("fails on a truncated patch", () => {
107
+ const truncated = writeMutatedCopy(patchFixture, "truncated.patch", (bytes) => bytes.subarray(0, Math.floor(bytes.length / 2)));
108
+ expect(() => applyPatch(baseFixture, truncated, workPath("out-truncated.bundle"))).toThrow(/hpatchz/);
109
+ });
110
+ });
111
+ /**
112
+ * A compressed diff carries no checksum of the base data, and the zstd streams
113
+ * inside it are written without content checksums, so an applier cannot tell every
114
+ * kind of wrong input apart from a legitimate one. These tests pin that down: the
115
+ * apply step may report success, but it never produces the target bytes. Callers
116
+ * must therefore verify the base and target hashes themselves rather than treating
117
+ * a successful apply as proof of a correct result.
118
+ */
119
+ describe("errors the codec cannot detect", () => {
120
+ it("never produces the target bytes when a byte inside the compressed diff data is corrupted", () => {
121
+ const corrupted = writeMutatedCopy(patchFixture, "corrupt-body.patch", (bytes) => {
122
+ bytes[Math.floor(bytes.length / 2)] ^= 0xff;
123
+ return bytes;
124
+ });
125
+ const restored = workPath("out-corrupt-body.bundle");
126
+ // Whether the corruption is reported depends on where it lands: a flipped bit
127
+ // inside a zstd literals block decodes to different bytes without any error,
128
+ // while one that invalidates a block header does fail. Either way the result
129
+ // is not the target, which is all the caller can rely on.
130
+ let restoredHash = null;
131
+ try {
132
+ applyPatch(baseFixture, corrupted, restored);
133
+ restoredHash = sha256(restored);
134
+ }
135
+ catch {
136
+ restoredHash = null;
137
+ }
138
+ expect(restoredHash).not.toBe(sha256(targetFixture));
139
+ });
140
+ it("applies but produces wrong bytes when the base has the expected size and different content", () => {
141
+ const wrongBase = writeMutatedCopy(baseFixture, "wrong-base.bundle", (bytes) => {
142
+ bytes[1000] ^= 0xff;
143
+ return bytes;
144
+ });
145
+ const restored = workPath("out-wrong-base.bundle");
146
+ applyPatch(wrongBase, patchFixture, restored);
147
+ expect(fs.statSync(wrongBase).size).toBe(fs.statSync(baseFixture).size);
148
+ expect(sha256(restored)).not.toBe(sha256(targetFixture));
149
+ });
150
+ });
151
+ /**
152
+ * The native appliers on Android and iOS are built from the vendored sources, so a
153
+ * host build of the same sources is what proves they agree with hdiffz/hpatchz.
154
+ */
155
+ const describeNativeApplier = hasCCompiler() ? describe : describe.skip;
156
+ describeNativeApplier("host build of the native applier", () => {
157
+ let hostApplier;
158
+ const runHostApplier = (oldPath, patch, outPath) => {
159
+ const result = spawnSync(hostApplier, [oldPath, patch, outPath], { encoding: "utf8" });
160
+ return result.status;
161
+ };
162
+ beforeAll(() => {
163
+ hostApplier = workPath("apply_patch_host");
164
+ const script = path.join(applierDir, "host", "build.sh");
165
+ const result = spawnSync(script, [hostApplier], { encoding: "utf8", timeout: BUILD_APPLIER_TIMEOUT_MS });
166
+ if (result.status !== 0) {
167
+ throw new Error(`${script} failed:\n${result.stdout ?? ""}${result.stderr ?? ""}`);
168
+ }
169
+ }, BUILD_APPLIER_TIMEOUT_MS);
170
+ it("restores the same target bytes as the CLI applier", () => {
171
+ const restored = workPath("out-host.bundle");
172
+ expect(runHostApplier(baseFixture, patchFixture, restored)).toBe(0);
173
+ expect(sha256(restored)).toBe(sha256(targetFixture));
174
+ });
175
+ it("reports a corrupted patch header as exit code 3", () => {
176
+ const corrupted = writeMutatedCopy(patchFixture, "host-corrupt-header.patch", (bytes) => {
177
+ bytes[0] ^= 0xff;
178
+ return bytes;
179
+ });
180
+ expect(runHostApplier(baseFixture, corrupted, workPath("out-host-corrupt-header.bundle"))).toBe(3);
181
+ });
182
+ it("reports a base of unexpected size as exit code 5", () => {
183
+ const shortBase = writeMutatedCopy(baseFixture, "host-short-base.bundle", (bytes) => bytes.subarray(0, bytes.length - 16));
184
+ expect(runHostApplier(shortBase, patchFixture, workPath("out-host-short-base.bundle"))).toBe(5);
185
+ });
186
+ it("reports a truncated patch as exit code 6", () => {
187
+ const truncated = writeMutatedCopy(patchFixture, "host-truncated.patch", (bytes) => bytes.subarray(0, Math.floor(bytes.length / 2)));
188
+ expect(runHostApplier(baseFixture, truncated, workPath("out-host-truncated.bundle"))).toBe(6);
189
+ });
190
+ });