@bravemobile/react-native-code-push 13.1.0-beta.3 → 13.1.0-beta.5
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 +20 -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/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 +55 -13
- package/cli/commands/releaseCommand/release.ts +39 -6
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- 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.js +23 -8
- package/cli/dist/commands/releaseCommand/release.test.js +37 -9
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
- package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
- package/cli/functions/resolveAssetDiffBases.ts +8 -1
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- 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 +152 -41
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
|
@@ -13,6 +13,7 @@ import { resolveAssetDiffBases } from "./resolveAssetDiffBases.js";
|
|
|
13
13
|
const CONTENTS_DIR_NAME = 'CodePush';
|
|
14
14
|
/** Where the resolver unpacks a base for verification, so leftovers are recognizable. */
|
|
15
15
|
const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
|
|
16
|
+
const TARGET_BINARY_VERSION = '1.0.0';
|
|
16
17
|
let workDir;
|
|
17
18
|
let bundleDirectory;
|
|
18
19
|
/** Releases one full archive the way the CLI does, and returns what the history records. */
|
|
@@ -49,16 +50,16 @@ function releaseHistoryOf(releases) {
|
|
|
49
50
|
/** Stands in for the consumer's downloader: hands back a local archive per download url. */
|
|
50
51
|
function fakeDownloader(archives) {
|
|
51
52
|
const calls = [];
|
|
52
|
-
const download = async (
|
|
53
|
-
calls.push([
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
throw
|
|
53
|
+
const download = async (downloadInfo, platform, identifier) => {
|
|
54
|
+
calls.push([downloadInfo, platform, identifier]);
|
|
55
|
+
const downloadedArchive = archives[downloadInfo.downloadUrl];
|
|
56
|
+
if (downloadedArchive instanceof Error) {
|
|
57
|
+
throw downloadedArchive;
|
|
57
58
|
}
|
|
58
|
-
if (!
|
|
59
|
-
throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
|
|
59
|
+
if (!downloadedArchive) {
|
|
60
|
+
throw new Error(`the downloader was called with an unexpected url: ${downloadInfo.downloadUrl}`);
|
|
60
61
|
}
|
|
61
|
-
return { downloadedFilePath:
|
|
62
|
+
return { downloadedFilePath: downloadedArchive };
|
|
62
63
|
};
|
|
63
64
|
return { download, calls };
|
|
64
65
|
}
|
|
@@ -86,14 +87,15 @@ describe("resolveAssetDiffBases", () => {
|
|
|
86
87
|
bundleDownloader: download,
|
|
87
88
|
platform: 'android',
|
|
88
89
|
identifier: 'my-app',
|
|
90
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
89
91
|
});
|
|
90
92
|
expect(bases).toEqual([
|
|
91
93
|
{ basePackageHash: newest.packageHash, baseBundleFilePath: newest.archivePath },
|
|
92
94
|
{ basePackageHash: second.packageHash, baseBundleFilePath: second.archivePath },
|
|
93
95
|
]);
|
|
94
96
|
expect(calls).toEqual([
|
|
95
|
-
[newest.downloadUrl, 'android', 'my-app'],
|
|
96
|
-
[second.downloadUrl, 'android', 'my-app'],
|
|
97
|
+
[{ downloadUrl: newest.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: newest.version, packageHash: newest.packageHash }, 'android', 'my-app'],
|
|
98
|
+
[{ downloadUrl: second.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: second.version, packageHash: second.packageHash }, 'android', 'my-app'],
|
|
97
99
|
]);
|
|
98
100
|
});
|
|
99
101
|
it("skips history entries that lack a download url or a package hash", async () => {
|
|
@@ -109,11 +111,16 @@ describe("resolveAssetDiffBases", () => {
|
|
|
109
111
|
diffBaseCount: 1,
|
|
110
112
|
bundleDownloader: download,
|
|
111
113
|
platform: 'ios',
|
|
114
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
112
115
|
});
|
|
113
116
|
expect(bases).toEqual([
|
|
114
117
|
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
115
118
|
]);
|
|
116
|
-
expect(calls).toEqual([[
|
|
119
|
+
expect(calls).toEqual([[
|
|
120
|
+
{ downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
|
|
121
|
+
'ios',
|
|
122
|
+
undefined,
|
|
123
|
+
]]);
|
|
117
124
|
});
|
|
118
125
|
it("ignores a history entry whose key is not a version, and resolves the rest", async () => {
|
|
119
126
|
const [released] = await stageReleases(['1.4.0']);
|
|
@@ -131,11 +138,16 @@ describe("resolveAssetDiffBases", () => {
|
|
|
131
138
|
diffBaseCount: 2,
|
|
132
139
|
bundleDownloader: download,
|
|
133
140
|
platform: 'ios',
|
|
141
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
134
142
|
});
|
|
135
143
|
expect(bases).toEqual([
|
|
136
144
|
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
137
145
|
]);
|
|
138
|
-
expect(calls).toEqual([[
|
|
146
|
+
expect(calls).toEqual([[
|
|
147
|
+
{ downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
|
|
148
|
+
'ios',
|
|
149
|
+
undefined,
|
|
150
|
+
]]);
|
|
139
151
|
});
|
|
140
152
|
it("drops a base whose downloaded bytes do not match its recorded package hash", async () => {
|
|
141
153
|
const [older, newer] = await stageReleases(['1.2.0', '1.2.1']);
|
|
@@ -150,6 +162,7 @@ describe("resolveAssetDiffBases", () => {
|
|
|
150
162
|
diffBaseCount: 2,
|
|
151
163
|
bundleDownloader: download,
|
|
152
164
|
platform: 'android',
|
|
165
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
153
166
|
});
|
|
154
167
|
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
155
168
|
expect(warn.mock.calls.join('\n')).toMatch(/1\.2\.1/);
|
|
@@ -167,6 +180,7 @@ describe("resolveAssetDiffBases", () => {
|
|
|
167
180
|
diffBaseCount: 2,
|
|
168
181
|
bundleDownloader: download,
|
|
169
182
|
platform: 'android',
|
|
183
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
170
184
|
});
|
|
171
185
|
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
172
186
|
expect(warn.mock.calls.join('\n')).toMatch(/1\.3\.1.*the network is down/s);
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
|
|
5
5
|
import { makeCodePushBundle } from "./makeCodePushBundle.js";
|
|
6
6
|
import { resolveAssetDiffBases } from "./resolveAssetDiffBases.js";
|
|
7
|
-
import type { CliConfigInterface, ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
|
|
7
|
+
import type { BundleDownloadInfo, CliConfigInterface, ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Exercises base acquisition against real archives and real hashes: a base is only usable
|
|
@@ -17,6 +17,7 @@ const CONTENTS_DIR_NAME = 'CodePush';
|
|
|
17
17
|
|
|
18
18
|
/** Where the resolver unpacks a base for verification, so leftovers are recognizable. */
|
|
19
19
|
const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
|
|
20
|
+
const TARGET_BINARY_VERSION = '1.0.0';
|
|
20
21
|
|
|
21
22
|
type StagedRelease = {
|
|
22
23
|
version: string;
|
|
@@ -68,22 +69,22 @@ function releaseHistoryOf(releases: StagedRelease[]): ReleaseHistoryInterface {
|
|
|
68
69
|
|
|
69
70
|
/** Stands in for the consumer's downloader: hands back a local archive per download url. */
|
|
70
71
|
function fakeDownloader(archives: Record<string, string | Error>) {
|
|
71
|
-
const calls: Array<[
|
|
72
|
+
const calls: Array<[BundleDownloadInfo, string, string | undefined]> = [];
|
|
72
73
|
const download: NonNullable<CliConfigInterface['bundleDownloader']> = async (
|
|
73
|
-
|
|
74
|
+
downloadInfo,
|
|
74
75
|
platform,
|
|
75
76
|
identifier,
|
|
76
77
|
) => {
|
|
77
|
-
calls.push([
|
|
78
|
+
calls.push([downloadInfo, platform, identifier]);
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
throw
|
|
80
|
+
const downloadedArchive = archives[downloadInfo.downloadUrl];
|
|
81
|
+
if (downloadedArchive instanceof Error) {
|
|
82
|
+
throw downloadedArchive;
|
|
82
83
|
}
|
|
83
|
-
if (!
|
|
84
|
-
throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
|
|
84
|
+
if (!downloadedArchive) {
|
|
85
|
+
throw new Error(`the downloader was called with an unexpected url: ${downloadInfo.downloadUrl}`);
|
|
85
86
|
}
|
|
86
|
-
return { downloadedFilePath:
|
|
87
|
+
return { downloadedFilePath: downloadedArchive };
|
|
87
88
|
};
|
|
88
89
|
return { download, calls };
|
|
89
90
|
}
|
|
@@ -119,6 +120,7 @@ describe("resolveAssetDiffBases", () => {
|
|
|
119
120
|
bundleDownloader: download,
|
|
120
121
|
platform: 'android',
|
|
121
122
|
identifier: 'my-app',
|
|
123
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
122
124
|
});
|
|
123
125
|
|
|
124
126
|
expect(bases).toEqual([
|
|
@@ -126,8 +128,8 @@ describe("resolveAssetDiffBases", () => {
|
|
|
126
128
|
{ basePackageHash: second.packageHash, baseBundleFilePath: second.archivePath },
|
|
127
129
|
]);
|
|
128
130
|
expect(calls).toEqual([
|
|
129
|
-
[newest.downloadUrl, 'android', 'my-app'],
|
|
130
|
-
[second.downloadUrl, 'android', 'my-app'],
|
|
131
|
+
[{ downloadUrl: newest.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: newest.version, packageHash: newest.packageHash }, 'android', 'my-app'],
|
|
132
|
+
[{ downloadUrl: second.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: second.version, packageHash: second.packageHash }, 'android', 'my-app'],
|
|
131
133
|
]);
|
|
132
134
|
});
|
|
133
135
|
|
|
@@ -145,12 +147,17 @@ describe("resolveAssetDiffBases", () => {
|
|
|
145
147
|
diffBaseCount: 1,
|
|
146
148
|
bundleDownloader: download,
|
|
147
149
|
platform: 'ios',
|
|
150
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
148
151
|
});
|
|
149
152
|
|
|
150
153
|
expect(bases).toEqual([
|
|
151
154
|
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
152
155
|
]);
|
|
153
|
-
expect(calls).toEqual([[
|
|
156
|
+
expect(calls).toEqual([[
|
|
157
|
+
{ downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
|
|
158
|
+
'ios',
|
|
159
|
+
undefined,
|
|
160
|
+
]]);
|
|
154
161
|
});
|
|
155
162
|
|
|
156
163
|
it("ignores a history entry whose key is not a version, and resolves the rest", async () => {
|
|
@@ -170,12 +177,17 @@ describe("resolveAssetDiffBases", () => {
|
|
|
170
177
|
diffBaseCount: 2,
|
|
171
178
|
bundleDownloader: download,
|
|
172
179
|
platform: 'ios',
|
|
180
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
173
181
|
});
|
|
174
182
|
|
|
175
183
|
expect(bases).toEqual([
|
|
176
184
|
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
177
185
|
]);
|
|
178
|
-
expect(calls).toEqual([[
|
|
186
|
+
expect(calls).toEqual([[
|
|
187
|
+
{ downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
|
|
188
|
+
'ios',
|
|
189
|
+
undefined,
|
|
190
|
+
]]);
|
|
179
191
|
});
|
|
180
192
|
|
|
181
193
|
it("drops a base whose downloaded bytes do not match its recorded package hash", async () => {
|
|
@@ -192,6 +204,7 @@ describe("resolveAssetDiffBases", () => {
|
|
|
192
204
|
diffBaseCount: 2,
|
|
193
205
|
bundleDownloader: download,
|
|
194
206
|
platform: 'android',
|
|
207
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
195
208
|
});
|
|
196
209
|
|
|
197
210
|
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
@@ -212,6 +225,7 @@ describe("resolveAssetDiffBases", () => {
|
|
|
212
225
|
diffBaseCount: 2,
|
|
213
226
|
bundleDownloader: download,
|
|
214
227
|
platform: 'android',
|
|
228
|
+
targetBinaryVersion: TARGET_BINARY_VERSION,
|
|
215
229
|
});
|
|
216
230
|
|
|
217
231
|
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
@@ -40,12 +40,14 @@ export async function resolveAssetDiffBases({
|
|
|
40
40
|
bundleDownloader,
|
|
41
41
|
platform,
|
|
42
42
|
identifier,
|
|
43
|
+
targetBinaryVersion,
|
|
43
44
|
}: {
|
|
44
45
|
releaseHistory: ReleaseHistoryInterface;
|
|
45
46
|
diffBaseCount: number;
|
|
46
47
|
bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
|
|
47
48
|
platform: 'ios' | 'android';
|
|
48
49
|
identifier?: string;
|
|
50
|
+
targetBinaryVersion: string;
|
|
49
51
|
}): Promise<AssetDiffBase[]> {
|
|
50
52
|
// The seeded entry for the update inside the app binary has neither a url nor a hash,
|
|
51
53
|
// and nothing can be diffed against a package that was never published. A key that is
|
|
@@ -60,7 +62,12 @@ export async function resolveAssetDiffBases({
|
|
|
60
62
|
const bases: AssetDiffBase[] = [];
|
|
61
63
|
for (const [version, releaseInfo] of candidates) {
|
|
62
64
|
try {
|
|
63
|
-
const { downloadedFilePath } = await bundleDownloader(
|
|
65
|
+
const { downloadedFilePath } = await bundleDownloader({
|
|
66
|
+
downloadUrl: releaseInfo.downloadUrl,
|
|
67
|
+
targetBinaryVersion,
|
|
68
|
+
releaseVersion: version,
|
|
69
|
+
packageHash: releaseInfo.packageHash,
|
|
70
|
+
}, platform, identifier);
|
|
64
71
|
|
|
65
72
|
if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
|
|
66
73
|
console.warn(
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
|
|
4
|
+
|
|
5
|
+
const STAGING_DIR_NAME = 'codepush-release-history';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Writes the release history to a file for the config to read, and takes it away again
|
|
9
|
+
* once the config has stored it.
|
|
10
|
+
*
|
|
11
|
+
* Written under a directory of the platform's own rather than straight into the directory
|
|
12
|
+
* the command ran in. The name follows from the binary version alone, so two commands
|
|
13
|
+
* releasing the same version of one app for different platforms shared one path: whichever
|
|
14
|
+
* finished first took the file away while the other was still about to read it, and before
|
|
15
|
+
* that each had overwritten the other's contents. The name itself is kept as it was,
|
|
16
|
+
* because the name is part of what the config is handed.
|
|
17
|
+
*
|
|
18
|
+
* A file whose history was never stored is left where it was written, so a release that
|
|
19
|
+
* failed can be read back from a path that is the same on every run.
|
|
20
|
+
*/
|
|
21
|
+
export async function stageReleaseHistoryFile<T>(
|
|
22
|
+
binaryVersion: string,
|
|
23
|
+
releaseHistory: ReleaseHistoryInterface,
|
|
24
|
+
platform: 'ios' | 'android',
|
|
25
|
+
store: (jsonFilePath: string) => Promise<T>,
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const jsonFileName = `${binaryVersion}.json`;
|
|
28
|
+
const stagingDir = path.resolve(process.cwd(), STAGING_DIR_NAME, platform);
|
|
29
|
+
const jsonFilePath = path.join(stagingDir, jsonFileName);
|
|
30
|
+
|
|
31
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
32
|
+
console.log(`log: creating JSON file... ("${jsonFileName}")\n`, JSON.stringify(releaseHistory, null, 2));
|
|
33
|
+
fs.writeFileSync(jsonFilePath, JSON.stringify(releaseHistory));
|
|
34
|
+
|
|
35
|
+
const stored = await store(jsonFilePath);
|
|
36
|
+
fs.rmSync(jsonFilePath, { force: true });
|
|
37
|
+
return stored;
|
|
38
|
+
}
|
package/ios/CodePush/CodePush.h
CHANGED
|
@@ -157,22 +157,25 @@ failCallback:(void (^)(NSError *err))failCallback;
|
|
|
157
157
|
@interface CodePushErrorUtils : NSObject
|
|
158
158
|
|
|
159
159
|
+ (NSError *)errorWithMessage:(NSString *)errorMessage;
|
|
160
|
+
+ (NSError *)errorWithMessage:(NSString *)errorMessage httpStatusCode:(NSInteger)statusCode;
|
|
160
161
|
+ (BOOL)isCodePushError:(NSError *)error;
|
|
162
|
+
+ (BOOL)isNetworkFailure:(NSError *)error;
|
|
163
|
+
+ (BOOL)isHttpStatusError:(NSError *)error;
|
|
161
164
|
|
|
162
165
|
@end
|
|
163
166
|
|
|
164
167
|
@interface CodePushPackage : NSObject
|
|
165
168
|
|
|
166
169
|
/*
|
|
167
|
-
* doneCallback is given what the
|
|
168
|
-
*
|
|
170
|
+
* doneCallback is given what the attempts at installing the update from its update
|
|
171
|
+
* archives ended in, or nil when the update had no archive to attempt. The result
|
|
169
172
|
* describes the download and not the update: it is never part of the update's metadata.
|
|
170
173
|
*/
|
|
171
174
|
+ (void)downloadPackage:(NSDictionary *)updatePackage
|
|
172
175
|
expectedBundleFileName:(NSString *)expectedBundleFileName
|
|
173
176
|
operationQueue:(dispatch_queue_t)operationQueue
|
|
174
177
|
progressCallback:(void (^)(long long, long long))progressCallback
|
|
175
|
-
doneCallback:(void (^)(NSDictionary *
|
|
178
|
+
doneCallback:(void (^)(NSDictionary *updateArchiveResult))doneCallback
|
|
176
179
|
failCallback:(void (^)(NSError *err))failCallback;
|
|
177
180
|
|
|
178
181
|
+ (NSDictionary *)getCurrentPackage:(NSError **)error;
|
package/ios/CodePush/CodePush.mm
CHANGED
|
@@ -63,7 +63,7 @@ static NSString *const PackageIsPendingKey = @"isPending";
|
|
|
63
63
|
|
|
64
64
|
// Not one of those keys: the field a download that tried a binary patch reports its outcome
|
|
65
65
|
// under, on the package it resolves with and on no package it stores.
|
|
66
|
-
static NSString *const
|
|
66
|
+
static NSString *const UpdateArchiveResultKey = @"updateArchiveResult";
|
|
67
67
|
|
|
68
68
|
#pragma mark - Static variables
|
|
69
69
|
|
|
@@ -740,8 +740,8 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
740
740
|
// The download is progressing forward
|
|
741
741
|
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
|
|
742
742
|
// Received bytes only run backwards when a new download starts a stream of its
|
|
743
|
-
// own - the
|
|
744
|
-
// from the finished
|
|
743
|
+
// own - the next archive after a binary patch fallback. The pause left over
|
|
744
|
+
// from the finished download would swallow that whole stream, so frame
|
|
745
745
|
// observation resumes here and the throttle forgets the previous stream.
|
|
746
746
|
if (notifyProgress && receivedContentLength < _latestReceivedConentLength) {
|
|
747
747
|
self.paused = NO;
|
|
@@ -767,21 +767,21 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
769
|
// The download completed
|
|
770
|
-
doneCallback:^(NSDictionary *
|
|
770
|
+
doneCallback:^(NSDictionary *updateArchiveResult) {
|
|
771
771
|
NSError *err;
|
|
772
772
|
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
|
|
773
773
|
|
|
774
774
|
if (err) {
|
|
775
|
-
return reject([NSString stringWithFormat: @"%
|
|
775
|
+
return reject([NSString stringWithFormat: @"%ld", (long)err.code], err.localizedDescription, err);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
if (
|
|
778
|
+
if (updateArchiveResult) {
|
|
779
779
|
// The result describes this download rather than the update, so it is put on
|
|
780
780
|
// what this call resolves with and nowhere else: the metadata that was
|
|
781
781
|
// written for the update knows nothing about it, and whoever asked for the
|
|
782
782
|
// download decides what the result is worth.
|
|
783
783
|
NSMutableDictionary *downloadedPackage = [newPackage mutableCopy];
|
|
784
|
-
downloadedPackage[
|
|
784
|
+
downloadedPackage[UpdateArchiveResultKey] = updateArchiveResult;
|
|
785
785
|
return resolve(downloadedPackage);
|
|
786
786
|
}
|
|
787
787
|
|
|
@@ -796,7 +796,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
|
|
|
796
796
|
// Stop observing frame updates if the download fails.
|
|
797
797
|
_didUpdateProgress = NO;
|
|
798
798
|
self.paused = YES;
|
|
799
|
-
reject([NSString stringWithFormat: @"%
|
|
799
|
+
reject([NSString stringWithFormat: @"%ld", (long)err.code], err.localizedDescription, err);
|
|
800
800
|
}];
|
|
801
801
|
}
|
|
802
802
|
|
|
@@ -876,7 +876,7 @@ RCT_EXPORT_METHOD(getUpdateMetadata:(double)updateStateValue
|
|
|
876
876
|
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
|
|
877
877
|
|
|
878
878
|
if (error) {
|
|
879
|
-
return reject([NSString stringWithFormat: @"%
|
|
879
|
+
return reject([NSString stringWithFormat: @"%ld", (long)error.code], error.localizedDescription, error);
|
|
880
880
|
} else if (package == nil) {
|
|
881
881
|
// The app hasn't downloaded any CodePush updates yet,
|
|
882
882
|
// so we simply return nil regardless if the user
|
|
@@ -930,7 +930,7 @@ RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
|
|
|
930
930
|
error:&error];
|
|
931
931
|
|
|
932
932
|
if (error) {
|
|
933
|
-
reject([NSString stringWithFormat: @"%
|
|
933
|
+
reject([NSString stringWithFormat: @"%ld", (long)error.code], error.localizedDescription, error);
|
|
934
934
|
} else {
|
|
935
935
|
[self savePendingUpdate:updatePackage[PackageHashKey]
|
|
936
936
|
isLoading:NO];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#import <Foundation/Foundation.h>
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* Why an update that was downloaded as
|
|
4
|
+
* Why an update that was downloaded as one of its archives has to be downloaded in full
|
|
5
5
|
* instead.
|
|
6
6
|
*
|
|
7
7
|
* A failed restore is not an error the user ever hears about: it is the signal to
|
|
@@ -11,22 +11,32 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** The bundle inside the app binary could not be opened or read. */
|
|
14
|
-
extern NSString *const
|
|
14
|
+
extern NSString *const CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
15
15
|
|
|
16
16
|
/** The bundle inside the app binary is not the one the patch was computed against. */
|
|
17
|
-
extern NSString *const
|
|
17
|
+
extern NSString *const CodePushArchiveFallbackReasonBaseHashMismatch;
|
|
18
18
|
|
|
19
19
|
/** The manifest is missing, malformed, points outside the archive, or asks for too much. */
|
|
20
|
-
extern NSString *const
|
|
20
|
+
extern NSString *const CodePushArchiveFallbackReasonInvalidManifest;
|
|
21
21
|
|
|
22
22
|
/** The patch was produced by a format or a codec this client cannot apply. */
|
|
23
|
-
extern NSString *const
|
|
23
|
+
extern NSString *const CodePushArchiveFallbackReasonUnsupportedFormat;
|
|
24
24
|
|
|
25
25
|
/** The applier refused the patch, or the restored bundle could not be written. */
|
|
26
|
-
extern NSString *const
|
|
26
|
+
extern NSString *const CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
27
27
|
|
|
28
28
|
/** The restored bundle is not the one the manifest promised. */
|
|
29
|
-
extern NSString *const
|
|
29
|
+
extern NSString *const CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The asset diff could not be merged with the installed update it was built against.
|
|
33
|
+
*
|
|
34
|
+
* Reported by the caller too, from the merge that follows the restore: the diff archive
|
|
35
|
+
* only carries what the installed update does not, so this covers reading the installed
|
|
36
|
+
* update's files, applying the deletion manifest, and everything else that stitches the
|
|
37
|
+
* two back together.
|
|
38
|
+
*/
|
|
39
|
+
extern NSString *const CodePushArchiveFallbackReasonAssetMergeFailed;
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* The update restored from the patch did not pass the checks every update passes before
|
|
@@ -35,7 +45,7 @@ extern NSString *const CodePushBinaryPatchReasonTargetVerificationFailed;
|
|
|
35
45
|
* This one is reported by the caller rather than by the applier: by the time it applies,
|
|
36
46
|
* the applier has already handed back a bundle it was happy with.
|
|
37
47
|
*/
|
|
38
|
-
extern NSString *const
|
|
48
|
+
extern NSString *const CodePushArchiveFallbackReasonPackageVerificationFailed;
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* Rebuilds the JS bundle of an update that was downloaded as a binary patch archive.
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
#include "binarypatch_zstd_decompressor.h"
|
|
15
15
|
#include "libHDiffPatch/HPatch/patch.h"
|
|
16
16
|
|
|
17
|
-
NSString *const
|
|
18
|
-
NSString *const
|
|
19
|
-
NSString *const
|
|
20
|
-
NSString *const
|
|
21
|
-
NSString *const
|
|
22
|
-
NSString *const
|
|
23
|
-
NSString *const
|
|
17
|
+
NSString *const CodePushArchiveFallbackReasonBaseBundleUnavailable = @"base_bundle_unavailable";
|
|
18
|
+
NSString *const CodePushArchiveFallbackReasonBaseHashMismatch = @"base_hash_mismatch";
|
|
19
|
+
NSString *const CodePushArchiveFallbackReasonInvalidManifest = @"invalid_manifest";
|
|
20
|
+
NSString *const CodePushArchiveFallbackReasonUnsupportedFormat = @"unsupported_format";
|
|
21
|
+
NSString *const CodePushArchiveFallbackReasonPatchApplyFailed = @"patch_apply_failed";
|
|
22
|
+
NSString *const CodePushArchiveFallbackReasonTargetVerificationFailed = @"target_verification_failed";
|
|
23
|
+
NSString *const CodePushArchiveFallbackReasonAssetMergeFailed = @"asset_merge_failed";
|
|
24
|
+
NSString *const CodePushArchiveFallbackReasonPackageVerificationFailed = @"package_verification_failed";
|
|
24
25
|
|
|
25
26
|
#pragma mark - Private constants
|
|
26
27
|
|
|
@@ -205,7 +206,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
205
206
|
NSString *contentsFolderPath = [self contentsFolderInUnzippedFolder:unzippedFolderPath];
|
|
206
207
|
NSString *manifestFilePath = [contentsFolderPath stringByAppendingPathComponent:BinaryPatchManifestFileName];
|
|
207
208
|
if (![self isFileAtPath:manifestFilePath]) {
|
|
208
|
-
return [self failWithReason:
|
|
209
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
209
210
|
outFailureReason:failureReason];
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -219,13 +220,13 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
219
220
|
: nil;
|
|
220
221
|
if (![manifest isKindOfClass:[NSDictionary class]]) {
|
|
221
222
|
CPLog(@"The binary patch manifest could not be read: %@", error);
|
|
222
|
-
return [self failWithReason:
|
|
223
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
223
224
|
outFailureReason:failureReason];
|
|
224
225
|
}
|
|
225
226
|
|
|
226
227
|
if ([[self numberInManifest:manifest forKey:BinaryPatchFormatVersionKey] integerValue] != BinaryPatchFormatVersion
|
|
227
228
|
|| ![BinaryPatchAlgorithm isEqualToString:[self stringInManifest:manifest forKey:BinaryPatchAlgorithmKey]]) {
|
|
228
|
-
return [self failWithReason:
|
|
229
|
+
return [self failWithReason:CodePushArchiveFallbackReasonUnsupportedFormat
|
|
229
230
|
outFailureReason:failureReason];
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -239,7 +240,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
239
240
|
if (targetBundleFilePath == nil || patchFilePath == nil || ![self isFileAtPath:patchFilePath]
|
|
240
241
|
|| baseBundleHash.length == 0 || targetBundleHash.length == 0
|
|
241
242
|
|| targetBundleSize <= 0 || targetBundleSize > BinaryPatchMaxTargetBundleSize) {
|
|
242
|
-
return [self failWithReason:
|
|
243
|
+
return [self failWithReason:CodePushArchiveFallbackReasonInvalidManifest
|
|
243
244
|
outFailureReason:failureReason];
|
|
244
245
|
}
|
|
245
246
|
|
|
@@ -250,7 +251,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
250
251
|
attributes:nil
|
|
251
252
|
error:&error]) {
|
|
252
253
|
CPLog(@"Unable to create the binary patch working directory at %@: %@", workingFolderPath, error);
|
|
253
|
-
return [self failWithReason:
|
|
254
|
+
return [self failWithReason:CodePushArchiveFallbackReasonPatchApplyFailed
|
|
254
255
|
outFailureReason:failureReason];
|
|
255
256
|
}
|
|
256
257
|
|
|
@@ -296,13 +297,13 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
296
297
|
error:nil][NSFileSystemFreeSize];
|
|
297
298
|
if (freeSize != nil && freeSize.longLongValue < targetBundleSize) {
|
|
298
299
|
CPLog(@"Not enough free space to restore a %lld byte bundle.", targetBundleSize);
|
|
299
|
-
return
|
|
300
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
NSString *baseBundlePath = [baseBundleURL path];
|
|
303
304
|
if (baseBundlePath == nil) {
|
|
304
305
|
CPLog(@"The app binary carries no JS bundle to patch against.");
|
|
305
|
-
return
|
|
306
|
+
return CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
NSError *error = nil;
|
|
@@ -318,10 +319,10 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
318
319
|
error:&error];
|
|
319
320
|
if (baseBundle == nil) {
|
|
320
321
|
CPLog(@"Unable to read the JS bundle inside the app binary: %@", error);
|
|
321
|
-
return
|
|
322
|
+
return CodePushArchiveFallbackReasonBaseBundleUnavailable;
|
|
322
323
|
}
|
|
323
324
|
if (![baseBundleHash isEqualToString:[CodePushUpdateUtils computeHashForData:baseBundle]]) {
|
|
324
|
-
return
|
|
325
|
+
return CodePushArchiveFallbackReasonBaseHashMismatch;
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
__attribute__((objc_precise_lifetime)) NSData *patch = [NSData dataWithContentsOfFile:patchFilePath
|
|
@@ -329,7 +330,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
329
330
|
error:&error];
|
|
330
331
|
if (patch == nil) {
|
|
331
332
|
CPLog(@"Unable to read the binary patch: %@", error);
|
|
332
|
-
return
|
|
333
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
NSString *restoredBundleFilePath = [workingFolderPath stringByAppendingPathComponent:BinaryPatchTargetFileName];
|
|
@@ -342,20 +343,20 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
342
343
|
if (applyResult != CodePushBinaryPatchApplyResultOK) {
|
|
343
344
|
CPLog(@"The binary patch applier returned %ld.", (long)applyResult);
|
|
344
345
|
return applyResult == CodePushBinaryPatchApplyResultUnsupportedCompression
|
|
345
|
-
?
|
|
346
|
-
:
|
|
346
|
+
? CodePushArchiveFallbackReasonUnsupportedFormat
|
|
347
|
+
: CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
NSDictionary *restoredBundleAttributes = [fileManager attributesOfItemAtPath:restoredBundleFilePath
|
|
350
351
|
error:&error];
|
|
351
352
|
if (restoredBundleAttributes == nil || (long long)[restoredBundleAttributes fileSize] != targetBundleSize) {
|
|
352
353
|
CPLog(@"The restored bundle is not %lld bytes long: %@", targetBundleSize, error);
|
|
353
|
-
return
|
|
354
|
+
return CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
NSString *restoredBundleHash = [CodePushUpdateUtils computeHashForFileAtPath:restoredBundleFilePath];
|
|
357
358
|
if (restoredBundleHash == nil || ![targetBundleHash isEqualToString:restoredBundleHash]) {
|
|
358
|
-
return
|
|
359
|
+
return CodePushArchiveFallbackReasonTargetVerificationFailed;
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
if (![self moveItemAtPath:restoredBundleFilePath toPath:targetBundleFilePath]
|
|
@@ -365,7 +366,7 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
|
|
|
365
366
|
// that follows empties the unzipped archive before it replaces it, which is what
|
|
366
367
|
// clears them.
|
|
367
368
|
CPLog(@"Unable to put the restored bundle in place of the patch: %@", error);
|
|
368
|
-
return
|
|
369
|
+
return CodePushArchiveFallbackReasonPatchApplyFailed;
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
return nil;
|
|
@@ -54,7 +54,8 @@ failCallback:(void (^)(NSError *err))failCallback {
|
|
|
54
54
|
if (statusCode >= 400) {
|
|
55
55
|
[self.outputFileStream close];
|
|
56
56
|
[connection cancel];
|
|
57
|
-
NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Received %ld response from %@", (long)statusCode, self.downloadUrl]
|
|
57
|
+
NSError *err = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat: @"Received %ld response from %@", (long)statusCode, self.downloadUrl]
|
|
58
|
+
httpStatusCode:statusCode];
|
|
58
59
|
self.failCallback(err);
|
|
59
60
|
return;
|
|
60
61
|
}
|