@bravemobile/react-native-code-push 13.1.0-beta.4 → 13.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CodePush.podspec +5 -0
- package/README.md +15 -145
- package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
- package/cli/commands/buildPatchToolsCommand/buildPatchTools.test.ts +118 -0
- package/cli/commands/buildPatchToolsCommand/buildPatchTools.ts +46 -0
- package/cli/commands/buildPatchToolsCommand/index.ts +44 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/release.test.ts +3 -3
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- package/cli/dist/commands/buildPatchToolsCommand/buildPatchTools.js +34 -0
- package/cli/dist/commands/buildPatchToolsCommand/buildPatchTools.test.js +88 -0
- package/cli/dist/commands/buildPatchToolsCommand/index.js +32 -0
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
- package/cli/dist/commands/releaseCommand/release.test.js +3 -3
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/dist/index.js +4 -0
- package/cli/dist/utils/binaryPatch.js +8 -5
- package/cli/dist/utils/binaryPatch.test.js +1 -1
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- package/cli/index.ts +5 -0
- package/cli/utils/binaryPatch.test.ts +1 -1
- package/cli/utils/binaryPatch.ts +8 -5
- package/ios/CodePush/CodePush.h +6 -3
- package/ios/CodePush/CodePush.mm +10 -10
- package/ios/CodePush/CodePushBinaryPatch.h +18 -8
- package/ios/CodePush/CodePushBinaryPatch.m +23 -22
- package/ios/CodePush/CodePushDownloadHandler.m +2 -1
- package/ios/CodePush/CodePushErrorUtils.m +55 -0
- package/ios/CodePush/CodePushPackage.m +242 -71
- package/package.json +2 -1
- package/src/CodePush.js +28 -18
- package/src/CodePush.test.js +110 -55
- package/src/package-mixins.js +19 -10
- package/typings/react-native-code-push.d.ts +110 -29
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { createReleaseHistory } from "./createReleaseHistory.js";
|
|
6
|
+
import { updateReleaseHistory } from "../updateHistoryCommand/updateReleaseHistory.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Covers where the release history is staged before the config is handed it.
|
|
10
|
+
*
|
|
11
|
+
* The config receives a path rather than the history itself, so the command has to put the
|
|
12
|
+
* file somewhere first. Two releases of one app for different platforms are the same binary
|
|
13
|
+
* version, so a staging path derived from that version alone is the same path for both, and
|
|
14
|
+
* they overwrite each other's contents before either has read its own.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const BINARY_VERSION = "1.0.0";
|
|
18
|
+
|
|
19
|
+
let workingDir: string;
|
|
20
|
+
let previousCwd: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
previousCwd = process.cwd();
|
|
24
|
+
workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-history-staging-"));
|
|
25
|
+
process.chdir(workingDir);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
process.chdir(previousCwd);
|
|
30
|
+
fs.rmSync(workingDir, { recursive: true, force: true });
|
|
31
|
+
jest.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function stagedPath(platform: "ios" | "android"): string {
|
|
35
|
+
return path.join(workingDir, "codepush-release-history", platform, `${BINARY_VERSION}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Turns the command's own exit into a throw, so a staging failure fails the test instead
|
|
40
|
+
* of taking the worker down with it.
|
|
41
|
+
*/
|
|
42
|
+
function failOnExit(): void {
|
|
43
|
+
jest.spyOn(process, "exit").mockImplementation((code) => {
|
|
44
|
+
throw new Error(`the command exited with code ${String(code)}`);
|
|
45
|
+
});
|
|
46
|
+
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
47
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("staging the release history a config is handed", () => {
|
|
51
|
+
it("gives each platform writing at the same time a file of its own", async () => {
|
|
52
|
+
failOnExit();
|
|
53
|
+
|
|
54
|
+
const staged: Record<string, { jsonFilePath: string; contents: string }> = {};
|
|
55
|
+
const setReleaseHistory = async (
|
|
56
|
+
_binaryVersion: string,
|
|
57
|
+
jsonFilePath: string,
|
|
58
|
+
_releaseInfo: unknown,
|
|
59
|
+
platform: "ios" | "android",
|
|
60
|
+
): Promise<void> => {
|
|
61
|
+
// Yields once, which is all it takes for the other platform to reach its own
|
|
62
|
+
// write and its own clean-up while this one still holds only a path.
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
staged[platform] = { jsonFilePath, contents: fs.readFileSync(jsonFilePath, "utf8") };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await Promise.all([
|
|
68
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840"),
|
|
69
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "android", "RN0840"),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
expect(staged.ios.jsonFilePath).not.toBe(staged.android.jsonFilePath);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("hands each platform the history that platform released", async () => {
|
|
76
|
+
failOnExit();
|
|
77
|
+
|
|
78
|
+
const released: Record<string, string> = {
|
|
79
|
+
ios: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "ios-url", packageHash: "ios-hash" } }),
|
|
80
|
+
android: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "android-url", packageHash: "android-hash" } }),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const staged: Record<string, string> = {};
|
|
84
|
+
const getReleaseHistory = async (
|
|
85
|
+
_binaryVersion: string,
|
|
86
|
+
platform: "ios" | "android",
|
|
87
|
+
) => JSON.parse(released[platform]);
|
|
88
|
+
const setReleaseHistory = async (
|
|
89
|
+
_binaryVersion: string,
|
|
90
|
+
jsonFilePath: string,
|
|
91
|
+
_releaseInfo: unknown,
|
|
92
|
+
platform: "ios" | "android",
|
|
93
|
+
): Promise<void> => {
|
|
94
|
+
await Promise.resolve();
|
|
95
|
+
staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await Promise.all([
|
|
99
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
|
|
100
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(staged.ios).toContain("ios-url");
|
|
104
|
+
expect(staged.ios).not.toContain("android-url");
|
|
105
|
+
expect(staged.android).toContain("android-url");
|
|
106
|
+
expect(staged.android).not.toContain("ios-url");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("takes the file away once the config has stored the history", async () => {
|
|
110
|
+
failOnExit();
|
|
111
|
+
|
|
112
|
+
const setReleaseHistory = async (): Promise<void> => undefined;
|
|
113
|
+
await createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840");
|
|
114
|
+
|
|
115
|
+
expect(fs.existsSync(stagedPath("ios"))).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("leaves the file behind when the config could not store the history", async () => {
|
|
119
|
+
failOnExit();
|
|
120
|
+
|
|
121
|
+
const setReleaseHistory = async (): Promise<void> => {
|
|
122
|
+
throw new Error("the storage backend rejected the history");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await expect(createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840")).rejects.toThrow(
|
|
126
|
+
"the command exited with code 1",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// What the release tried to store is worth reading back, so a failed release keeps
|
|
130
|
+
// its history at a path that is the same on every run.
|
|
131
|
+
expect(Object.keys(JSON.parse(fs.readFileSync(stagedPath("ios"), "utf8")))).toEqual([BINARY_VERSION]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import type { CliConfigInterface, ReleaseHistoryInterface, ReleaseInfo } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function createReleaseHistory(
|
|
6
5
|
targetVersion: string,
|
|
@@ -20,15 +19,8 @@ export async function createReleaseHistory(
|
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(INITIAL_HISTORY, null, 2));
|
|
27
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(INITIAL_HISTORY));
|
|
28
|
-
|
|
29
|
-
await setReleaseHistory(targetVersion, JSON_FILE_PATH, INITIAL_HISTORY, platform, identifier)
|
|
30
|
-
|
|
31
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
22
|
+
await stageReleaseHistoryFile(targetVersion, INITIAL_HISTORY, platform, (jsonFilePath) =>
|
|
23
|
+
setReleaseHistory(targetVersion, jsonFilePath, INITIAL_HISTORY, platform, identifier));
|
|
32
24
|
} catch (error) {
|
|
33
25
|
console.error('Error occurred while creating new history:', error);
|
|
34
26
|
process.exit(1)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
1
|
import type { CliConfigInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function addToReleaseHistory(
|
|
6
5
|
appVersion: string,
|
|
@@ -51,15 +50,8 @@ export async function addToReleaseHistory(
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(newReleaseHistory, null, 2));
|
|
58
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(newReleaseHistory));
|
|
59
|
-
|
|
60
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, newReleaseHistory, platform, identifier)
|
|
61
|
-
|
|
62
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
53
|
+
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) =>
|
|
54
|
+
setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
63
55
|
} catch (error) {
|
|
64
56
|
console.error('Error occurred while updating history:', error);
|
|
65
57
|
process.exit(1)
|
|
@@ -206,9 +206,9 @@ beforeEach(() => {
|
|
|
206
206
|
|
|
207
207
|
afterEach(() => {
|
|
208
208
|
jest.restoreAllMocks();
|
|
209
|
-
// addToReleaseHistory writes its JSON
|
|
210
|
-
// when the history was stored successfully.
|
|
211
|
-
fs.rmSync(path.resolve(process.cwd(),
|
|
209
|
+
// addToReleaseHistory writes its JSON under the directory it was invoked in, and only
|
|
210
|
+
// removes it when the history was stored successfully.
|
|
211
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
describe("release without --binary-bundle-path", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import type { CliConfigInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
2
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
4
3
|
|
|
5
4
|
export async function updateReleaseHistory(
|
|
6
5
|
appVersion: string,
|
|
@@ -23,15 +22,8 @@ export async function updateReleaseHistory(
|
|
|
23
22
|
if (typeof rollout === "number") updateInfo.rollout = rollout;
|
|
24
23
|
|
|
25
24
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
30
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
|
|
31
|
-
|
|
32
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier)
|
|
33
|
-
|
|
34
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
25
|
+
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) =>
|
|
26
|
+
setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
|
|
35
27
|
} catch (error) {
|
|
36
28
|
console.error('Error occurred while updating history:', error);
|
|
37
29
|
process.exit(1)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { TOOLS_DIR_ENV_NAME } from "../../utils/binaryPatch.js";
|
|
6
|
+
/**
|
|
7
|
+
* Runs the build script with the install directory the caller chose.
|
|
8
|
+
*
|
|
9
|
+
* The script's output goes straight to the terminal: a first build clones and compiles
|
|
10
|
+
* HDiffPatch, which takes minutes, and a silent wait would look like a hang. So when the
|
|
11
|
+
* script fails, its own message has already been shown, and the error raised here only
|
|
12
|
+
* has to say that it did.
|
|
13
|
+
*/
|
|
14
|
+
export function buildPatchTools({ buildScriptPath, toolsDir, force }) {
|
|
15
|
+
const result = spawnSync(buildScriptPath, force ? ['--force'] : [], {
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
env: { ...process.env, [TOOLS_DIR_ENV_NAME]: toolsDir },
|
|
18
|
+
});
|
|
19
|
+
if (result.error) {
|
|
20
|
+
throw new Error(`failed to run ${buildScriptPath}: ${result.error.message}`);
|
|
21
|
+
}
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
const reason = result.status === null ? `signal ${result.signal}` : `exit code ${result.status}`;
|
|
24
|
+
throw new Error(`${path.basename(buildScriptPath)} failed with ${reason}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* SHA-256 of the build script's bytes, for keying a CI cache of the installed tools. The
|
|
29
|
+
* script pins the sources and the build flags, so it changes whenever the tools it would
|
|
30
|
+
* build do.
|
|
31
|
+
*/
|
|
32
|
+
export function hashBuildScript(buildScriptPath) {
|
|
33
|
+
return crypto.createHash('sha256').update(fs.readFileSync(buildScriptPath)).digest('hex');
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
|
|
5
|
+
import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
|
|
6
|
+
/**
|
|
7
|
+
* The command is a thin wrapper around the build script this package ships. What it has
|
|
8
|
+
* to get right is what it hands the script: the directory the CLI will later look in, and
|
|
9
|
+
* `--force` when asked. A script that records what it was handed proves both without
|
|
10
|
+
* cloning and compiling HDiffPatch. The shipped script is run once at the end, against an
|
|
11
|
+
* install it already finds complete, to show the wrapper speaks its actual interface.
|
|
12
|
+
*/
|
|
13
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
|
|
14
|
+
const SHIPPED_BUILD_SCRIPT = path.join(REPO_ROOT, "scripts", "binary-patch", "build-hdiffpatch.sh");
|
|
15
|
+
/** Where `jest.globalSetup.ts` built the tools for this run. */
|
|
16
|
+
const INSTALLED_TOOLS_DIR = process.env.HDIFFPATCH_TOOLS_DIR || path.join(REPO_ROOT, ".hdiffpatch-tools");
|
|
17
|
+
let workDir;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "build-patch-tools-"));
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
function writeScript(body) {
|
|
25
|
+
const scriptPath = path.join(workDir, "build.sh");
|
|
26
|
+
fs.writeFileSync(scriptPath, `#!/bin/sh\n${body}\n`, { mode: 0o755 });
|
|
27
|
+
return scriptPath;
|
|
28
|
+
}
|
|
29
|
+
/** A script that writes the install directory it was given, then each argument, one per line. */
|
|
30
|
+
function writeRecordingScript() {
|
|
31
|
+
return writeScript('printf \'%s\\n\' "$HDIFFPATCH_TOOLS_DIR" "$@" > "$HDIFFPATCH_TOOLS_DIR/invocation"');
|
|
32
|
+
}
|
|
33
|
+
function readInvocation(toolsDir) {
|
|
34
|
+
return fs.readFileSync(path.join(toolsDir, "invocation"), "utf8").trimEnd().split("\n");
|
|
35
|
+
}
|
|
36
|
+
describe("buildPatchTools", () => {
|
|
37
|
+
it("runs the build script against the tools directory it was given, without forcing a rebuild", () => {
|
|
38
|
+
const toolsDir = path.join(workDir, "tools");
|
|
39
|
+
fs.mkdirSync(toolsDir);
|
|
40
|
+
buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: false });
|
|
41
|
+
expect(readInvocation(toolsDir)).toEqual([toolsDir]);
|
|
42
|
+
});
|
|
43
|
+
it("asks the build script to rebuild when forced", () => {
|
|
44
|
+
const toolsDir = path.join(workDir, "tools");
|
|
45
|
+
fs.mkdirSync(toolsDir);
|
|
46
|
+
buildPatchTools({ buildScriptPath: writeRecordingScript(), toolsDir, force: true });
|
|
47
|
+
expect(readInvocation(toolsDir)).toEqual([toolsDir, "--force"]);
|
|
48
|
+
});
|
|
49
|
+
it("fails with the exit code when the build script fails", () => {
|
|
50
|
+
const buildScriptPath = writeScript("exit 3");
|
|
51
|
+
expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(/exit code 3/);
|
|
52
|
+
});
|
|
53
|
+
it("fails naming the script when it cannot be started", () => {
|
|
54
|
+
const buildScriptPath = path.join(workDir, "no-such-script.sh");
|
|
55
|
+
expect(() => buildPatchTools({ buildScriptPath, toolsDir: workDir, force: false })).toThrow(/no-such-script\.sh/);
|
|
56
|
+
});
|
|
57
|
+
it("succeeds against an install the shipped script already finds complete", () => {
|
|
58
|
+
// Without an install the shipped script clones and compiles for minutes, and
|
|
59
|
+
// `spawnSync` holds the worker past any jest timeout. `jest.globalSetup.ts`
|
|
60
|
+
// provides the install; this says so when it did not.
|
|
61
|
+
const missing = ["hdiffz", "hpatchz"].filter((tool) => !fs.existsSync(path.join(INSTALLED_TOOLS_DIR, tool)));
|
|
62
|
+
if (missing.length > 0) {
|
|
63
|
+
throw new Error(`${missing.join(", ")} not installed in ${INSTALLED_TOOLS_DIR}: jest.globalSetup.ts should have built the tools first`);
|
|
64
|
+
}
|
|
65
|
+
expect(() => buildPatchTools({ buildScriptPath: SHIPPED_BUILD_SCRIPT, toolsDir: INSTALLED_TOOLS_DIR, force: false })).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* A CI cache of the installed tools is keyed by this hash, so it has to stay the same for as
|
|
70
|
+
* long as the script would build the same tools, and change as soon as it would not.
|
|
71
|
+
*/
|
|
72
|
+
describe("hashBuildScript", () => {
|
|
73
|
+
it("hashes two scripts with the same contents to the same hex digest", () => {
|
|
74
|
+
const first = path.join(workDir, "first.sh");
|
|
75
|
+
const second = path.join(workDir, "second.sh");
|
|
76
|
+
fs.writeFileSync(first, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
|
|
77
|
+
fs.writeFileSync(second, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
|
|
78
|
+
expect(hashBuildScript(first)).toBe(hashBuildScript(second));
|
|
79
|
+
expect(hashBuildScript(first)).toMatch(/^[0-9a-f]{64}$/);
|
|
80
|
+
});
|
|
81
|
+
it("hashes a script whose pinned version changed to a different value", () => {
|
|
82
|
+
const before = path.join(workDir, "before.sh");
|
|
83
|
+
const after = path.join(workDir, "after.sh");
|
|
84
|
+
fs.writeFileSync(before, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.3\n");
|
|
85
|
+
fs.writeFileSync(after, "#!/bin/sh\nHDIFFPATCH_TAG=v5.1.4\n");
|
|
86
|
+
expect(hashBuildScript(after)).not.toBe(hashBuildScript(before));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { TOOLS_DIR_ENV_NAME, TOOLS_DIR_NAME } from "../../utils/binaryPatch.js";
|
|
5
|
+
import { buildPatchTools, hashBuildScript } from "./buildPatchTools.js";
|
|
6
|
+
/**
|
|
7
|
+
* The CLI runs compiled, from `cli/dist/commands/buildPatchToolsCommand/`, which puts the
|
|
8
|
+
* package root - and the script shipped under it - four levels up.
|
|
9
|
+
*/
|
|
10
|
+
const BUILD_SCRIPT_PATH = fileURLToPath(new URL("../../../../scripts/binary-patch/build-hdiffpatch.sh", import.meta.url));
|
|
11
|
+
/**
|
|
12
|
+
* Installs where `release` will look: `HDIFFPATCH_TOOLS_DIR` when it is set, and otherwise
|
|
13
|
+
* a `.hdiffpatch-tools` directory in the working directory, the first place the lookup
|
|
14
|
+
* checks before walking up.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_TOOLS_DIR = process.env[TOOLS_DIR_ENV_NAME] || path.resolve(process.cwd(), TOOLS_DIR_NAME);
|
|
17
|
+
program.command('build-patch-tools')
|
|
18
|
+
.description('Builds hdiffz and hpatchz from source and installs them where `release --binary-bundle-path` looks for them.\nThe build clones the pinned HDiffPatch sources, so it needs git, make, a C/C++ compiler and network access. It does nothing when the tools are already installed.')
|
|
19
|
+
.option('--tools-dir <path>', 'directory to install the tools into', DEFAULT_TOOLS_DIR)
|
|
20
|
+
.option('--force', 'rebuild even when the tools are already installed', false)
|
|
21
|
+
.option('--print-hash', 'print a hash of the build script, for keying a CI cache of the tools, and exit without building', false)
|
|
22
|
+
.action((options) => {
|
|
23
|
+
if (options.printHash) {
|
|
24
|
+
console.log(hashBuildScript(BUILD_SCRIPT_PATH));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
buildPatchTools({
|
|
28
|
+
buildScriptPath: BUILD_SCRIPT_PATH,
|
|
29
|
+
toolsDir: path.resolve(options.toolsDir),
|
|
30
|
+
force: options.force,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "path";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function createReleaseHistory(targetVersion, setReleaseHistory, platform, identifier) {
|
|
4
3
|
const BINARY_RELEASE = {
|
|
5
4
|
enabled: true,
|
|
@@ -11,12 +10,7 @@ export async function createReleaseHistory(targetVersion, setReleaseHistory, pla
|
|
|
11
10
|
[targetVersion]: BINARY_RELEASE
|
|
12
11
|
};
|
|
13
12
|
try {
|
|
14
|
-
|
|
15
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
16
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(INITIAL_HISTORY, null, 2));
|
|
17
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(INITIAL_HISTORY));
|
|
18
|
-
await setReleaseHistory(targetVersion, JSON_FILE_PATH, INITIAL_HISTORY, platform, identifier);
|
|
19
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
13
|
+
await stageReleaseHistoryFile(targetVersion, INITIAL_HISTORY, platform, (jsonFilePath) => setReleaseHistory(targetVersion, jsonFilePath, INITIAL_HISTORY, platform, identifier));
|
|
20
14
|
}
|
|
21
15
|
catch (error) {
|
|
22
16
|
console.error('Error occurred while creating new history:', error);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { createReleaseHistory } from "./createReleaseHistory.js";
|
|
6
|
+
import { updateReleaseHistory } from "../updateHistoryCommand/updateReleaseHistory.js";
|
|
7
|
+
/**
|
|
8
|
+
* Covers where the release history is staged before the config is handed it.
|
|
9
|
+
*
|
|
10
|
+
* The config receives a path rather than the history itself, so the command has to put the
|
|
11
|
+
* file somewhere first. Two releases of one app for different platforms are the same binary
|
|
12
|
+
* version, so a staging path derived from that version alone is the same path for both, and
|
|
13
|
+
* they overwrite each other's contents before either has read its own.
|
|
14
|
+
*/
|
|
15
|
+
const BINARY_VERSION = "1.0.0";
|
|
16
|
+
let workingDir;
|
|
17
|
+
let previousCwd;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
previousCwd = process.cwd();
|
|
20
|
+
workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-history-staging-"));
|
|
21
|
+
process.chdir(workingDir);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.chdir(previousCwd);
|
|
25
|
+
fs.rmSync(workingDir, { recursive: true, force: true });
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
function stagedPath(platform) {
|
|
29
|
+
return path.join(workingDir, "codepush-release-history", platform, `${BINARY_VERSION}.json`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Turns the command's own exit into a throw, so a staging failure fails the test instead
|
|
33
|
+
* of taking the worker down with it.
|
|
34
|
+
*/
|
|
35
|
+
function failOnExit() {
|
|
36
|
+
jest.spyOn(process, "exit").mockImplementation((code) => {
|
|
37
|
+
throw new Error(`the command exited with code ${String(code)}`);
|
|
38
|
+
});
|
|
39
|
+
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
40
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
describe("staging the release history a config is handed", () => {
|
|
43
|
+
it("gives each platform writing at the same time a file of its own", async () => {
|
|
44
|
+
failOnExit();
|
|
45
|
+
const staged = {};
|
|
46
|
+
const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
|
|
47
|
+
// Yields once, which is all it takes for the other platform to reach its own
|
|
48
|
+
// write and its own clean-up while this one still holds only a path.
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
staged[platform] = { jsonFilePath, contents: fs.readFileSync(jsonFilePath, "utf8") };
|
|
51
|
+
};
|
|
52
|
+
await Promise.all([
|
|
53
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840"),
|
|
54
|
+
createReleaseHistory(BINARY_VERSION, setReleaseHistory, "android", "RN0840"),
|
|
55
|
+
]);
|
|
56
|
+
expect(staged.ios.jsonFilePath).not.toBe(staged.android.jsonFilePath);
|
|
57
|
+
});
|
|
58
|
+
it("hands each platform the history that platform released", async () => {
|
|
59
|
+
failOnExit();
|
|
60
|
+
const released = {
|
|
61
|
+
ios: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "ios-url", packageHash: "ios-hash" } }),
|
|
62
|
+
android: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "android-url", packageHash: "android-hash" } }),
|
|
63
|
+
};
|
|
64
|
+
const staged = {};
|
|
65
|
+
const getReleaseHistory = async (_binaryVersion, platform) => JSON.parse(released[platform]);
|
|
66
|
+
const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
|
|
67
|
+
await Promise.resolve();
|
|
68
|
+
staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
|
|
69
|
+
};
|
|
70
|
+
await Promise.all([
|
|
71
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
|
|
72
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
|
|
73
|
+
]);
|
|
74
|
+
expect(staged.ios).toContain("ios-url");
|
|
75
|
+
expect(staged.ios).not.toContain("android-url");
|
|
76
|
+
expect(staged.android).toContain("android-url");
|
|
77
|
+
expect(staged.android).not.toContain("ios-url");
|
|
78
|
+
});
|
|
79
|
+
it("takes the file away once the config has stored the history", async () => {
|
|
80
|
+
failOnExit();
|
|
81
|
+
const setReleaseHistory = async () => undefined;
|
|
82
|
+
await createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840");
|
|
83
|
+
expect(fs.existsSync(stagedPath("ios"))).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it("leaves the file behind when the config could not store the history", async () => {
|
|
86
|
+
failOnExit();
|
|
87
|
+
const setReleaseHistory = async () => {
|
|
88
|
+
throw new Error("the storage backend rejected the history");
|
|
89
|
+
};
|
|
90
|
+
await expect(createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840")).rejects.toThrow("the command exited with code 1");
|
|
91
|
+
// What the release tried to store is worth reading back, so a failed release keeps
|
|
92
|
+
// its history at a path that is the same on every run.
|
|
93
|
+
expect(Object.keys(JSON.parse(fs.readFileSync(stagedPath("ios"), "utf8")))).toEqual([BINARY_VERSION]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from "fs";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages) {
|
|
4
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
4
|
const updateInfo = releaseHistory[appVersion];
|
|
@@ -28,12 +27,7 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
|
|
|
28
27
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
29
28
|
}
|
|
30
29
|
try {
|
|
31
|
-
|
|
32
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
33
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(newReleaseHistory, null, 2));
|
|
34
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(newReleaseHistory));
|
|
35
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, newReleaseHistory, platform, identifier);
|
|
36
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
30
|
+
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
37
31
|
}
|
|
38
32
|
catch (error) {
|
|
39
33
|
console.error('Error occurred while updating history:', error);
|
|
@@ -119,9 +119,9 @@ beforeEach(() => {
|
|
|
119
119
|
});
|
|
120
120
|
afterEach(() => {
|
|
121
121
|
jest.restoreAllMocks();
|
|
122
|
-
// addToReleaseHistory writes its JSON
|
|
123
|
-
// when the history was stored successfully.
|
|
124
|
-
fs.rmSync(path.resolve(process.cwd(),
|
|
122
|
+
// addToReleaseHistory writes its JSON under the directory it was invoked in, and only
|
|
123
|
+
// removes it when the history was stored successfully.
|
|
124
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
125
125
|
});
|
|
126
126
|
describe("release without --binary-bundle-path", () => {
|
|
127
127
|
it("keeps existing three-argument uploaders callable", async () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "path";
|
|
1
|
+
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
3
2
|
export async function updateReleaseHistory(appVersion, binaryVersion, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
4
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
4
|
const updateInfo = releaseHistory[appVersion];
|
|
@@ -12,12 +11,7 @@ export async function updateReleaseHistory(appVersion, binaryVersion, getRelease
|
|
|
12
11
|
if (typeof rollout === "number")
|
|
13
12
|
updateInfo.rollout = rollout;
|
|
14
13
|
try {
|
|
15
|
-
|
|
16
|
-
const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
|
|
17
|
-
console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
18
|
-
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
|
|
19
|
-
await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier);
|
|
20
|
-
fs.unlinkSync(JSON_FILE_PATH);
|
|
14
|
+
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
|
|
21
15
|
}
|
|
22
16
|
catch (error) {
|
|
23
17
|
console.error('Error occurred while updating history:', error);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const STAGING_DIR_NAME = 'codepush-release-history';
|
|
4
|
+
/**
|
|
5
|
+
* Writes the release history to a file for the config to read, and takes it away again
|
|
6
|
+
* once the config has stored it.
|
|
7
|
+
*
|
|
8
|
+
* Written under a directory of the platform's own rather than straight into the directory
|
|
9
|
+
* the command ran in. The name follows from the binary version alone, so two commands
|
|
10
|
+
* releasing the same version of one app for different platforms shared one path: whichever
|
|
11
|
+
* finished first took the file away while the other was still about to read it, and before
|
|
12
|
+
* that each had overwritten the other's contents. The name itself is kept as it was,
|
|
13
|
+
* because the name is part of what the config is handed.
|
|
14
|
+
*
|
|
15
|
+
* A file whose history was never stored is left where it was written, so a release that
|
|
16
|
+
* failed can be read back from a path that is the same on every run.
|
|
17
|
+
*/
|
|
18
|
+
export async function stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, store) {
|
|
19
|
+
const jsonFileName = `${binaryVersion}.json`;
|
|
20
|
+
const stagingDir = path.resolve(process.cwd(), STAGING_DIR_NAME, platform);
|
|
21
|
+
const jsonFilePath = path.join(stagingDir, jsonFileName);
|
|
22
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
23
|
+
console.log(`log: creating JSON file... ("${jsonFileName}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
24
|
+
fs.writeFileSync(jsonFilePath, JSON.stringify(releaseHistory));
|
|
25
|
+
const stored = await store(jsonFilePath);
|
|
26
|
+
fs.rmSync(jsonFilePath, { force: true });
|
|
27
|
+
return stored;
|
|
28
|
+
}
|
package/cli/dist/index.js
CHANGED
|
@@ -26,6 +26,10 @@ import "./commands/showHistoryCommand/index.js";
|
|
|
26
26
|
* npx code-push init
|
|
27
27
|
*/
|
|
28
28
|
import "./commands/initCommand/index.js";
|
|
29
|
+
/**
|
|
30
|
+
* npx code-push build-patch-tools
|
|
31
|
+
*/
|
|
32
|
+
import "./commands/buildPatchToolsCommand/index.js";
|
|
29
33
|
shell.set("-e");
|
|
30
34
|
shell.set("+v");
|
|
31
35
|
program
|
|
@@ -31,14 +31,17 @@ export const BINARY_PATCH_ALGORITHM = 'hdiffpatch-m-zstd';
|
|
|
31
31
|
export const BINARY_PATCH_FORMAT_VERSION = 1;
|
|
32
32
|
const HDIFFZ_OPTIONS = ['-f', '-m-6', '-c-zstd-21-24'];
|
|
33
33
|
const HPATCHZ_OPTIONS = ['-f', '-m'];
|
|
34
|
-
const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
|
|
35
|
-
const TOOLS_DIR_NAME = '.hdiffpatch-tools';
|
|
36
|
-
const
|
|
34
|
+
export const TOOLS_DIR_ENV_NAME = 'HDIFFPATCH_TOOLS_DIR';
|
|
35
|
+
export const TOOLS_DIR_NAME = '.hdiffpatch-tools';
|
|
36
|
+
const BUILD_COMMAND = 'npx code-push build-patch-tools';
|
|
37
37
|
/**
|
|
38
38
|
* Finds the hdiffz/hpatchz executable, looking at `HDIFFPATCH_TOOLS_DIR` first and
|
|
39
39
|
* then at a `.hdiffpatch-tools` directory in the working directory or any directory
|
|
40
40
|
* above it. The tools are built from source rather than installed as a package
|
|
41
41
|
* dependency, so the error explains how to get them.
|
|
42
|
+
*
|
|
43
|
+
* `build-patch-tools` (`cli/commands/buildPatchToolsCommand/index.ts`) installs into the
|
|
44
|
+
* first place this looks; a change to the lookup order has to be mirrored there.
|
|
42
45
|
*/
|
|
43
46
|
export function resolveBinaryPatchTool(tool) {
|
|
44
47
|
const configuredDir = process.env[TOOLS_DIR_ENV_NAME];
|
|
@@ -48,7 +51,7 @@ export function resolveBinaryPatchTool(tool) {
|
|
|
48
51
|
return configured;
|
|
49
52
|
}
|
|
50
53
|
throw new Error(`${TOOLS_DIR_ENV_NAME} is set to '${configuredDir}' but it does not contain '${tool}'. ` +
|
|
51
|
-
`Build the tools with '${
|
|
54
|
+
`Build the tools with '${BUILD_COMMAND}'.`);
|
|
52
55
|
}
|
|
53
56
|
let directory = path.resolve(process.cwd());
|
|
54
57
|
for (;;) {
|
|
@@ -63,7 +66,7 @@ export function resolveBinaryPatchTool(tool) {
|
|
|
63
66
|
directory = parent;
|
|
64
67
|
}
|
|
65
68
|
throw new Error(`'${tool}' not found in any '${TOOLS_DIR_NAME}' directory at or above '${process.cwd()}'. ` +
|
|
66
|
-
`Build it with '${
|
|
69
|
+
`Build it with '${BUILD_COMMAND}', or set ${TOOLS_DIR_ENV_NAME} to a directory that contains it.`);
|
|
67
70
|
}
|
|
68
71
|
/** Writes the patch that turns the bundle at `basePath` into the one at `targetPath`. */
|
|
69
72
|
export function generatePatch(basePath, targetPath, patchPath) {
|
|
@@ -82,7 +82,7 @@ describe("generatePatch/applyPatch", () => {
|
|
|
82
82
|
const previous = process.env.HDIFFPATCH_TOOLS_DIR;
|
|
83
83
|
process.env.HDIFFPATCH_TOOLS_DIR = workPath("no-such-dir");
|
|
84
84
|
try {
|
|
85
|
-
expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/build-
|
|
85
|
+
expect(() => resolveBinaryPatchTool("hdiffz")).toThrow(/npx code-push build-patch-tools/);
|
|
86
86
|
}
|
|
87
87
|
finally {
|
|
88
88
|
if (previous === undefined) {
|