@ycodium-ai/plugin-devices 0.2.3510 → 0.2.3566
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/README.md +112 -86
- package/assets/ui.js +16 -4
- package/dist/environment-helper.mjs +239 -0
- package/dist/helper.mjs +77 -128
- package/dist/index.js +1247 -556
- package/locales/en.json +64 -8
- package/locales/zh-CN.json +64 -8
- package/package.json +4 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// 独立 host:node 助手,只处理指定 SDK 或 Apple 官方工具,不修改系统设置。
|
|
2
|
+
import * as NodeFS from "node:fs";
|
|
3
|
+
import * as NodePath from "node:path";
|
|
4
|
+
import * as NodeChildProcess from "node:child_process";
|
|
5
|
+
import * as NodeNet from "node:net";
|
|
6
|
+
import * as NodeModule from "node:module";
|
|
7
|
+
// oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程助手,没有宿主 Effect 上下文。
|
|
8
|
+
const hostPlatform = process.platform;
|
|
9
|
+
|
|
10
|
+
/** 以交付文件和实际装载结果判定;原生助手可由对应平台的构建阶段提供,不能在用户机器现编。 */
|
|
11
|
+
export function probeIosTools(runtimeDir) {
|
|
12
|
+
if (hostPlatform !== "darwin" || !runtimeDir) return { available: false };
|
|
13
|
+
const dist = NodePath.join(
|
|
14
|
+
runtimeDir,
|
|
15
|
+
"node_modules",
|
|
16
|
+
"expo-device-hub",
|
|
17
|
+
"vendor",
|
|
18
|
+
"serve-sim",
|
|
19
|
+
"dist",
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
for (const file of [
|
|
23
|
+
"serve-sim.js",
|
|
24
|
+
"bin/LiveKitWebRTC.framework/LiveKitWebRTC",
|
|
25
|
+
"simax/serve-sim-ax-settings",
|
|
26
|
+
"simcam/serve-sim-camera-helper",
|
|
27
|
+
"simcam/libSimCameraInjector.dylib",
|
|
28
|
+
"capability-loader/libServeSimCapabilityLoader.dylib",
|
|
29
|
+
])
|
|
30
|
+
NodeFS.accessSync(NodePath.join(dist, file), NodeFS.constants.R_OK);
|
|
31
|
+
NodeModule.createRequire(import.meta.url)(
|
|
32
|
+
NodePath.join(dist, "native", "serve-sim-native.node"),
|
|
33
|
+
);
|
|
34
|
+
return { available: true };
|
|
35
|
+
} catch {
|
|
36
|
+
return { available: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function privateAndroidEnvironment(root, sdk, env = process.env) {
|
|
41
|
+
const user = NodePath.join(root, "user");
|
|
42
|
+
return {
|
|
43
|
+
...env,
|
|
44
|
+
ANDROID_HOME: sdk,
|
|
45
|
+
ANDROID_SDK_ROOT: sdk,
|
|
46
|
+
ANDROID_USER_HOME: user,
|
|
47
|
+
ANDROID_EMULATOR_HOME: user,
|
|
48
|
+
ANDROID_AVD_HOME: NodePath.join(root, "avd"),
|
|
49
|
+
ANDROID_SDK_HOME: root,
|
|
50
|
+
XDG_CONFIG_HOME: NodePath.join(root, "config"),
|
|
51
|
+
XDG_CACHE_HOME: NodePath.join(root, "cache"),
|
|
52
|
+
TMPDIR: NodePath.join(root, "temp"),
|
|
53
|
+
TMP: NodePath.join(root, "temp"),
|
|
54
|
+
TEMP: NodePath.join(root, "temp"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** adb 自身会结束协议版本不符的服务器;在调用客户端之前只读握手,发现冲突就让用户处理。 */
|
|
59
|
+
export async function guardAdbServer(
|
|
60
|
+
sdk,
|
|
61
|
+
env = process.env,
|
|
62
|
+
versionProbe = () =>
|
|
63
|
+
run(NodePath.join(sdk, "platform-tools", hostPlatform === "win32" ? "adb.exe" : "adb"), [
|
|
64
|
+
"version",
|
|
65
|
+
]),
|
|
66
|
+
) {
|
|
67
|
+
const version = versionProbe();
|
|
68
|
+
const protocol = /Android Debug Bridge version \d+\.\d+\.(\d+)/.exec(version.stdout)?.[1];
|
|
69
|
+
if (version.code !== 0 || !protocol) throw new Error("The selected Android SDK adb cannot run");
|
|
70
|
+
const specified = env.ADB_SERVER_SOCKET;
|
|
71
|
+
const endpoint = specified ? /^tcp:([^:]+):(\d+)$/.exec(specified) : null;
|
|
72
|
+
if (specified && !endpoint)
|
|
73
|
+
throw new Error(
|
|
74
|
+
"The configured ADB server socket cannot be checked safely; use a TCP server socket",
|
|
75
|
+
);
|
|
76
|
+
const host = endpoint?.[1] ?? "127.0.0.1";
|
|
77
|
+
const port = Number(endpoint?.[2] ?? env.ANDROID_ADB_SERVER_PORT ?? "5037");
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
const socket = NodeNet.createConnection({ host, port });
|
|
80
|
+
let received = Buffer.alloc(0);
|
|
81
|
+
const finish = (error) => {
|
|
82
|
+
socket.destroy();
|
|
83
|
+
if (error) reject(error);
|
|
84
|
+
else resolve();
|
|
85
|
+
};
|
|
86
|
+
socket.setTimeout(3000, () =>
|
|
87
|
+
finish(new Error("The existing ADB server did not answer; Ycodium will not restart it")),
|
|
88
|
+
);
|
|
89
|
+
socket.on("error", (error) =>
|
|
90
|
+
error.code === "ECONNREFUSED"
|
|
91
|
+
? finish()
|
|
92
|
+
: finish(new Error("The configured ADB server could not be checked")),
|
|
93
|
+
);
|
|
94
|
+
socket.on("connect", () => socket.write("000chost:version"));
|
|
95
|
+
socket.on("data", (data) => {
|
|
96
|
+
received = Buffer.concat([received, data]);
|
|
97
|
+
if (received.length < 8) return;
|
|
98
|
+
const length = Number.parseInt(received.subarray(4, 8).toString(), 16);
|
|
99
|
+
if (received.subarray(0, 4).toString() !== "OKAY" || !Number.isFinite(length) || length > 64)
|
|
100
|
+
return finish(new Error("The existing ADB server returned an incompatible protocol"));
|
|
101
|
+
if (received.length < 8 + length) return;
|
|
102
|
+
const server = Number.parseInt(received.subarray(8, 8 + length).toString(), 16);
|
|
103
|
+
finish(
|
|
104
|
+
server === Number(protocol)
|
|
105
|
+
? undefined
|
|
106
|
+
: new Error(
|
|
107
|
+
"An incompatible ADB server is already running. Ycodium will not stop or restart it; select the matching SDK or manage that server yourself",
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
socket.on("end", () => {
|
|
112
|
+
if (received.length < 12)
|
|
113
|
+
finish(new Error("The existing ADB server closed before reporting its version"));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 仅私有 SDK 的属主标记允许重定向 AVD;外部 SDK 保留用户自己的 AVD 查找路径。 */
|
|
119
|
+
export function applyPrivateAndroidEnvironment(sdk, env = process.env) {
|
|
120
|
+
if (!sdk) return false;
|
|
121
|
+
const root = NodePath.dirname(sdk);
|
|
122
|
+
try {
|
|
123
|
+
const record = JSON.parse(
|
|
124
|
+
NodeFS.readFileSync(NodePath.join(root, ".ycodium-devices-environment.json"), "utf8"),
|
|
125
|
+
);
|
|
126
|
+
if (
|
|
127
|
+
record.owner !== "ycodium-devices" ||
|
|
128
|
+
NodePath.resolve(record.sdk) !== NodePath.resolve(sdk)
|
|
129
|
+
)
|
|
130
|
+
return false;
|
|
131
|
+
Object.assign(env, privateAndroidEnvironment(root, sdk, env));
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function run(command, args, timeout = 15000) {
|
|
139
|
+
const result = NodeChildProcess.spawnSync(command, args, {
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
timeout,
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
code: result.status ?? 127,
|
|
146
|
+
stdout: result.stdout ?? "",
|
|
147
|
+
stderr: result.stderr ?? result.error?.message ?? "",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function probeXcode() {
|
|
151
|
+
if (hostPlatform !== "darwin")
|
|
152
|
+
return { ready: false, reasonCode: "ios-needs-macos", runtimes: [] };
|
|
153
|
+
const selected = run("xcode-select", ["--print-path"]);
|
|
154
|
+
const directory = selected.stdout.trim();
|
|
155
|
+
if (selected.code !== 0 || !directory.includes(".app/Contents/Developer"))
|
|
156
|
+
return { ready: false, reasonCode: "xcode-missing", runtimes: [] };
|
|
157
|
+
if (run("xcodebuild", ["-checkFirstLaunchStatus"]).code !== 0)
|
|
158
|
+
return { ready: false, reasonCode: "xcode-incomplete", runtimes: [] };
|
|
159
|
+
const result = run("xcrun", ["simctl", "list", "runtimes", "--json"]);
|
|
160
|
+
try {
|
|
161
|
+
const runtimes = JSON.parse(result.stdout).runtimes.filter(
|
|
162
|
+
(item) => item.isAvailable && item.identifier?.includes("iOS"),
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
ready: true,
|
|
166
|
+
directory,
|
|
167
|
+
runtimes,
|
|
168
|
+
reasonCode: runtimes.length ? undefined : "ios-runtime-missing",
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return { ready: false, reasonCode: "xcode-incomplete", runtimes: [] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function checkAcceleration(sdk) {
|
|
175
|
+
const command = NodePath.join(
|
|
176
|
+
sdk,
|
|
177
|
+
"emulator",
|
|
178
|
+
hostPlatform === "win32" ? "emulator.exe" : "emulator",
|
|
179
|
+
);
|
|
180
|
+
return run(command, ["-accel-check"]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** HypervisorPresent 说明当前 hypervisor 是否运行,不代表 CPU 或固件的虚拟化开关。 */
|
|
184
|
+
export function windowsAccelerationStatus(facts) {
|
|
185
|
+
if (!facts || typeof facts !== "object")
|
|
186
|
+
return { available: false, reasonCode: "virtualization-probe-failed" };
|
|
187
|
+
const bool = (value) => (typeof value === "boolean" ? value : null);
|
|
188
|
+
const state = {
|
|
189
|
+
hardwareSupported: bool(facts.hardwareSupported),
|
|
190
|
+
firmwareEnabled: bool(facts.firmwareEnabled),
|
|
191
|
+
slatSupported: bool(facts.slatSupported),
|
|
192
|
+
hypervisorPresent: bool(facts.hypervisorPresent),
|
|
193
|
+
whpxEnabled:
|
|
194
|
+
facts.whpxInstallState === 1
|
|
195
|
+
? true
|
|
196
|
+
: facts.whpxInstallState === 2 || facts.whpxInstallState === 3
|
|
197
|
+
? false
|
|
198
|
+
: null,
|
|
199
|
+
};
|
|
200
|
+
if (state.hypervisorPresent === true && state.whpxEnabled === true)
|
|
201
|
+
return { ...state, available: true };
|
|
202
|
+
if (state.hypervisorPresent === false && state.firmwareEnabled === false)
|
|
203
|
+
return { ...state, available: false, reasonCode: "virtualization-firmware-disabled" };
|
|
204
|
+
if (state.whpxEnabled === false)
|
|
205
|
+
return { ...state, available: false, reasonCode: "windows-hypervisor-platform-disabled" };
|
|
206
|
+
if (state.whpxEnabled === true && state.hypervisorPresent === false)
|
|
207
|
+
return { ...state, available: false, reasonCode: "windows-hypervisor-not-running" };
|
|
208
|
+
return { ...state, available: false, reasonCode: "virtualization-probe-failed" };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function probeWindowsAcceleration(execute = run) {
|
|
212
|
+
const result = execute("powershell.exe", [
|
|
213
|
+
"-NoProfile",
|
|
214
|
+
"-NonInteractive",
|
|
215
|
+
"-Command",
|
|
216
|
+
"$ErrorActionPreference = 'Stop'; $cpu = @(Get-CimInstance Win32_Processor)[0]; $machine = Get-CimInstance Win32_ComputerSystem; $feature = Get-CimInstance Win32_OptionalFeature -Filter \"Name='HypervisorPlatform'\"; @{ hardwareSupported = $cpu.VMMonitorModeExtensions; firmwareEnabled = $cpu.VirtualizationFirmwareEnabled; slatSupported = $cpu.SecondLevelAddressTranslationExtensions; hypervisorPresent = $machine.HypervisorPresent; whpxInstallState = $feature.InstallState } | ConvertTo-Json -Compress",
|
|
217
|
+
]);
|
|
218
|
+
if (result.code !== 0) return windowsAccelerationStatus(null);
|
|
219
|
+
try {
|
|
220
|
+
return windowsAccelerationStatus(JSON.parse(result.stdout));
|
|
221
|
+
} catch {
|
|
222
|
+
return windowsAccelerationStatus(null);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function probeVirtualization() {
|
|
227
|
+
if (hostPlatform === "darwin")
|
|
228
|
+
return { available: run("sysctl", ["-n", "kern.hv_support"]).stdout.trim() === "1" };
|
|
229
|
+
if (hostPlatform === "linux") {
|
|
230
|
+
try {
|
|
231
|
+
NodeFS.accessSync("/dev/kvm", NodeFS.constants.R_OK | NodeFS.constants.W_OK);
|
|
232
|
+
return { available: true };
|
|
233
|
+
} catch {
|
|
234
|
+
return { available: false };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (hostPlatform === "win32") return probeWindowsAcceleration();
|
|
238
|
+
return { available: false };
|
|
239
|
+
}
|
package/dist/helper.mjs
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// 跑在服务器自己的 Node 上(桌面版是带 ELECTRON_RUN_AS_NODE 的 Electron)。
|
|
3
3
|
//
|
|
4
4
|
// 宿主不许插件给子进程改 PATH,而 hub 在 Windows 上只靠 PATH 找模拟器、agent-device 的守护进程
|
|
5
|
-
// 也要 adb
|
|
6
|
-
// 再在同一进程里装载目标脚本(`node`、`npm` 模式)或起目标程序(`run` 模式)。插件停用时宿主杀的
|
|
5
|
+
// 也要 adb。这几类子进程经这里补好 PATH,再装载随包入口(`node` 模式)或起目标程序(`run` 模式)。插件停用时宿主杀的
|
|
7
6
|
// 是这个进程,装载进来的目标脚本也就跟着没了。
|
|
8
7
|
//
|
|
9
8
|
// 只用 node 内置模块;输出给插件解析的一律是单行 JSON。
|
|
@@ -21,23 +20,33 @@ import * as NodeDiagnosticsChannel from "node:diagnostics_channel";
|
|
|
21
20
|
import * as NodeFS from "node:fs";
|
|
22
21
|
import * as NodePath from "node:path";
|
|
23
22
|
import * as NodeURL from "node:url";
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
applyPrivateAndroidEnvironment,
|
|
26
|
+
probeXcode,
|
|
27
|
+
probeVirtualization,
|
|
28
|
+
checkAcceleration,
|
|
29
|
+
guardAdbServer,
|
|
30
|
+
probeIosTools,
|
|
31
|
+
} from "./environment-helper.mjs";
|
|
25
32
|
|
|
26
33
|
// oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程脚本,没有 Effect 运行时可注入。
|
|
27
34
|
const hostPlatform = process.platform;
|
|
35
|
+
// oxlint-disable-next-line ycodium/no-global-process-runtime -- 独立子进程助手,没有宿主 Effect 上下文。
|
|
36
|
+
const hostArch = process.arch;
|
|
28
37
|
const isWindows = hostPlatform === "win32";
|
|
29
38
|
const exe = (name) => (isWindows ? `${name}.exe` : name);
|
|
30
39
|
|
|
31
40
|
/** `--sdk x --shim y -- rest…`:选项在 `--` 之前,其余原样交给目标。其它环境变量由插件经 spawn 的 env 给。 */
|
|
32
41
|
function parseArgs(argv) {
|
|
33
|
-
const options = { sdk: null, shim: null, rest: [] };
|
|
42
|
+
const options = { sdk: null, shim: null, tools: null, rest: [] };
|
|
34
43
|
for (let index = 0; index < argv.length; index += 1) {
|
|
35
44
|
const value = argv[index];
|
|
36
45
|
if (value === "--") {
|
|
37
46
|
options.rest = argv.slice(index + 1);
|
|
38
47
|
break;
|
|
39
48
|
}
|
|
40
|
-
if (value === "--sdk" || value === "--shim") {
|
|
49
|
+
if (value === "--sdk" || value === "--shim" || value === "--tools") {
|
|
41
50
|
options[value.slice(2)] = argv[index + 1] || null;
|
|
42
51
|
index += 1;
|
|
43
52
|
}
|
|
@@ -145,103 +154,6 @@ function observeEmulatorLaunches(shim) {
|
|
|
145
154
|
});
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
/**
|
|
149
|
-
* 在 `directory` 里放一个叫 node 的垫片,指向跑这个助手的可执行文件。npm 的安装脚本
|
|
150
|
-
* (比如原生依赖的预编译下载器)按名字在 PATH 上找 node,而这台机器的 PATH 上可能根本没有。
|
|
151
|
-
* ELECTRON_RUN_AS_NODE 已经在本进程环境里(宿主起 host:node 时补的),垫片起的进程照样继承。
|
|
152
|
-
*/
|
|
153
|
-
function writeNodeShim(directory) {
|
|
154
|
-
NodeFS.mkdirSync(directory, { recursive: true });
|
|
155
|
-
if (isWindows) {
|
|
156
|
-
NodeFS.writeFileSync(NodePath.join(directory, "node.cmd"), `@"${process.execPath}" %*\r\n`);
|
|
157
|
-
} else {
|
|
158
|
-
const file = NodePath.join(directory, "node");
|
|
159
|
-
NodeFS.writeFileSync(
|
|
160
|
-
file,
|
|
161
|
-
`#!/bin/sh\nexec '${process.execPath.replaceAll("'", `'"'"'`)}' "$@"\n`,
|
|
162
|
-
);
|
|
163
|
-
NodeFS.chmodSync(file, 0o755);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 读 tar 头里的一个八进制数字段。 */
|
|
168
|
-
function octal(block, start, length) {
|
|
169
|
-
const text = block
|
|
170
|
-
.subarray(start, start + length)
|
|
171
|
-
.toString("utf8")
|
|
172
|
-
.replaceAll("\0", "")
|
|
173
|
-
.trim();
|
|
174
|
-
return text.length === 0 ? 0 : Number.parseInt(text, 8);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function text(block, start, length) {
|
|
178
|
-
const raw = block.subarray(start, start + length);
|
|
179
|
-
const end = raw.indexOf(0);
|
|
180
|
-
return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** PAX 扩展头里的 `path=`;其余键这里用不到。 */
|
|
184
|
-
function paxPath(body) {
|
|
185
|
-
let offset = 0;
|
|
186
|
-
while (offset < body.length) {
|
|
187
|
-
const space = body.indexOf(0x20, offset);
|
|
188
|
-
if (space === -1) break;
|
|
189
|
-
const length = Number.parseInt(body.subarray(offset, space).toString("utf8"), 10);
|
|
190
|
-
if (!Number.isFinite(length) || length <= 0) break;
|
|
191
|
-
const record = body.subarray(space + 1, offset + length - 1).toString("utf8");
|
|
192
|
-
if (record.startsWith("path=")) return record.slice("path=".length);
|
|
193
|
-
offset += length;
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 解一个 npm 包的 .tgz 到 `destination`,去掉包内统一的第一层目录(npm 包是 `package/`)。
|
|
200
|
-
* 只认普通文件和目录;条目路径解析后必须仍在 `destination` 内。
|
|
201
|
-
*/
|
|
202
|
-
function extractTarball(file, destination) {
|
|
203
|
-
const archive = NodeZlib.gunzipSync(NodeFS.readFileSync(file));
|
|
204
|
-
const root = NodePath.resolve(destination);
|
|
205
|
-
let offset = 0;
|
|
206
|
-
let pendingName = null;
|
|
207
|
-
let count = 0;
|
|
208
|
-
while (offset + 512 <= archive.length) {
|
|
209
|
-
const block = archive.subarray(offset, offset + 512);
|
|
210
|
-
if (block.every((byte) => byte === 0)) break;
|
|
211
|
-
const size = octal(block, 124, 12);
|
|
212
|
-
const type = String.fromCharCode(block[156] || 0x30);
|
|
213
|
-
const bodyStart = offset + 512;
|
|
214
|
-
const body = archive.subarray(bodyStart, bodyStart + size);
|
|
215
|
-
offset = bodyStart + Math.ceil(size / 512) * 512;
|
|
216
|
-
if (type === "x") {
|
|
217
|
-
pendingName = paxPath(body);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (type === "L") {
|
|
221
|
-
pendingName = text(body, 0, body.length);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (type === "g") continue;
|
|
225
|
-
const prefix = text(block, 345, 155);
|
|
226
|
-
const name = pendingName ?? (prefix ? `${prefix}/${text(block, 0, 100)}` : text(block, 0, 100));
|
|
227
|
-
pendingName = null;
|
|
228
|
-
const relative = name.split("/").slice(1).join("/");
|
|
229
|
-
if (relative.length === 0) continue;
|
|
230
|
-
const target = NodePath.resolve(root, relative);
|
|
231
|
-
if (target !== root && !target.startsWith(root + NodePath.sep)) {
|
|
232
|
-
throw new Error(`tar entry escapes the destination: ${name}`);
|
|
233
|
-
}
|
|
234
|
-
if (type === "5") {
|
|
235
|
-
NodeFS.mkdirSync(target, { recursive: true });
|
|
236
|
-
} else if (type === "0" || type === "\0" || type === "7") {
|
|
237
|
-
NodeFS.mkdirSync(NodePath.dirname(target), { recursive: true });
|
|
238
|
-
NodeFS.writeFileSync(target, body, { mode: octal(block, 100, 8) & 0o777 || 0o644 });
|
|
239
|
-
count += 1;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return count;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
157
|
function exists(file) {
|
|
246
158
|
try {
|
|
247
159
|
NodeFS.accessSync(file);
|
|
@@ -294,20 +206,13 @@ function probeAndroidSdk(override) {
|
|
|
294
206
|
return { root: null, adb: false, emulator: false, avdmanager: false, legacyAvdmanager: false };
|
|
295
207
|
}
|
|
296
208
|
|
|
297
|
-
function hasXcrun() {
|
|
298
|
-
if (hostPlatform !== "darwin") return false;
|
|
299
|
-
const result = NodeChildProcess.spawnSync("xcrun", ["--version"], {
|
|
300
|
-
stdio: "ignore",
|
|
301
|
-
timeout: 10_000,
|
|
302
|
-
});
|
|
303
|
-
return result.status === 0;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
209
|
/** 把 SDK 的两个目录排到 PATH 最前,并设好 ANDROID_HOME。 */
|
|
307
210
|
function applySdkEnvironment(sdk) {
|
|
308
211
|
if (!sdk) return;
|
|
309
212
|
prependPath([NodePath.join(sdk, "platform-tools"), NodePath.join(sdk, "emulator")]);
|
|
310
213
|
process.env.ANDROID_HOME = sdk;
|
|
214
|
+
process.env.ANDROID_SDK_ROOT = sdk;
|
|
215
|
+
applyPrivateAndroidEnvironment(sdk);
|
|
311
216
|
}
|
|
312
217
|
|
|
313
218
|
/** 在本进程里装载一个 Node 入口脚本,就像 `node entry …args` 起的一样。 */
|
|
@@ -319,23 +224,75 @@ async function loadEntry(entry, args) {
|
|
|
319
224
|
async function main() {
|
|
320
225
|
const [mode, ...rest] = process.argv.slice(2);
|
|
321
226
|
const options = parseArgs(rest);
|
|
227
|
+
if (mode === "environment-probe") {
|
|
228
|
+
process.stdout.write(
|
|
229
|
+
`${JSON.stringify({ platform: hostPlatform, arch: hostArch, iosToolsAvailable: probeIosTools(options.tools).available, virtualization: probeVirtualization(), xcode: probeXcode(), ...(options.sdk ? { acceleration: checkAcceleration(options.sdk) } : {}) })}\n`,
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
322
233
|
if (mode === "probe") {
|
|
234
|
+
const android = probeAndroidSdk(options.sdk);
|
|
235
|
+
applySdkEnvironment(android.root);
|
|
236
|
+
const androidValidated =
|
|
237
|
+
android.adb &&
|
|
238
|
+
android.emulator &&
|
|
239
|
+
[
|
|
240
|
+
[NodePath.join(android.root, "platform-tools", exe("adb")), ["version"]],
|
|
241
|
+
[NodePath.join(android.root, "emulator", exe("emulator")), ["-version"]],
|
|
242
|
+
].every(
|
|
243
|
+
([program, argv]) =>
|
|
244
|
+
NodeChildProcess.spawnSync(program, argv, {
|
|
245
|
+
stdio: "ignore",
|
|
246
|
+
timeout: 10000,
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
}).status === 0,
|
|
249
|
+
);
|
|
250
|
+
const xcode = probeXcode();
|
|
323
251
|
process.stdout.write(
|
|
324
252
|
`${JSON.stringify({
|
|
325
253
|
node: process.version,
|
|
326
254
|
execPath: process.execPath,
|
|
327
255
|
electron: process.versions.electron ?? null,
|
|
328
256
|
platform: hostPlatform,
|
|
329
|
-
|
|
330
|
-
|
|
257
|
+
arch: hostArch,
|
|
258
|
+
android,
|
|
259
|
+
androidValidated,
|
|
260
|
+
xcrun: xcode.ready,
|
|
261
|
+
iosRuntimeAvailable: xcode.runtimes.length > 0,
|
|
262
|
+
iosToolsAvailable: probeIosTools(options.tools).available,
|
|
331
263
|
})}\n`,
|
|
332
264
|
);
|
|
333
265
|
return;
|
|
334
266
|
}
|
|
267
|
+
if (mode === "validate-android") {
|
|
268
|
+
if (!options.sdk) throw new Error("SDK directory is required");
|
|
269
|
+
applySdkEnvironment(options.sdk);
|
|
270
|
+
const commands = [
|
|
271
|
+
[NodePath.join(options.sdk, "platform-tools", exe("adb")), ["version"]],
|
|
272
|
+
[NodePath.join(options.sdk, "emulator", exe("emulator")), ["-version"]],
|
|
273
|
+
[NodePath.join(options.sdk, "emulator", exe("emulator")), ["-list-avds"]],
|
|
274
|
+
[NodePath.join(options.sdk, "emulator", exe("emulator")), ["-accel-check"]],
|
|
275
|
+
];
|
|
276
|
+
for (const [program, argv] of commands) {
|
|
277
|
+
const result = NodeChildProcess.spawnSync(program, argv, {
|
|
278
|
+
encoding: "utf8",
|
|
279
|
+
timeout: 15000,
|
|
280
|
+
windowsHide: true,
|
|
281
|
+
});
|
|
282
|
+
if (
|
|
283
|
+
result.status !== 0 ||
|
|
284
|
+
(argv[0] === "-list-avds" && !result.stdout.includes("Ycodium_API_35"))
|
|
285
|
+
)
|
|
286
|
+
throw new Error(`Private SDK validation failed: ${argv[0]}`);
|
|
287
|
+
}
|
|
288
|
+
process.stdout.write(`${JSON.stringify({ validated: true })}\n`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
335
291
|
if (mode === "node") {
|
|
336
292
|
const [entry, ...args] = options.rest;
|
|
337
293
|
if (!entry) throw new Error("helper node: missing entry script");
|
|
338
294
|
applySdkEnvironment(options.sdk);
|
|
295
|
+
if (options.sdk) await guardAdbServer(options.sdk);
|
|
339
296
|
const shim = prepareBreakawayShim({
|
|
340
297
|
platform: hostPlatform,
|
|
341
298
|
directory: options.shim,
|
|
@@ -345,24 +302,12 @@ async function main() {
|
|
|
345
302
|
await loadEntry(entry, args);
|
|
346
303
|
return;
|
|
347
304
|
}
|
|
348
|
-
if (mode === "npm") {
|
|
349
|
-
const [npmCli, ...args] = options.rest;
|
|
350
|
-
if (!npmCli || !options.shim) throw new Error("helper npm: missing npm-cli.js or --shim");
|
|
351
|
-
writeNodeShim(options.shim);
|
|
352
|
-
prependPath([options.shim]);
|
|
353
|
-
await loadEntry(npmCli, args);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (mode === "extract") {
|
|
357
|
-
const [file, destination] = options.rest;
|
|
358
|
-
if (!file || !destination) throw new Error("helper extract: missing archive or destination");
|
|
359
|
-
process.stdout.write(`${JSON.stringify({ files: extractTarball(file, destination) })}\n`);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
305
|
if (mode === "run") {
|
|
363
306
|
const [command, ...args] = options.rest;
|
|
364
307
|
if (!command) throw new Error("helper run: missing command");
|
|
365
308
|
applySdkEnvironment(options.sdk);
|
|
309
|
+
if (options.sdk && command === "adb" && args[0] !== "version")
|
|
310
|
+
await guardAdbServer(options.sdk);
|
|
366
311
|
const resolved =
|
|
367
312
|
command === "emulator" && options.sdk
|
|
368
313
|
? NodePath.join(options.sdk, "emulator", exe("emulator"))
|
|
@@ -390,7 +335,11 @@ async function main() {
|
|
|
390
335
|
throw new Error(`helper: unknown mode ${String(mode)}`);
|
|
391
336
|
}
|
|
392
337
|
|
|
393
|
-
if (
|
|
338
|
+
if (
|
|
339
|
+
process.argv[1] &&
|
|
340
|
+
NodeFS.realpathSync(process.argv[1]) ===
|
|
341
|
+
NodeFS.realpathSync(NodeURL.fileURLToPath(import.meta.url))
|
|
342
|
+
)
|
|
394
343
|
main().catch((error) => {
|
|
395
344
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
396
345
|
process.exitCode = 1;
|