@expo-harmony/cli 55.0.26-harmony.3 → 55.0.26-harmony.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/build/cli.js +2 -0
- package/build/doctor/doctor.js +4 -3
- package/build/prebuild/check.js +14 -8
- package/build/process.d.ts +1 -1
- package/build/process.js +147 -75
- package/build/tools.js +25 -23
- package/package.json +3 -3
- package/src/cli.ts +3 -0
- package/src/doctor/doctor.ts +6 -3
- package/src/prebuild/check.ts +28 -14
- package/src/process.ts +137 -63
- package/src/tools.ts +27 -22
package/README.md
CHANGED
|
@@ -50,7 +50,9 @@ npx expo-harmony start --port 8082
|
|
|
50
50
|
npx expo-harmony start --clear
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
所有命令都会在 CLI 入口、读取应用配置之前设置 `EXPO_HARMONY=1` 和 `EXPO_METRO_TARGET=harmony`。
|
|
54
|
+
|
|
55
|
+
`start` 调用项目本地的 Expo CLI,以 `--dev-client` 模式启动 Metro。它只启动 JS 开发服务,不执行原生工程生成、构建、安装、应用启动或设备端口映射,也不要求 HarmonyOS SDK 和设备就绪。新启动的 Metro 在当前终端输出日志,按 Ctrl+C 退出。
|
|
54
56
|
|
|
55
57
|
支持 `--port <number>`(默认 `8081`)和 `--reset-cache`(别名 `--clear`、`-c`)。端口上已有 Metro 时提示并退出,不停止已有服务;此时缓存选项不会生效,需要先停止已有 Metro 后重新执行命令。端口被其他进程占用时会报错。
|
|
56
58
|
|
|
@@ -66,7 +68,7 @@ npx expo-harmony prebuild --check
|
|
|
66
68
|
|
|
67
69
|
`prebuild` 固定使用 HarmonyOS 平台和 `@expo-harmony/template` 模板,因此不像官方接受 `--platform` 和 `--template`。依赖安装相关选项会透传给 Expo CLI:`--no-install`、`--npm`/`--yarn`/`--pnpm`/`--bun`(最多选择一个)和 `--skip-dependency-update <packages>`。执行前会先运行一次 doctor(此阶段不要求构建工具就绪),发现阻塞错误时直接中止。
|
|
68
70
|
|
|
69
|
-
`--clean` 只会清理带有 Expo Harmony 模板标记的原生目录:CNG manifest 缺失、目标不是项目内的普通目录或模板标记异常时都会拒绝删除。`--check` 是只读操作,会把项目镜像到临时目录(`node_modules`
|
|
71
|
+
`--clean` 只会清理带有 Expo Harmony 模板标记的原生目录:CNG manifest 缺失、目标不是项目内的普通目录或模板标记异常时都会拒绝删除。`--check` 是只读操作,会把项目镜像到临时目录(`node_modules` 中的包目录通过链接共享,Windows 下的普通文件复制到临时目录,无须文件符号链接权限),在其中执行一次隔离的 prebuild 后比较受管文件;无差异时退出码为 `0`,有差异时列出变更并以 `2` 退出,且不能与其他会修改工程的选项同时使用。
|
|
70
72
|
|
|
71
73
|
## Build
|
|
72
74
|
|
package/build/cli.js
CHANGED
|
@@ -78,6 +78,8 @@ function parseInvocation(argv) {
|
|
|
78
78
|
return { command, parsed, projectRoot };
|
|
79
79
|
}
|
|
80
80
|
async function runAsync(argv = process.argv.slice(2), io = console) {
|
|
81
|
+
process.env.EXPO_HARMONY = '1';
|
|
82
|
+
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
81
83
|
const invocation = parseInvocation(argv);
|
|
82
84
|
if (invocation.command === 'help') {
|
|
83
85
|
io.log(Help);
|
package/build/doctor/doctor.js
CHANGED
|
@@ -165,6 +165,7 @@ async function doctorUnlockedAsync(projectRoot, options = {}) {
|
|
|
165
165
|
tools.unshift(['hdc', toolchain.hdc, ['-v'], unavailableToolStatus]);
|
|
166
166
|
}
|
|
167
167
|
for (const [id, tool, versionArgs, unavailableStatus] of tools) {
|
|
168
|
+
const command = [tool.command, ...tool.args].map(value => JSON.stringify(value)).join(' ');
|
|
168
169
|
try {
|
|
169
170
|
const result = await (0, process_1.spawnAsync)(tool.command, [...tool.args, ...versionArgs], {
|
|
170
171
|
capture: true,
|
|
@@ -173,11 +174,11 @@ async function doctorUnlockedAsync(projectRoot, options = {}) {
|
|
|
173
174
|
timeoutMs: 10_000,
|
|
174
175
|
});
|
|
175
176
|
checks.push(result.code === 0 && !result.timedOut
|
|
176
|
-
? check(id, 'pass', `${
|
|
177
|
-
: check(id, unavailableStatus, `${
|
|
177
|
+
? check(id, 'pass', `${command} is available through ${tool.source}.`)
|
|
178
|
+
: check(id, unavailableStatus, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
|
|
178
179
|
}
|
|
179
180
|
catch {
|
|
180
|
-
checks.push(check(id, unavailableStatus, `${
|
|
181
|
+
checks.push(check(id, unavailableStatus, `${command} is unavailable; generation remains available.`));
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
if (options.validateGeneratedProject !== false) {
|
package/build/prebuild/check.js
CHANGED
|
@@ -31,6 +31,16 @@ function mirrorRoot(temp, project) {
|
|
|
31
31
|
const segments = absolute.slice(parsed.root.length).split(node_path_1.default.sep).filter(Boolean);
|
|
32
32
|
return node_path_1.default.join(temp, 'filesystem', volume, ...segments);
|
|
33
33
|
}
|
|
34
|
+
async function linkModuleEntryAsync(source, target, entry) {
|
|
35
|
+
const directory = entry.isSymbolicLink()
|
|
36
|
+
? (await node_fs_1.default.promises.stat(source)).isDirectory()
|
|
37
|
+
: entry.isDirectory();
|
|
38
|
+
if (process.platform === 'win32' && !directory) {
|
|
39
|
+
await node_fs_1.default.promises.copyFile(source, target);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await node_fs_1.default.promises.symlink(node_path_1.default.resolve(source), target, directory ? process.platform === 'win32' ? 'junction' : 'dir' : 'file');
|
|
43
|
+
}
|
|
34
44
|
async function linkModulesAsync(source, target) {
|
|
35
45
|
await node_fs_1.default.promises.mkdir(target, { recursive: true });
|
|
36
46
|
for (const entry of await node_fs_1.default.promises.readdir(source, { withFileTypes: true })) {
|
|
@@ -39,15 +49,11 @@ async function linkModulesAsync(source, target) {
|
|
|
39
49
|
if (entry.name.startsWith('@') && entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
40
50
|
await node_fs_1.default.promises.mkdir(to);
|
|
41
51
|
for (const child of await node_fs_1.default.promises.readdir(from, { withFileTypes: true })) {
|
|
42
|
-
await
|
|
43
|
-
? 'junction'
|
|
44
|
-
: child.isDirectory() ? 'dir' : 'file');
|
|
52
|
+
await linkModuleEntryAsync(node_path_1.default.join(from, child.name), node_path_1.default.join(to, child.name), child);
|
|
45
53
|
}
|
|
46
54
|
continue;
|
|
47
55
|
}
|
|
48
|
-
await
|
|
49
|
-
? 'junction'
|
|
50
|
-
: entry.isDirectory() ? 'dir' : 'file');
|
|
56
|
+
await linkModuleEntryAsync(from, to, entry);
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
function isAppLocalHarmonyPath(project, source) {
|
|
@@ -90,8 +96,8 @@ async function copyAsync(project, target, plan, temp = node_path_1.default.dirna
|
|
|
90
96
|
// Keep a real node_modules directory in the isolated project and link its
|
|
91
97
|
// entries. Dependency scanners then retain the isolated lexical package path
|
|
92
98
|
// (including scoped packages) instead of collapsing the entire node_modules
|
|
93
|
-
// root to the source project's realpath.
|
|
94
|
-
//
|
|
99
|
+
// root to the source project's realpath. Package directories stay linked;
|
|
100
|
+
// Windows copies loose files to avoid requiring file-symlink privileges.
|
|
95
101
|
await linkModulesAsync(modules, node_path_1.default.join(target, 'node_modules'));
|
|
96
102
|
await (0, check_1.stageAsync)(project, target, temp);
|
|
97
103
|
}
|
package/build/process.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ declare function startManagedProcess(command: string, args: string[], options?:
|
|
|
25
25
|
completion: Promise<ProcessResult>;
|
|
26
26
|
getStderr: () => string;
|
|
27
27
|
getStdout: () => string;
|
|
28
|
-
stop: (signal?: NodeJS.Signals, graceMs?: number) => Promise<
|
|
28
|
+
stop: (signal?: NodeJS.Signals, graceMs?: number) => Promise<ProcessResult>;
|
|
29
29
|
wasStopped: () => boolean;
|
|
30
30
|
};
|
|
31
31
|
export { formatDiagnostics, spawnAsync, startManagedProcess };
|
package/build/process.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.formatDiagnostics = formatDiagnostics;
|
|
|
7
7
|
exports.spawnAsync = spawnAsync;
|
|
8
8
|
exports.startManagedProcess = startManagedProcess;
|
|
9
9
|
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
10
|
+
const tool_command_1 = require("@expo-harmony/expo-modules-autolinking/tool-command");
|
|
10
11
|
const errors_1 = require("./errors");
|
|
11
12
|
const DefaultOutputLimit = 1024 * 1024;
|
|
12
13
|
const DefaultStopGraceMs = 3_000;
|
|
@@ -53,8 +54,14 @@ function formatDiagnostics(result, limit = 4_000) {
|
|
|
53
54
|
}
|
|
54
55
|
function spawnAsync(command, args, options = {}) {
|
|
55
56
|
return new Promise((resolve, reject) => {
|
|
57
|
+
if (options.signal?.aborted) {
|
|
58
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: operation aborted.`, {
|
|
59
|
+
cause: options.signal.reason,
|
|
60
|
+
operation: options.operation || 'spawn',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
56
63
|
const piped = Boolean(options.capture || options.onStdout || options.onStderr);
|
|
57
|
-
const
|
|
64
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
58
65
|
const child = (0, cross_spawn_1.default)(command, args, {
|
|
59
66
|
cwd: options.cwd,
|
|
60
67
|
env: options.env || process.env,
|
|
@@ -62,11 +69,12 @@ function spawnAsync(command, args, options = {}) {
|
|
|
62
69
|
stdio: piped ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
63
70
|
windowsHide: true,
|
|
64
71
|
});
|
|
65
|
-
const stdout = new BoundedCapture(
|
|
66
|
-
const stderr = new BoundedCapture(
|
|
72
|
+
const stdout = new BoundedCapture(limit);
|
|
73
|
+
const stderr = new BoundedCapture(limit);
|
|
67
74
|
let timedOut = false;
|
|
68
75
|
let settled = false;
|
|
69
|
-
let
|
|
76
|
+
let escalation = null;
|
|
77
|
+
let stopping;
|
|
70
78
|
if (piped) {
|
|
71
79
|
child.stdout.on('data', (chunk) => {
|
|
72
80
|
stdout.append(chunk);
|
|
@@ -77,67 +85,91 @@ function spawnAsync(command, args, options = {}) {
|
|
|
77
85
|
options.onStderr?.(chunk);
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
const stop = (signal = 'SIGTERM') => {
|
|
89
|
+
if (stopping || settled)
|
|
90
|
+
return;
|
|
91
|
+
stopping = Promise.resolve().then(() => (0, tool_command_1.terminateProcess)(child, signal));
|
|
92
|
+
if (process.platform === 'win32') {
|
|
93
|
+
void finish(null, signal);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
stopping.catch((cause) => {
|
|
97
|
+
void finish(null, null, cause);
|
|
98
|
+
});
|
|
99
|
+
escalation = setTimeout(() => {
|
|
100
|
+
stopping = (0, tool_command_1.terminateProcess)(child, 'SIGKILL');
|
|
101
|
+
stopping.catch((cause) => {
|
|
102
|
+
void finish(null, null, cause);
|
|
103
|
+
});
|
|
104
|
+
}, options.stopGraceMs || DefaultStopGraceMs);
|
|
105
|
+
escalation.unref?.();
|
|
85
106
|
}
|
|
86
107
|
};
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
process.once('SIGINT',
|
|
90
|
-
process.once('SIGTERM',
|
|
108
|
+
const interrupt = () => stop('SIGINT');
|
|
109
|
+
const terminate = () => stop('SIGTERM');
|
|
110
|
+
process.once('SIGINT', interrupt);
|
|
111
|
+
process.once('SIGTERM', terminate);
|
|
91
112
|
const timeout = options.timeoutMs
|
|
92
113
|
? setTimeout(() => {
|
|
93
114
|
timedOut = true;
|
|
94
|
-
|
|
115
|
+
stop('SIGTERM');
|
|
95
116
|
}, options.timeoutMs)
|
|
96
117
|
: null;
|
|
97
118
|
timeout?.unref?.();
|
|
98
119
|
const cleanup = () => {
|
|
99
120
|
if (timeout)
|
|
100
121
|
clearTimeout(timeout);
|
|
101
|
-
if (
|
|
102
|
-
clearTimeout(
|
|
103
|
-
process.removeListener('SIGINT',
|
|
104
|
-
process.removeListener('SIGTERM',
|
|
122
|
+
if (escalation)
|
|
123
|
+
clearTimeout(escalation);
|
|
124
|
+
process.removeListener('SIGINT', interrupt);
|
|
125
|
+
process.removeListener('SIGTERM', terminate);
|
|
105
126
|
options.signal?.removeEventListener('abort', abort);
|
|
106
127
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
const finish = async (code, signal, cause) => {
|
|
129
|
+
try {
|
|
130
|
+
await stopping;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
cause = error;
|
|
134
|
+
}
|
|
113
135
|
if (settled)
|
|
114
136
|
return;
|
|
115
137
|
settled = true;
|
|
116
138
|
cleanup();
|
|
117
|
-
|
|
118
|
-
cause
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
if (cause)
|
|
140
|
+
reject(new errors_1.HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: ${cause.message}`, {
|
|
141
|
+
cause,
|
|
142
|
+
operation: options.operation || 'spawn',
|
|
143
|
+
}));
|
|
144
|
+
else
|
|
145
|
+
resolve({
|
|
146
|
+
code: code === null ? 1 : code,
|
|
147
|
+
signal,
|
|
148
|
+
stderr: stderr.toString(),
|
|
149
|
+
stdout: stdout.toString(),
|
|
150
|
+
timedOut,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
child.once('error', (cause) => {
|
|
154
|
+
void finish(null, null, cause);
|
|
121
155
|
});
|
|
122
|
-
// `close` runs after stdout/stderr have closed, so captured diagnostics are
|
|
123
|
-
// complete. `exit` can fire while pipe data is still pending.
|
|
124
156
|
child.once('close', (code, signal) => {
|
|
125
|
-
|
|
126
|
-
return;
|
|
127
|
-
settled = true;
|
|
128
|
-
cleanup();
|
|
129
|
-
resolve({
|
|
130
|
-
code: code === null ? 1 : code,
|
|
131
|
-
signal,
|
|
132
|
-
stderr: stderr.toString(),
|
|
133
|
-
stdout: stdout.toString(),
|
|
134
|
-
timedOut,
|
|
135
|
-
});
|
|
157
|
+
void finish(code, signal);
|
|
136
158
|
});
|
|
159
|
+
const abort = () => stop('SIGTERM');
|
|
160
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
161
|
+
if (options.signal?.aborted)
|
|
162
|
+
abort();
|
|
137
163
|
});
|
|
138
164
|
}
|
|
139
165
|
function startManagedProcess(command, args, options = {}) {
|
|
140
|
-
|
|
166
|
+
if (options.signal?.aborted) {
|
|
167
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: operation aborted.`, {
|
|
168
|
+
cause: options.signal.reason,
|
|
169
|
+
operation: options.operation || 'spawn',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
141
173
|
const piped = options.stdio !== 'inherit';
|
|
142
174
|
const child = (0, cross_spawn_1.default)(command, args, {
|
|
143
175
|
cwd: options.cwd,
|
|
@@ -146,11 +178,14 @@ function startManagedProcess(command, args, options = {}) {
|
|
|
146
178
|
stdio: piped ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
147
179
|
windowsHide: true,
|
|
148
180
|
});
|
|
149
|
-
const stdout = new BoundedCapture(
|
|
150
|
-
const stderr = new BoundedCapture(
|
|
151
|
-
let
|
|
181
|
+
const stdout = new BoundedCapture(limit);
|
|
182
|
+
const stderr = new BoundedCapture(limit);
|
|
183
|
+
let failure = null;
|
|
152
184
|
let closed = false;
|
|
153
|
-
let
|
|
185
|
+
let stopped = false;
|
|
186
|
+
let stopping;
|
|
187
|
+
let stopPromise;
|
|
188
|
+
let finish;
|
|
154
189
|
if (piped) {
|
|
155
190
|
child.stdout.on('data', (chunk) => {
|
|
156
191
|
stdout.append(chunk);
|
|
@@ -161,32 +196,42 @@ function startManagedProcess(command, args, options = {}) {
|
|
|
161
196
|
options.onStderr?.(chunk);
|
|
162
197
|
});
|
|
163
198
|
}
|
|
164
|
-
const
|
|
165
|
-
void stop('SIGINT');
|
|
199
|
+
const interrupt = () => {
|
|
200
|
+
void stop('SIGINT').catch(() => { });
|
|
166
201
|
};
|
|
167
|
-
const
|
|
168
|
-
void stop('SIGTERM');
|
|
202
|
+
const terminate = () => {
|
|
203
|
+
void stop('SIGTERM').catch(() => { });
|
|
169
204
|
};
|
|
170
205
|
const abort = () => {
|
|
171
|
-
void stop('SIGTERM');
|
|
206
|
+
void stop('SIGTERM').catch(() => { });
|
|
172
207
|
};
|
|
173
208
|
const cleanup = () => {
|
|
174
|
-
process.removeListener('SIGINT',
|
|
175
|
-
process.removeListener('SIGTERM',
|
|
209
|
+
process.removeListener('SIGINT', interrupt);
|
|
210
|
+
process.removeListener('SIGTERM', terminate);
|
|
176
211
|
options.signal?.removeEventListener('abort', abort);
|
|
177
212
|
};
|
|
178
213
|
const completion = new Promise((resolve, reject) => {
|
|
179
214
|
child.once('error', (cause) => {
|
|
180
|
-
|
|
215
|
+
failure = new errors_1.HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: ${cause.message}`, {
|
|
181
216
|
cause,
|
|
182
217
|
operation: options.operation || 'spawn',
|
|
183
218
|
});
|
|
184
219
|
});
|
|
185
|
-
|
|
220
|
+
finish = async (code, signal) => {
|
|
221
|
+
try {
|
|
222
|
+
await stopping;
|
|
223
|
+
}
|
|
224
|
+
catch (cause) {
|
|
225
|
+
failure = new errors_1.HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot stop ${command}: ${cause.message}`, {
|
|
226
|
+
cause, operation: options.operation || 'spawn',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (closed)
|
|
230
|
+
return;
|
|
186
231
|
closed = true;
|
|
187
232
|
cleanup();
|
|
188
|
-
if (
|
|
189
|
-
reject(
|
|
233
|
+
if (failure)
|
|
234
|
+
reject(failure);
|
|
190
235
|
else
|
|
191
236
|
resolve({
|
|
192
237
|
code: code === null ? 1 : code,
|
|
@@ -195,32 +240,59 @@ function startManagedProcess(command, args, options = {}) {
|
|
|
195
240
|
stdout: stdout.toString(),
|
|
196
241
|
timedOut: false,
|
|
197
242
|
});
|
|
243
|
+
};
|
|
244
|
+
child.once('close', (code, signal) => {
|
|
245
|
+
void finish(code, signal);
|
|
198
246
|
});
|
|
199
247
|
});
|
|
200
248
|
// A readiness probe may be the first consumer. Keep early spawn failures from
|
|
201
249
|
// becoming unhandled rejections while the probe is still polling.
|
|
202
250
|
completion.catch(() => { });
|
|
203
251
|
async function stop(signal = 'SIGTERM', graceMs = DefaultStopGraceMs) {
|
|
252
|
+
if (stopPromise)
|
|
253
|
+
return stopPromise;
|
|
204
254
|
if (closed)
|
|
205
255
|
return completion;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
256
|
+
stopped = true;
|
|
257
|
+
stopping = Promise.resolve().then(() => (0, tool_command_1.terminateProcess)(child, signal));
|
|
258
|
+
stopPromise = (async () => {
|
|
259
|
+
if (process.platform === 'win32') {
|
|
260
|
+
void finish(null, signal);
|
|
261
|
+
return completion;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await stopping;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
await finish(null, signal);
|
|
268
|
+
return completion;
|
|
269
|
+
}
|
|
270
|
+
let timer;
|
|
271
|
+
await Promise.race([
|
|
272
|
+
completion.catch(() => undefined),
|
|
273
|
+
new Promise((resolve) => {
|
|
274
|
+
timer = setTimeout(resolve, graceMs);
|
|
275
|
+
timer.unref?.();
|
|
276
|
+
}),
|
|
277
|
+
]);
|
|
278
|
+
if (timer)
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
if (!closed) {
|
|
281
|
+
stopping = (0, tool_command_1.terminateProcess)(child, 'SIGKILL');
|
|
282
|
+
try {
|
|
283
|
+
await stopping;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
await finish(null, signal);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return completion.catch(() => undefined);
|
|
290
|
+
})();
|
|
291
|
+
stopPromise.catch(() => { });
|
|
292
|
+
return stopPromise;
|
|
221
293
|
}
|
|
222
|
-
process.once('SIGINT',
|
|
223
|
-
process.once('SIGTERM',
|
|
294
|
+
process.once('SIGINT', interrupt);
|
|
295
|
+
process.once('SIGTERM', terminate);
|
|
224
296
|
if (options.signal?.aborted)
|
|
225
297
|
abort();
|
|
226
298
|
else
|
|
@@ -231,6 +303,6 @@ function startManagedProcess(command, args, options = {}) {
|
|
|
231
303
|
getStderr: () => stderr.toString(),
|
|
232
304
|
getStdout: () => stdout.toString(),
|
|
233
305
|
stop,
|
|
234
|
-
wasStopped: () =>
|
|
306
|
+
wasStopped: () => stopped,
|
|
235
307
|
};
|
|
236
308
|
}
|
package/build/tools.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.resolveHarmonyEmulator = resolveHarmonyEmulator;
|
|
|
10
10
|
exports.resolveHarmonyToolchain = resolveHarmonyToolchain;
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const tool_command_1 = require("@expo-harmony/expo-modules-autolinking/tool-command");
|
|
13
14
|
const build_descriptor_1 = require("@expo-harmony/prebuild-config/build-descriptor");
|
|
14
15
|
const check_1 = require("@expo-harmony/prebuild-config/check");
|
|
15
16
|
const errors_1 = require("./errors");
|
|
@@ -99,38 +100,35 @@ function resolveHarmonyToolchain() {
|
|
|
99
100
|
const layouts = sdkHome ? devEcoLayouts(sdkHome) : [];
|
|
100
101
|
let ohpm;
|
|
101
102
|
if (env.HARMONY_OHPM) {
|
|
102
|
-
ohpm = {
|
|
103
|
+
ohpm = { ...(0, tool_command_1.resolveHarmonyCommand)('ohpm', [], env), source: 'override' };
|
|
103
104
|
}
|
|
104
105
|
else {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const entry = layouts.map(layout => ({
|
|
107
|
+
node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_OHPM_NODE || env.HARMONY_NODE }),
|
|
108
|
+
script: node_path_1.default.join(layout.toolsRoot, 'ohpm', 'bin', 'pm-cli.js'),
|
|
109
|
+
})).find(candidate => candidate.node && node_fs_1.default.existsSync(candidate.script));
|
|
110
|
+
const command = existingFile(layouts.map(layout => node_path_1.default.join(layout.toolsRoot, 'ohpm', 'bin', platform === 'win32' ? 'ohpm.bat' : 'ohpm')));
|
|
111
|
+
ohpm = entry
|
|
112
|
+
? { args: [entry.script], command: entry.node.command, source: 'deveco' }
|
|
113
|
+
: command
|
|
114
|
+
? { args: [], command, source: 'deveco' }
|
|
115
|
+
: { args: [], command: platform === 'win32' ? 'ohpm.bat' : 'ohpm', source: 'path' };
|
|
109
116
|
}
|
|
110
117
|
let hvigor;
|
|
111
118
|
if (env.HARMONY_HVIGORW) {
|
|
112
|
-
|
|
113
|
-
hvigor = {
|
|
114
|
-
args: [env.HARMONY_HVIGORW],
|
|
115
|
-
command: env.HARMONY_NODE || process.execPath,
|
|
116
|
-
source: 'override',
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
hvigor = { args: [], command: env.HARMONY_HVIGORW, source: 'override' };
|
|
121
|
-
}
|
|
119
|
+
hvigor = { ...(0, tool_command_1.resolveHarmonyCommand)('hvigorw', [], env), source: 'override' };
|
|
122
120
|
}
|
|
123
121
|
else {
|
|
124
|
-
const
|
|
125
|
-
layout,
|
|
126
|
-
node: devEcoNode(layout, platform, env),
|
|
122
|
+
const entry = layouts.map(layout => ({
|
|
123
|
+
node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_HVIGOR_NODE || env.HARMONY_NODE }),
|
|
127
124
|
script: node_path_1.default.join(layout.toolsRoot, 'hvigor', 'bin', 'hvigorw.js'),
|
|
128
125
|
})).find(candidate => candidate.node && node_fs_1.default.existsSync(candidate.script));
|
|
129
|
-
hvigor =
|
|
130
|
-
? { args: [
|
|
126
|
+
hvigor = entry
|
|
127
|
+
? { args: [entry.script], command: entry.node.command, source: 'deveco' }
|
|
131
128
|
: { args: [], command: platform === 'win32' ? 'hvigorw.bat' : 'hvigorw', source: 'path' };
|
|
132
129
|
}
|
|
133
130
|
const toolsRoot = layouts.find(layout => (hvigor.args[0]?.startsWith(`${layout.toolsRoot}${node_path_1.default.sep}`)
|
|
131
|
+
|| ohpm.args[0]?.startsWith(`${layout.toolsRoot}${node_path_1.default.sep}`)
|
|
134
132
|
|| ohpm.command.startsWith(`${layout.toolsRoot}${node_path_1.default.sep}`)))?.toolsRoot || null;
|
|
135
133
|
let hdc;
|
|
136
134
|
if (env.HARMONY_HDC) {
|
|
@@ -151,10 +149,14 @@ function resolveHarmonyToolchain() {
|
|
|
151
149
|
function createHarmonyToolchainEnv(toolchain = resolveHarmonyToolchain()) {
|
|
152
150
|
return {
|
|
153
151
|
...process.env,
|
|
154
|
-
HARMONY_OHPM: toolchain.ohpm.command,
|
|
155
|
-
|
|
152
|
+
HARMONY_OHPM: toolchain.ohpm.args[0] || toolchain.ohpm.command,
|
|
153
|
+
HARMONY_OHPM_NODE: toolchain.ohpm.args.length > 0 ? toolchain.ohpm.command : undefined,
|
|
154
|
+
// Script-based tools must retain both their script and Node executable.
|
|
156
155
|
HARMONY_HVIGORW: toolchain.hvigor.args[0] || toolchain.hvigor.command,
|
|
157
|
-
|
|
156
|
+
HARMONY_HVIGOR_NODE: toolchain.hvigor.args.length > 0 ? toolchain.hvigor.command : undefined,
|
|
157
|
+
...(toolchain.hvigor.args.length > 0
|
|
158
|
+
? { HARMONY_NODE: toolchain.hvigor.command }
|
|
159
|
+
: toolchain.ohpm.args.length > 0 ? { HARMONY_NODE: toolchain.ohpm.command } : {}),
|
|
158
160
|
...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
|
|
159
161
|
? { DEVECO_SDK_HOME: toolchain.sdkHome }
|
|
160
162
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo-harmony/cli",
|
|
3
|
-
"version": "55.0.26-harmony.
|
|
3
|
+
"version": "55.0.26-harmony.5",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"react-native",
|
|
6
6
|
"expo",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@expo-harmony/config-plugins": "55.0.10-harmony.2",
|
|
40
|
-
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.
|
|
41
|
-
"@expo-harmony/prebuild-config": "55.0.0-harmony.
|
|
40
|
+
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.3",
|
|
41
|
+
"@expo-harmony/prebuild-config": "55.0.0-harmony.4",
|
|
42
42
|
"@expo/config": "55.0.17",
|
|
43
43
|
"cross-spawn": "^7.0.6",
|
|
44
44
|
"json5": "2.2.3"
|
package/src/cli.ts
CHANGED
|
@@ -90,6 +90,9 @@ async function runAsync(
|
|
|
90
90
|
argv: string[] = process.argv.slice(2),
|
|
91
91
|
io: Pick<Console, 'error' | 'log' | 'warn'> = console
|
|
92
92
|
): Promise<number> {
|
|
93
|
+
process.env.EXPO_HARMONY = '1';
|
|
94
|
+
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
95
|
+
|
|
93
96
|
const invocation = parseInvocation(argv);
|
|
94
97
|
if (invocation.command === 'help') {
|
|
95
98
|
io.log(Help);
|
package/src/doctor/doctor.ts
CHANGED
|
@@ -208,6 +208,8 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
208
208
|
tools.unshift(['hdc', toolchain.hdc, ['-v'], unavailableToolStatus]);
|
|
209
209
|
}
|
|
210
210
|
for (const [id, tool, versionArgs, unavailableStatus] of tools) {
|
|
211
|
+
const command = [tool.command, ...tool.args].map(value => JSON.stringify(value)).join(' ');
|
|
212
|
+
|
|
211
213
|
try {
|
|
212
214
|
const result = await spawnAsync(tool.command, [...tool.args, ...versionArgs], {
|
|
213
215
|
capture: true,
|
|
@@ -215,11 +217,12 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
215
217
|
operation: `doctor-${id}`,
|
|
216
218
|
timeoutMs: 10_000,
|
|
217
219
|
});
|
|
220
|
+
|
|
218
221
|
checks.push(result.code === 0 && !result.timedOut
|
|
219
|
-
? check(id, 'pass', `${
|
|
220
|
-
: check(id, unavailableStatus, `${
|
|
222
|
+
? check(id, 'pass', `${command} is available through ${tool.source}.`)
|
|
223
|
+
: check(id, unavailableStatus, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
|
|
221
224
|
} catch {
|
|
222
|
-
checks.push(check(id, unavailableStatus, `${
|
|
225
|
+
checks.push(check(id, unavailableStatus, `${command} is unavailable; generation remains available.`));
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
|
package/src/prebuild/check.ts
CHANGED
|
@@ -40,7 +40,28 @@ function mirrorRoot(temp, project) {
|
|
|
40
40
|
return path.join(temp, 'filesystem', volume, ...segments);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
43
|
+
async function linkModuleEntryAsync(
|
|
44
|
+
source: string,
|
|
45
|
+
target: string,
|
|
46
|
+
entry: fs.Dirent
|
|
47
|
+
) {
|
|
48
|
+
const directory = entry.isSymbolicLink()
|
|
49
|
+
? (await fs.promises.stat(source)).isDirectory()
|
|
50
|
+
: entry.isDirectory();
|
|
51
|
+
|
|
52
|
+
if (process.platform === 'win32' && !directory) {
|
|
53
|
+
await fs.promises.copyFile(source, target);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await fs.promises.symlink(
|
|
58
|
+
path.resolve(source),
|
|
59
|
+
target,
|
|
60
|
+
directory ? process.platform === 'win32' ? 'junction' : 'dir' : 'file'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function linkModulesAsync(source: string, target: string) {
|
|
44
65
|
await fs.promises.mkdir(target, { recursive: true });
|
|
45
66
|
|
|
46
67
|
for (const entry of await fs.promises.readdir(source, { withFileTypes: true })) {
|
|
@@ -49,25 +70,18 @@ async function linkModulesAsync(source, target) {
|
|
|
49
70
|
|
|
50
71
|
if (entry.name.startsWith('@') && entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
51
72
|
await fs.promises.mkdir(to);
|
|
73
|
+
|
|
52
74
|
for (const child of await fs.promises.readdir(from, { withFileTypes: true })) {
|
|
53
|
-
await
|
|
75
|
+
await linkModuleEntryAsync(
|
|
54
76
|
path.join(from, child.name),
|
|
55
77
|
path.join(to, child.name),
|
|
56
|
-
|
|
57
|
-
? 'junction'
|
|
58
|
-
: child.isDirectory() ? 'dir' : 'file'
|
|
78
|
+
child
|
|
59
79
|
);
|
|
60
80
|
}
|
|
61
81
|
continue;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
await
|
|
65
|
-
from,
|
|
66
|
-
to,
|
|
67
|
-
process.platform === 'win32' && (entry.isDirectory() || entry.isSymbolicLink())
|
|
68
|
-
? 'junction'
|
|
69
|
-
: entry.isDirectory() ? 'dir' : 'file'
|
|
70
|
-
);
|
|
84
|
+
await linkModuleEntryAsync(from, to, entry);
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
|
|
@@ -126,8 +140,8 @@ async function copyAsync(
|
|
|
126
140
|
// Keep a real node_modules directory in the isolated project and link its
|
|
127
141
|
// entries. Dependency scanners then retain the isolated lexical package path
|
|
128
142
|
// (including scoped packages) instead of collapsing the entire node_modules
|
|
129
|
-
// root to the source project's realpath.
|
|
130
|
-
//
|
|
143
|
+
// root to the source project's realpath. Package directories stay linked;
|
|
144
|
+
// Windows copies loose files to avoid requiring file-symlink privileges.
|
|
131
145
|
await linkModulesAsync(modules, path.join(target, 'node_modules'));
|
|
132
146
|
await stageAsync(project, target, temp);
|
|
133
147
|
}
|
package/src/process.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
|
+
import { terminateProcess } from '@expo-harmony/expo-modules-autolinking/tool-command';
|
|
2
3
|
|
|
3
4
|
import { HarmonyCliError } from './errors';
|
|
4
5
|
|
|
@@ -76,8 +77,15 @@ function formatDiagnostics(result: Pick<ProcessResult, 'stderr' | 'stdout'>, lim
|
|
|
76
77
|
|
|
77
78
|
function spawnAsync(command: string, args: string[], options: ProcessOptions = {}): Promise<ProcessResult> {
|
|
78
79
|
return new Promise<ProcessResult>((resolve, reject) => {
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
throw new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: operation aborted.`, {
|
|
82
|
+
cause: options.signal.reason,
|
|
83
|
+
operation: options.operation || 'spawn',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
const piped = Boolean(options.capture || options.onStdout || options.onStderr);
|
|
80
|
-
const
|
|
88
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
81
89
|
const child = spawn(command, args, {
|
|
82
90
|
cwd: options.cwd,
|
|
83
91
|
env: options.env || process.env,
|
|
@@ -86,11 +94,12 @@ function spawnAsync(command: string, args: string[], options: ProcessOptions = {
|
|
|
86
94
|
windowsHide: true,
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
const stdout = new BoundedCapture(
|
|
90
|
-
const stderr = new BoundedCapture(
|
|
97
|
+
const stdout = new BoundedCapture(limit);
|
|
98
|
+
const stderr = new BoundedCapture(limit);
|
|
91
99
|
let timedOut = false;
|
|
92
100
|
let settled = false;
|
|
93
|
-
let
|
|
101
|
+
let escalation: NodeJS.Timeout | null = null;
|
|
102
|
+
let stopping: Promise<void> | undefined;
|
|
94
103
|
|
|
95
104
|
if (piped) {
|
|
96
105
|
child.stdout.on('data', (chunk) => {
|
|
@@ -103,72 +112,96 @@ function spawnAsync(command: string, args: string[], options: ProcessOptions = {
|
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
const stop = (signal: NodeJS.Signals = 'SIGTERM') => {
|
|
116
|
+
if (stopping || settled) return;
|
|
117
|
+
|
|
118
|
+
stopping = Promise.resolve().then(() => terminateProcess(child, signal));
|
|
119
|
+
if (process.platform === 'win32') {
|
|
120
|
+
void finish(null, signal);
|
|
121
|
+
} else {
|
|
122
|
+
stopping.catch((cause) => {
|
|
123
|
+
void finish(null, null, cause);
|
|
124
|
+
});
|
|
125
|
+
escalation = setTimeout(
|
|
126
|
+
() => {
|
|
127
|
+
stopping = terminateProcess(child, 'SIGKILL');
|
|
128
|
+
stopping.catch((cause) => {
|
|
129
|
+
void finish(null, null, cause);
|
|
130
|
+
});
|
|
131
|
+
},
|
|
111
132
|
options.stopGraceMs || DefaultStopGraceMs
|
|
112
133
|
);
|
|
113
|
-
|
|
134
|
+
escalation.unref?.();
|
|
114
135
|
}
|
|
115
136
|
};
|
|
116
137
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
process.once('SIGINT',
|
|
120
|
-
process.once('SIGTERM',
|
|
138
|
+
const interrupt = () => stop('SIGINT');
|
|
139
|
+
const terminate = () => stop('SIGTERM');
|
|
140
|
+
process.once('SIGINT', interrupt);
|
|
141
|
+
process.once('SIGTERM', terminate);
|
|
121
142
|
|
|
122
143
|
const timeout = options.timeoutMs
|
|
123
144
|
? setTimeout(() => {
|
|
124
145
|
timedOut = true;
|
|
125
|
-
|
|
146
|
+
stop('SIGTERM');
|
|
126
147
|
}, options.timeoutMs)
|
|
127
148
|
: null;
|
|
128
149
|
timeout?.unref?.();
|
|
129
150
|
|
|
130
151
|
const cleanup = () => {
|
|
131
152
|
if (timeout) clearTimeout(timeout);
|
|
132
|
-
if (
|
|
133
|
-
process.removeListener('SIGINT',
|
|
134
|
-
process.removeListener('SIGTERM',
|
|
153
|
+
if (escalation) clearTimeout(escalation);
|
|
154
|
+
process.removeListener('SIGINT', interrupt);
|
|
155
|
+
process.removeListener('SIGTERM', terminate);
|
|
135
156
|
options.signal?.removeEventListener('abort', abort);
|
|
136
157
|
};
|
|
137
158
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
const finish = async (code, signal, cause?) => {
|
|
160
|
+
try {
|
|
161
|
+
await stopping;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
cause = error;
|
|
164
|
+
}
|
|
141
165
|
|
|
142
|
-
child.once('error', (cause) => {
|
|
143
166
|
if (settled) return;
|
|
144
167
|
settled = true;
|
|
145
168
|
cleanup();
|
|
146
169
|
|
|
147
|
-
reject(new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot
|
|
170
|
+
if (cause) reject(new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: ${cause.message}`, {
|
|
148
171
|
cause,
|
|
149
172
|
operation: options.operation || 'spawn',
|
|
150
173
|
}));
|
|
151
|
-
|
|
152
|
-
// `close` runs after stdout/stderr have closed, so captured diagnostics are
|
|
153
|
-
// complete. `exit` can fire while pipe data is still pending.
|
|
154
|
-
child.once('close', (code, signal) => {
|
|
155
|
-
if (settled) return;
|
|
156
|
-
settled = true;
|
|
157
|
-
cleanup();
|
|
158
|
-
|
|
159
|
-
resolve({
|
|
174
|
+
else resolve({
|
|
160
175
|
code: code === null ? 1 : code,
|
|
161
176
|
signal,
|
|
162
177
|
stderr: stderr.toString(),
|
|
163
178
|
stdout: stdout.toString(),
|
|
164
179
|
timedOut,
|
|
165
180
|
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
child.once('error', (cause) => {
|
|
184
|
+
void finish(null, null, cause);
|
|
166
185
|
});
|
|
186
|
+
child.once('close', (code, signal) => {
|
|
187
|
+
void finish(code, signal);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const abort = () => stop('SIGTERM');
|
|
191
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
192
|
+
if (options.signal?.aborted) abort();
|
|
167
193
|
});
|
|
168
194
|
}
|
|
169
195
|
|
|
170
196
|
function startManagedProcess(command: string, args: string[], options: ProcessOptions = {}) {
|
|
171
|
-
|
|
197
|
+
if (options.signal?.aborted) {
|
|
198
|
+
throw new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: operation aborted.`, {
|
|
199
|
+
cause: options.signal.reason,
|
|
200
|
+
operation: options.operation || 'spawn',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
172
205
|
const piped = options.stdio !== 'inherit';
|
|
173
206
|
const child = spawn(command, args, {
|
|
174
207
|
cwd: options.cwd,
|
|
@@ -178,11 +211,14 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
178
211
|
windowsHide: true,
|
|
179
212
|
});
|
|
180
213
|
|
|
181
|
-
const stdout = new BoundedCapture(
|
|
182
|
-
const stderr = new BoundedCapture(
|
|
183
|
-
let
|
|
214
|
+
const stdout = new BoundedCapture(limit);
|
|
215
|
+
const stderr = new BoundedCapture(limit);
|
|
216
|
+
let failure: HarmonyCliError | null = null;
|
|
184
217
|
let closed = false;
|
|
185
|
-
let
|
|
218
|
+
let stopped = false;
|
|
219
|
+
let stopping: Promise<void> | undefined;
|
|
220
|
+
let stopPromise: Promise<ProcessResult | undefined> | undefined;
|
|
221
|
+
let finish: (code: number | null, signal: NodeJS.Signals | null) => Promise<void>;
|
|
186
222
|
|
|
187
223
|
if (piped) {
|
|
188
224
|
child.stdout.on('data', (chunk) => {
|
|
@@ -195,34 +231,43 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
195
231
|
});
|
|
196
232
|
}
|
|
197
233
|
|
|
198
|
-
const
|
|
199
|
-
void stop('SIGINT');
|
|
234
|
+
const interrupt = () => {
|
|
235
|
+
void stop('SIGINT').catch(() => {});
|
|
200
236
|
};
|
|
201
|
-
const
|
|
202
|
-
void stop('SIGTERM');
|
|
237
|
+
const terminate = () => {
|
|
238
|
+
void stop('SIGTERM').catch(() => {});
|
|
203
239
|
};
|
|
204
240
|
const abort = () => {
|
|
205
|
-
void stop('SIGTERM');
|
|
241
|
+
void stop('SIGTERM').catch(() => {});
|
|
206
242
|
};
|
|
207
243
|
|
|
208
244
|
const cleanup = () => {
|
|
209
|
-
process.removeListener('SIGINT',
|
|
210
|
-
process.removeListener('SIGTERM',
|
|
245
|
+
process.removeListener('SIGINT', interrupt);
|
|
246
|
+
process.removeListener('SIGTERM', terminate);
|
|
211
247
|
options.signal?.removeEventListener('abort', abort);
|
|
212
248
|
};
|
|
213
249
|
|
|
214
250
|
const completion = new Promise<ProcessResult>((resolve, reject) => {
|
|
215
251
|
child.once('error', (cause) => {
|
|
216
|
-
|
|
252
|
+
failure = new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: ${cause.message}`, {
|
|
217
253
|
cause,
|
|
218
254
|
operation: options.operation || 'spawn',
|
|
219
255
|
});
|
|
220
256
|
});
|
|
221
|
-
|
|
257
|
+
finish = async (code, signal) => {
|
|
258
|
+
try {
|
|
259
|
+
await stopping;
|
|
260
|
+
} catch (cause) {
|
|
261
|
+
failure = new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot stop ${command}: ${cause.message}`, {
|
|
262
|
+
cause, operation: options.operation || 'spawn',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (closed) return;
|
|
222
267
|
closed = true;
|
|
223
268
|
cleanup();
|
|
224
269
|
|
|
225
|
-
if (
|
|
270
|
+
if (failure) reject(failure);
|
|
226
271
|
else resolve({
|
|
227
272
|
code: code === null ? 1 : code,
|
|
228
273
|
signal,
|
|
@@ -230,6 +275,10 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
230
275
|
stdout: stdout.toString(),
|
|
231
276
|
timedOut: false,
|
|
232
277
|
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
child.once('close', (code, signal) => {
|
|
281
|
+
void finish(code, signal);
|
|
233
282
|
});
|
|
234
283
|
});
|
|
235
284
|
// A readiness probe may be the first consumer. Keep early spawn failures from
|
|
@@ -237,28 +286,53 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
237
286
|
completion.catch(() => {});
|
|
238
287
|
|
|
239
288
|
async function stop(signal: NodeJS.Signals = 'SIGTERM', graceMs = DefaultStopGraceMs) {
|
|
289
|
+
if (stopPromise) return stopPromise;
|
|
240
290
|
if (closed) return completion;
|
|
241
291
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
292
|
+
stopped = true;
|
|
293
|
+
stopping = Promise.resolve().then(() => terminateProcess(child, signal));
|
|
294
|
+
stopPromise = (async () => {
|
|
295
|
+
if (process.platform === 'win32') {
|
|
296
|
+
void finish(null, signal);
|
|
297
|
+
return completion;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await stopping;
|
|
302
|
+
} catch {
|
|
303
|
+
await finish(null, signal);
|
|
304
|
+
return completion;
|
|
305
|
+
}
|
|
245
306
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
let timer: NodeJS.Timeout | undefined;
|
|
308
|
+
|
|
309
|
+
await Promise.race([
|
|
310
|
+
completion.catch(() => undefined),
|
|
311
|
+
new Promise((resolve) => {
|
|
312
|
+
timer = setTimeout(resolve, graceMs);
|
|
313
|
+
timer.unref?.();
|
|
314
|
+
}),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
if (timer) clearTimeout(timer);
|
|
318
|
+
if (!closed) {
|
|
319
|
+
stopping = terminateProcess(child, 'SIGKILL');
|
|
320
|
+
try {
|
|
321
|
+
await stopping;
|
|
322
|
+
} catch {
|
|
323
|
+
await finish(null, signal);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
253
326
|
|
|
254
|
-
|
|
255
|
-
|
|
327
|
+
return completion.catch(() => undefined);
|
|
328
|
+
})();
|
|
329
|
+
stopPromise.catch(() => {});
|
|
256
330
|
|
|
257
|
-
return
|
|
331
|
+
return stopPromise;
|
|
258
332
|
}
|
|
259
333
|
|
|
260
|
-
process.once('SIGINT',
|
|
261
|
-
process.once('SIGTERM',
|
|
334
|
+
process.once('SIGINT', interrupt);
|
|
335
|
+
process.once('SIGTERM', terminate);
|
|
262
336
|
if (options.signal?.aborted) abort();
|
|
263
337
|
else options.signal?.addEventListener('abort', abort, { once: true });
|
|
264
338
|
|
|
@@ -268,7 +342,7 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
268
342
|
getStderr: () => stderr.toString(),
|
|
269
343
|
getStdout: () => stdout.toString(),
|
|
270
344
|
stop,
|
|
271
|
-
wasStopped: () =>
|
|
345
|
+
wasStopped: () => stopped,
|
|
272
346
|
};
|
|
273
347
|
}
|
|
274
348
|
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { resolveHarmonyCommand } from '@expo-harmony/expo-modules-autolinking/tool-command';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
resolveHarmonyBuildPath,
|
|
@@ -141,43 +142,43 @@ function resolveHarmonyToolchain(): HarmonyToolchain {
|
|
|
141
142
|
|
|
142
143
|
let ohpm: HarmonyTool;
|
|
143
144
|
if (env.HARMONY_OHPM) {
|
|
144
|
-
ohpm = {
|
|
145
|
+
ohpm = { ...resolveHarmonyCommand('ohpm', [], env), source: 'override' };
|
|
145
146
|
} else {
|
|
146
|
-
const
|
|
147
|
+
const entry = layouts.map(layout => ({
|
|
148
|
+
node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_OHPM_NODE || env.HARMONY_NODE }),
|
|
149
|
+
script: path.join(layout.toolsRoot, 'ohpm', 'bin', 'pm-cli.js'),
|
|
150
|
+
})).find(candidate => candidate.node && fs.existsSync(candidate.script));
|
|
151
|
+
const command = existingFile(layouts.map(layout => path.join(
|
|
147
152
|
layout.toolsRoot,
|
|
148
153
|
'ohpm',
|
|
149
154
|
'bin',
|
|
150
155
|
platform === 'win32' ? 'ohpm.bat' : 'ohpm'
|
|
151
156
|
)));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
|
|
158
|
+
ohpm = entry
|
|
159
|
+
? { args: [entry.script], command: entry.node.command, source: 'deveco' }
|
|
160
|
+
: command
|
|
161
|
+
? { args: [], command, source: 'deveco' }
|
|
162
|
+
: { args: [], command: platform === 'win32' ? 'ohpm.bat' : 'ohpm', source: 'path' };
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
let hvigor: HarmonyTool;
|
|
158
166
|
if (env.HARMONY_HVIGORW) {
|
|
159
|
-
|
|
160
|
-
hvigor = {
|
|
161
|
-
args: [env.HARMONY_HVIGORW],
|
|
162
|
-
command: env.HARMONY_NODE || process.execPath,
|
|
163
|
-
source: 'override',
|
|
164
|
-
};
|
|
165
|
-
} else {
|
|
166
|
-
hvigor = { args: [], command: env.HARMONY_HVIGORW, source: 'override' };
|
|
167
|
-
}
|
|
167
|
+
hvigor = { ...resolveHarmonyCommand('hvigorw', [], env), source: 'override' };
|
|
168
168
|
} else {
|
|
169
|
-
const
|
|
170
|
-
layout,
|
|
171
|
-
node: devEcoNode(layout, platform, env),
|
|
169
|
+
const entry = layouts.map(layout => ({
|
|
170
|
+
node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_HVIGOR_NODE || env.HARMONY_NODE }),
|
|
172
171
|
script: path.join(layout.toolsRoot, 'hvigor', 'bin', 'hvigorw.js'),
|
|
173
172
|
})).find(candidate => candidate.node && fs.existsSync(candidate.script));
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
|
|
174
|
+
hvigor = entry
|
|
175
|
+
? { args: [entry.script], command: entry.node.command, source: 'deveco' }
|
|
176
176
|
: { args: [], command: platform === 'win32' ? 'hvigorw.bat' : 'hvigorw', source: 'path' };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
const toolsRoot = layouts.find(layout => (
|
|
180
180
|
hvigor.args[0]?.startsWith(`${layout.toolsRoot}${path.sep}`)
|
|
181
|
+
|| ohpm.args[0]?.startsWith(`${layout.toolsRoot}${path.sep}`)
|
|
181
182
|
|| ohpm.command.startsWith(`${layout.toolsRoot}${path.sep}`)
|
|
182
183
|
))?.toolsRoot || null;
|
|
183
184
|
|
|
@@ -201,10 +202,14 @@ function resolveHarmonyToolchain(): HarmonyToolchain {
|
|
|
201
202
|
function createHarmonyToolchainEnv(toolchain = resolveHarmonyToolchain()): NodeJS.ProcessEnv {
|
|
202
203
|
return {
|
|
203
204
|
...process.env,
|
|
204
|
-
HARMONY_OHPM: toolchain.ohpm.command,
|
|
205
|
-
|
|
205
|
+
HARMONY_OHPM: toolchain.ohpm.args[0] || toolchain.ohpm.command,
|
|
206
|
+
HARMONY_OHPM_NODE: toolchain.ohpm.args.length > 0 ? toolchain.ohpm.command : undefined,
|
|
207
|
+
// Script-based tools must retain both their script and Node executable.
|
|
206
208
|
HARMONY_HVIGORW: toolchain.hvigor.args[0] || toolchain.hvigor.command,
|
|
207
|
-
|
|
209
|
+
HARMONY_HVIGOR_NODE: toolchain.hvigor.args.length > 0 ? toolchain.hvigor.command : undefined,
|
|
210
|
+
...(toolchain.hvigor.args.length > 0
|
|
211
|
+
? { HARMONY_NODE: toolchain.hvigor.command }
|
|
212
|
+
: toolchain.ohpm.args.length > 0 ? { HARMONY_NODE: toolchain.ohpm.command } : {}),
|
|
208
213
|
...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
|
|
209
214
|
? { DEVECO_SDK_HOME: toolchain.sdkHome }
|
|
210
215
|
: {}),
|