rn-update-cli 1.0.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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/README.zh-CN.md +167 -0
- package/cli.json +525 -0
- package/lib/api.d.ts +56 -0
- package/lib/api.js +671 -0
- package/lib/app.d.ts +107 -0
- package/lib/app.js +329 -0
- package/lib/auto-update.d.ts +35 -0
- package/lib/auto-update.js +507 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +133 -0
- package/lib/bundle-pack.d.ts +6 -0
- package/lib/bundle-pack.js +91 -0
- package/lib/bundle-runner.d.ts +168 -0
- package/lib/bundle-runner.js +1305 -0
- package/lib/bundle.d.ts +45 -0
- package/lib/bundle.js +226 -0
- package/lib/cache.d.ts +10 -0
- package/lib/cache.js +25 -0
- package/lib/commands.d.ts +11 -0
- package/lib/commands.js +72 -0
- package/lib/diff.d.ts +18 -0
- package/lib/diff.js +690 -0
- package/lib/exports.d.ts +11 -0
- package/lib/exports.js +20 -0
- package/lib/install.d.ts +9 -0
- package/lib/install.js +51 -0
- package/lib/locales/en.d.ts +187 -0
- package/lib/locales/en.js +194 -0
- package/lib/locales/zh.d.ts +187 -0
- package/lib/locales/zh.js +194 -0
- package/lib/native-package.d.ts +21 -0
- package/lib/native-package.js +298 -0
- package/lib/package.d.ts +68 -0
- package/lib/package.js +487 -0
- package/lib/provider.d.ts +24 -0
- package/lib/provider.js +244 -0
- package/lib/standalone-publish.d.ts +17 -0
- package/lib/standalone-publish.js +433 -0
- package/lib/symbolicate.d.ts +35 -0
- package/lib/symbolicate.js +130 -0
- package/lib/types.d.ts +152 -0
- package/lib/types.js +2 -0
- package/lib/user.d.ts +8 -0
- package/lib/user.js +50 -0
- package/lib/utils/add-gitignore.d.ts +6 -0
- package/lib/utils/add-gitignore.js +44 -0
- package/lib/utils/app-info-parser/aab.d.ts +27 -0
- package/lib/utils/app-info-parser/aab.js +256 -0
- package/lib/utils/app-info-parser/apk.d.ts +19 -0
- package/lib/utils/app-info-parser/apk.js +95 -0
- package/lib/utils/app-info-parser/app.d.ts +9 -0
- package/lib/utils/app-info-parser/app.js +15 -0
- package/lib/utils/app-info-parser/index.d.ts +21 -0
- package/lib/utils/app-info-parser/index.js +51 -0
- package/lib/utils/app-info-parser/ipa.d.ts +19 -0
- package/lib/utils/app-info-parser/ipa.js +101 -0
- package/lib/utils/app-info-parser/resource-finder.d.ts +49 -0
- package/lib/utils/app-info-parser/resource-finder.js +429 -0
- package/lib/utils/app-info-parser/utils.d.ts +36 -0
- package/lib/utils/app-info-parser/utils.js +140 -0
- package/lib/utils/app-info-parser/xml-parser/binary.d.ts +61 -0
- package/lib/utils/app-info-parser/xml-parser/binary.js +577 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.d.ts +15 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.js +207 -0
- package/lib/utils/app-info-parser/zip.d.ts +29 -0
- package/lib/utils/app-info-parser/zip.js +97 -0
- package/lib/utils/check-lockfile.d.ts +6 -0
- package/lib/utils/check-lockfile.js +93 -0
- package/lib/utils/check-plugin.d.ts +12 -0
- package/lib/utils/check-plugin.js +36 -0
- package/lib/utils/command-result.d.ts +8 -0
- package/lib/utils/command-result.js +27 -0
- package/lib/utils/constants.d.ts +10 -0
- package/lib/utils/constants.js +39 -0
- package/lib/utils/dep-versions.d.ts +20 -0
- package/lib/utils/dep-versions.js +129 -0
- package/lib/utils/git.d.ts +14 -0
- package/lib/utils/git.js +64 -0
- package/lib/utils/hbcTransform.d.ts +111 -0
- package/lib/utils/hbcTransform.js +561 -0
- package/lib/utils/hermes-base.d.ts +227 -0
- package/lib/utils/hermes-base.js +1287 -0
- package/lib/utils/hermes-literals.d.ts +64 -0
- package/lib/utils/hermes-literals.js +260 -0
- package/lib/utils/http-helper.d.ts +10 -0
- package/lib/utils/http-helper.js +150 -0
- package/lib/utils/i18n.d.ts +22 -0
- package/lib/utils/i18n.js +46 -0
- package/lib/utils/index.d.ts +67 -0
- package/lib/utils/index.js +545 -0
- package/lib/utils/latest-version/index.d.ts +154 -0
- package/lib/utils/latest-version/index.js +331 -0
- package/lib/utils/options.d.ts +10 -0
- package/lib/utils/options.js +68 -0
- package/lib/utils/plugin-config.d.ts +14 -0
- package/lib/utils/plugin-config.js +66 -0
- package/lib/utils/runtime.d.ts +54 -0
- package/lib/utils/runtime.js +263 -0
- package/lib/utils/slim-sourcemap.d.ts +28 -0
- package/lib/utils/slim-sourcemap.js +106 -0
- package/lib/utils/zip-entries.d.ts +10 -0
- package/lib/utils/zip-entries.js +211 -0
- package/lib/utils/zip-options.d.ts +17 -0
- package/lib/utils/zip-options.js +190 -0
- package/lib/utils/zip-range.d.ts +148 -0
- package/lib/utils/zip-range.js +581 -0
- package/lib/utils/zip-raw-writer.d.ts +52 -0
- package/lib/utils/zip-raw-writer.js +209 -0
- package/lib/version-operations.d.ts +21 -0
- package/lib/version-operations.js +320 -0
- package/lib/versions.d.ts +66 -0
- package/lib/versions.js +402 -0
- package/package.json +113 -0
- package/proto/Configuration.proto +183 -0
- package/proto/Resources.proto +569 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* [INPUT]: 依赖 Node 子进程/文件流/HTTP Range、Pakta CLI 临时目录与 ZIP 读取原语
|
|
4
|
+
* [OUTPUT]: 对外提供 Hermes base 选择、缓存、二进制字面量/指令等价校验、下载、清理及发布元数据能力
|
|
5
|
+
* [POS]: CLI Hermes delta 优化边界,所有失败降级到普通编译,不阻塞发布流程
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.LITERAL_SEPARATOR = exports.DEFAULT_CACHE_MAX_FILES = exports.DEFAULT_CACHE_MAX_BYTES = exports.BUNDLE_ENTRY_NAMES = exports.MIN_HERMES_COMPILER_COMMITLY_DATE = exports.MIN_HERMES_COMPILER_BUILD = exports.HERMES_BASE_DETAIL_MAX_CHARS = exports.BundleHashMismatchError = exports.PermanentBaseError = void 0;
|
|
45
|
+
exports.classifyHermesCommand = classifyHermesCommand;
|
|
46
|
+
exports.sha256Hex = sha256Hex;
|
|
47
|
+
exports.sha256File = sha256File;
|
|
48
|
+
exports.readFileHbcVersion = readFileHbcVersion;
|
|
49
|
+
exports.probeHbcVersion = probeHbcVersion;
|
|
50
|
+
exports.bundleEntryMatcher = bundleEntryMatcher;
|
|
51
|
+
exports.extractBundleFromArchive = extractBundleFromArchive;
|
|
52
|
+
exports.cacheDir = cacheDir;
|
|
53
|
+
exports.tmpDir = tmpDir;
|
|
54
|
+
exports.cacheLimits = cacheLimits;
|
|
55
|
+
exports.cacheLookup = cacheLookup;
|
|
56
|
+
exports.cachePut = cachePut;
|
|
57
|
+
exports.enforceCacheLimits = enforceCacheLimits;
|
|
58
|
+
exports.cleanCache = cleanCache;
|
|
59
|
+
exports.cacheStats = cacheStats;
|
|
60
|
+
exports.cleanStaleTmp = cleanStaleTmp;
|
|
61
|
+
exports.downloadToFile = downloadToFile;
|
|
62
|
+
exports.fetchBaseBundle = fetchBaseBundle;
|
|
63
|
+
exports.resolveHermesBase = resolveHermesBase;
|
|
64
|
+
exports.hermescArgsWithBase = hermescArgsWithBase;
|
|
65
|
+
exports.hermesBaseMeta = hermesBaseMeta;
|
|
66
|
+
exports.truncateHermesBaseDetail = truncateHermesBaseDetail;
|
|
67
|
+
exports.normalizeDisassemblyLine = normalizeDisassemblyLine;
|
|
68
|
+
exports.compareHermesBytecode = compareHermesBytecode;
|
|
69
|
+
exports.verifyHermesBaseEquivalence = verifyHermesBaseEquivalence;
|
|
70
|
+
// Hermes `-base-bytecode` (delta optimizing mode) support.
|
|
71
|
+
//
|
|
72
|
+
// Hermes re-sorts its string table by identifier frequency on every compile, so
|
|
73
|
+
// any source change renumbers most string IDs and the bytecode diff explodes
|
|
74
|
+
// (see hermes-base-bytecode-方案.md). Compiling with the previous HBC of the
|
|
75
|
+
// same app as `-base-bytecode` keeps old IDs stable and shrinks patches 5-30×.
|
|
76
|
+
// This module resolves that base: gating by hermesc build, HBC version probe,
|
|
77
|
+
// server lookup, a sha256-named local cache, download + verification, and the
|
|
78
|
+
// optional disassembly equivalence check. Every failure degrades to the plain
|
|
79
|
+
// compile — nothing here may block a release.
|
|
80
|
+
const child_process_1 = require("child_process");
|
|
81
|
+
const crypto_1 = require("crypto");
|
|
82
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
83
|
+
const os_1 = __importDefault(require("os"));
|
|
84
|
+
const path_1 = __importDefault(require("path"));
|
|
85
|
+
const stream_1 = require("stream");
|
|
86
|
+
const promises_1 = require("stream/promises");
|
|
87
|
+
const constants_1 = require("./constants");
|
|
88
|
+
const hbcTransform_1 = require("./hbcTransform");
|
|
89
|
+
const hermes_literals_1 = require("./hermes-literals");
|
|
90
|
+
const i18n_1 = require("./i18n");
|
|
91
|
+
const runtime_1 = require("./runtime");
|
|
92
|
+
const zip_entries_1 = require("./zip-entries");
|
|
93
|
+
const zip_range_1 = require("./zip-range");
|
|
94
|
+
/**
|
|
95
|
+
* The base cannot be obtained whatever the transport or how often it is
|
|
96
|
+
* retried (wrong content on the server, no bundle in the package, ...).
|
|
97
|
+
*/
|
|
98
|
+
class PermanentBaseError extends Error {
|
|
99
|
+
}
|
|
100
|
+
exports.PermanentBaseError = PermanentBaseError;
|
|
101
|
+
/** The fetched bundle's sha256 differs from what the server recorded. */
|
|
102
|
+
class BundleHashMismatchError extends PermanentBaseError {
|
|
103
|
+
constructor(actual, expected) {
|
|
104
|
+
super(`bundleHash mismatch (${actual.slice(0, 12)} != ${expected.slice(0, 12)})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.BundleHashMismatchError = BundleHashMismatchError;
|
|
108
|
+
/** time allowed for a response to start (headers) */
|
|
109
|
+
const FETCH_HEADERS_TIMEOUT_MS = 30_000;
|
|
110
|
+
/** a download that receives nothing for this long is abandoned */
|
|
111
|
+
const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
112
|
+
/** cachePut leftovers (`<hash>.<pid>.tmp`) older than this are evicted */
|
|
113
|
+
const STALE_CACHE_TMP_MS = 60 * 60 * 1000;
|
|
114
|
+
/** the server column is VARCHAR(500); it truncates too, this just saves bytes */
|
|
115
|
+
exports.HERMES_BASE_DETAIL_MAX_CHARS = 500;
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// hermesc gating
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/**
|
|
120
|
+
* Hermes V1 (static_h) builds before 2025-07-11 carried a delta-mode bug when a
|
|
121
|
+
* delta-produced base is reused; every released `hermes-compiler` is newer, and
|
|
122
|
+
* classic hermesc (HBC ≤96, react-native/sdks or hermes-engine) upgrades kinds
|
|
123
|
+
* in place and was never affected. Anything else is refused.
|
|
124
|
+
*/
|
|
125
|
+
exports.MIN_HERMES_COMPILER_BUILD = 250829098;
|
|
126
|
+
exports.MIN_HERMES_COMPILER_COMMITLY_DATE = 20250901;
|
|
127
|
+
function classifyHermesCommand(hermesCommand) {
|
|
128
|
+
const normalized = hermesCommand.split(path_1.default.sep).join('/');
|
|
129
|
+
if (/\/react-native\/sdks\/hermesc\//.test(normalized) ||
|
|
130
|
+
/\/hermes-engine\//.test(normalized)) {
|
|
131
|
+
return { allowed: true, kind: 'classic' };
|
|
132
|
+
}
|
|
133
|
+
const match = /\/hermes-compiler\//.exec(normalized);
|
|
134
|
+
if (match) {
|
|
135
|
+
const packageRoot = normalized.slice(0, match.index + match[0].length);
|
|
136
|
+
let version = '';
|
|
137
|
+
try {
|
|
138
|
+
version = String(JSON.parse(fs_extra_1.default.readFileSync(path_1.default.join(packageRoot, 'package.json'), 'utf8')).version ?? '');
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {
|
|
142
|
+
allowed: false,
|
|
143
|
+
kind: 'hermes-compiler',
|
|
144
|
+
reason: 'unreadable hermes-compiler version',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const build = /^(\d{9})\./.exec(version);
|
|
148
|
+
if (build) {
|
|
149
|
+
const ok = Number(build[1]) >= exports.MIN_HERMES_COMPILER_BUILD;
|
|
150
|
+
return {
|
|
151
|
+
allowed: ok,
|
|
152
|
+
kind: 'hermes-compiler',
|
|
153
|
+
version,
|
|
154
|
+
reason: ok
|
|
155
|
+
? undefined
|
|
156
|
+
: `hermes-compiler ${version} predates the delta-mode fix`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const commitly = /^0\.(\d+)\.\d+-commitly-(\d{8})/.exec(version);
|
|
160
|
+
if (commitly) {
|
|
161
|
+
const ok = Number(commitly[1]) >= 14 &&
|
|
162
|
+
Number(commitly[2]) >= exports.MIN_HERMES_COMPILER_COMMITLY_DATE;
|
|
163
|
+
return {
|
|
164
|
+
allowed: ok,
|
|
165
|
+
kind: 'hermes-compiler',
|
|
166
|
+
version,
|
|
167
|
+
reason: ok
|
|
168
|
+
? undefined
|
|
169
|
+
: `hermes-compiler ${version} predates the delta-mode fix`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const stable = /^0\.(\d+)\.\d+$/.exec(version);
|
|
173
|
+
if (stable) {
|
|
174
|
+
const ok = Number(stable[1]) >= 14;
|
|
175
|
+
return {
|
|
176
|
+
allowed: ok,
|
|
177
|
+
kind: 'hermes-compiler',
|
|
178
|
+
version,
|
|
179
|
+
reason: ok
|
|
180
|
+
? undefined
|
|
181
|
+
: `hermes-compiler ${version} predates the delta-mode fix`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
allowed: false,
|
|
186
|
+
kind: 'hermes-compiler',
|
|
187
|
+
version,
|
|
188
|
+
reason: `unrecognized hermes-compiler version ${version}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
allowed: false,
|
|
193
|
+
kind: 'unknown',
|
|
194
|
+
reason: 'hermesc location not recognized',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// HBC helpers
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
function sha256Hex(data) {
|
|
201
|
+
return (0, crypto_1.createHash)('sha256').update(data).digest('hex');
|
|
202
|
+
}
|
|
203
|
+
/** sha256 of a file, streamed (a cached base can be tens of MB) */
|
|
204
|
+
async function sha256File(file) {
|
|
205
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
206
|
+
for await (const chunk of fs_extra_1.default.createReadStream(file))
|
|
207
|
+
hash.update(chunk);
|
|
208
|
+
return hash.digest('hex');
|
|
209
|
+
}
|
|
210
|
+
/** HBC version from the first bytes of a file, without reading the rest */
|
|
211
|
+
async function readFileHbcVersion(file) {
|
|
212
|
+
const header = Buffer.alloc(128);
|
|
213
|
+
const fd = await fs_extra_1.default.open(file, 'r');
|
|
214
|
+
try {
|
|
215
|
+
const { bytesRead } = await fs_extra_1.default.read(fd, header, 0, 128, 0);
|
|
216
|
+
return (0, hbcTransform_1.getHbcVersion)(header.subarray(0, bytesRead));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
await fs_extra_1.default.close(fd);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const PROBE_CACHE_FILE = 'hbc-versions.json';
|
|
226
|
+
const PROBE_CACHE_ENTRIES = 16;
|
|
227
|
+
/** identity of a hermesc binary for the probe cache: path + size + mtime */
|
|
228
|
+
function probeCacheKey(hermesCommand) {
|
|
229
|
+
try {
|
|
230
|
+
const stat = fs_extra_1.default.statSync(hermesCommand);
|
|
231
|
+
return `${path_1.default.resolve(hermesCommand)}|${stat.size}|${Math.floor(stat.mtimeMs)}`;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function readProbeCache() {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(fs_extra_1.default.readFileSync(path_1.default.join(cacheDir(), PROBE_CACHE_FILE), 'utf8'));
|
|
240
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return {};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function writeProbeCache(entries) {
|
|
247
|
+
try {
|
|
248
|
+
const keys = Object.keys(entries);
|
|
249
|
+
// keep the most recently inserted entries only
|
|
250
|
+
const trimmed = Object.fromEntries(keys.slice(-PROBE_CACHE_ENTRIES).map((key) => [key, entries[key]]));
|
|
251
|
+
fs_extra_1.default.ensureDirSync(cacheDir());
|
|
252
|
+
fs_extra_1.default.writeFileSync(path_1.default.join(cacheDir(), PROBE_CACHE_FILE), JSON.stringify(trimmed));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// the cache is a convenience; probing again next time is fine
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Which HBC version this hermesc emits. The answer is remembered per binary
|
|
260
|
+
* (path, size, mtime) in the cache dir so only the first run of a given
|
|
261
|
+
* hermesc pays for the empty-file compile.
|
|
262
|
+
*/
|
|
263
|
+
function probeHbcVersion(hermesCommand) {
|
|
264
|
+
const key = probeCacheKey(hermesCommand);
|
|
265
|
+
if (key) {
|
|
266
|
+
const cached = readProbeCache()[key];
|
|
267
|
+
if (Number.isInteger(cached) && cached > 0)
|
|
268
|
+
return cached;
|
|
269
|
+
}
|
|
270
|
+
const version = compileProbe(hermesCommand);
|
|
271
|
+
if (key && version !== null) {
|
|
272
|
+
const { [key]: _stale, ...others } = readProbeCache();
|
|
273
|
+
writeProbeCache({ ...others, [key]: version });
|
|
274
|
+
}
|
|
275
|
+
return version;
|
|
276
|
+
}
|
|
277
|
+
/** Compile an empty file to learn which HBC version this hermesc emits. */
|
|
278
|
+
function compileProbe(hermesCommand) {
|
|
279
|
+
const dir = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'rnu-hermes-probe-'));
|
|
280
|
+
try {
|
|
281
|
+
const input = path_1.default.join(dir, 'probe.js');
|
|
282
|
+
const output = path_1.default.join(dir, 'probe.hbc');
|
|
283
|
+
fs_extra_1.default.writeFileSync(input, '');
|
|
284
|
+
const result = (0, child_process_1.spawnSync)(hermesCommand, ['-emit-binary', '-out', output, input, '-O', '-w'], { stdio: 'ignore' });
|
|
285
|
+
if (result.status !== 0 || !fs_extra_1.default.existsSync(output)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const header = Buffer.alloc(128);
|
|
289
|
+
const fd = fs_extra_1.default.openSync(output, 'r');
|
|
290
|
+
try {
|
|
291
|
+
fs_extra_1.default.readSync(fd, header, 0, 128, 0);
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
fs_extra_1.default.closeSync(fd);
|
|
295
|
+
}
|
|
296
|
+
return (0, hbcTransform_1.getHbcVersion)(header);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
fs_extra_1.default.removeSync(dir);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.BUNDLE_ENTRY_NAMES = [
|
|
306
|
+
'index.bundlejs',
|
|
307
|
+
'main.jsbundle',
|
|
308
|
+
'bundle.harmony.js',
|
|
309
|
+
];
|
|
310
|
+
const APK_BUNDLE = /^assets\/index\.android\.bundle$/;
|
|
311
|
+
const IPA_BUNDLE = /^payload\/[^/]+\.app\/main\.jsbundle$/i;
|
|
312
|
+
/** Which zip entry holds the JS/HBC bundle in each kind of artifact. */
|
|
313
|
+
function bundleEntryMatcher(artifactType) {
|
|
314
|
+
switch (artifactType) {
|
|
315
|
+
case 'apk':
|
|
316
|
+
return (name) => APK_BUNDLE.test(name);
|
|
317
|
+
case 'ipa':
|
|
318
|
+
return (name) => IPA_BUNDLE.test(name.toLowerCase());
|
|
319
|
+
default:
|
|
320
|
+
return (name) => exports.BUNDLE_ENTRY_NAMES.includes(name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Extract the JS/HBC bundle from a local artifact: a raw bundle file, a ppk,
|
|
325
|
+
* or a native package (apk / ipa / harmony app). Returns null when not found.
|
|
326
|
+
*/
|
|
327
|
+
async function extractBundleFromArchive(filePath) {
|
|
328
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
329
|
+
if (ext === '.hbc' ||
|
|
330
|
+
ext === '.bundle' ||
|
|
331
|
+
ext === '.jsbundle' ||
|
|
332
|
+
ext === '.bundlejs' ||
|
|
333
|
+
ext === '.js') {
|
|
334
|
+
return fs_extra_1.default.readFile(filePath);
|
|
335
|
+
}
|
|
336
|
+
if (ext === '.ppk' || ext === '.apk' || ext === '.ipa') {
|
|
337
|
+
return readFirstZipEntry(filePath, bundleEntryMatcher(ext.slice(1)));
|
|
338
|
+
}
|
|
339
|
+
if (ext === '.app') {
|
|
340
|
+
// harmony .app wraps .hap archives; reuse the existing parser
|
|
341
|
+
const { default: AppInfoParser } = await Promise.resolve().then(() => __importStar(require('./app-info-parser')));
|
|
342
|
+
const parser = new AppInfoParser(filePath);
|
|
343
|
+
const [bundle] = await parser.parser.getEntriesFromHarmonyApp([
|
|
344
|
+
/^resources\/rawfile\/bundle\.harmony\.js$/,
|
|
345
|
+
]);
|
|
346
|
+
return bundle ?? null;
|
|
347
|
+
}
|
|
348
|
+
// unknown extension: try as zip, then as raw file
|
|
349
|
+
const fromZip = await readFirstZipEntry(filePath, (name) => exports.BUNDLE_ENTRY_NAMES.includes(name)).catch(() => null);
|
|
350
|
+
return fromZip ?? fs_extra_1.default.readFile(filePath);
|
|
351
|
+
}
|
|
352
|
+
async function readFirstZipEntry(zipPath, matches) {
|
|
353
|
+
let found = null;
|
|
354
|
+
await (0, zip_entries_1.enumZipEntries)(zipPath, async (entry, zipFile) => {
|
|
355
|
+
if (!found && matches(entry.fileName)) {
|
|
356
|
+
found = await (0, zip_entries_1.readEntry)(entry, zipFile);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return found;
|
|
360
|
+
}
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// cache (sha256-named bundles)
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
exports.DEFAULT_CACHE_MAX_BYTES = 500 * 1024 * 1024;
|
|
365
|
+
exports.DEFAULT_CACHE_MAX_FILES = 20;
|
|
366
|
+
function cacheDir() {
|
|
367
|
+
return process.env.PAKTA_CACHE_DIR || path_1.default.join(constants_1.tempDir, 'cache');
|
|
368
|
+
}
|
|
369
|
+
function tmpDir() {
|
|
370
|
+
return path_1.default.join(constants_1.tempDir, 'tmp');
|
|
371
|
+
}
|
|
372
|
+
function cacheLimits(overrideMaxMb) {
|
|
373
|
+
const envMb = Number(process.env.PAKTA_CACHE_MAX_MB);
|
|
374
|
+
const maxBytes = overrideMaxMb && overrideMaxMb > 0
|
|
375
|
+
? overrideMaxMb * 1024 * 1024
|
|
376
|
+
: Number.isFinite(envMb) && envMb > 0
|
|
377
|
+
? envMb * 1024 * 1024
|
|
378
|
+
: exports.DEFAULT_CACHE_MAX_BYTES;
|
|
379
|
+
return { maxBytes, maxFiles: exports.DEFAULT_CACHE_MAX_FILES };
|
|
380
|
+
}
|
|
381
|
+
/** Returns the cached bundle path when present and its sha256 still matches. */
|
|
382
|
+
async function cacheLookup(bundleHash) {
|
|
383
|
+
if (!/^[0-9a-f]{64}$/.test(bundleHash))
|
|
384
|
+
return null;
|
|
385
|
+
const file = path_1.default.join(cacheDir(), bundleHash);
|
|
386
|
+
if (!(await fs_extra_1.default.pathExists(file)))
|
|
387
|
+
return null;
|
|
388
|
+
if ((await sha256File(file).catch(() => '')) !== bundleHash) {
|
|
389
|
+
// corrupt entry: never trust cache content
|
|
390
|
+
await fs_extra_1.default.remove(file).catch(() => { });
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const now = new Date();
|
|
394
|
+
await fs_extra_1.default.utimes(file, now, now).catch(() => { });
|
|
395
|
+
return file;
|
|
396
|
+
}
|
|
397
|
+
/** Store a bundle under its sha256 (LRU-evicting to the configured limits). */
|
|
398
|
+
async function cachePut(bundle, maxMb, knownHash) {
|
|
399
|
+
const bundleHash = knownHash ?? sha256Hex(bundle);
|
|
400
|
+
const dir = cacheDir();
|
|
401
|
+
await fs_extra_1.default.ensureDir(dir);
|
|
402
|
+
const file = path_1.default.join(dir, bundleHash);
|
|
403
|
+
if (!(await fs_extra_1.default.pathExists(file))) {
|
|
404
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
405
|
+
await fs_extra_1.default.writeFile(tmp, bundle);
|
|
406
|
+
await fs_extra_1.default.rename(tmp, file);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const now = new Date();
|
|
410
|
+
await fs_extra_1.default.utimes(file, now, now).catch(() => { });
|
|
411
|
+
}
|
|
412
|
+
await enforceCacheLimits(maxMb, bundleHash);
|
|
413
|
+
return { path: file, bundleHash };
|
|
414
|
+
}
|
|
415
|
+
async function enforceCacheLimits(maxMb, keep) {
|
|
416
|
+
const dir = cacheDir();
|
|
417
|
+
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
418
|
+
return;
|
|
419
|
+
const { maxBytes, maxFiles } = cacheLimits(maxMb);
|
|
420
|
+
const entries = [];
|
|
421
|
+
const staleTmpCutoff = Date.now() - STALE_CACHE_TMP_MS;
|
|
422
|
+
for (const name of await fs_extra_1.default.readdir(dir)) {
|
|
423
|
+
const isTmp = isCacheTmpName(name);
|
|
424
|
+
if (!isTmp && !/^[0-9a-f]{64}$/.test(name))
|
|
425
|
+
continue;
|
|
426
|
+
const stat = await fs_extra_1.default.stat(path_1.default.join(dir, name)).catch(() => null);
|
|
427
|
+
if (!stat?.isFile())
|
|
428
|
+
continue;
|
|
429
|
+
if (isTmp) {
|
|
430
|
+
// an interrupted cachePut; nothing will ever rename it into place
|
|
431
|
+
if (stat.mtimeMs < staleTmpCutoff)
|
|
432
|
+
await fs_extra_1.default.remove(path_1.default.join(dir, name)).catch(() => { });
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
entries.push({ name, size: stat.size, mtime: stat.mtimeMs });
|
|
436
|
+
}
|
|
437
|
+
entries.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
438
|
+
let total = 0;
|
|
439
|
+
let count = 0;
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
total += entry.size;
|
|
442
|
+
count += 1;
|
|
443
|
+
if (entry.name !== keep && (total > maxBytes || count > maxFiles)) {
|
|
444
|
+
await fs_extra_1.default.remove(path_1.default.join(dir, entry.name)).catch(() => { });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** `<sha256>.<pid>.tmp`, the staging name cachePut writes before renaming */
|
|
449
|
+
function isCacheTmpName(name) {
|
|
450
|
+
return /^[0-9a-f]{64}\.\d+\.tmp$/.test(name);
|
|
451
|
+
}
|
|
452
|
+
/** Remove every cached bundle (and staging leftovers); returns the bundle count. */
|
|
453
|
+
async function cleanCache() {
|
|
454
|
+
const dir = cacheDir();
|
|
455
|
+
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
456
|
+
return 0;
|
|
457
|
+
let bundles = 0;
|
|
458
|
+
for (const name of await fs_extra_1.default.readdir(dir)) {
|
|
459
|
+
const isBundle = /^[0-9a-f]{64}$/.test(name);
|
|
460
|
+
if (!isBundle && !isCacheTmpName(name))
|
|
461
|
+
continue;
|
|
462
|
+
await fs_extra_1.default.remove(path_1.default.join(dir, name));
|
|
463
|
+
if (isBundle)
|
|
464
|
+
bundles += 1;
|
|
465
|
+
}
|
|
466
|
+
return bundles;
|
|
467
|
+
}
|
|
468
|
+
async function cacheStats() {
|
|
469
|
+
const dir = cacheDir();
|
|
470
|
+
let files = 0;
|
|
471
|
+
let bytes = 0;
|
|
472
|
+
if (await fs_extra_1.default.pathExists(dir)) {
|
|
473
|
+
for (const name of await fs_extra_1.default.readdir(dir)) {
|
|
474
|
+
if (!/^[0-9a-f]{64}$/.test(name))
|
|
475
|
+
continue;
|
|
476
|
+
const stat = await fs_extra_1.default.stat(path_1.default.join(dir, name)).catch(() => null);
|
|
477
|
+
if (stat?.isFile()) {
|
|
478
|
+
files += 1;
|
|
479
|
+
bytes += stat.size;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { dir, files, bytes };
|
|
484
|
+
}
|
|
485
|
+
/** Remove leftovers of interrupted runs (older than 24h) from the tmp dir. */
|
|
486
|
+
async function cleanStaleTmp(maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
487
|
+
const dir = tmpDir();
|
|
488
|
+
if (!(await fs_extra_1.default.pathExists(dir)))
|
|
489
|
+
return;
|
|
490
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
491
|
+
for (const name of await fs_extra_1.default.readdir(dir)) {
|
|
492
|
+
const file = path_1.default.join(dir, name);
|
|
493
|
+
const stat = await fs_extra_1.default.stat(file).catch(() => null);
|
|
494
|
+
if (stat && stat.mtimeMs < cutoff)
|
|
495
|
+
await fs_extra_1.default.remove(file).catch(() => { });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// download
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
async function downloadToFile(url, destination) {
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
const headersTimer = setTimeout(() => controller.abort(new Error('timeout waiting for response')), FETCH_HEADERS_TIMEOUT_MS);
|
|
504
|
+
let response;
|
|
505
|
+
try {
|
|
506
|
+
response = await (0, runtime_1.webFetch)(url, { signal: controller.signal });
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
clearTimeout(headersTimer);
|
|
510
|
+
}
|
|
511
|
+
if (!response.ok || !response.body) {
|
|
512
|
+
throw new Error(`download failed: HTTP ${response.status} ${url}`);
|
|
513
|
+
}
|
|
514
|
+
await saveResponse(response, destination, controller);
|
|
515
|
+
}
|
|
516
|
+
function hasBundleLocation(record) {
|
|
517
|
+
return (record.bundleOffset != null &&
|
|
518
|
+
record.bundleCompressedSize != null &&
|
|
519
|
+
record.bundleCompression != null &&
|
|
520
|
+
(record.bundleCompression === zip_range_1.ZIP_STORED ||
|
|
521
|
+
record.bundleCompression === zip_range_1.ZIP_DEFLATED));
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Stream a response body to disk, giving up when no bytes arrive for
|
|
525
|
+
* STREAM_IDLE_TIMEOUT_MS. `controller` is the one the request was made with
|
|
526
|
+
* (aborting it also tears down the socket); a foreign response gets its own.
|
|
527
|
+
*/
|
|
528
|
+
async function saveResponse(response, destination, controller = new AbortController()) {
|
|
529
|
+
if (!response.body)
|
|
530
|
+
throw new Error('empty response body');
|
|
531
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(destination));
|
|
532
|
+
const abort = () => controller.abort(new Error(`download stalled for ${STREAM_IDLE_TIMEOUT_MS / 1000}s`));
|
|
533
|
+
let idle = setTimeout(abort, STREAM_IDLE_TIMEOUT_MS);
|
|
534
|
+
const watchdog = new stream_1.PassThrough({
|
|
535
|
+
transform(chunk, _encoding, callback) {
|
|
536
|
+
clearTimeout(idle);
|
|
537
|
+
idle = setTimeout(abort, STREAM_IDLE_TIMEOUT_MS);
|
|
538
|
+
callback(null, chunk);
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
try {
|
|
542
|
+
await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(response.body), watchdog, fs_extra_1.default.createWriteStream(destination), { signal: controller.signal });
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
clearTimeout(idle);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Obtain the base bundle with as little traffic as possible, in order:
|
|
550
|
+
* 1. one Range request for the bundle's compressed bytes when the server
|
|
551
|
+
* knows their location (ppk / apk / ipa published by a CLI that
|
|
552
|
+
* reported it);
|
|
553
|
+
* 2. the archive's central directory over Range, then just the bundle
|
|
554
|
+
* entry (any zip: ppk, apk, ipa — harmony .app nests a second zip and
|
|
555
|
+
* is skipped);
|
|
556
|
+
* 3. the whole archive, as before.
|
|
557
|
+
* A server that ignores Range (HTTP 200) streams the full archive into
|
|
558
|
+
* `archive` without a second request. Every result is verified against
|
|
559
|
+
* `record.bundleHash`. A mismatch of the located bytes (1) may only mean the
|
|
560
|
+
* recorded location is stale, so the directory transport (2) still runs; a
|
|
561
|
+
* mismatch of the entry itself (2, 3) is final — the same bytes would come
|
|
562
|
+
* back however they are fetched — and raises BundleHashMismatchError.
|
|
563
|
+
*/
|
|
564
|
+
async function fetchBaseBundle(record, artifactType, archive, log = () => { }) {
|
|
565
|
+
const matches = bundleEntryMatcher(artifactType);
|
|
566
|
+
const verified = (bundle) => {
|
|
567
|
+
if (!bundle)
|
|
568
|
+
throw new PermanentBaseError('bundle entry not found');
|
|
569
|
+
const bundleHash = sha256Hex(bundle);
|
|
570
|
+
if (record.bundleHash && bundleHash !== record.bundleHash) {
|
|
571
|
+
throw new BundleHashMismatchError(bundleHash, record.bundleHash);
|
|
572
|
+
}
|
|
573
|
+
return { bundle, bundleHash };
|
|
574
|
+
};
|
|
575
|
+
const fromFullResponse = async (response) => {
|
|
576
|
+
await saveResponse(response, archive);
|
|
577
|
+
return {
|
|
578
|
+
...verified(await extractBundleFromArchive(archive)),
|
|
579
|
+
transport: 'full',
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
const skip = (reason) => log((0, i18n_1.t)('hermesBaseRangeFallback', { reason }));
|
|
583
|
+
// harmony .app nests the bundle in a second zip, so a location inside the
|
|
584
|
+
// outer archive is never reported for it
|
|
585
|
+
if (artifactType !== 'app' && hasBundleLocation(record)) {
|
|
586
|
+
try {
|
|
587
|
+
const { data, fetchedBytes, totalBytes } = await (0, zip_range_1.fetchZipEntryData)(record.url, {
|
|
588
|
+
dataOffset: record.bundleOffset,
|
|
589
|
+
compressedSize: record.bundleCompressedSize,
|
|
590
|
+
compressionMethod: record.bundleCompression,
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
...verified(data),
|
|
594
|
+
transport: 'range-entry',
|
|
595
|
+
fetchedBytes,
|
|
596
|
+
totalBytes,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
if (error instanceof zip_range_1.RangeUnsupportedError) {
|
|
601
|
+
skip('server ignores Range');
|
|
602
|
+
return fromFullResponse(error.response);
|
|
603
|
+
}
|
|
604
|
+
skip(`entry range: ${error?.message ?? error}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (artifactType !== 'app') {
|
|
608
|
+
try {
|
|
609
|
+
const remote = await (0, zip_range_1.openRemoteZip)(record.url);
|
|
610
|
+
if (remote.kind === 'full') {
|
|
611
|
+
skip('server ignores Range');
|
|
612
|
+
return fromFullResponse(remote.response);
|
|
613
|
+
}
|
|
614
|
+
const bundle = await (0, zip_range_1.readRemoteZipEntry)(remote.zipFile, matches, remote.reader);
|
|
615
|
+
return {
|
|
616
|
+
...verified(bundle),
|
|
617
|
+
transport: 'range-zip',
|
|
618
|
+
fetchedBytes: remote.reader.fetchedBytes,
|
|
619
|
+
totalBytes: remote.reader.totalSize,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (error instanceof zip_range_1.RangeUnsupportedError) {
|
|
624
|
+
skip('server ignores Range');
|
|
625
|
+
return fromFullResponse(error.response);
|
|
626
|
+
}
|
|
627
|
+
// the real entry was read and is not the recorded one: final
|
|
628
|
+
if (error instanceof PermanentBaseError)
|
|
629
|
+
throw error;
|
|
630
|
+
skip(`zip range: ${error?.message ?? error}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
await downloadToFile(record.url, archive);
|
|
634
|
+
return {
|
|
635
|
+
...verified(await extractBundleFromArchive(archive)),
|
|
636
|
+
transport: 'full',
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Pick and materialize the base bundle for this compile. Returns null (with
|
|
641
|
+
* the reason logged) whenever a base cannot be used; callers then compile
|
|
642
|
+
* without one.
|
|
643
|
+
*/
|
|
644
|
+
async function resolveHermesBase(params) {
|
|
645
|
+
const log = params.log ?? (() => { });
|
|
646
|
+
const { option, bytecodeVersion } = params;
|
|
647
|
+
if (option === 'none') {
|
|
648
|
+
log((0, i18n_1.t)('hermesBaseNone', { reason: 'disabled by option' }));
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
if (option !== 'auto') {
|
|
652
|
+
// explicit local artifact
|
|
653
|
+
try {
|
|
654
|
+
const bundle = await extractBundleFromArchive(option);
|
|
655
|
+
if (!bundle)
|
|
656
|
+
throw new Error('bundle entry not found');
|
|
657
|
+
const version = (0, hbcTransform_1.getHbcVersion)(bundle);
|
|
658
|
+
if (version !== bytecodeVersion) {
|
|
659
|
+
throw new Error(`HBC version ${version ?? 'n/a'} != ${bytecodeVersion}`);
|
|
660
|
+
}
|
|
661
|
+
const cached = await cachePut(bundle, params.cacheMaxMb);
|
|
662
|
+
log((0, i18n_1.t)('hermesBaseUsing', { source: option, version: bytecodeVersion }));
|
|
663
|
+
return {
|
|
664
|
+
path: cached.path,
|
|
665
|
+
bytecodeVersion,
|
|
666
|
+
bundleHash: cached.bundleHash,
|
|
667
|
+
source: 'local',
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
672
|
+
reason: `${option}: ${error?.message ?? error}`,
|
|
673
|
+
}));
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!params.appId) {
|
|
678
|
+
log((0, i18n_1.t)('hermesBaseNone', { reason: 'app not selected' }));
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
let record = null;
|
|
682
|
+
try {
|
|
683
|
+
record = await retryOnce(() => params.fetchBase(params.appId, bytecodeVersion));
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
687
|
+
reason: `server lookup failed: ${error?.message ?? error}`,
|
|
688
|
+
}));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const source = record?.artifactType && record.artifactType !== 'ppk'
|
|
692
|
+
? 'native-package'
|
|
693
|
+
: record?.bytecodeVersion == null
|
|
694
|
+
? 'latest-version'
|
|
695
|
+
: 'download';
|
|
696
|
+
if (!record) {
|
|
697
|
+
log((0, i18n_1.t)('hermesBaseNone', { reason: 'no published version yet' }));
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (record.bytecodeVersion != null &&
|
|
701
|
+
record.bytecodeVersion !== bytecodeVersion) {
|
|
702
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
703
|
+
reason: `server base is HBC ${record.bytecodeVersion}, need ${bytecodeVersion}`,
|
|
704
|
+
}));
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
// 1. cache by bundleHash
|
|
708
|
+
if (record.bundleHash) {
|
|
709
|
+
const hit = await cacheLookup(record.bundleHash);
|
|
710
|
+
if (hit && record.bytecodeVersion == null) {
|
|
711
|
+
// the server does not know the HBC version (legacy version, native
|
|
712
|
+
// package): the download path checks it, the cache path must too
|
|
713
|
+
const version = await readFileHbcVersion(hit);
|
|
714
|
+
if (version !== bytecodeVersion) {
|
|
715
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
716
|
+
reason: `cached base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`,
|
|
717
|
+
}));
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (hit) {
|
|
722
|
+
log((0, i18n_1.t)('hermesBaseUsing', {
|
|
723
|
+
source: `cache ${record.bundleHash.slice(0, 12)}${record.versionId == null ? '' : ` (version ${record.versionId})`}`,
|
|
724
|
+
version: bytecodeVersion,
|
|
725
|
+
}));
|
|
726
|
+
return {
|
|
727
|
+
path: hit,
|
|
728
|
+
bytecodeVersion,
|
|
729
|
+
bundleHash: record.bundleHash,
|
|
730
|
+
versionId: record.versionId ?? undefined,
|
|
731
|
+
hash: record.hash,
|
|
732
|
+
source: 'cache',
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// 2. download, extract, verify, cache — transport errors get a second try
|
|
737
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
738
|
+
const dir = tmpDir();
|
|
739
|
+
await fs_extra_1.default.ensureDir(dir);
|
|
740
|
+
const artifactType = [
|
|
741
|
+
'ppk',
|
|
742
|
+
'apk',
|
|
743
|
+
'ipa',
|
|
744
|
+
'app',
|
|
745
|
+
].includes(record.artifactType ?? '')
|
|
746
|
+
? record.artifactType
|
|
747
|
+
: 'ppk';
|
|
748
|
+
const archive = path_1.default.join(dir, `base-${process.pid}-${Date.now()}-${attempt}.${artifactType}`);
|
|
749
|
+
try {
|
|
750
|
+
log((0, i18n_1.t)('hermesBaseDownloading', { url: record.url }));
|
|
751
|
+
const fetched = await fetchBaseBundle(record, artifactType, archive, log);
|
|
752
|
+
const { bundle, bundleHash: actualHash } = fetched;
|
|
753
|
+
if (fetched.transport !== 'full') {
|
|
754
|
+
log((0, i18n_1.t)('hermesBaseRangeFetched', {
|
|
755
|
+
fetchedKb: Math.ceil((fetched.fetchedBytes ?? 0) / 1024),
|
|
756
|
+
totalKb: Math.ceil((fetched.totalBytes ?? 0) / 1024),
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
const version = (0, hbcTransform_1.getHbcVersion)(bundle);
|
|
760
|
+
if (version !== bytecodeVersion) {
|
|
761
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
762
|
+
reason: `downloaded base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`,
|
|
763
|
+
}));
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const cached = await cachePut(bundle, params.cacheMaxMb, actualHash);
|
|
767
|
+
log((0, i18n_1.t)('hermesBaseUsing', {
|
|
768
|
+
source: record.versionId == null
|
|
769
|
+
? `native package ${record.hash.slice(0, 8)}`
|
|
770
|
+
: `version ${record.versionId} ${record.hash.slice(0, 8)}`,
|
|
771
|
+
version: bytecodeVersion,
|
|
772
|
+
}));
|
|
773
|
+
return {
|
|
774
|
+
path: cached.path,
|
|
775
|
+
bytecodeVersion,
|
|
776
|
+
bundleHash: cached.bundleHash,
|
|
777
|
+
versionId: record.versionId ?? undefined,
|
|
778
|
+
hash: record.hash,
|
|
779
|
+
source,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
if (attempt === 2 || error instanceof PermanentBaseError) {
|
|
784
|
+
log((0, i18n_1.t)('hermesBaseNone', {
|
|
785
|
+
reason: `download/verify failed: ${error?.message ?? error}`,
|
|
786
|
+
}));
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
await fs_extra_1.default.remove(archive).catch(() => { });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
/** one retry after a short pause, for transient network failures */
|
|
797
|
+
async function retryOnce(action) {
|
|
798
|
+
try {
|
|
799
|
+
return await action();
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
803
|
+
return action();
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function hermescArgsWithBase(baseArgs, basePath) {
|
|
807
|
+
return basePath ? [...baseArgs, `-base-bytecode=${basePath}`] : baseArgs;
|
|
808
|
+
}
|
|
809
|
+
function hermesBaseMeta(selection, bytecodeVersion, check) {
|
|
810
|
+
const meta = {
|
|
811
|
+
bytecodeVersion,
|
|
812
|
+
baseVersionId: selection?.versionId ?? null,
|
|
813
|
+
baseHash: selection?.hash ?? null,
|
|
814
|
+
};
|
|
815
|
+
if (check) {
|
|
816
|
+
meta.hermesBaseOutcome = check.outcome;
|
|
817
|
+
const detail = truncateHermesBaseDetail(check.detail);
|
|
818
|
+
if (detail)
|
|
819
|
+
meta.hermesBaseDetail = detail;
|
|
820
|
+
}
|
|
821
|
+
return meta;
|
|
822
|
+
}
|
|
823
|
+
/** one line, at most HERMES_BASE_DETAIL_MAX_CHARS code points; '' when empty */
|
|
824
|
+
function truncateHermesBaseDetail(detail) {
|
|
825
|
+
const compact = (detail ?? '').replace(/\s+/g, ' ').trim();
|
|
826
|
+
const chars = Array.from(compact);
|
|
827
|
+
return chars.length <= exports.HERMES_BASE_DETAIL_MAX_CHARS
|
|
828
|
+
? compact
|
|
829
|
+
: chars.slice(0, exports.HERMES_BASE_DETAIL_MAX_CHARS).join('');
|
|
830
|
+
}
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// equivalence check (streams two `-dump-bytecode` outputs, never writes them)
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
const STRING_TABLE_LINE = /^\s*[is](\d+)\[[^\]]*\](?: #[0-9A-F]+)?: (.*)$/;
|
|
835
|
+
/**
|
|
836
|
+
* Normalize one disassembly line so that a delta-mode compile and a plain
|
|
837
|
+
* compile of the same source compare equal: string IDs are renumbered, literal
|
|
838
|
+
* buffers are laid out differently (offsets, short/long variants), and jump
|
|
839
|
+
* distances follow instruction widths. Everything else must match exactly.
|
|
840
|
+
*/
|
|
841
|
+
/** separates rendered literal entries inside one normalized line */
|
|
842
|
+
exports.LITERAL_SEPARATOR = '\u001f';
|
|
843
|
+
/**
|
|
844
|
+
* `NewArrayWithBuffer rX, sizeHint, count, offset` and
|
|
845
|
+
* `NewObjectWithBuffer rX, sizeHint, count, keyOffset, valueOffset` (HBC
|
|
846
|
+
* v87–96) or `NewObjectWithBuffer[AndParent] rX, [rP,] shapeIndex,
|
|
847
|
+
* valueOffset` (v98) with the literals decoded from the binary buffers at
|
|
848
|
+
* those offsets, so two builds that lay their buffers out differently still
|
|
849
|
+
* compare by what each instruction builds. The shape index is dropped like an
|
|
850
|
+
* offset: it only locates the keys. Null when the operands do not fit the
|
|
851
|
+
* layout; an offset that cannot be decoded is spelled out (and so never
|
|
852
|
+
* equals a decoded one).
|
|
853
|
+
*/
|
|
854
|
+
function renderBufferInstruction(opcode, operands, literals) {
|
|
855
|
+
const [sizeHint, count] = operands;
|
|
856
|
+
if (opcode.startsWith('NewArray')) {
|
|
857
|
+
if (operands.length !== 3)
|
|
858
|
+
return null;
|
|
859
|
+
const entries = literals.array(operands[2], count);
|
|
860
|
+
return `size=${sizeHint} n=${count} [${entries ? entries.join(exports.LITERAL_SEPARATOR) : `<undecodable@${operands[2]}>`}]`;
|
|
861
|
+
}
|
|
862
|
+
if (literals.layout === 'shaped') {
|
|
863
|
+
if (operands.length !== 2)
|
|
864
|
+
return null;
|
|
865
|
+
const [shapeIndex, valueOffset] = operands;
|
|
866
|
+
const shape = literals.shape(shapeIndex);
|
|
867
|
+
const keys = shape && literals.objectKeys(shape.keyOffset, shape.count);
|
|
868
|
+
const values = shape && literals.objectValues(valueOffset, shape.count);
|
|
869
|
+
if (!shape || !keys || !values) {
|
|
870
|
+
return `n=${shape?.count ?? '?'} {<undecodable@shape${shapeIndex}/${valueOffset}>}`;
|
|
871
|
+
}
|
|
872
|
+
const pairs = keys.map((k, i) => `${k}: ${values[i]}`);
|
|
873
|
+
return `n=${shape.count} {${pairs.join(exports.LITERAL_SEPARATOR)}}`;
|
|
874
|
+
}
|
|
875
|
+
if (operands.length !== 4)
|
|
876
|
+
return null;
|
|
877
|
+
const keys = literals.objectKeys(operands[2], count);
|
|
878
|
+
const values = literals.objectValues(operands[3], count);
|
|
879
|
+
if (!keys || !values) {
|
|
880
|
+
return `size=${sizeHint} n=${count} {<undecodable@${operands[2]}/${operands[3]}>}`;
|
|
881
|
+
}
|
|
882
|
+
const pairs = keys.map((k, i) => `${k}: ${values[i]}`);
|
|
883
|
+
return `size=${sizeHint} n=${count} {${pairs.join(exports.LITERAL_SEPARATOR)}}`;
|
|
884
|
+
}
|
|
885
|
+
function normalizeDisassemblyLine(line, strings, literals) {
|
|
886
|
+
// Cheap dispatch on the opcode before touching any regex: dumps run to
|
|
887
|
+
// millions of lines and only a handful of instructions need rewriting.
|
|
888
|
+
let indent = 0;
|
|
889
|
+
while (indent < line.length && isSpace(line.charCodeAt(indent)))
|
|
890
|
+
indent++;
|
|
891
|
+
let opEnd = indent;
|
|
892
|
+
while (opEnd < line.length && isLetter(line.charCodeAt(opEnd)))
|
|
893
|
+
opEnd++;
|
|
894
|
+
const opcode = line.slice(indent, opEnd);
|
|
895
|
+
if (opcode === 'Offset' && line.startsWith('Offset in debug table', indent)) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
let m;
|
|
899
|
+
if (opcode.startsWith('New') && opcode.includes('WithBuffer')) {
|
|
900
|
+
// v98's AndParent form takes the parent object in a second register,
|
|
901
|
+
// which is compared as is (it is not a buffer operand)
|
|
902
|
+
m =
|
|
903
|
+
/^(\s*New(?:Array|Object)WithBuffer)(?:Long)?(AndParent)?\s+(r\d+)(?:, (r\d+))?(.*)$/.exec(line);
|
|
904
|
+
if (m) {
|
|
905
|
+
const op = `${m[1]}${m[2] ?? ''}`;
|
|
906
|
+
const regs = m[4] ? `${m[3]} ${m[4]}` : m[3];
|
|
907
|
+
const nums = m[5].match(/\d+/g) ?? [];
|
|
908
|
+
if (literals) {
|
|
909
|
+
const rendered = renderBufferInstruction(m[1].trim(), nums.map(Number), literals);
|
|
910
|
+
if (rendered)
|
|
911
|
+
return `${op} ${regs} ${rendered}`;
|
|
912
|
+
}
|
|
913
|
+
// no binary buffers: only the size hint survives; the buffer content
|
|
914
|
+
// is compared as a whole instead (compareBuffers)
|
|
915
|
+
return `${op} ${regs} sizes=${nums.slice(0, 1).join(',')}`;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (opcode.charCodeAt(0) === 0x4a /* J */) {
|
|
919
|
+
m = /^(\s*J[A-Za-z]+?)(Long)?\s+(L\d+|\d+)(.*)$/.exec(line);
|
|
920
|
+
if (m)
|
|
921
|
+
return `${m[1]} <tgt>${m[4]}`;
|
|
922
|
+
}
|
|
923
|
+
if (opcode.startsWith('DefineOwnById')) {
|
|
924
|
+
m = /^(\s*DefineOwnById\w*\s+r\d+, r\d+, \d+, )(\d+)$/.exec(line);
|
|
925
|
+
if (m)
|
|
926
|
+
line = `${m[1]}"${strings.get(Number(m[2])) ?? `?${m[2]}`}"`;
|
|
927
|
+
}
|
|
928
|
+
// Operand-width variants of one instruction (GetByIdShort/GetById/GetByIdLong,
|
|
929
|
+
// LoadConstString/LoadConstStringLongIndex, ...) only differ by how wide a
|
|
930
|
+
// string/function id or offset is encoded — a foreign base hands the new
|
|
931
|
+
// code's hot strings large ids, so the delta build legitimately picks the
|
|
932
|
+
// wider form. Fold the suffix and the column padding that follows it.
|
|
933
|
+
if (opEnd > indent &&
|
|
934
|
+
(opEnd === line.length || isSpace(line.charCodeAt(opEnd)))) {
|
|
935
|
+
const folded = foldWidthSuffix(opcode);
|
|
936
|
+
line = `${line.slice(0, indent)}${folded}${line.slice(opEnd).replace(/\s+/g, ' ')}`;
|
|
937
|
+
// Switch jump tables sit after the instructions; their relative offset (and
|
|
938
|
+
// the table header hermesc prints for them) moves with instruction widths.
|
|
939
|
+
// The two switch instructions carry that offset in different operands:
|
|
940
|
+
// StringSwitchImm rX, <id>, <jtOffset>, <defaultLabel>, <count>
|
|
941
|
+
// UIntSwitchImm rX, <jtOffset>, <defaultLabel>, <min>, <max>
|
|
942
|
+
// Folding only the first shape let a shifted UIntSwitchImm offset read as a
|
|
943
|
+
// real difference and drop an otherwise good delta build.
|
|
944
|
+
if (folded === 'StringSwitchImm') {
|
|
945
|
+
m = /^(\s*StringSwitchImm r\d+, \d+, )\d+(, L\d+, \d+)$/.exec(line);
|
|
946
|
+
if (m)
|
|
947
|
+
line = `${m[1]}<jt>${m[2]}`;
|
|
948
|
+
}
|
|
949
|
+
else if (folded === 'UIntSwitchImm') {
|
|
950
|
+
m = /^(\s*UIntSwitchImm r\d+, )\d+(, L\d+, \d+, \d+)$/.exec(line);
|
|
951
|
+
if (m)
|
|
952
|
+
line = `${m[1]}<jt>${m[2]}`;
|
|
953
|
+
}
|
|
954
|
+
else if (folded === 'offset' && /^\s*offset \d+$/.test(line)) {
|
|
955
|
+
line = line.replace(/\d+$/, '<jt>');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return line;
|
|
959
|
+
}
|
|
960
|
+
function isSpace(code) {
|
|
961
|
+
return code === 0x20 || code === 0x09 || code === 0x0d;
|
|
962
|
+
}
|
|
963
|
+
function isLetter(code) {
|
|
964
|
+
return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
|
|
965
|
+
}
|
|
966
|
+
/** strip one operand-width suffix (LongIndex, Long, Short) off an opcode */
|
|
967
|
+
function foldWidthSuffix(opcode) {
|
|
968
|
+
for (const suffix of ['LongIndex', 'Long', 'Short']) {
|
|
969
|
+
if (opcode.length > suffix.length && opcode.endsWith(suffix)) {
|
|
970
|
+
return opcode.slice(0, -suffix.length);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return opcode;
|
|
974
|
+
}
|
|
975
|
+
/** Split a readable stream into lines without readline (Bun's readline async iterator misbehaves when pulled manually). */
|
|
976
|
+
async function* streamLines(stream) {
|
|
977
|
+
let rest = '';
|
|
978
|
+
for await (const chunk of stream) {
|
|
979
|
+
rest += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
980
|
+
let index = rest.indexOf('\n');
|
|
981
|
+
while (index >= 0) {
|
|
982
|
+
yield rest.slice(0, index);
|
|
983
|
+
rest = rest.slice(index + 1);
|
|
984
|
+
index = rest.indexOf('\n');
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (rest)
|
|
988
|
+
yield rest;
|
|
989
|
+
}
|
|
990
|
+
/** `Array Buffer:`, `Object Key Buffer:`, `Debug filename table:`, ... */
|
|
991
|
+
const SECTION_HEADER = /^([A-Z][A-Za-z ]*):$/;
|
|
992
|
+
/** literal buffer entry that refers to the string table by id */
|
|
993
|
+
const BUFFER_STRING_ENTRY = /^\[String (\d+)\]$/;
|
|
994
|
+
/** sections after the functions that only serve debuggers and profilers */
|
|
995
|
+
const TRAILER_SECTION = /^(?:Debug |Textified callees table)/;
|
|
996
|
+
/** stderr kept from a dump process */
|
|
997
|
+
const DUMP_STDERR_KEEP = 4 * 1024;
|
|
998
|
+
/** a diagnostic line is cut to this many characters */
|
|
999
|
+
const DETAIL_LINE_MAX = 120;
|
|
1000
|
+
function isFunctionHeader(line) {
|
|
1001
|
+
return line.startsWith('Function<') || line.startsWith('NCFunction<');
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Structured reader over one `hermesc -dump-bytecode -pretty-disassemble`
|
|
1005
|
+
* stream. Everything before the functions is either the string table (kept
|
|
1006
|
+
* to resolve ids), a literal buffer (kept, with string ids resolved to text,
|
|
1007
|
+
* so buffer *content* is compared) or ignored; the functions come out one at
|
|
1008
|
+
* a time, normalized; the debug tables after them are drained and ignored.
|
|
1009
|
+
*/
|
|
1010
|
+
class DumpReader {
|
|
1011
|
+
proc;
|
|
1012
|
+
iterator;
|
|
1013
|
+
strings = new Map();
|
|
1014
|
+
/** literal buffers by section name, entries with string ids resolved */
|
|
1015
|
+
buffers = new Map();
|
|
1016
|
+
exit;
|
|
1017
|
+
section = null;
|
|
1018
|
+
pending = null;
|
|
1019
|
+
inFunctions = false;
|
|
1020
|
+
finished = false;
|
|
1021
|
+
/** set when the binary literal buffers of this side could be read */
|
|
1022
|
+
literals;
|
|
1023
|
+
constructor(proc, dumpTo, buffers) {
|
|
1024
|
+
this.proc = proc;
|
|
1025
|
+
// the string table is filled while the preamble streams by, before the
|
|
1026
|
+
// first instruction needs it
|
|
1027
|
+
if (buffers)
|
|
1028
|
+
this.literals = new hermes_literals_1.LiteralResolver(buffers, this.strings);
|
|
1029
|
+
const pass = new stream_1.PassThrough();
|
|
1030
|
+
proc.stdout.pipe(pass);
|
|
1031
|
+
if (dumpTo)
|
|
1032
|
+
proc.stdout.pipe(fs_extra_1.default.createWriteStream(dumpTo));
|
|
1033
|
+
this.iterator = streamLines(pass);
|
|
1034
|
+
let stderr = '';
|
|
1035
|
+
proc.stderr?.on('data', (chunk) => {
|
|
1036
|
+
stderr = (stderr + chunk.toString()).slice(-DUMP_STDERR_KEEP);
|
|
1037
|
+
});
|
|
1038
|
+
this.exit = new Promise((resolve) => {
|
|
1039
|
+
// a spawn failure (ENOENT) may leave stdout open and never 'close'
|
|
1040
|
+
proc.on('error', (error) => {
|
|
1041
|
+
pass.end();
|
|
1042
|
+
resolve({ code: null, signal: null, error, stderr });
|
|
1043
|
+
});
|
|
1044
|
+
proc.on('close', (code, signal) => resolve({ code, signal, stderr }));
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
async nextLine() {
|
|
1048
|
+
if (this.pending !== null) {
|
|
1049
|
+
const line = this.pending;
|
|
1050
|
+
this.pending = null;
|
|
1051
|
+
return line;
|
|
1052
|
+
}
|
|
1053
|
+
if (this.finished)
|
|
1054
|
+
return null;
|
|
1055
|
+
const { value, done } = await this.iterator.next();
|
|
1056
|
+
if (done) {
|
|
1057
|
+
this.finished = true;
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return value;
|
|
1061
|
+
}
|
|
1062
|
+
/** consume the preamble up to (and returning) the first function header */
|
|
1063
|
+
async skipPreamble() {
|
|
1064
|
+
for (;;) {
|
|
1065
|
+
const line = await this.nextLine();
|
|
1066
|
+
if (line === null)
|
|
1067
|
+
return null;
|
|
1068
|
+
if (isFunctionHeader(line)) {
|
|
1069
|
+
this.inFunctions = true;
|
|
1070
|
+
return line;
|
|
1071
|
+
}
|
|
1072
|
+
const header = SECTION_HEADER.exec(line);
|
|
1073
|
+
if (header) {
|
|
1074
|
+
this.section = header[1];
|
|
1075
|
+
if (this.section.endsWith('Buffer'))
|
|
1076
|
+
this.buffers.set(this.section, []);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (this.section === 'Global String Table') {
|
|
1080
|
+
const m = STRING_TABLE_LINE.exec(line);
|
|
1081
|
+
if (m)
|
|
1082
|
+
this.strings.set(Number(m[1]), m[2]);
|
|
1083
|
+
}
|
|
1084
|
+
else if (this.section?.endsWith('Buffer') && line.trim() !== '') {
|
|
1085
|
+
const entry = line.trim();
|
|
1086
|
+
const ref = BUFFER_STRING_ENTRY.exec(entry);
|
|
1087
|
+
const text = ref ? this.strings.get(Number(ref[1])) : undefined;
|
|
1088
|
+
this.buffers
|
|
1089
|
+
.get(this.section)
|
|
1090
|
+
.push(ref
|
|
1091
|
+
? `[String ${text === undefined ? `?${ref[1]}` : JSON.stringify(text)}]`
|
|
1092
|
+
: entry);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/** next function (header + normalized body), or null after the last one */
|
|
1097
|
+
async nextFunction() {
|
|
1098
|
+
const header = this.inFunctions
|
|
1099
|
+
? await this.nextLine()
|
|
1100
|
+
: await this.skipPreamble();
|
|
1101
|
+
if (header === null)
|
|
1102
|
+
return null;
|
|
1103
|
+
// a non-function header here is a trailer section: no more functions
|
|
1104
|
+
if (!isFunctionHeader(header))
|
|
1105
|
+
return null;
|
|
1106
|
+
const lines = [];
|
|
1107
|
+
for (;;) {
|
|
1108
|
+
const line = await this.nextLine();
|
|
1109
|
+
if (line === null)
|
|
1110
|
+
break;
|
|
1111
|
+
if (isFunctionHeader(line) || TRAILER_SECTION.test(line)) {
|
|
1112
|
+
this.pending = line;
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
if (line.trim() === '')
|
|
1116
|
+
continue;
|
|
1117
|
+
const normalized = normalizeDisassemblyLine(line, this.strings, this.literals);
|
|
1118
|
+
if (normalized !== null)
|
|
1119
|
+
lines.push(normalized);
|
|
1120
|
+
}
|
|
1121
|
+
return { header, lines };
|
|
1122
|
+
}
|
|
1123
|
+
/** drain whatever is left and report how the process ended */
|
|
1124
|
+
async finish() {
|
|
1125
|
+
while ((await this.nextLine()) !== null) { }
|
|
1126
|
+
return this.exit;
|
|
1127
|
+
}
|
|
1128
|
+
kill() {
|
|
1129
|
+
this.proc.kill();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function describeExit(exit) {
|
|
1133
|
+
if (exit.error)
|
|
1134
|
+
return exit.error.message;
|
|
1135
|
+
const how = exit.signal !== null ? `signal ${exit.signal}` : `exit ${exit.code}`;
|
|
1136
|
+
const stderr = exit.stderr.trim().split('\n').pop()?.trim();
|
|
1137
|
+
return stderr ? `${how}: ${stderr}` : how;
|
|
1138
|
+
}
|
|
1139
|
+
function clip(line) {
|
|
1140
|
+
const s = line.trim().split(exports.LITERAL_SEPARATOR).join(', ');
|
|
1141
|
+
return s.length > DETAIL_LINE_MAX ? `${s.slice(0, DETAIL_LINE_MAX - 1)}…` : s;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* For two New*WithBuffer lines that differ, the first literal that differs
|
|
1145
|
+
* (the whole line would be clipped before the difference for long literals).
|
|
1146
|
+
*/
|
|
1147
|
+
function literalDifference(a, b) {
|
|
1148
|
+
const open = /^(\s*New\w+WithBuffer(?:AndParent)? r\d+(?: r\d+)?(?: size=\d+)? n=\d+ [[{])/;
|
|
1149
|
+
const ma = open.exec(a);
|
|
1150
|
+
const mb = open.exec(b);
|
|
1151
|
+
if (!ma || !mb)
|
|
1152
|
+
return null;
|
|
1153
|
+
const ea = a.slice(ma[1].length, -1).split(exports.LITERAL_SEPARATOR);
|
|
1154
|
+
const eb = b.slice(mb[1].length, -1).split(exports.LITERAL_SEPARATOR);
|
|
1155
|
+
const n = Math.min(ea.length, eb.length);
|
|
1156
|
+
for (let i = 0; i < n; i++) {
|
|
1157
|
+
if (ea[i] !== eb[i]) {
|
|
1158
|
+
return `${clip(ma[1].slice(0, -1))} entry ${i}: ${clip(ea[i])} vs ${clip(eb[i])}`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
function compareBuffers(a, b) {
|
|
1164
|
+
for (const name of new Set([...a.keys(), ...b.keys()])) {
|
|
1165
|
+
const ea = a.get(name) ?? [];
|
|
1166
|
+
const eb = b.get(name) ?? [];
|
|
1167
|
+
const n = Math.min(ea.length, eb.length);
|
|
1168
|
+
for (let i = 0; i < n; i++) {
|
|
1169
|
+
if (ea[i] !== eb[i]) {
|
|
1170
|
+
return `${name} entry ${i}: ${clip(ea[i])} vs ${clip(eb[i])}`;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (ea.length !== eb.length) {
|
|
1174
|
+
return `${name}: ${ea.length} vs ${eb.length} entries`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
function compareFunctions(a, b) {
|
|
1180
|
+
if (a.header !== b.header) {
|
|
1181
|
+
return `${clip(a.header)} vs ${clip(b.header)}`;
|
|
1182
|
+
}
|
|
1183
|
+
const n = Math.min(a.lines.length, b.lines.length);
|
|
1184
|
+
for (let i = 0; i < n; i++) {
|
|
1185
|
+
if (a.lines[i] !== b.lines[i]) {
|
|
1186
|
+
const literal = literalDifference(a.lines[i], b.lines[i]);
|
|
1187
|
+
return literal
|
|
1188
|
+
? `${clip(a.header)} +${i + 1}: ${literal}`
|
|
1189
|
+
: `${clip(a.header)} +${i + 1}: ${clip(a.lines[i])} vs ${clip(b.lines[i])}`;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (a.lines.length !== b.lines.length) {
|
|
1193
|
+
return `${clip(a.header)}: ${a.lines.length} vs ${b.lines.length} lines`;
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Compare two HBC files by disassembly (see normalizeDisassemblyLine): the
|
|
1199
|
+
* literal buffers by content, then function by function. `-b` forces hermesc
|
|
1200
|
+
* to treat inputs as bytecode whatever their extension. Both dumps are
|
|
1201
|
+
* consumed as streams so the ~100 MB of text never touches the disk (unless
|
|
1202
|
+
* `dumpTo` asks for it). A dump process that fails or ends early is reported
|
|
1203
|
+
* as `dump-failed`, never as a difference: the two cases need different
|
|
1204
|
+
* follow-ups. The detail names the first difference so a rejected base can be
|
|
1205
|
+
* turned into a normalization rule or a bug report.
|
|
1206
|
+
*/
|
|
1207
|
+
async function compareHermesBytecode(hermesCommand, withBase, plain, options = {}) {
|
|
1208
|
+
const spawnDump = (file) => (0, child_process_1.spawn)(hermesCommand, ['-b', '-dump-bytecode', '-pretty-disassemble', file], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1209
|
+
// The literal buffers are read from both files up front (header + three
|
|
1210
|
+
// byte ranges). Only when both sides can be read do the instructions
|
|
1211
|
+
// compare decoded literals; otherwise both fall back to the dumped buffers
|
|
1212
|
+
// as a whole, so the two sides always normalize the same way.
|
|
1213
|
+
const [buffersA, buffersB] = await Promise.all([
|
|
1214
|
+
(0, hermes_literals_1.readLiteralBuffers)(withBase).catch(() => null),
|
|
1215
|
+
(0, hermes_literals_1.readLiteralBuffers)(plain).catch(() => null),
|
|
1216
|
+
]);
|
|
1217
|
+
const binary = buffersA && buffersB && buffersA.version === buffersB.version;
|
|
1218
|
+
const literals = binary ? 'instruction' : 'buffer';
|
|
1219
|
+
const a = new DumpReader(spawnDump(withBase), options.dumpTo?.withBase, binary ? buffersA : null);
|
|
1220
|
+
const b = new DumpReader(spawnDump(plain), options.dumpTo?.plain, binary ? buffersB : null);
|
|
1221
|
+
let functions = 0;
|
|
1222
|
+
const different = (detail) => ({
|
|
1223
|
+
status: 'different',
|
|
1224
|
+
detail,
|
|
1225
|
+
functions,
|
|
1226
|
+
literals,
|
|
1227
|
+
});
|
|
1228
|
+
const dumpFailed = (side, exit) => ({
|
|
1229
|
+
status: 'dump-failed',
|
|
1230
|
+
detail: `${side} dump: ${describeExit(exit)}`,
|
|
1231
|
+
functions,
|
|
1232
|
+
literals,
|
|
1233
|
+
});
|
|
1234
|
+
try {
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
const [fa, fb] = await Promise.all([a.nextFunction(), b.nextFunction()]);
|
|
1237
|
+
if (fa === null || fb === null) {
|
|
1238
|
+
// whoever ended: was that the end of a good dump or a failure?
|
|
1239
|
+
if (fa === null) {
|
|
1240
|
+
const exit = await a.finish();
|
|
1241
|
+
if (exit.error || exit.code !== 0)
|
|
1242
|
+
return dumpFailed('base', exit);
|
|
1243
|
+
}
|
|
1244
|
+
if (fb === null) {
|
|
1245
|
+
const exit = await b.finish();
|
|
1246
|
+
if (exit.error || exit.code !== 0)
|
|
1247
|
+
return dumpFailed('plain', exit);
|
|
1248
|
+
}
|
|
1249
|
+
if (fa !== null || fb !== null) {
|
|
1250
|
+
const extra = fa ?? fb;
|
|
1251
|
+
return different(`function count: ${clip(extra.header)} only in the ${fa ? 'base' : 'plain'} compile`);
|
|
1252
|
+
}
|
|
1253
|
+
if (functions === 0) {
|
|
1254
|
+
return {
|
|
1255
|
+
status: 'dump-failed',
|
|
1256
|
+
detail: 'no functions in the disassembly',
|
|
1257
|
+
functions,
|
|
1258
|
+
literals,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return { status: 'equivalent', functions, literals };
|
|
1262
|
+
}
|
|
1263
|
+
if (functions === 0 && !binary) {
|
|
1264
|
+
// buffers precede the functions, so both are complete by now. This
|
|
1265
|
+
// whole-buffer comparison is the fallback: the builder overlaps and
|
|
1266
|
+
// deduplicates serialized literals, so a delta build may lay the same
|
|
1267
|
+
// literals out differently and this can reject a good build.
|
|
1268
|
+
const detail = compareBuffers(a.buffers, b.buffers);
|
|
1269
|
+
if (detail)
|
|
1270
|
+
return different(detail);
|
|
1271
|
+
}
|
|
1272
|
+
const detail = compareFunctions(fa, fb);
|
|
1273
|
+
if (detail)
|
|
1274
|
+
return different(detail);
|
|
1275
|
+
functions++;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
finally {
|
|
1279
|
+
a.kill();
|
|
1280
|
+
b.kill();
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/** Resolves true when equivalent; see compareHermesBytecode for the details. */
|
|
1284
|
+
async function verifyHermesBaseEquivalence(hermesCommand, withBase, plain) {
|
|
1285
|
+
const result = await compareHermesBytecode(hermesCommand, withBase, plain);
|
|
1286
|
+
return result.status === 'equivalent';
|
|
1287
|
+
}
|