catalyst-core 1.1.0 → 1.2.0-canary.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -0,0 +1,642 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.MIN_JAVA_MAJOR = exports.MAX_JAVA_MAJOR = exports.CMDLINE_TOOLS = exports.AVD_NAME = exports.ANDROID_API = void 0;
7
+ exports.applyJavaHome = applyJavaHome;
8
+ exports.cleanSdkTemp = cleanSdkTemp;
9
+ exports.cmdlineTool = cmdlineTool;
10
+ exports.cmdlineToolsArchive = cmdlineToolsArchive;
11
+ exports.defaultSdkPath = defaultSdkPath;
12
+ exports.ensureAndroidSdk = ensureAndroidSdk;
13
+ exports.ensureJava = ensureJava;
14
+ exports.halfInstalledPaths = halfInstalledPaths;
15
+ exports.installCmdlineTools = installCmdlineTools;
16
+ exports.installSdkPackages = installSdkPackages;
17
+ exports.knownAvds = knownAvds;
18
+ exports.licensesAccepted = licensesAccepted;
19
+ exports.missingAndroidPackages = missingAndroidPackages;
20
+ exports.parseJavaMajor = parseJavaMajor;
21
+ exports.requiredAndroidPackages = requiredAndroidPackages;
22
+ exports.resolveJavaHome = resolveJavaHome;
23
+ exports.resolveSdkPath = resolveSdkPath;
24
+ exports.sha256File = sha256File;
25
+ exports.systemImageFor = systemImageFor;
26
+ var _child_process = require("child_process");
27
+ var _crypto = _interopRequireDefault(require("crypto"));
28
+ var _fs = _interopRequireDefault(require("fs"));
29
+ var _os = _interopRequireDefault(require("os"));
30
+ var _path = _interopRequireDefault(require("path"));
31
+ var _utils = require("./utils.js");
32
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
33
+ // ---------------------------------------------------------------------------
34
+ // Java
35
+ // ---------------------------------------------------------------------------
36
+
37
+ // Gradle 8.13 runs on JDK 17 to 23. A newer JDK is skipped, not used: Gradle
38
+ // fails on it with an "Unsupported class file major version" error.
39
+ const MIN_JAVA_MAJOR = exports.MIN_JAVA_MAJOR = 17;
40
+ const MAX_JAVA_MAJOR = exports.MAX_JAVA_MAJOR = 23;
41
+ // The LTS releases in range, best first.
42
+ const PREFERRED_JAVA_MAJORS = [21, 17];
43
+
44
+ /** The major version in `java -version` output: "17.0.9" -> 17, "1.8.0_392" -> 8. */
45
+ function parseJavaMajor(output) {
46
+ const match = /version "(\d+)(?:\.(\d+))?/.exec(String(output || ""));
47
+ if (!match) return null;
48
+ const major = Number(match[1]);
49
+ return major === 1 && match[2] ? Number(match[2]) : major;
50
+ }
51
+
52
+ /** `java -version` writes to stderr, so both streams are read. */
53
+ function defaultRun(command, args) {
54
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Fixed probe commands (java -version, java_home) with argv arrays.
55
+ const result = (0, _child_process.spawnSync)(command, args, {
56
+ encoding: "utf8",
57
+ timeout: 15000
58
+ });
59
+ if (result.error || result.status !== 0) return null;
60
+ return `${result.stdout || ""}${result.stderr || ""}`;
61
+ }
62
+
63
+ /**
64
+ * Where a JDK Gradle 8.13 can run on (17 to 23) lives, without touching any
65
+ * shell profile: JAVA_HOME, then macOS's /usr/libexec/java_home, then the JBR
66
+ * bundled with Android Studio, then (off macOS) the `java` on PATH. Each
67
+ * candidate must run and report a version in range; JDK 21 is preferred,
68
+ * then 17, then any other in range. A JDK 24 or newer is skipped.
69
+ *
70
+ * @param {object} [options]
71
+ * @param {NodeJS.ProcessEnv} [options.env]
72
+ * @param {string} [options.platform]
73
+ * @param {(command: string, args: string[]) => string | null} [options.run] - combined output, or null on failure
74
+ * @param {(file: string) => boolean} [options.exists]
75
+ * @param {(candidate: string, reason: string) => void} [options.onSkip]
76
+ * @returns {{ home: string, version: number, source: string } | null}
77
+ */
78
+ function resolveJavaHome({
79
+ env = process.env,
80
+ platform = process.platform,
81
+ run = defaultRun,
82
+ exists = _fs.default.existsSync,
83
+ onSkip = () => {}
84
+ } = {}) {
85
+ const home = _os.default.homedir();
86
+ const candidates = [];
87
+ const add = (dir, source) => {
88
+ if (dir && !candidates.some(([known]) => known === dir)) candidates.push([dir, source]);
89
+ };
90
+ if (env.JAVA_HOME) add(env.JAVA_HOME, "JAVA_HOME");
91
+ if (platform === "darwin") {
92
+ // Ask for the preferred majors by name: a bare `-v 17+` answers with
93
+ // the newest JDK, which may be one Gradle cannot use.
94
+ for (const version of [...PREFERRED_JAVA_MAJORS.map(String), `${MIN_JAVA_MAJOR}+`]) {
95
+ const found = run("/usr/libexec/java_home", ["-v", version]);
96
+ if (found && found.trim()) add(found.trim().split("\n")[0], "java_home");
97
+ }
98
+ add("/Applications/Android Studio.app/Contents/jbr/Contents/Home", "Android Studio");
99
+ add(_path.default.join(home, "Applications/Android Studio.app/Contents/jbr/Contents/Home"), "Android Studio");
100
+ } else if (platform === "linux") {
101
+ add("/opt/android-studio/jbr", "Android Studio");
102
+ add(_path.default.join(home, "android-studio", "jbr"), "Android Studio");
103
+ const java = run("sh", ["-c", 'readlink -f "$(command -v java)"']);
104
+ if (java && java.trim()) add(_path.default.dirname(_path.default.dirname(java.trim())), "PATH");
105
+ }
106
+ const exe = platform === "win32" ? "java.exe" : "java";
107
+ const usable = [];
108
+ for (const [candidate, source] of candidates) {
109
+ const bin = _path.default.join(candidate, "bin", exe);
110
+ if (!exists(bin)) {
111
+ if (source === "JAVA_HOME") onSkip(candidate, "has no bin/java");
112
+ continue;
113
+ }
114
+ const version = parseJavaMajor(run(bin, ["-version"]));
115
+ if (!version) {
116
+ onSkip(candidate, "did not run");
117
+ } else if (version < MIN_JAVA_MAJOR) {
118
+ onSkip(candidate, `is Java ${version}, older than ${MIN_JAVA_MAJOR}`);
119
+ } else if (version > MAX_JAVA_MAJOR) {
120
+ onSkip(candidate, `is Java ${version}, newer than Gradle 8.13 supports (${MIN_JAVA_MAJOR} to ${MAX_JAVA_MAJOR})`);
121
+ } else {
122
+ const java = {
123
+ home: candidate,
124
+ version,
125
+ source
126
+ };
127
+ if (version === PREFERRED_JAVA_MAJORS[0]) return java;
128
+ usable.push(java);
129
+ }
130
+ }
131
+ for (const major of PREFERRED_JAVA_MAJORS) {
132
+ const match = usable.find(java => java.version === major);
133
+ if (match) return match;
134
+ }
135
+ return usable[0] || null;
136
+ }
137
+
138
+ /** Point this process (and so every child it spawns) at a JDK. No profile is edited. */
139
+ function applyJavaHome(java, env = process.env) {
140
+ if (!java?.home) return env;
141
+ env.JAVA_HOME = java.home;
142
+ const bin = _path.default.join(java.home, "bin");
143
+ const parts = String(env.PATH || "").split(_path.default.delimiter);
144
+ if (parts[0] !== bin) env.PATH = [bin, ...parts.filter(part => part && part !== bin)].join(_path.default.delimiter);
145
+ return env;
146
+ }
147
+
148
+ /**
149
+ * Find a usable JDK (17 to 23), or install Zulu 17 with Homebrew once the
150
+ * install is agreed to (a terminal yes, or `--yes`). Without a terminal and
151
+ * without `--yes` the question fails with NATIVE-005; off macOS, without
152
+ * Homebrew, or when declined it is ANDROID-007.
153
+ *
154
+ * @param {object} [options]
155
+ * @param {string} [options.platform]
156
+ * @param {{ yes?: boolean }} [options.flags] - from parseNativeArgs
157
+ * @param {boolean} [options.isTTY]
158
+ * @param {(kind: "install" | "licence" | "key", question: string, options?: object) => Promise<boolean>} [options.ask]
159
+ * @param {(question: string, options?: object) => Promise<string>} [options.prompt]
160
+ * @param {(command: string, args: string[]) => Promise<void>} [options.runInherited]
161
+ * @param {(options: { onSkip: (candidate: string, reason: string) => void }) => { home: string, version: number, source: string } | null} [options.resolve]
162
+ * @param {(name: string) => boolean} [options.hasCommand]
163
+ * @param {(message: string, type?: string) => void} [options.log]
164
+ * @returns {Promise<{ home: string, version: number, source: string }>}
165
+ */
166
+ async function ensureJava({
167
+ platform = process.platform,
168
+ flags = {},
169
+ isTTY,
170
+ ask = _utils.askConsent,
171
+ prompt = _utils.promptUser,
172
+ runInherited = _utils.runInheritedCommand,
173
+ resolve = resolveJavaHome,
174
+ hasCommand = name => defaultRun("sh", ["-c", `command -v ${name}`]) !== null,
175
+ log = () => {}
176
+ } = {}) {
177
+ const onSkip = (candidate, reason) => log(`Skipping ${candidate}: it ${reason}`, "note");
178
+ let java = resolve({
179
+ onSkip
180
+ });
181
+ if (java) return java;
182
+ if (platform !== "darwin") {
183
+ throw (0, _utils.nativeError)("ANDROID-007", "install a JDK 17 or 21 (e.g. sudo apt install openjdk-17-jdk) and set JAVA_HOME");
184
+ }
185
+ if (!hasCommand("brew")) {
186
+ throw (0, _utils.nativeError)("ANDROID-007", "no JDK 17 to 23 found and Homebrew is not installed");
187
+ }
188
+ const yes = await ask("install", "No JDK 17 to 23 found. Install the Zulu JDK 17 with Homebrew (brew install --cask zulu@17)", {
189
+ flags,
190
+ isTTY,
191
+ prompt
192
+ });
193
+ if (!yes) throw (0, _utils.nativeError)("ANDROID-007", "install declined");
194
+ await runInherited("brew", ["install", "--cask", "zulu@17"]);
195
+ java = resolve({
196
+ onSkip
197
+ });
198
+ if (!java) throw (0, _utils.nativeError)("ANDROID-007", "Homebrew finished but no JDK 17 to 23 was found");
199
+ return java;
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Android SDK
204
+ // ---------------------------------------------------------------------------
205
+
206
+ // Pinned from https://developer.android.com/studio#command-tools (build
207
+ // 15859902). The archive is verified against the published SHA-256 before it
208
+ // is unpacked.
209
+ const CMDLINE_TOOLS = exports.CMDLINE_TOOLS = {
210
+ build: "15859902",
211
+ archives: {
212
+ "darwin-arm64": {
213
+ file: "commandlinetools-mac_arm64-15859902_latest.zip",
214
+ sha256: "835b62a26162b229b441d1f6d4680383815a270809eb33522c0d480fa5002c4e"
215
+ },
216
+ "darwin-x64": {
217
+ file: "commandlinetools-mac_x86_64-15859902_latest.zip",
218
+ sha256: "c5a6378ab5cf7e0d5701921405115befff13e9ff7417fb588389338f8bd050f3"
219
+ },
220
+ linux: {
221
+ file: "commandlinetools-linux-15859902_latest.zip",
222
+ sha256: "4e4c464f145a7512b57d088ac6c278c03c9eea610886b35a5e0804e74eedf583"
223
+ },
224
+ win32: {
225
+ file: "commandlinetools-win-15859902_latest.zip",
226
+ sha256: "90ae805d20434428bffcb699c290860f19bb5f66a67e6b330067e3de801fb04a"
227
+ }
228
+ }
229
+ };
230
+ const REPOSITORY_URL = "https://dl.google.com/android/repository";
231
+ const ANDROID_API = exports.ANDROID_API = 36;
232
+ const AVD_NAME = exports.AVD_NAME = `catalyst-api${ANDROID_API}`;
233
+ const AVD_DEVICE = "pixel_7";
234
+
235
+ /** The pinned cmdline-tools archive for this machine. */
236
+ function cmdlineToolsArchive(platform = process.platform, arch = _os.default.arch()) {
237
+ const key = platform === "darwin" ? `darwin-${arch === "arm64" ? "arm64" : "x64"}` : platform;
238
+ const archive = CMDLINE_TOOLS.archives[key];
239
+ if (!archive) throw new Error(`No Android command-line tools for ${platform}/${arch}`);
240
+ return {
241
+ ...archive,
242
+ url: `${REPOSITORY_URL}/${archive.file}`
243
+ };
244
+ }
245
+
246
+ /** The emulator system image that runs natively on this CPU. */
247
+ function systemImageFor(arch = _os.default.arch()) {
248
+ const abi = arch === "arm64" ? "arm64-v8a" : "x86_64";
249
+ return `system-images;android-${ANDROID_API};google_apis;${abi}`;
250
+ }
251
+
252
+ /**
253
+ * The SDK packages a Catalyst app needs: the emulator only when the app runs
254
+ * on one (not for a release or a connected phone), plus the system image
255
+ * when an AVD is to be created.
256
+ */
257
+ function requiredAndroidPackages({
258
+ arch = _os.default.arch(),
259
+ withSystemImage = false,
260
+ withEmulator = true
261
+ } = {}) {
262
+ const packages = ["platform-tools", ...(withEmulator ? ["emulator"] : []), `platforms;android-${ANDROID_API}`, "build-tools;36.0.0"];
263
+ return withSystemImage ? [...packages, systemImageFor(arch)] : packages;
264
+ }
265
+
266
+ /** sdkmanager installs `a;b;c` at <sdk>/a/b/c, with a package.xml beside it. */
267
+ function isPackageInstalled(sdkPath, id, exists = _fs.default.existsSync) {
268
+ return exists(_path.default.join(sdkPath, ...id.split(";"), "package.xml"));
269
+ }
270
+ function missingAndroidPackages(sdkPath, packages, exists = _fs.default.existsSync) {
271
+ return packages.filter(id => !isPackageInstalled(sdkPath, id, exists));
272
+ }
273
+
274
+ /** Where the SDK goes when none is configured or detected. */
275
+ function defaultSdkPath(env = process.env, platform = process.platform) {
276
+ if (env.ANDROID_HOME) return env.ANDROID_HOME;
277
+ if (env.ANDROID_SDK_ROOT) return env.ANDROID_SDK_ROOT;
278
+ return platform === "darwin" ? _path.default.join(_os.default.homedir(), "Library", "Android", "sdk") : _path.default.join(_os.default.homedir(), "Android", "Sdk");
279
+ }
280
+
281
+ /**
282
+ * The SDK this app builds with: WEBVIEW_CONFIG.android.sdkPath, then
283
+ * ANDROID_HOME, ANDROID_SDK_ROOT and the default location (the first that has
284
+ * adb), else where a new SDK goes. Read only: never asked for, never written.
285
+ *
286
+ * @param {{ sdkPath?: string }} [androidConfig]
287
+ * @param {NodeJS.ProcessEnv} [env]
288
+ */
289
+ function resolveSdkPath(androidConfig = {}, env = process.env) {
290
+ return (0, _utils.expandHome)(androidConfig.sdkPath) || (0, _utils.detectAndroidSdk)(env) || defaultSdkPath(env);
291
+ }
292
+ const toolExt = platform => platform === "win32" ? ".bat" : "";
293
+
294
+ /** sdkmanager/avdmanager from cmdline-tools/latest, else any versioned cmdline-tools dir. */
295
+ function cmdlineTool(sdkPath, tool, {
296
+ platform = process.platform,
297
+ exists = _fs.default.existsSync
298
+ } = {}) {
299
+ const name = `${tool}${toolExt(platform)}`;
300
+ const latest = _path.default.join(sdkPath, "cmdline-tools", "latest", "bin", name);
301
+ if (exists(latest)) return latest;
302
+ try {
303
+ for (const dir of _fs.default.readdirSync(_path.default.join(sdkPath, "cmdline-tools")).sort().reverse()) {
304
+ const candidate = _path.default.join(sdkPath, "cmdline-tools", dir, "bin", name);
305
+ if (exists(candidate)) return candidate;
306
+ }
307
+ } catch (error) {
308
+ // No cmdline-tools directory yet.
309
+ }
310
+ return null;
311
+ }
312
+
313
+ /** The SDK licence sdkmanager writes once it has been accepted. */
314
+ function licensesAccepted(sdkPath, exists = _fs.default.existsSync) {
315
+ return exists(_path.default.join(sdkPath, "licenses", "android-sdk-license"));
316
+ }
317
+ function sha256File(file) {
318
+ const hash = _crypto.default.createHash("sha256");
319
+ const fd = _fs.default.openSync(file, "r");
320
+ try {
321
+ const chunk = Buffer.alloc(1024 * 1024);
322
+ let read;
323
+ while ((read = _fs.default.readSync(fd, chunk, 0, chunk.length, null)) > 0) hash.update(chunk.subarray(0, read));
324
+ } finally {
325
+ _fs.default.closeSync(fd);
326
+ }
327
+ return hash.digest("hex");
328
+ }
329
+
330
+ /** curl honours the system proxy and certificate store, which Node's fetch does not. */
331
+ async function curlDownload(url, dest) {
332
+ await (0, _utils.runInteractiveCommand)("curl", ["-fsSL", "--retry", "2", "-o", dest, url]);
333
+ }
334
+ function unzipArchive(zip, dest) {
335
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - unzip with an argv array on a checksum-verified archive.
336
+ (0, _child_process.execFileSync)("unzip", ["-q", "-o", zip, "-d", dest], {
337
+ stdio: ["ignore", "ignore", "pipe"]
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Download the pinned cmdline-tools, check the SHA-256, and unpack them to
343
+ * <sdk>/cmdline-tools/latest. Any failure is ANDROID-009.
344
+ */
345
+ async function installCmdlineTools(sdkPath, {
346
+ archive = cmdlineToolsArchive(),
347
+ download = curlDownload,
348
+ unzip = unzipArchive,
349
+ log = () => {}
350
+ } = {}) {
351
+ const tmp = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), "catalyst-cmdline-tools-"));
352
+ const toolsRoot = _path.default.join(sdkPath, "cmdline-tools");
353
+ // Unpacked beside its destination so the final move is a rename, not a copy across devices.
354
+ let staging = null;
355
+ try {
356
+ const zip = _path.default.join(tmp, archive.file);
357
+ log(`Downloading ${archive.file}`);
358
+ try {
359
+ await download(archive.url, zip);
360
+ } catch (error) {
361
+ const output = String(error.output || "").trim();
362
+ throw (0, _utils.nativeError)("ANDROID-009", `download of ${archive.url} failed: ${output || error.message}`);
363
+ }
364
+ const actual = sha256File(zip);
365
+ if (actual !== archive.sha256) {
366
+ throw (0, _utils.nativeError)("ANDROID-009", `${archive.file} has SHA-256 ${actual}, expected ${archive.sha256}`);
367
+ }
368
+ _fs.default.mkdirSync(toolsRoot, {
369
+ recursive: true
370
+ });
371
+ staging = _fs.default.mkdtempSync(_path.default.join(toolsRoot, ".catalyst-"));
372
+ try {
373
+ unzip(zip, staging);
374
+ } catch (error) {
375
+ throw (0, _utils.nativeError)("ANDROID-009", `could not unpack ${archive.file}: ${error.stderr || error.message}`);
376
+ }
377
+ const unpacked = _path.default.join(staging, "cmdline-tools");
378
+ if (!_fs.default.existsSync(unpacked)) throw (0, _utils.nativeError)("ANDROID-009", `${archive.file} has no cmdline-tools folder`);
379
+ const latest = _path.default.join(toolsRoot, "latest");
380
+ _fs.default.rmSync(latest, {
381
+ recursive: true,
382
+ force: true
383
+ });
384
+ _fs.default.renameSync(unpacked, latest);
385
+ return latest;
386
+ } finally {
387
+ _fs.default.rmSync(tmp, {
388
+ recursive: true,
389
+ force: true
390
+ });
391
+ if (staging) _fs.default.rmSync(staging, {
392
+ recursive: true,
393
+ force: true
394
+ });
395
+ }
396
+ }
397
+
398
+ /** True when `child` is strictly inside `parent`. */
399
+ function isInside(parent, child) {
400
+ const relative = _path.default.relative(parent, child);
401
+ return Boolean(relative) && !relative.startsWith("..") && !_path.default.isAbsolute(relative);
402
+ }
403
+
404
+ /**
405
+ * Remove what an interrupted sdkmanager left in <sdk>/.temp. A stale partial
406
+ * download there makes the next install fail (#301).
407
+ *
408
+ * @param {string} sdkPath
409
+ * @param {{ log?: (message: string, type?: string) => void }} [options]
410
+ * @returns {string[]} the entries removed
411
+ */
412
+ function cleanSdkTemp(sdkPath, {
413
+ log = () => {}
414
+ } = {}) {
415
+ const temp = _path.default.join(sdkPath, ".temp");
416
+ let entries = [];
417
+ try {
418
+ entries = _fs.default.readdirSync(temp);
419
+ } catch {
420
+ return [];
421
+ }
422
+ for (const entry of entries) _fs.default.rmSync(_path.default.join(temp, entry), {
423
+ recursive: true,
424
+ force: true
425
+ });
426
+ if (entries.length) log(`Removed ${entries.length} stale partial download(s) from ${temp}`, "note");
427
+ return entries;
428
+ }
429
+
430
+ /**
431
+ * The folders of `ids` that an earlier install left half done: the folder is
432
+ * there but its package.xml is not. Also the folder a FileAlreadyExistsException
433
+ * in `output` names, when it is one of those packages or under <sdk>/.temp.
434
+ * Nothing outside the SDK, and never a folder another package lives in.
435
+ */
436
+ function halfInstalledPaths(sdkPath, ids, output = "", exists = _fs.default.existsSync) {
437
+ const targets = ids.map(id => _path.default.join(sdkPath, ...id.split(";")));
438
+ const found = new Set(targets.filter(dir => exists(dir) && !exists(_path.default.join(dir, "package.xml"))));
439
+ for (const match of String(output).matchAll(/FileAlreadyExistsException:\s*(.+)/g)) {
440
+ const named = _path.default.resolve(match[1].trim());
441
+ if (targets.includes(named) || isInside(_path.default.join(sdkPath, ".temp"), named)) found.add(named);
442
+ }
443
+ return [...found].filter(dir => isInside(sdkPath, dir));
444
+ }
445
+
446
+ /**
447
+ * Run an sdkmanager install of `ids`, cleaning <sdk>/.temp first. When it
448
+ * fails or leaves a package half installed (FileAlreadyExistsException, #301),
449
+ * the half-installed folders are removed and the install runs once more.
450
+ *
451
+ * @param {string} sdkPath
452
+ * @param {string[]} ids
453
+ * @param {() => Promise<unknown>} install - one sdkmanager run; may reject with `output`
454
+ * @param {{ log?: (message: string, type?: string) => void }} [options]
455
+ */
456
+ async function installSdkPackages(sdkPath, ids, install, {
457
+ log = () => {}
458
+ } = {}) {
459
+ let lastError = null;
460
+ for (let attempt = 1; attempt <= 2; attempt++) {
461
+ cleanSdkTemp(sdkPath, {
462
+ log
463
+ });
464
+ let output = "";
465
+ lastError = null;
466
+ try {
467
+ output = String((await install()) || "");
468
+ } catch (error) {
469
+ lastError = error;
470
+ output = String(error.output || error.message || "");
471
+ }
472
+ const still = missingAndroidPackages(sdkPath, ids);
473
+ if (!still.length) return;
474
+ const stale = halfInstalledPaths(sdkPath, still, output);
475
+ if (attempt === 2 || !stale.length && !/FileAlreadyExistsException/.test(output)) {
476
+ if (lastError) throw lastError;
477
+ throw new Error(`sdkmanager did not install ${still.join(", ")}`);
478
+ }
479
+ for (const dir of stale) _fs.default.rmSync(dir, {
480
+ recursive: true,
481
+ force: true
482
+ });
483
+ log(`sdkmanager left ${stale.map(dir => _path.default.relative(sdkPath, dir)).join(", ") || "a package"} half installed; removed it and retrying once`, "warning");
484
+ }
485
+ }
486
+
487
+ /** AVDs from the emulator, or from ~/.android/avd when the emulator isn't installed yet. */
488
+ function knownAvds(sdkPath, {
489
+ env = process.env
490
+ } = {}) {
491
+ const names = new Set((0, _utils.listAvds)(sdkPath));
492
+ const avdHome = env.ANDROID_AVD_HOME || _path.default.join(env.ANDROID_USER_HOME || _path.default.join(_os.default.homedir(), ".android"), "avd");
493
+ try {
494
+ for (const file of _fs.default.readdirSync(avdHome)) if (file.endsWith(".ini")) names.add(file.slice(0, -4));
495
+ } catch (error) {
496
+ // No AVDs yet.
497
+ }
498
+ return [...names];
499
+ }
500
+
501
+ /**
502
+ * Bring the Android SDK to what a Catalyst app needs, from nothing if need be:
503
+ * cmdline-tools, licences, the platform/build packages and, when the app runs
504
+ * on an emulator, the emulator and an AVD.
505
+ *
506
+ * Downloads and the AVD only happen after a terminal yes or `--yes`;
507
+ * licences only after a terminal yes (then sdkmanager shows them) or
508
+ * `--accept-licenses`. Without a terminal and without the flag, NATIVE-005
509
+ * names it; a "no" is ANDROID-008.
510
+ *
511
+ * The AVD: the configured `emulatorName` if it exists, else `preferredAvd`
512
+ * (the last one used), else catalyst-api36, else the first existing one;
513
+ * only when there is none is catalyst-api36 created.
514
+ *
515
+ * @param {object} options
516
+ * @param {string} options.sdkPath
517
+ * @param {string} [options.emulatorName] - WEBVIEW_CONFIG.android.emulatorName, if any
518
+ * @param {string} [options.preferredAvd] - the AVD the last run used
519
+ * @param {boolean} [options.needEmulator] - false for a release or a connected phone
520
+ * @param {{ yes?: boolean, acceptLicenses?: boolean }} [options.flags] - from parseNativeArgs
521
+ * @param {boolean} [options.isTTY]
522
+ * @param {string} [options.arch]
523
+ * @param {string} [options.platform]
524
+ * @param {(kind: "install" | "licence" | "key", question: string, options?: object) => Promise<boolean>} [options.ask]
525
+ * @param {typeof promptUser} [options.prompt]
526
+ * @param {(command: string, args: string[], responses?: object) => Promise<string>} [options.run] - captured output
527
+ * @param {(command: string, args: string[]) => Promise<void>} [options.runInherited] - on the user's terminal
528
+ * @param {(sdkPath: string, options?: object) => Promise<unknown>} [options.installTools] - installs cmdline-tools
529
+ * @param {(sdkPath: string) => string[]} [options.listAvds]
530
+ * @param {(message: string, type?: string) => void} [options.log]
531
+ * @returns {Promise<{ sdkPath: string, emulatorName: string | null, installed: string[], createdAvd: boolean }>}
532
+ */
533
+ async function ensureAndroidSdk({
534
+ sdkPath,
535
+ emulatorName,
536
+ preferredAvd,
537
+ needEmulator = true,
538
+ flags = {},
539
+ isTTY = (0, _utils.isInteractive)(),
540
+ arch = _os.default.arch(),
541
+ platform = process.platform,
542
+ ask = _utils.askConsent,
543
+ prompt = _utils.promptUser,
544
+ run = (command, args, responses) => (0, _utils.runInteractiveCommand)(command, args, responses),
545
+ runInherited = _utils.runInheritedCommand,
546
+ installTools = (dir, options) => installCmdlineTools(dir, options),
547
+ listAvds: avdLister = dir => knownAvds(dir),
548
+ log = () => {}
549
+ }) {
550
+ let avdName = null;
551
+ let createAvd = false;
552
+ if (needEmulator) {
553
+ const avds = avdLister(sdkPath);
554
+ if (emulatorName && avds.includes(emulatorName)) avdName = emulatorName;else {
555
+ if (emulatorName) log(`The configured emulator "${emulatorName}" does not exist`, "warning");
556
+ avdName = [preferredAvd, AVD_NAME].find(name => name && avds.includes(name)) || avds[0] || null;
557
+ }
558
+ createAvd = !avdName;
559
+ if (createAvd) avdName = AVD_NAME;
560
+ }
561
+ const packages = requiredAndroidPackages({
562
+ arch,
563
+ withSystemImage: createAvd,
564
+ withEmulator: needEmulator
565
+ });
566
+ const missing = missingAndroidPackages(sdkPath, packages);
567
+ let sdkmanager = cmdlineTool(sdkPath, "sdkmanager", {
568
+ platform
569
+ });
570
+ const needTools = !sdkmanager && (missing.length > 0 || createAvd);
571
+ if (!missing.length && !createAvd) return {
572
+ sdkPath,
573
+ emulatorName: avdName,
574
+ installed: [],
575
+ createdAvd: false
576
+ };
577
+
578
+ // One question for everything that downloads or creates.
579
+ const plan = [...(needTools ? [`Android command-line tools ${CMDLINE_TOOLS.build}`] : []), ...missing, ...(createAvd ? [`emulator "${avdName}" (${AVD_DEVICE})`] : [])];
580
+ const install = await ask("install", `The Android SDK at ${sdkPath} needs ${plan.join(", ")}. Download and install ${plan.length === 1 ? "it" : "them"}${createAvd ? " (the system image is about 1.5 GB)" : ""}`, {
581
+ flags,
582
+ isTTY,
583
+ prompt
584
+ });
585
+ if (!install) throw (0, _utils.nativeError)("ANDROID-008", `${sdkPath} still needs ${plan.join(", ")}`);
586
+ if (needTools) {
587
+ await installTools(sdkPath, {
588
+ log
589
+ });
590
+ sdkmanager = cmdlineTool(sdkPath, "sdkmanager", {
591
+ platform
592
+ });
593
+ if (!sdkmanager) throw (0, _utils.nativeError)("ANDROID-009", `no sdkmanager under ${sdkPath}/cmdline-tools/latest`);
594
+ }
595
+ const sdkRoot = `--sdk_root=${sdkPath}`;
596
+ if (missing.length && !licensesAccepted(sdkPath)) {
597
+ const accept = await ask("licence", "Review and accept the Android SDK licences (https://developer.android.com/studio/terms)", {
598
+ flags,
599
+ isTTY,
600
+ prompt
601
+ });
602
+ if (!accept) throw (0, _utils.nativeError)("ANDROID-008", "the Android SDK licences were not accepted");
603
+ if (flags.acceptLicenses) {
604
+ await run(sdkmanager, [sdkRoot, "--licenses"], {
605
+ "(y/N)": "y"
606
+ });
607
+ } else {
608
+ // On the terminal, so the person reads and answers each licence.
609
+ await runInherited(sdkmanager, [sdkRoot, "--licenses"]);
610
+ }
611
+ if (!licensesAccepted(sdkPath)) {
612
+ throw (0, _utils.nativeError)("ANDROID-008", "the Android SDK licences were not accepted");
613
+ }
614
+ }
615
+ if (missing.length) {
616
+ log(`Installing ${missing.join(", ")}`);
617
+ await installSdkPackages(sdkPath, missing, () => isTTY && !flags.acceptLicenses ?
618
+ // On the terminal: the download progress shows, and a licence
619
+ // this package adds is asked about, not answered for the user.
620
+ runInherited(sdkmanager, [sdkRoot, ...missing]) : run(sdkmanager, [sdkRoot, ...missing], {
621
+ "(y/N)": flags.acceptLicenses ? "y" : "n"
622
+ }), {
623
+ log
624
+ });
625
+ }
626
+ if (createAvd) {
627
+ const avdmanager = cmdlineTool(sdkPath, "avdmanager", {
628
+ platform
629
+ });
630
+ if (!avdmanager) throw (0, _utils.nativeError)("ANDROID-009", `no avdmanager under ${sdkPath}/cmdline-tools/latest`);
631
+ log(`Creating the "${avdName}" emulator`);
632
+ await run(avdmanager, ["create", "avd", "-n", avdName, "-k", systemImageFor(arch), "-d", AVD_DEVICE], {
633
+ "custom hardware profile": "no"
634
+ });
635
+ }
636
+ return {
637
+ sdkPath,
638
+ emulatorName: avdName,
639
+ installed: missing,
640
+ createdAvd: createAvd
641
+ };
642
+ }