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.
- package/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- 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
|
+
}
|