@expo-harmony/cli 55.0.26-harmony.1 → 55.0.26-harmony.10
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 +87 -38
- package/build/args.d.ts +19 -0
- package/build/args.js +27 -0
- package/build/bin/expo-harmony.d.ts +2 -0
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +18 -0
- package/build/buildHap/build.js +38 -0
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/buildHap/options.d.ts +7 -0
- package/build/buildHap/options.js +30 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.js +40 -0
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/development/manifest.d.ts +1 -0
- package/build/development/manifest.js +100 -0
- package/build/development/protocol.d.ts +2 -0
- package/build/development/protocol.js +26 -0
- package/build/development/session.d.ts +5 -0
- package/build/development/session.js +66 -0
- package/build/doctor/doctor.d.ts +20 -0
- package/build/doctor/doctor.js +226 -0
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/doctor/options.d.ts +5 -0
- package/build/doctor/options.js +15 -0
- package/build/entry.d.ts +2 -0
- package/build/entry.js +47 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.js +16 -0
- package/build/expo.d.ts +19 -0
- package/build/expo.js +53 -0
- package/build/exportEmbed/export.d.ts +18 -0
- package/build/exportEmbed/export.js +124 -0
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/exportEmbed/manifest.d.ts +44 -0
- package/build/exportEmbed/manifest.js +171 -0
- package/build/exportEmbed/options.d.ts +7 -0
- package/build/exportEmbed/options.js +22 -0
- package/build/file.d.ts +10 -0
- package/build/file.js +110 -0
- package/build/help.d.ts +1 -0
- package/build/help.js +44 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +6 -5
- package/build/internal/development.d.ts +1 -0
- package/build/internal/development.js +8 -0
- package/build/internal/export.d.ts +1 -0
- package/build/internal/export.js +80 -0
- package/build/internal/prebuild.d.ts +3 -0
- package/build/internal/prebuild.js +9 -0
- package/build/log.d.ts +4 -0
- package/build/log.js +6 -0
- package/build/modules/index.d.ts +2 -0
- package/build/modules/index.js +16 -0
- package/build/modules/modules.d.ts +42 -0
- package/build/modules/modules.js +84 -0
- package/build/modules/options.d.ts +10 -0
- package/build/modules/options.js +45 -0
- package/build/native/bare.d.ts +4 -0
- package/build/native/bare.js +130 -0
- package/build/native/build.d.ts +15 -0
- package/build/native/build.js +69 -0
- package/build/native/cache.d.ts +11 -0
- package/build/native/cache.js +73 -0
- package/build/native/hvigor.d.ts +3 -0
- package/build/native/hvigor.js +83 -0
- package/build/native/install.d.ts +5 -0
- package/build/native/install.js +13 -0
- package/build/native/prepareProject.d.ts +11 -0
- package/build/native/prepareProject.js +68 -0
- package/build/native/project.d.ts +8 -0
- package/build/native/project.js +58 -0
- package/build/native/toolchain.d.ts +16 -0
- package/build/native/toolchain.js +172 -0
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/path.d.ts +4 -0
- package/build/path.js +27 -0
- package/build/prebuild/check.d.ts +10 -0
- package/build/prebuild/check.js +151 -0
- package/build/prebuild/clean.d.ts +2 -0
- package/build/prebuild/clean.js +50 -0
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/options.d.ts +10 -0
- package/build/prebuild/options.js +50 -0
- package/build/prebuild/prebuild.d.ts +6 -0
- package/build/prebuild/prebuild.js +70 -0
- package/build/prebuild/template.d.ts +8 -0
- package/build/prebuild/template.js +86 -0
- package/build/process.d.ts +41 -0
- package/build/process.js +324 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/project.d.ts +2 -0
- package/build/project.js +23 -0
- package/build/projectLock.d.ts +2 -0
- package/build/projectLock.js +185 -0
- package/build/run/devices.d.ts +29 -0
- package/build/run/devices.js +216 -0
- package/build/run/emulators.d.ts +13 -0
- package/build/run/emulators.js +82 -0
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.d.ts +21 -0
- package/build/run/metro.js +162 -0
- package/build/run/options.d.ts +14 -0
- package/build/run/options.js +62 -0
- package/build/run/run.d.ts +46 -0
- package/build/run/run.js +135 -0
- package/build/runtime/config.d.ts +35 -0
- package/build/runtime/config.js +118 -0
- package/build/runtime/contract.d.ts +5 -0
- package/build/runtime/contract.js +108 -0
- package/build/runtime/fingerprint.d.ts +1 -0
- package/build/runtime/fingerprint.js +44 -0
- package/build/runtime/index.d.ts +2 -0
- package/build/runtime/index.js +50 -0
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +38 -0
- package/build/start/options.d.ts +9 -0
- package/build/start/options.js +38 -0
- package/build/updates/export.d.ts +14 -0
- package/build/updates/export.js +46 -0
- package/build/updates/index.d.ts +2 -0
- package/build/updates/index.js +96 -0
- package/build/upstream.d.ts +9 -0
- package/build/upstream.js +17 -0
- package/package.json +21 -5
- package/src/bin/expo-harmony.ts +2 -2
- package/src/buildHap/build.ts +13 -105
- package/src/buildHap/index.ts +19 -0
- package/src/cli.ts +27 -225
- package/src/command.ts +24 -0
- package/src/development/manifest.ts +106 -0
- package/src/development/protocol.ts +37 -0
- package/src/development/session.ts +83 -0
- package/src/doctor/doctor.ts +67 -62
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +73 -72
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +41 -0
- package/src/index.ts +5 -3
- package/src/internal/development.ts +8 -0
- package/src/internal/export.ts +82 -0
- package/src/internal/prebuild.ts +3 -0
- package/src/log.ts +5 -0
- package/src/modules/index.ts +16 -0
- package/src/native/bare.ts +145 -0
- package/src/native/build.ts +83 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +88 -0
- package/src/native/install.ts +17 -0
- package/src/native/prepareProject.ts +99 -0
- package/src/native/project.ts +88 -0
- package/src/{tools.ts → native/toolchain.ts} +38 -140
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +56 -31
- package/src/prebuild/clean.ts +3 -3
- package/src/prebuild/index.ts +56 -0
- package/src/prebuild/prebuild.ts +11 -1
- package/src/prebuild/template.ts +2 -2
- package/src/process.ts +175 -64
- package/src/profile.ts +13 -0
- package/src/projectLock.ts +9 -1
- package/src/run/devices.ts +16 -11
- package/src/run/emulators.ts +4 -6
- package/src/run/index.ts +25 -0
- package/src/run/metro.ts +10 -1
- package/src/run/options.ts +2 -0
- package/src/run/run.ts +63 -129
- package/src/runtime/config.ts +150 -0
- package/src/runtime/contract.ts +165 -0
- package/src/runtime/fingerprint.ts +39 -0
- package/src/runtime/index.ts +54 -0
- package/src/start/index.ts +37 -0
- package/src/start/options.ts +12 -0
- package/src/updates/export.ts +60 -0
- package/src/updates/index.ts +89 -0
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/prebuild/clean.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/
|
|
4
|
+
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/internal';
|
|
5
5
|
|
|
6
6
|
import { HarmonyCliError } from '../errors';
|
|
7
7
|
import { isInside } from '../path';
|
|
8
|
-
import { resolveHarmonyBuildPlanIfPresentAsync } from '../
|
|
8
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
|
|
9
9
|
|
|
10
10
|
async function assertSafeCleanTarget(projectRoot) {
|
|
11
11
|
const root = await fs.promises.realpath(projectRoot);
|
|
@@ -24,7 +24,7 @@ async function assertSafeCleanTarget(projectRoot) {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if (!plan) {
|
|
27
|
+
if (!plan || plan.workflow !== 'cng') {
|
|
28
28
|
throw new HarmonyCliError(
|
|
29
29
|
'ERR_HARMONY_CLEAN_TARGET',
|
|
30
30
|
`Refusing to clean ${target} because its CNG manifest is missing. Please delete it manually or run prebuild without --clean.`,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { HarmonyCliError } from '../errors';
|
|
3
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
4
|
+
import { parsePrebuildArgs } from './options';
|
|
5
|
+
|
|
6
|
+
export const command: Command = async (argv, io) => {
|
|
7
|
+
const args = parseCommandArgs(args => parsePrebuildArgs(args, { allowProject: true }), argv, io);
|
|
8
|
+
if (!args) return 0;
|
|
9
|
+
|
|
10
|
+
const { projectRoot: root, options } = args;
|
|
11
|
+
const { check, clean, passthrough } = options;
|
|
12
|
+
|
|
13
|
+
if (check && passthrough.length) {
|
|
14
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return withHarmonyProjectLockAsync(
|
|
18
|
+
root,
|
|
19
|
+
check ? 'prebuild-check' : 'prebuild',
|
|
20
|
+
async () => {
|
|
21
|
+
if (clean) {
|
|
22
|
+
const { assertSafeCleanTarget } = await import('./clean.js');
|
|
23
|
+
await assertSafeCleanTarget(root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { doctorAsync, formatDoctor } = await import('../doctor/doctor.js');
|
|
27
|
+
const doctor = await doctorAsync(root, {
|
|
28
|
+
requireBuildTools: false,
|
|
29
|
+
validateGeneratedProject: !clean,
|
|
30
|
+
validateModules: false,
|
|
31
|
+
});
|
|
32
|
+
if (!doctor.ok) {
|
|
33
|
+
io.error(formatDoctor(doctor));
|
|
34
|
+
throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
|
|
35
|
+
operation: 'doctor',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const item of doctor.checks.filter(item => item.status === 'warn')) io.warn(`! ${item.message}`);
|
|
40
|
+
|
|
41
|
+
if (check) {
|
|
42
|
+
const { checkAsync } = await import('./check.js');
|
|
43
|
+
const result = await checkAsync(root);
|
|
44
|
+
|
|
45
|
+
if (result.clean) io.log('Harmony CNG output is up to date.');
|
|
46
|
+
else for (const change of result.changes) io.log(`${change.type}: ${change.path}`);
|
|
47
|
+
|
|
48
|
+
return result.clean ? 0 : 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { prebuildParsedAsync } = await import('./prebuild.js');
|
|
52
|
+
await prebuildParsedAsync(root, passthrough);
|
|
53
|
+
|
|
54
|
+
return 0;
|
|
55
|
+
});
|
|
56
|
+
};
|
package/src/prebuild/prebuild.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
|
|
3
3
|
import { HarmonyCliError } from '../errors';
|
|
4
|
+
import { isBareHarmonyProject } from '../native/bare';
|
|
4
5
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
5
6
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
6
7
|
import { resolveExpoCli } from '../expo';
|
|
7
|
-
import { createHarmonyToolchainEnv
|
|
8
|
+
import { createHarmonyToolchainEnv } from '../native/toolchain';
|
|
9
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
8
10
|
import { assertSafeCleanTarget } from './clean';
|
|
9
11
|
import { packAsync } from './template';
|
|
10
12
|
|
|
@@ -18,6 +20,9 @@ async function prebuildParsedUnlockedAsync(
|
|
|
18
20
|
passthrough: string[],
|
|
19
21
|
options: PrebuildOptions = {}
|
|
20
22
|
) {
|
|
23
|
+
if (isBareHarmonyProject(projectRoot)) {
|
|
24
|
+
throw new HarmonyCliError('ERR_HARMONY_BARE_PREBUILD', 'This Harmony project is manually maintained. Build it directly; prebuild cannot overwrite a bare project.', { operation: 'prebuild' });
|
|
25
|
+
}
|
|
21
26
|
if (passthrough.includes('--clean')) await assertSafeCleanTarget(projectRoot);
|
|
22
27
|
|
|
23
28
|
const expo = resolveExpoCli(projectRoot);
|
|
@@ -44,6 +49,7 @@ async function prebuildParsedUnlockedAsync(
|
|
|
44
49
|
|
|
45
50
|
if (result.code !== 0) {
|
|
46
51
|
const diagnostics = options.capture ? formatDiagnostics(result) : '';
|
|
52
|
+
|
|
47
53
|
throw new HarmonyCliError('ERR_HARMONY_PREBUILD_FAILED', `Expo prebuild exited with code ${result.code}.${diagnostics ? `\n${diagnostics}` : ''}`, {
|
|
48
54
|
exitCode: result.code,
|
|
49
55
|
operation: 'expo-prebuild',
|
|
@@ -54,6 +60,10 @@ async function prebuildParsedUnlockedAsync(
|
|
|
54
60
|
const plan = await resolveHarmonyBuildPlanAsync(projectRoot, {
|
|
55
61
|
buildMode: options.buildType,
|
|
56
62
|
});
|
|
63
|
+
if (plan.workflow !== 'cng') {
|
|
64
|
+
throw new HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Prebuild did not produce a CNG project.', { operation: 'prebuild' });
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
await fs.promises.access(plan.projectFiles.templateMarker, fs.constants.R_OK);
|
|
58
68
|
} catch (cause) {
|
|
59
69
|
throw new HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Expo prebuild exited successfully but the Harmony marker or CNG manifest is invalid.', { cause, operation: 'verify-prebuild' });
|
package/src/prebuild/template.ts
CHANGED
|
@@ -19,11 +19,11 @@ function resolve(project: string) {
|
|
|
19
19
|
let api: Api;
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
|
-
api = load('@expo-harmony/prebuild-config/template');
|
|
22
|
+
api = load('@expo-harmony/prebuild-config/internal/template');
|
|
23
23
|
} catch (cause) {
|
|
24
24
|
throw new HarmonyCliError(
|
|
25
25
|
'ERR_HARMONY_TEMPLATE_INVALID',
|
|
26
|
-
'Cannot load @expo-harmony/prebuild-config/template from the project. Update the project-local prebuild config to a compatible version.',
|
|
26
|
+
'Cannot load @expo-harmony/prebuild-config/internal/template from the project. Update the project-local prebuild config to a compatible version.',
|
|
27
27
|
{ cause, operation: 'resolve-template' }
|
|
28
28
|
);
|
|
29
29
|
}
|
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
|
|
|
@@ -27,6 +28,16 @@ export interface ProcessResult {
|
|
|
27
28
|
timedOut: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
interface CheckedProcessOptions {
|
|
32
|
+
code: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
message: string;
|
|
36
|
+
operation: string;
|
|
37
|
+
outputLimit?: number;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
class BoundedCapture {
|
|
31
42
|
private buffers: Uint8Array[] = [];
|
|
32
43
|
private head = 0;
|
|
@@ -76,8 +87,15 @@ function formatDiagnostics(result: Pick<ProcessResult, 'stderr' | 'stdout'>, lim
|
|
|
76
87
|
|
|
77
88
|
function spawnAsync(command: string, args: string[], options: ProcessOptions = {}): Promise<ProcessResult> {
|
|
78
89
|
return new Promise<ProcessResult>((resolve, reject) => {
|
|
90
|
+
if (options.signal?.aborted) {
|
|
91
|
+
throw new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: operation aborted.`, {
|
|
92
|
+
cause: options.signal.reason,
|
|
93
|
+
operation: options.operation || 'spawn',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
const piped = Boolean(options.capture || options.onStdout || options.onStderr);
|
|
80
|
-
const
|
|
98
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
81
99
|
const child = spawn(command, args, {
|
|
82
100
|
cwd: options.cwd,
|
|
83
101
|
env: options.env || process.env,
|
|
@@ -86,11 +104,12 @@ function spawnAsync(command: string, args: string[], options: ProcessOptions = {
|
|
|
86
104
|
windowsHide: true,
|
|
87
105
|
});
|
|
88
106
|
|
|
89
|
-
const stdout = new BoundedCapture(
|
|
90
|
-
const stderr = new BoundedCapture(
|
|
107
|
+
const stdout = new BoundedCapture(limit);
|
|
108
|
+
const stderr = new BoundedCapture(limit);
|
|
91
109
|
let timedOut = false;
|
|
92
110
|
let settled = false;
|
|
93
|
-
let
|
|
111
|
+
let escalation: NodeJS.Timeout | null = null;
|
|
112
|
+
let stopping: Promise<void> | undefined;
|
|
94
113
|
|
|
95
114
|
if (piped) {
|
|
96
115
|
child.stdout.on('data', (chunk) => {
|
|
@@ -103,72 +122,96 @@ function spawnAsync(command: string, args: string[], options: ProcessOptions = {
|
|
|
103
122
|
});
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
const stop = (signal: NodeJS.Signals = 'SIGTERM') => {
|
|
126
|
+
if (stopping || settled) return;
|
|
127
|
+
|
|
128
|
+
stopping = Promise.resolve().then(() => terminateProcess(child, signal));
|
|
129
|
+
if (process.platform === 'win32') {
|
|
130
|
+
void finish(null, signal);
|
|
131
|
+
} else {
|
|
132
|
+
stopping.catch((cause) => {
|
|
133
|
+
void finish(null, null, cause);
|
|
134
|
+
});
|
|
135
|
+
escalation = setTimeout(
|
|
136
|
+
() => {
|
|
137
|
+
stopping = terminateProcess(child, 'SIGKILL');
|
|
138
|
+
stopping.catch((cause) => {
|
|
139
|
+
void finish(null, null, cause);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
111
142
|
options.stopGraceMs || DefaultStopGraceMs
|
|
112
143
|
);
|
|
113
|
-
|
|
144
|
+
escalation.unref?.();
|
|
114
145
|
}
|
|
115
146
|
};
|
|
116
147
|
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
process.once('SIGINT',
|
|
120
|
-
process.once('SIGTERM',
|
|
148
|
+
const interrupt = () => stop('SIGINT');
|
|
149
|
+
const terminate = () => stop('SIGTERM');
|
|
150
|
+
process.once('SIGINT', interrupt);
|
|
151
|
+
process.once('SIGTERM', terminate);
|
|
121
152
|
|
|
122
153
|
const timeout = options.timeoutMs
|
|
123
154
|
? setTimeout(() => {
|
|
124
155
|
timedOut = true;
|
|
125
|
-
|
|
156
|
+
stop('SIGTERM');
|
|
126
157
|
}, options.timeoutMs)
|
|
127
158
|
: null;
|
|
128
159
|
timeout?.unref?.();
|
|
129
160
|
|
|
130
161
|
const cleanup = () => {
|
|
131
162
|
if (timeout) clearTimeout(timeout);
|
|
132
|
-
if (
|
|
133
|
-
process.removeListener('SIGINT',
|
|
134
|
-
process.removeListener('SIGTERM',
|
|
163
|
+
if (escalation) clearTimeout(escalation);
|
|
164
|
+
process.removeListener('SIGINT', interrupt);
|
|
165
|
+
process.removeListener('SIGTERM', terminate);
|
|
135
166
|
options.signal?.removeEventListener('abort', abort);
|
|
136
167
|
};
|
|
137
168
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
const finish = async (code, signal, cause?) => {
|
|
170
|
+
try {
|
|
171
|
+
await stopping;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
cause = error;
|
|
174
|
+
}
|
|
141
175
|
|
|
142
|
-
child.once('error', (cause) => {
|
|
143
176
|
if (settled) return;
|
|
144
177
|
settled = true;
|
|
145
178
|
cleanup();
|
|
146
179
|
|
|
147
|
-
reject(new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot
|
|
180
|
+
if (cause) reject(new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot run ${command}: ${cause.message}`, {
|
|
148
181
|
cause,
|
|
149
182
|
operation: options.operation || 'spawn',
|
|
150
183
|
}));
|
|
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({
|
|
184
|
+
else resolve({
|
|
160
185
|
code: code === null ? 1 : code,
|
|
161
186
|
signal,
|
|
162
187
|
stderr: stderr.toString(),
|
|
163
188
|
stdout: stdout.toString(),
|
|
164
189
|
timedOut,
|
|
165
190
|
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
child.once('error', (cause) => {
|
|
194
|
+
void finish(null, null, cause);
|
|
195
|
+
});
|
|
196
|
+
child.once('close', (code, signal) => {
|
|
197
|
+
void finish(code, signal);
|
|
166
198
|
});
|
|
199
|
+
|
|
200
|
+
const abort = () => stop('SIGTERM');
|
|
201
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
202
|
+
if (options.signal?.aborted) abort();
|
|
167
203
|
});
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
function startManagedProcess(command: string, args: string[], options: ProcessOptions = {}) {
|
|
171
|
-
|
|
207
|
+
if (options.signal?.aborted) {
|
|
208
|
+
throw new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: operation aborted.`, {
|
|
209
|
+
cause: options.signal.reason,
|
|
210
|
+
operation: options.operation || 'spawn',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const limit = options.outputLimit || DefaultOutputLimit;
|
|
172
215
|
const piped = options.stdio !== 'inherit';
|
|
173
216
|
const child = spawn(command, args, {
|
|
174
217
|
cwd: options.cwd,
|
|
@@ -178,11 +221,14 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
178
221
|
windowsHide: true,
|
|
179
222
|
});
|
|
180
223
|
|
|
181
|
-
const stdout = new BoundedCapture(
|
|
182
|
-
const stderr = new BoundedCapture(
|
|
183
|
-
let
|
|
224
|
+
const stdout = new BoundedCapture(limit);
|
|
225
|
+
const stderr = new BoundedCapture(limit);
|
|
226
|
+
let failure: HarmonyCliError | null = null;
|
|
184
227
|
let closed = false;
|
|
185
|
-
let
|
|
228
|
+
let stopped = false;
|
|
229
|
+
let stopping: Promise<void> | undefined;
|
|
230
|
+
let stopPromise: Promise<ProcessResult | undefined> | undefined;
|
|
231
|
+
let finish: (code: number | null, signal: NodeJS.Signals | null) => Promise<void>;
|
|
186
232
|
|
|
187
233
|
if (piped) {
|
|
188
234
|
child.stdout.on('data', (chunk) => {
|
|
@@ -195,34 +241,43 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
195
241
|
});
|
|
196
242
|
}
|
|
197
243
|
|
|
198
|
-
const
|
|
199
|
-
void stop('SIGINT');
|
|
244
|
+
const interrupt = () => {
|
|
245
|
+
void stop('SIGINT').catch(() => {});
|
|
200
246
|
};
|
|
201
|
-
const
|
|
202
|
-
void stop('SIGTERM');
|
|
247
|
+
const terminate = () => {
|
|
248
|
+
void stop('SIGTERM').catch(() => {});
|
|
203
249
|
};
|
|
204
250
|
const abort = () => {
|
|
205
|
-
void stop('SIGTERM');
|
|
251
|
+
void stop('SIGTERM').catch(() => {});
|
|
206
252
|
};
|
|
207
253
|
|
|
208
254
|
const cleanup = () => {
|
|
209
|
-
process.removeListener('SIGINT',
|
|
210
|
-
process.removeListener('SIGTERM',
|
|
255
|
+
process.removeListener('SIGINT', interrupt);
|
|
256
|
+
process.removeListener('SIGTERM', terminate);
|
|
211
257
|
options.signal?.removeEventListener('abort', abort);
|
|
212
258
|
};
|
|
213
259
|
|
|
214
260
|
const completion = new Promise<ProcessResult>((resolve, reject) => {
|
|
215
261
|
child.once('error', (cause) => {
|
|
216
|
-
|
|
262
|
+
failure = new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot launch ${command}: ${cause.message}`, {
|
|
217
263
|
cause,
|
|
218
264
|
operation: options.operation || 'spawn',
|
|
219
265
|
});
|
|
220
266
|
});
|
|
221
|
-
|
|
267
|
+
finish = async (code, signal) => {
|
|
268
|
+
try {
|
|
269
|
+
await stopping;
|
|
270
|
+
} catch (cause) {
|
|
271
|
+
failure = new HarmonyCliError('ERR_HARMONY_PROCESS_FAILED', `Cannot stop ${command}: ${cause.message}`, {
|
|
272
|
+
cause, operation: options.operation || 'spawn',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (closed) return;
|
|
222
277
|
closed = true;
|
|
223
278
|
cleanup();
|
|
224
279
|
|
|
225
|
-
if (
|
|
280
|
+
if (failure) reject(failure);
|
|
226
281
|
else resolve({
|
|
227
282
|
code: code === null ? 1 : code,
|
|
228
283
|
signal,
|
|
@@ -230,6 +285,10 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
230
285
|
stdout: stdout.toString(),
|
|
231
286
|
timedOut: false,
|
|
232
287
|
});
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
child.once('close', (code, signal) => {
|
|
291
|
+
void finish(code, signal);
|
|
233
292
|
});
|
|
234
293
|
});
|
|
235
294
|
// A readiness probe may be the first consumer. Keep early spawn failures from
|
|
@@ -237,28 +296,53 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
237
296
|
completion.catch(() => {});
|
|
238
297
|
|
|
239
298
|
async function stop(signal: NodeJS.Signals = 'SIGTERM', graceMs = DefaultStopGraceMs) {
|
|
299
|
+
if (stopPromise) return stopPromise;
|
|
240
300
|
if (closed) return completion;
|
|
241
301
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
302
|
+
stopped = true;
|
|
303
|
+
stopping = Promise.resolve().then(() => terminateProcess(child, signal));
|
|
304
|
+
stopPromise = (async () => {
|
|
305
|
+
if (process.platform === 'win32') {
|
|
306
|
+
void finish(null, signal);
|
|
307
|
+
return completion;
|
|
308
|
+
}
|
|
245
309
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
310
|
+
try {
|
|
311
|
+
await stopping;
|
|
312
|
+
} catch {
|
|
313
|
+
await finish(null, signal);
|
|
314
|
+
return completion;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let timer: NodeJS.Timeout | undefined;
|
|
318
|
+
|
|
319
|
+
await Promise.race([
|
|
320
|
+
completion.catch(() => undefined),
|
|
321
|
+
new Promise((resolve) => {
|
|
322
|
+
timer = setTimeout(resolve, graceMs);
|
|
323
|
+
timer.unref?.();
|
|
324
|
+
}),
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
if (timer) clearTimeout(timer);
|
|
328
|
+
if (!closed) {
|
|
329
|
+
stopping = terminateProcess(child, 'SIGKILL');
|
|
330
|
+
try {
|
|
331
|
+
await stopping;
|
|
332
|
+
} catch {
|
|
333
|
+
await finish(null, signal);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
253
336
|
|
|
254
|
-
|
|
255
|
-
|
|
337
|
+
return completion.catch(() => undefined);
|
|
338
|
+
})();
|
|
339
|
+
stopPromise.catch(() => {});
|
|
256
340
|
|
|
257
|
-
return
|
|
341
|
+
return stopPromise;
|
|
258
342
|
}
|
|
259
343
|
|
|
260
|
-
process.once('SIGINT',
|
|
261
|
-
process.once('SIGTERM',
|
|
344
|
+
process.once('SIGINT', interrupt);
|
|
345
|
+
process.once('SIGTERM', terminate);
|
|
262
346
|
if (options.signal?.aborted) abort();
|
|
263
347
|
else options.signal?.addEventListener('abort', abort, { once: true });
|
|
264
348
|
|
|
@@ -268,8 +352,35 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
268
352
|
getStderr: () => stderr.toString(),
|
|
269
353
|
getStdout: () => stdout.toString(),
|
|
270
354
|
stop,
|
|
271
|
-
wasStopped: () =>
|
|
355
|
+
wasStopped: () => stopped,
|
|
272
356
|
};
|
|
273
357
|
}
|
|
274
358
|
|
|
275
|
-
|
|
359
|
+
async function runCheckedAsync(
|
|
360
|
+
command: string,
|
|
361
|
+
args: string[],
|
|
362
|
+
options: CheckedProcessOptions
|
|
363
|
+
) {
|
|
364
|
+
const result = await spawnAsync(command, args, {
|
|
365
|
+
capture: true,
|
|
366
|
+
cwd: options.cwd,
|
|
367
|
+
env: options.env,
|
|
368
|
+
operation: options.operation,
|
|
369
|
+
outputLimit: options.outputLimit || 2 * 1024 * 1024,
|
|
370
|
+
timeoutMs: options.timeoutMs,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (result.code !== 0 || result.timedOut) {
|
|
374
|
+
const diagnostics = formatDiagnostics(result);
|
|
375
|
+
|
|
376
|
+
throw new HarmonyCliError(
|
|
377
|
+
options.code,
|
|
378
|
+
`${options.message} exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
379
|
+
{ exitCode: result.code || 1, operation: options.operation }
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export { formatDiagnostics, runCheckedAsync, spawnAsync, startManagedProcess };
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function timedAsync<T>(
|
|
2
|
+
steps: Record<string, number>,
|
|
3
|
+
name: string,
|
|
4
|
+
operation: () => Promise<T> | T
|
|
5
|
+
): Promise<T> {
|
|
6
|
+
const started = performance.now();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return await operation();
|
|
10
|
+
} finally {
|
|
11
|
+
steps[name] = Math.max(0, Math.round(performance.now() - started));
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/projectLock.ts
CHANGED
|
@@ -162,6 +162,7 @@ async function acquireHarmonyProjectLockAsync(
|
|
|
162
162
|
await handle.sync();
|
|
163
163
|
} catch (cause) {
|
|
164
164
|
await releaseOwnedLockAsync(handle, lockPath, owned);
|
|
165
|
+
|
|
165
166
|
throw new HarmonyCliError(
|
|
166
167
|
'ERR_HARMONY_PROJECT_LOCK',
|
|
167
168
|
'Cannot initialize the Harmony native operation lock.',
|
|
@@ -183,7 +184,14 @@ async function acquireHarmonyProjectLockAsync(
|
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
186
|
} catch (cause) {
|
|
186
|
-
if (cause instanceof HarmonyCliError)
|
|
187
|
+
if (cause instanceof HarmonyCliError) {
|
|
188
|
+
throw new HarmonyCliError(cause.code, cause.message, {
|
|
189
|
+
cause,
|
|
190
|
+
exitCode: cause.exitCode,
|
|
191
|
+
operation: cause.operation,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
if (cause?.code !== 'EEXIST') {
|
|
188
196
|
throw new HarmonyCliError(
|
|
189
197
|
'ERR_HARMONY_PROJECT_LOCK',
|
package/src/run/devices.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
2
2
|
|
|
3
3
|
import { listEmulatorsAsync, startEmulator } from './emulators';
|
|
4
4
|
import { HarmonyCliError } from '../errors';
|
|
5
|
-
import type
|
|
5
|
+
import { type HarmonyTool } from '../native/toolchain';
|
|
6
6
|
import { formatDiagnostics, spawnAsync, type ProcessResult } from '../process';
|
|
7
7
|
|
|
8
8
|
interface Device {
|
|
@@ -44,8 +44,6 @@ function parseHdcTargets(output: string): Device[] {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
|
-
// Only the first HDC column is a selectable target name. The remaining
|
|
48
|
-
// verbose columns describe transport, state, location and connect tool.
|
|
49
47
|
aliases: [fields[0]],
|
|
50
48
|
connectTool: fields[4] || null,
|
|
51
49
|
id: fields[0],
|
|
@@ -59,10 +57,7 @@ function parseHdcTargets(output: string): Device[] {
|
|
|
59
57
|
function hasCommandFailure(result: ProcessResult): boolean {
|
|
60
58
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
61
59
|
return result.code !== 0 || result.timedOut
|
|
62
|
-
// HDC
|
|
63
|
-
// stdout while still returning exit code 0 (for example,
|
|
64
|
-
// "Connect server failed"). Treat its documented failure vocabulary as
|
|
65
|
-
// authoritative regardless of where it appears on the line.
|
|
60
|
+
// HDC can print failures to stdout while returning exit code 0.
|
|
66
61
|
|| /\[(?:Fail|Error)\]|\b(?:Failure|failed)\b|(?:失败|错误)/iu.test(output);
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -146,7 +141,6 @@ async function selectDeviceAsync(
|
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
const instances = await listEmulatorsAsync(options.emulator, { cwd: options.cwd });
|
|
149
|
-
// Prefer an instance that is already booting when HDC is not connected yet.
|
|
150
144
|
const running = instances.filter(instance => instance.running);
|
|
151
145
|
const candidates = requested
|
|
152
146
|
? instances.filter(instance => instance.name === requested)
|
|
@@ -242,13 +236,24 @@ async function configureMetroPortAsync(
|
|
|
242
236
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
243
237
|
const hostEndpoint = `tcp:${port}`;
|
|
244
238
|
|
|
245
|
-
await runHdcAsync(hdc, ['-t', device.id, 'fport', '
|
|
246
|
-
allowFailure: true,
|
|
239
|
+
const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
|
|
247
240
|
cwd: options.cwd,
|
|
248
|
-
operation: '
|
|
241
|
+
operation: 'list-metro-ports',
|
|
249
242
|
timeoutMs: 15_000,
|
|
250
243
|
});
|
|
251
244
|
|
|
245
|
+
for (const line of forwards.stdout.split(/\r?\n/u)) {
|
|
246
|
+
const [target, remote, local, direction] = line.trim().split(/\s+/u);
|
|
247
|
+
if (target !== device.id || remote !== deviceEndpoint || direction !== '[Reverse]') continue;
|
|
248
|
+
if (local === hostEndpoint) return;
|
|
249
|
+
|
|
250
|
+
await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
|
|
251
|
+
cwd: options.cwd,
|
|
252
|
+
operation: 'remove-metro-port',
|
|
253
|
+
timeoutMs: 15_000,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
252
257
|
await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
|
|
253
258
|
code: 'ERR_HARMONY_METRO_FORWARD',
|
|
254
259
|
cwd: options.cwd,
|
package/src/run/emulators.ts
CHANGED
|
@@ -4,7 +4,7 @@ import spawn from 'cross-spawn';
|
|
|
4
4
|
|
|
5
5
|
import { HarmonyCliError } from '../errors';
|
|
6
6
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
7
|
-
import type
|
|
7
|
+
import { type HarmonyTool } from '../native/toolchain';
|
|
8
8
|
|
|
9
9
|
interface HarmonyEmulator {
|
|
10
10
|
name: string;
|
|
@@ -67,10 +67,9 @@ function startEmulator(tool: HarmonyTool, name: string, logFile: string, cwd?: s
|
|
|
67
67
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
68
68
|
const log = fs.openSync(logFile, 'w', 0o600);
|
|
69
69
|
let failure: string | null = null;
|
|
70
|
+
|
|
70
71
|
try {
|
|
71
|
-
//
|
|
72
|
-
// process group and file-backed output so it survives the CLI/Metro exit.
|
|
73
|
-
// Default instance/image paths match the ones used by -list -details.
|
|
72
|
+
// A detached process with file output lets the emulator survive CLI exit.
|
|
74
73
|
const child = spawn(tool.command, [...tool.args, '-start', name], {
|
|
75
74
|
cwd,
|
|
76
75
|
detached: true,
|
|
@@ -82,8 +81,7 @@ function startEmulator(tool: HarmonyTool, name: string, logFile: string, cwd?: s
|
|
|
82
81
|
failure = cause.message;
|
|
83
82
|
});
|
|
84
83
|
child.once('exit', (code, signal) => {
|
|
85
|
-
// Some
|
|
86
|
-
// prove readiness; the caller still checks the guest's name and boot state.
|
|
84
|
+
// Some launchers exit before the guest boots; HDC still has to confirm readiness.
|
|
87
85
|
if (code !== 0) failure = signal ? `terminated by ${signal}` : `exited with code ${code}`;
|
|
88
86
|
});
|
|
89
87
|
child.unref();
|
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseRunArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseRunArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { runHarmonySessionAsync } = await import('./run.js');
|
|
10
|
+
|
|
11
|
+
const session = await runHarmonySessionAsync(root, {
|
|
12
|
+
...options,
|
|
13
|
+
interactiveBundler: true,
|
|
14
|
+
io,
|
|
15
|
+
});
|
|
16
|
+
const { result } = session;
|
|
17
|
+
|
|
18
|
+
io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
|
|
19
|
+
if (session.metro.owner === 'started') {
|
|
20
|
+
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
21
|
+
await session.metro.waitAsync();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return 0;
|
|
25
|
+
};
|