@expo-harmony/cli 55.0.26-harmony.1
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 +150 -0
- package/build/bin/expo-harmony.js +5 -0
- package/build/index.js +22 -0
- package/package.json +67 -0
- package/src/args.ts +42 -0
- package/src/bin/expo-harmony.ts +5 -0
- package/src/buildHap/build.ts +164 -0
- package/src/buildHap/common.ts +145 -0
- package/src/buildHap/options.ts +35 -0
- package/src/cli.ts +242 -0
- package/src/doctor/doctor.ts +267 -0
- package/src/doctor/options.ts +17 -0
- package/src/entry.ts +47 -0
- package/src/errors.ts +22 -0
- package/src/expo.ts +89 -0
- package/src/exportEmbed/export.ts +177 -0
- package/src/exportEmbed/manifest.ts +222 -0
- package/src/exportEmbed/options.ts +25 -0
- package/src/file.ts +110 -0
- package/src/index.ts +12 -0
- package/src/modules/modules.ts +154 -0
- package/src/modules/options.ts +70 -0
- package/src/path.ts +27 -0
- package/src/prebuild/check.ts +189 -0
- package/src/prebuild/clean.ts +67 -0
- package/src/prebuild/options.ts +69 -0
- package/src/prebuild/prebuild.ts +80 -0
- package/src/prebuild/template.ts +119 -0
- package/src/process.ts +275 -0
- package/src/project.ts +21 -0
- package/src/projectLock.ts +229 -0
- package/src/run/cache.ts +105 -0
- package/src/run/devices.ts +302 -0
- package/src/run/emulators.ts +107 -0
- package/src/run/install.ts +41 -0
- package/src/run/metro.ts +237 -0
- package/src/run/options.ts +71 -0
- package/src/run/run.ts +302 -0
- package/src/start/options.ts +36 -0
- package/src/tools.ts +316 -0
- package/src/upstream.ts +18 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @expo-harmony/cli
|
|
2
|
+
|
|
3
|
+
[**GitHub 仓库**](https://github.com/renbaoshuo/expo-harmony/tree/master/packages/cli)
|
|
4
|
+
|
|
5
|
+
为 Expo 项目提供 HarmonyOS 平台的命令行工具,支持 Metro 开发服务、CNG 原生工程生成、环境诊断、Harmony Expo Module 发现与校验、生产 Bundle 导出,以及 HAP 的构建、安装和启动。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install --save-dev @expo-harmony/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
CLI 必须安装在项目本地,JS 打包调用项目内的 Expo CLI,Hermes 字节码编译调用项目内的 `@expo/metro-config`,Release 原生构建也会回调项目本地的 `expo-harmony` 二进制生成 Bundle。
|
|
14
|
+
|
|
15
|
+
项目需要 Node.js 20 或更高版本,在 Expo 配置中注册 `@expo-harmony/prebuild-config` 插件,并使用 `@expo-harmony/metro-config` 组合 Metro 配置。
|
|
16
|
+
|
|
17
|
+
HAP 构建需要完整的 HarmonyOS SDK(含 HMS 与 OpenHarmony 组件)、OHPM、Hvigor 和 HDC,这些环境可用 `doctor` 命令检查。
|
|
18
|
+
|
|
19
|
+
## 命令
|
|
20
|
+
|
|
21
|
+
| 命令 | 作用 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `expo-harmony start` | 启动 Harmony 开发所需的 Expo Metro 服务 |
|
|
24
|
+
| `expo-harmony prebuild` | 使用 Expo CNG 生成或更新 HarmonyOS 原生工程 |
|
|
25
|
+
| `expo-harmony prebuild --clean` | 安全删除并重新生成受管理的 `harmony` 目录 |
|
|
26
|
+
| `expo-harmony prebuild --check` | 在隔离目录中生成期望状态并比较差异,不修改项目文件 |
|
|
27
|
+
| `expo-harmony build` | 构建 HAP,不选择设备,也不安装或启动应用 |
|
|
28
|
+
| `expo-harmony doctor` | 检查配置、依赖版本、Metro、RNOH、SDK、构建工具和签名 |
|
|
29
|
+
| `expo-harmony modules list` | 列出发现的 Harmony Expo Module 候选及其来源 |
|
|
30
|
+
| `expo-harmony modules inspect` | 展示模块解析后的 Harmony 元数据,如 HAR 与 ArkTS modules |
|
|
31
|
+
| `expo-harmony modules verify` | 检查 canonical metadata、应用级 registration/OHPM 冲突、路径边界和约定产物 |
|
|
32
|
+
| `expo-harmony export:embed` | 导出 Hermes 字节码、资源和 Source Map 并写入原生工程 |
|
|
33
|
+
| `expo-harmony run` | 构建 HAP,在连接的 HarmonyOS 设备上安装并启动应用 |
|
|
34
|
+
|
|
35
|
+
所有命令都可接收可选的项目路径(`modules` 命令写在子命令之后);未提供时从当前目录向上查找最近的项目根目录作为默认值:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx expo-harmony start ./my-app
|
|
39
|
+
npx expo-harmony doctor ./my-app
|
|
40
|
+
npx expo-harmony prebuild ./my-app
|
|
41
|
+
npx expo-harmony build ./my-app --variant release
|
|
42
|
+
npx expo-harmony run ./my-app --device <hdc-target>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Start
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx expo-harmony start
|
|
49
|
+
npx expo-harmony start --port 8082
|
|
50
|
+
npx expo-harmony start --clear
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`start` 调用项目本地的 Expo CLI,以 `--dev-client` 模式启动 Metro,并自动设置 `EXPO_METRO_TARGET=harmony`。它只启动 JS 开发服务,不执行原生工程生成、构建、安装、应用启动或设备端口映射,也不要求 HarmonyOS SDK 和设备就绪。新启动的 Metro 在当前终端输出日志,按 Ctrl+C 退出。
|
|
54
|
+
|
|
55
|
+
支持 `--port <number>`(默认 `8081`)和 `--reset-cache`(别名 `--clear`、`-c`)。端口上已有 Metro 时提示并退出,不停止已有服务;此时缓存选项不会生效,需要先停止已有 Metro 后重新执行命令。端口被其他进程占用时会报错。
|
|
56
|
+
|
|
57
|
+
需要构建并启动应用时,在另一个终端运行 `expo-harmony run --no-bundler`;若指定了端口,两个命令使用相同的 `--port`。
|
|
58
|
+
|
|
59
|
+
## Prebuild
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx expo-harmony prebuild
|
|
63
|
+
npx expo-harmony prebuild --clean
|
|
64
|
+
npx expo-harmony prebuild --check
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`prebuild` 固定使用 HarmonyOS 平台和 `@expo-harmony/template` 模板,因此不像官方接受 `--platform` 和 `--template`。依赖安装相关选项会透传给 Expo CLI:`--no-install`、`--npm`/`--yarn`/`--pnpm`/`--bun`(最多选择一个)和 `--skip-dependency-update <packages>`。执行前会先运行一次 doctor(此阶段不要求构建工具就绪),发现阻塞错误时直接中止。
|
|
68
|
+
|
|
69
|
+
`--clean` 只会清理带有 Expo Harmony 模板标记的原生目录:CNG manifest 缺失、目标不是项目内的普通目录或模板标记异常时都会拒绝删除。`--check` 是只读操作,会把项目镜像到临时目录(`node_modules` 以符号链接共享,不复制),在其中执行一次隔离的 prebuild 后比较受管文件;无差异时退出码为 `0`,有差异时列出变更并以 `2` 退出,且不能与其他会修改工程的选项同时使用。
|
|
70
|
+
|
|
71
|
+
## Build
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx expo-harmony build
|
|
75
|
+
npx expo-harmony build --variant release
|
|
76
|
+
npx expo-harmony build --sync
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`build` 在不使用设备的前提下完成 HAP 构建。缺少原生工程时自动执行 prebuild,`--sync` 强制重新生成,默认则校验 CNG 期望状态并在发现差错时报错。随后依次执行 OHPM 依赖安装、原生构建缓存校验和 Hvigor `assembleHap`,最后确认产物 HAP 存在且非空。原生依赖指纹变化时会自动失效旧的 `.cxx` 和 `build` 缓存,避免链接到过期的产物。
|
|
80
|
+
|
|
81
|
+
选项:`--variant debug|release`(默认 `debug`)和 `--sync`。Release 构建会先执行生产导出,再带着预生成的 Bundle 进入 Hvigor。
|
|
82
|
+
|
|
83
|
+
## Doctor
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
npx expo-harmony doctor
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`doctor` 逐项检查 Expo `harmony` 配置与 `@expo-harmony/prebuild-config` 插件注册、签名配置文件、启用 Harmony 的 Metro 配置、必需依赖(RNOH 运行时与 CLI、`@expo-harmony/expo-modules-autolinking`)、Harmony Expo Modules 校验、完整的 HarmonyOS SDK、HDC/OHPM/Hvigor 可用性,以及生成工程中的 Hvigor 文件和 RNOH 自动链接禁用状态。存在 error 级别的问题时命令以非零状态退出;未配置外部签名文件只会警告,不影响未签名构建。
|
|
90
|
+
|
|
91
|
+
## Modules
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npx expo-harmony modules list
|
|
95
|
+
npx expo-harmony modules inspect --package expo-linear-gradient
|
|
96
|
+
npx expo-harmony modules verify
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`list` 列出发现的模块候选(包名、版本、来源),并标出不支持 Harmony 的模块、重复版本和缺失的必需依赖。`inspect` 展示模块解析后的完整元数据,包括包根、ArkTS modules 和 HAR 路径,`--package <name>` 可筛选单个模块,未发现时报错。`verify` 输出诊断信息,存在 error 时以非零状态退出。
|
|
100
|
+
|
|
101
|
+
三个子命令都支持 `--variant debug|release` 和 `--native-modules-dir <dir>`(app-local 模块目录,默认 `./modules`)。
|
|
102
|
+
|
|
103
|
+
## Export
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
npx expo-harmony export:embed
|
|
107
|
+
npx expo-harmony export:embed --check
|
|
108
|
+
npx expo-harmony export:embed --reset-cache
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`export:embed` 先运行 doctor,再用项目本地的 Expo CLI 生成 Harmony 平台的 JS Bundle 和资源,并交给项目本地 `@expo/metro-config` 的 Hermes 导出器编译字节码、合并 Source Map。产物经过校验(Hermes 字节码魔数、Source Map 不含宿主机绝对路径)后原子写入原生工程:Bundle 和资源进入模块的 `rawfile` 目录,Source Map 与清单写入 `.expo/harmony/export/`。清单 `.expo/harmony/export-manifest.json` 记录每个文件的哈希与大小,`--check` 据此校验已有导出而不重新构建,`--reset-cache` 会透传给 Metro 清除转换缓存。
|
|
112
|
+
|
|
113
|
+
## Run
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
npx expo-harmony run \
|
|
117
|
+
--device <id-or-name> \
|
|
118
|
+
--variant debug \
|
|
119
|
+
--port 8081
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`run` 会依次执行环境诊断、确保原生工程(缺失时自动预构建,`--sync` 强制重新生成,否则校验 CNG 状态)、选择设备、Release 生产导出、OHPM 安装、Hvigor 构建、Metro 端口反向映射(仅 Debug)、HAP 安装和 Ability 启动。模块接线的最终正确性由 OHPM、Hvigor、CMake 和 ArkTS 编译器验证。
|
|
123
|
+
|
|
124
|
+
常用选项:
|
|
125
|
+
|
|
126
|
+
- `--variant debug|release`:选择构建模式,默认为 `debug`。Release 构建会先执行生产导出,不启动 Metro。
|
|
127
|
+
- `--device <id-or-name>`:选择已连接的 HDC 设备,或按完整名称启动本地模拟器,例如 `--device "Pura 90 Pro"`。存在多个候选目标时必须指定。
|
|
128
|
+
- `--port <number>`:设置 Metro 端口及设备反向映射端口,默认为 `8081`。
|
|
129
|
+
- `--no-bundler`:连接已经运行的 Expo Metro,不启动新的服务;端口空闲或被其他进程占用时会报错。不加此选项时,若端口上已有 Metro 在运行则直接复用。
|
|
130
|
+
- `--reset-cache`:Debug 模式下启动 Metro 时清除 Metro 缓存;Release 模式下清除生产导出缓存。
|
|
131
|
+
- `--no-install`:跳过 HAP 安装,直接启动设备上已有的应用。
|
|
132
|
+
- `--app-id <bundleName>`:指定要启动的应用;与生成包名不同时必须同时使用 `--no-install`。
|
|
133
|
+
- `--sync`:构建前强制重新执行一次预构建。
|
|
134
|
+
|
|
135
|
+
Debug 模式下由 CLI 启动的 Metro 会接管终端输出日志,按 Ctrl+C 退出。
|
|
136
|
+
|
|
137
|
+
未指定 `--device` 时,优先使用已连接的设备;没有连接的设备时,优先等待正在启动的模拟器,否则自动启动唯一的本地模拟器。存在多个候选模拟器时会列出名称,使用 `--device "模拟器名称"` 选择。通过模拟器名称选择或自动拉起时,等待 HDC 连接并确认该实例开机完成后才继续构建、安装和启动应用,最多等待 120 秒;退出 CLI 或 Metro 后模拟器继续运行。
|
|
138
|
+
|
|
139
|
+
自动启动需要 DevEco Studio 6.1.0 或更新版本,并已在 Device Manager 中创建模拟器。CLI 按[华为模拟器命令行文档](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-emulator-command-line)调用 `Emulator -list -details` 和 `Emulator -start <name>`,使用 DevEco Studio 配置的实例和镜像路径。启动日志保存在项目的 `.expo/harmony/emulator.log`。若启动失败,请先在 DevEco Studio 中处理首次使用协议或登录要求;文档注明需要登录开发者账号的模拟器版本无法通过命令行启动。
|
|
140
|
+
|
|
141
|
+
## 工具链与环境变量
|
|
142
|
+
|
|
143
|
+
工具链按「环境变量覆盖 → DevEco Studio 安装布局 → PATH」的顺序解析。`HARMONY_HDC`、`HARMONY_EMULATOR`、`HARMONY_OHPM`、`HARMONY_HVIGORW` 和 `HARMONY_NODE` 可覆盖对应工具的路径,SDK 根目录可通过 `DEVECO_SDK_HOME`、`HARMONY_HOME` 或 `OHOS_SDK_HOME` 指定。Emulator 同时支持 DevEco Studio 的 `tools/emulator` 和 Command Line Tools 的 `emulator` 布局,仅在需要模拟器时调用。原生构建回调 CLI 时使用的 Node.js 可通过 `EXPO_HARMONY_NODE` 显式指定。
|
|
144
|
+
|
|
145
|
+
## Author
|
|
146
|
+
|
|
147
|
+
**expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
|
|
148
|
+
Authored and maintained by Baoshuo with help from [contributors](https://github.com/renbaoshuo/expo-harmony/contributors).
|
|
149
|
+
|
|
150
|
+
> [Personal Website](https://baoshuo.ren) · [Blog](https://blog.baoshuo.ren) · GitHub [@renbaoshuo](https://github.com/renbaoshuo) · Twitter [@baoshuo](https://twitter.com/baoshuo)
|
package/build/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runHarmonyAsync = exports.prepareHarmonyNativeBuildCacheAsync = exports.commitHarmonyNativeBuildCacheAsync = exports.runModulesCommandAsync = exports.formatModulesResult = exports.installHarmonyDependenciesAsync = exports.resolveHarmonyToolchain = exports.resolveHarmonyBuildPlanAsync = exports.exportEmbedAsync = exports.buildHarmonyAsync = exports.runAsync = void 0;
|
|
4
|
+
var cli_1 = require("./cli");
|
|
5
|
+
Object.defineProperty(exports, "runAsync", { enumerable: true, get: function () { return cli_1.runAsync; } });
|
|
6
|
+
var build_1 = require("./buildHap/build");
|
|
7
|
+
Object.defineProperty(exports, "buildHarmonyAsync", { enumerable: true, get: function () { return build_1.buildHarmonyAsync; } });
|
|
8
|
+
var export_1 = require("./exportEmbed/export");
|
|
9
|
+
Object.defineProperty(exports, "exportEmbedAsync", { enumerable: true, get: function () { return export_1.exportEmbedAsync; } });
|
|
10
|
+
var tools_1 = require("./tools");
|
|
11
|
+
Object.defineProperty(exports, "resolveHarmonyBuildPlanAsync", { enumerable: true, get: function () { return tools_1.resolveHarmonyBuildPlanAsync; } });
|
|
12
|
+
Object.defineProperty(exports, "resolveHarmonyToolchain", { enumerable: true, get: function () { return tools_1.resolveHarmonyToolchain; } });
|
|
13
|
+
var install_1 = require("./run/install");
|
|
14
|
+
Object.defineProperty(exports, "installHarmonyDependenciesAsync", { enumerable: true, get: function () { return install_1.installHarmonyDependenciesAsync; } });
|
|
15
|
+
var modules_1 = require("./modules/modules");
|
|
16
|
+
Object.defineProperty(exports, "formatModulesResult", { enumerable: true, get: function () { return modules_1.formatModulesResult; } });
|
|
17
|
+
Object.defineProperty(exports, "runModulesCommandAsync", { enumerable: true, get: function () { return modules_1.runModulesCommandAsync; } });
|
|
18
|
+
var cache_1 = require("./run/cache");
|
|
19
|
+
Object.defineProperty(exports, "commitHarmonyNativeBuildCacheAsync", { enumerable: true, get: function () { return cache_1.commitHarmonyNativeBuildCacheAsync; } });
|
|
20
|
+
Object.defineProperty(exports, "prepareHarmonyNativeBuildCacheAsync", { enumerable: true, get: function () { return cache_1.prepareHarmonyNativeBuildCacheAsync; } });
|
|
21
|
+
var run_1 = require("./run/run");
|
|
22
|
+
Object.defineProperty(exports, "runHarmonyAsync", { enumerable: true, get: function () { return run_1.runHarmonyAsync; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expo-harmony/cli",
|
|
3
|
+
"version": "55.0.26-harmony.1",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"react-native",
|
|
6
|
+
"expo",
|
|
7
|
+
"expo-harmony",
|
|
8
|
+
"harmonyos"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/renbaoshuo/expo-harmony/tree/master/packages/cli#readme",
|
|
11
|
+
"bugs": "https://github.com/renbaoshuo/expo-harmony/issues",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Baoshuo <i@baoshuo.ren>",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/renbaoshuo/expo-harmony.git",
|
|
17
|
+
"directory": "packages/cli"
|
|
18
|
+
},
|
|
19
|
+
"main": "build/index.js",
|
|
20
|
+
"types": "build/index.d.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"prebuild": "yarn workspace @expo-harmony/prebuild-config build",
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"prepare": "yarn build",
|
|
25
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./build/index.d.ts",
|
|
30
|
+
"require": "./build/index.js",
|
|
31
|
+
"default": "./build/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./bin/expo-harmony": "./build/bin/expo-harmony.js"
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"expo-harmony": "build/bin/expo-harmony.js"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@expo-harmony/config-plugins": "55.0.10-harmony.1",
|
|
40
|
+
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.1",
|
|
41
|
+
"@expo-harmony/prebuild-config": "55.0.0-harmony.1",
|
|
42
|
+
"@expo/config": "55.0.17",
|
|
43
|
+
"cross-spawn": "^7.0.6",
|
|
44
|
+
"json5": "2.2.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"expo": "55.0.26"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"expo": {
|
|
51
|
+
"optional": false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "18.19.0",
|
|
56
|
+
"expo": "55.0.26",
|
|
57
|
+
"react": "19.2.3",
|
|
58
|
+
"react-native": "0.84.1",
|
|
59
|
+
"typescript": "6.0.3"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseArgs as parseNodeArgs,
|
|
3
|
+
} from 'node:util';
|
|
4
|
+
|
|
5
|
+
import { HarmonyCliError } from './errors';
|
|
6
|
+
|
|
7
|
+
type Option = {
|
|
8
|
+
short?: string;
|
|
9
|
+
type: 'boolean' | 'string';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Options = Record<string, Option>;
|
|
13
|
+
type Values<T extends Options> = {
|
|
14
|
+
[K in keyof T]?: T[K]['type'] extends 'boolean' ? boolean : string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const CommonOptions = {
|
|
18
|
+
help: { short: 'h', type: 'boolean' },
|
|
19
|
+
} as const satisfies Options;
|
|
20
|
+
|
|
21
|
+
function parseArgs<T extends Options>(
|
|
22
|
+
options: T,
|
|
23
|
+
argv: string[]
|
|
24
|
+
): { positionals: string[]; values: Values<T> } {
|
|
25
|
+
try {
|
|
26
|
+
return parseNodeArgs({
|
|
27
|
+
allowPositionals: true,
|
|
28
|
+
args: argv,
|
|
29
|
+
options,
|
|
30
|
+
strict: true,
|
|
31
|
+
}) as { positionals: string[]; values: Values<T> };
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
34
|
+
|
|
35
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', message, {
|
|
36
|
+
cause,
|
|
37
|
+
operation: 'parse-arguments',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { CommonOptions, parseArgs };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { HarmonyCliError } from '../errors';
|
|
4
|
+
import { exportEmbedAsync } from '../exportEmbed/export';
|
|
5
|
+
import type { HarmonyExportManifest } from '../exportEmbed/manifest';
|
|
6
|
+
import { toPosixPath } from '../path';
|
|
7
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
8
|
+
import {
|
|
9
|
+
commitHarmonyNativeBuildCacheAsync,
|
|
10
|
+
prepareHarmonyNativeBuildCacheAsync,
|
|
11
|
+
} from '../run/cache';
|
|
12
|
+
import { installHarmonyDependenciesAsync } from '../run/install';
|
|
13
|
+
import {
|
|
14
|
+
resolveHarmonyBuildPlanAsync,
|
|
15
|
+
resolveHarmonyToolchain,
|
|
16
|
+
} from '../tools';
|
|
17
|
+
import {
|
|
18
|
+
ensureGeneratedProjectAsync,
|
|
19
|
+
isNonEmptyRegularFile,
|
|
20
|
+
progress,
|
|
21
|
+
runCheckedAsync,
|
|
22
|
+
timed,
|
|
23
|
+
} from './common';
|
|
24
|
+
|
|
25
|
+
export interface HarmonyBuildOptions {
|
|
26
|
+
io?: Pick<Console, 'error' | 'log' | 'warn'>;
|
|
27
|
+
/** @internal For release verification after a successful isolated prebuild check. */
|
|
28
|
+
skipGeneratedProjectCheck?: boolean;
|
|
29
|
+
sync?: boolean;
|
|
30
|
+
variant?: 'debug' | 'release';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HarmonyBuildResult {
|
|
34
|
+
bundleName: string;
|
|
35
|
+
export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
|
|
36
|
+
hapPath: string;
|
|
37
|
+
headless: true;
|
|
38
|
+
installed: false;
|
|
39
|
+
launched: false;
|
|
40
|
+
ok: true;
|
|
41
|
+
schemaVersion: 1;
|
|
42
|
+
steps: Record<string, number>;
|
|
43
|
+
variant: 'debug' | 'release';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type NormalizedBuildOptions = Required<HarmonyBuildOptions> & {
|
|
47
|
+
requireDeviceTools: false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function buildHarmonyUnlockedAsync(
|
|
51
|
+
projectRoot: string,
|
|
52
|
+
options: HarmonyBuildOptions = {}
|
|
53
|
+
): Promise<HarmonyBuildResult> {
|
|
54
|
+
const normalizedOptions: NormalizedBuildOptions = {
|
|
55
|
+
io: options.io || console,
|
|
56
|
+
requireDeviceTools: false,
|
|
57
|
+
skipGeneratedProjectCheck: Boolean(options.skipGeneratedProjectCheck),
|
|
58
|
+
sync: Boolean(options.sync),
|
|
59
|
+
variant: options.variant || 'debug',
|
|
60
|
+
};
|
|
61
|
+
const steps: Record<string, number> = {
|
|
62
|
+
device: 0,
|
|
63
|
+
install: 0,
|
|
64
|
+
launch: 0,
|
|
65
|
+
metro: 0,
|
|
66
|
+
metroPort: 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await ensureGeneratedProjectAsync(projectRoot, normalizedOptions, steps);
|
|
70
|
+
|
|
71
|
+
const plan = await timed(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(
|
|
72
|
+
projectRoot,
|
|
73
|
+
{ buildMode: normalizedOptions.variant }
|
|
74
|
+
));
|
|
75
|
+
const toolchain = resolveHarmonyToolchain();
|
|
76
|
+
|
|
77
|
+
let exportManifest: HarmonyExportManifest | null = null;
|
|
78
|
+
if (normalizedOptions.variant === 'release') {
|
|
79
|
+
progress(normalizedOptions, 'Exporting the release Hermes bundle');
|
|
80
|
+
exportManifest = await timed(steps, 'export', () => exportEmbedAsync(
|
|
81
|
+
projectRoot,
|
|
82
|
+
{ skipDoctor: true }
|
|
83
|
+
));
|
|
84
|
+
} else {
|
|
85
|
+
steps.export = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
progress(normalizedOptions, 'Installing Harmony project dependencies');
|
|
89
|
+
await timed(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, toolchain));
|
|
90
|
+
|
|
91
|
+
progress(normalizedOptions, 'Checking Harmony native dependency cache');
|
|
92
|
+
const nativeBuildCache = await timed(steps, 'nativeCache', () => (
|
|
93
|
+
prepareHarmonyNativeBuildCacheAsync(projectRoot, plan)
|
|
94
|
+
));
|
|
95
|
+
|
|
96
|
+
if (nativeBuildCache.changed) {
|
|
97
|
+
progress(normalizedOptions, 'Invalidated stale Harmony native build objects');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
progress(normalizedOptions, `Building the ${normalizedOptions.variant} HAP without a device`);
|
|
101
|
+
const buildEnv = {
|
|
102
|
+
...process.env,
|
|
103
|
+
EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
|
|
104
|
+
EXPO_METRO_TARGET: 'harmony',
|
|
105
|
+
HERMES_V1_ENABLED: 'true',
|
|
106
|
+
...(normalizedOptions.variant === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
|
|
107
|
+
...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
|
|
108
|
+
? { DEVECO_SDK_HOME: toolchain.sdkHome }
|
|
109
|
+
: {}),
|
|
110
|
+
};
|
|
111
|
+
await timed(steps, 'build', () => runCheckedAsync(toolchain.hvigor.command, [
|
|
112
|
+
...toolchain.hvigor.args,
|
|
113
|
+
...plan.hvigorArgs,
|
|
114
|
+
], {
|
|
115
|
+
code: 'ERR_HARMONY_BUILD_FAILED',
|
|
116
|
+
cwd: plan.harmonyRoot,
|
|
117
|
+
env: buildEnv,
|
|
118
|
+
message: 'Hvigor build',
|
|
119
|
+
operation: 'hvigor-build',
|
|
120
|
+
timeoutMs: 15 * 60_000,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
if (!isNonEmptyRegularFile(plan.expectedHap)) {
|
|
124
|
+
throw new HarmonyCliError(
|
|
125
|
+
'ERR_HARMONY_HAP_MISSING',
|
|
126
|
+
'Hvigor completed without producing the expected non-empty regular HAP.',
|
|
127
|
+
{ operation: 'verify-hap' }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await timed(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(nativeBuildCache));
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
bundleName: plan.bundleName,
|
|
135
|
+
export: exportManifest
|
|
136
|
+
? {
|
|
137
|
+
assetCount: exportManifest.assets.length,
|
|
138
|
+
bundleSha256: exportManifest.bundle.sha256,
|
|
139
|
+
sourceMapSha256: exportManifest.sourceMap.sha256,
|
|
140
|
+
}
|
|
141
|
+
: null,
|
|
142
|
+
hapPath: toPosixPath(path.relative(projectRoot, plan.expectedHap)),
|
|
143
|
+
headless: true,
|
|
144
|
+
installed: false,
|
|
145
|
+
launched: false,
|
|
146
|
+
ok: true,
|
|
147
|
+
schemaVersion: 1,
|
|
148
|
+
steps,
|
|
149
|
+
variant: normalizedOptions.variant,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function buildHarmonyAsync(
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
options: HarmonyBuildOptions = {}
|
|
156
|
+
): Promise<HarmonyBuildResult> {
|
|
157
|
+
return withHarmonyProjectLockAsync(
|
|
158
|
+
projectRoot,
|
|
159
|
+
'build',
|
|
160
|
+
() => buildHarmonyUnlockedAsync(projectRoot, options)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { buildHarmonyAsync };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { doctorAsync, type DoctorResult } from '../doctor/doctor';
|
|
4
|
+
import { HarmonyCliError } from '../errors';
|
|
5
|
+
import { checkAsync } from '../prebuild/check';
|
|
6
|
+
import { prebuildParsedAsync } from '../prebuild/prebuild';
|
|
7
|
+
import { formatDiagnostics, spawnAsync } from '../process';
|
|
8
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../tools';
|
|
9
|
+
|
|
10
|
+
interface HarmonyBuildPipelineOptions {
|
|
11
|
+
io?: Pick<Console, 'error' | 'log' | 'warn'>;
|
|
12
|
+
requireDeviceTools?: boolean;
|
|
13
|
+
/** @internal The caller already completed an isolated prebuild check. */
|
|
14
|
+
skipGeneratedProjectCheck?: boolean;
|
|
15
|
+
sync: boolean;
|
|
16
|
+
variant: 'debug' | 'release';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CheckedProcessOptions {
|
|
20
|
+
code: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
message: string;
|
|
24
|
+
operation: string;
|
|
25
|
+
outputLimit?: number;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function progress(options: HarmonyBuildPipelineOptions, message: string): void {
|
|
30
|
+
options.io?.log?.(`› ${message}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function timed<T>(
|
|
34
|
+
steps: Record<string, number>,
|
|
35
|
+
name: string,
|
|
36
|
+
operation: () => Promise<T> | T
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
try {
|
|
40
|
+
return await operation();
|
|
41
|
+
} finally {
|
|
42
|
+
steps[name] = Math.max(0, Math.round(performance.now() - started));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertDoctor(result: DoctorResult): void {
|
|
47
|
+
if (result.ok) return;
|
|
48
|
+
|
|
49
|
+
const failing = result.checks.filter(check => check.status === 'error').map(check => check.id);
|
|
50
|
+
|
|
51
|
+
throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', `Harmony doctor found blocking checks: ${failing.join(', ') || 'unknown'}.`, { operation: 'doctor' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isNonEmptyRegularFile(file: string): boolean {
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.lstatSync(file);
|
|
57
|
+
return !stat.isSymbolicLink() && stat.isFile() && stat.size > 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runCheckedAsync(
|
|
64
|
+
command: string,
|
|
65
|
+
args: string[],
|
|
66
|
+
options: CheckedProcessOptions
|
|
67
|
+
) {
|
|
68
|
+
const result = await spawnAsync(command, args, {
|
|
69
|
+
capture: true,
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
env: options.env,
|
|
72
|
+
operation: options.operation,
|
|
73
|
+
outputLimit: options.outputLimit || 2 * 1024 * 1024,
|
|
74
|
+
timeoutMs: options.timeoutMs,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (result.code !== 0 || result.timedOut) {
|
|
78
|
+
const diagnostics = formatDiagnostics(result);
|
|
79
|
+
throw new HarmonyCliError(
|
|
80
|
+
options.code,
|
|
81
|
+
`${options.message} exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
82
|
+
{ exitCode: result.code || 1, operation: options.operation }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function ensureGeneratedProjectAsync(
|
|
90
|
+
projectRoot: string,
|
|
91
|
+
options: HarmonyBuildPipelineOptions,
|
|
92
|
+
steps: Record<string, number>
|
|
93
|
+
) {
|
|
94
|
+
const plan = await resolveHarmonyBuildPlanIfPresentAsync(projectRoot, {
|
|
95
|
+
buildMode: options.variant,
|
|
96
|
+
});
|
|
97
|
+
const exists = Boolean(plan && fs.existsSync(plan.harmonyRoot));
|
|
98
|
+
|
|
99
|
+
progress(options, 'Checking the Harmony project');
|
|
100
|
+
const doctor = await timed(steps, 'doctor', () => doctorAsync(projectRoot, {
|
|
101
|
+
requireBuildTools: true,
|
|
102
|
+
requireDeviceTools: options.requireDeviceTools,
|
|
103
|
+
validateGeneratedProject: exists,
|
|
104
|
+
validateModules: false,
|
|
105
|
+
}));
|
|
106
|
+
assertDoctor(doctor);
|
|
107
|
+
|
|
108
|
+
if (!exists || options.sync) {
|
|
109
|
+
progress(options, exists
|
|
110
|
+
? 'Synchronizing the generated Harmony project'
|
|
111
|
+
: 'Generating the missing Harmony project');
|
|
112
|
+
await timed(steps, 'prebuild', () => prebuildParsedAsync(
|
|
113
|
+
projectRoot,
|
|
114
|
+
[],
|
|
115
|
+
{ buildType: options.variant }
|
|
116
|
+
));
|
|
117
|
+
steps.cngCheck = 0;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
steps.prebuild = 0;
|
|
122
|
+
if (options.skipGeneratedProjectCheck) {
|
|
123
|
+
steps.cngCheck = 0;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
progress(options, 'Checking CNG ownership and drift');
|
|
127
|
+
const check = await timed(steps, 'cngCheck', () => checkAsync(projectRoot, { buildType: options.variant }));
|
|
128
|
+
if (!check.clean) {
|
|
129
|
+
const summary = check.changes.slice(0, 8).map(change => `${change.type}:${change.path}`).join(', ');
|
|
130
|
+
throw new HarmonyCliError(
|
|
131
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
132
|
+
`Generated Harmony files differ from CNG desired state${summary ? ` (${summary})` : ''}. Run expo-harmony prebuild or retry with --sync.`,
|
|
133
|
+
{ operation: 'check' }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
ensureGeneratedProjectAsync,
|
|
140
|
+
isNonEmptyRegularFile,
|
|
141
|
+
progress,
|
|
142
|
+
runCheckedAsync,
|
|
143
|
+
timed,
|
|
144
|
+
};
|
|
145
|
+
export type { CheckedProcessOptions, HarmonyBuildPipelineOptions };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CommonOptions, parseArgs } from '../args';
|
|
2
|
+
import { HarmonyCliError } from '../errors';
|
|
3
|
+
|
|
4
|
+
const BuildOptions = {
|
|
5
|
+
...CommonOptions,
|
|
6
|
+
sync: { type: 'boolean' },
|
|
7
|
+
variant: { type: 'string' },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
function parseBuildArgs(argv: string[]) {
|
|
11
|
+
const { positionals, values } = parseArgs(BuildOptions, argv);
|
|
12
|
+
|
|
13
|
+
if (positionals.length > 1) {
|
|
14
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected build positional argument: ${positionals[1]}`, {
|
|
15
|
+
operation: 'parse-arguments',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const variant = values.variant || 'debug';
|
|
20
|
+
|
|
21
|
+
if (variant !== 'debug' && variant !== 'release') {
|
|
22
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--variant must be debug or release.', {
|
|
23
|
+
operation: 'parse-arguments',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
help: Boolean(values.help),
|
|
29
|
+
project: positionals[0],
|
|
30
|
+
sync: Boolean(values.sync),
|
|
31
|
+
variant: variant as 'debug' | 'release',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { parseBuildArgs };
|