cj-tauri 2.0.0 → 2.1.0

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.
Files changed (158) hide show
  1. package/README.md +113 -99
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +632 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +526 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3740 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,770 +1,770 @@
1
- /**
2
- * cj-tauri CLI:项目构建 / 运行 / 诊断(dev / build / run / info)
3
- */
4
- package cli
5
-
6
- import std.env.*
7
- import std.process.*
8
- import std.fs.*
9
- import std.collection.*
10
-
11
- /** CLI 版本:须与 cli/cjpm.toml 的 version、CHANGELOG 顶部版本一致(scripts/check-version.sh 校验) */
12
- func cliVersion(): String {
13
- return "2.0.0"
14
- }
15
-
16
- /** 一次命令所需的路径上下文 */
17
- public class AppContext {
18
- let projectRoot: String
19
- let frameworkRoot: String
20
- let stdx: String
21
- let cjpm: String
22
- /** 新形态工程(工程根带 cj-tauri.toml 清单):可执行产物在 targets/desktop 骨架,前端在 assets/ui */
23
- let newForm: Bool
24
-
25
- public init(projectRoot: String, frameworkRoot: String, stdx: String, cjpm: String, newForm: Bool) {
26
- this.projectRoot = projectRoot
27
- this.frameworkRoot = frameworkRoot
28
- this.stdx = stdx
29
- this.cjpm = cjpm
30
- this.newForm = newForm
31
- }
32
- }
33
-
34
- /** 项目根目录:从当前目录向上找含 cjpm.toml 的目录 */
35
- func findProjectRoot(): Option<String> {
36
- var cur: Option<String> = Some(currentDir())
37
- var guard = 0
38
- while (guard < 64) {
39
- guard++
40
- if (let Some(d) <- cur) {
41
- if (exists(joinPath(d, "cjpm.toml"))) {
42
- return Some(d)
43
- }
44
- cur = parentDir(d)
45
- } else {
46
- return None
47
- }
48
- }
49
- return None
50
- }
51
-
52
- /** 可执行产物:旧形态 = 工程根 target/release/bin/main[.exe];新形态 = targets/desktop 骨架 */
53
- func findAppBinary(projectRoot: String, newForm: Bool): Option<String> {
54
- let base = if (newForm) { joinPath(projectRoot, "targets/desktop") } else { projectRoot }
55
- let binDir = joinPath(joinPath(joinPath(base, "target"), "release"), "bin")
56
- if (!exists(binDir)) {
57
- return None
58
- }
59
- let preferred = "main" + exeSuffix()
60
- var fallback = ""
61
- for (info in listDir(binDir)) {
62
- if (info.isDirectory()) {
63
- continue
64
- }
65
- let nm = baseName(info.path.toString())
66
- if (nm.endsWith(".cjo") || nm.endsWith(".o") || nm.endsWith(".d") || nm.endsWith(".bc")) {
67
- continue
68
- }
69
- if (hostIsWindows() && !nm.endsWith(".exe")) {
70
- continue
71
- }
72
- if (nm == preferred) {
73
- return Some(joinPath(binDir, nm))
74
- }
75
- fallback = joinPath(binDir, nm)
76
- }
77
- if (fallback.size > 0) {
78
- return Some(fallback)
79
- }
80
- return None
81
- }
82
-
83
- /** 把若干目录前置到某个路径型环境变量(PATH / LD_LIBRARY_PATH) */
84
- func prependPath(key: String, dirs: ArrayList<String>): Unit {
85
- let sep = pathListSep()
86
- var head = ""
87
- for (d in dirs) {
88
- if (d.size == 0) {
89
- continue
90
- }
91
- head = head + d + sep
92
- }
93
- setVariable(key, head + envOr(key, ""))
94
- }
95
-
96
- /**
97
- * 准备子进程运行环境:
98
- * 动态库搜索路径需要「框架 native(含同步的 WebView2 loader)+ stdx + 仓颉运行时」,
99
- * PATH 还需要「桥、cjpm、cjc」(Windows 下 DLL 也走 PATH)。
100
- */
101
- func prepareEnv(fwRoot: String, stdx: String): Unit {
102
- let libDirs = ArrayList<String>()
103
- libDirs.add(joinPath(fwRoot, "native"))
104
- libDirs.add(joinPath(joinPath(fwRoot, "native"), "webview2"))
105
- if (stdx.size > 0) {
106
- libDirs.add(stdx)
107
- }
108
- let pathDirs = ArrayList<String>()
109
- for (d in libDirs) {
110
- pathDirs.add(d)
111
- }
112
- if (let Some(home) <- resolveCangjieHome()) {
113
- if (let Some(rt) <- findRuntimeLibDir(home)) {
114
- libDirs.add(rt)
115
- pathDirs.add(rt)
116
- }
117
- pathDirs.add(joinPath(joinPath(home, "tools"), "bin")) // cjpm
118
- pathDirs.add(joinPath(joinPath(home, "tools"), "lib")) // cjpm 依赖
119
- pathDirs.add(joinPath(home, "bin")) // cjc
120
- }
121
- prependPath("PATH", pathDirs)
122
- if (!hostIsWindows()) {
123
- prependPath("LD_LIBRARY_PATH", libDirs)
124
- }
125
- }
126
-
127
- /** 确保 C 桥已构建(缺失时调用平台构建脚本) */
128
- func ensureBridge(fwRoot: String): Bool {
129
- let nativeDir = joinPath(fwRoot, "native")
130
- let lib = joinPath(nativeDir, bridgeLibName())
131
- if (exists(lib)) {
132
- return true
133
- }
134
- println("[cj-tauri] C 桥未构建,正在构建: ${lib}")
135
- if (hostIsWindows()) {
136
- let script = joinPath(nativeDir, "build_win.bat")
137
- if (!exists(script)) {
138
- eprintln("[cj-tauri] 错误: 缺少 ${script}")
139
- return false
140
- }
141
- let code = execute("cmd.exe", ["/c", "\"" + script + "\""], workingDirectory: Path(nativeDir))
142
- if (code != 0) {
143
- eprintln("[cj-tauri] 错误: C 桥构建失败(exit=${code})")
144
- return false
145
- }
146
- if (exists(lib)) {
147
- return true
148
- }
149
- eprintln("[cj-tauri] 错误: 构建脚本执行完仍未生成 ${lib}")
150
- return false
151
- }
152
- let script = joinPath(nativeDir, "build_linux.sh")
153
- if (!exists(script)) {
154
- eprintln("[cj-tauri] 错误: 缺少 ${script}")
155
- return false
156
- }
157
- let code = execute("bash", [script], workingDirectory: Path(nativeDir))
158
- if (code != 0) {
159
- eprintln("[cj-tauri] 错误: C 桥构建失败(exit=${code})")
160
- return false
161
- }
162
- if (exists(lib)) {
163
- return true
164
- }
165
- eprintln("[cj-tauri] 错误: 构建脚本执行完仍未生成 ${lib}")
166
- return false
167
- }
168
-
169
- /** 解析项目上下文(项目根 / 框架根 / stdx / cjpm) */
170
- func loadContext(): Option<AppContext> {
171
- var proj = ""
172
- if (let Some(p) <- findProjectRoot()) {
173
- proj = p
174
- } else {
175
- eprintln("[cj-tauri] 错误: 当前目录不在 cj-tauri 项目内(未找到 cjpm.toml)")
176
- return None
177
- }
178
- var fw = ""
179
- if (let Some(f) <- frameworkFromProject(proj)) {
180
- fw = f
181
- } else if (let Some(f) <- resolveFrameworkRoot()) {
182
- fw = f
183
- } else {
184
- eprintln("[cj-tauri] 错误: 无法确定 cj-tauri 框架目录")
185
- eprintln(" cjpm.toml 里的 cjTauri 依赖路径不可用,且未设置 CJ_TAURI_ROOT")
186
- return None
187
- }
188
- if (!exists(fw)) {
189
- eprintln("[cj-tauri] 错误: 框架目录不存在: ${fw}")
190
- eprintln(" 请修改 cjpm.toml 的 cjTauri 依赖路径,或设置 CJ_TAURI_ROOT")
191
- return None
192
- }
193
- var stdx = ""
194
- if (let Some(s) <- resolveStdx(fw, Some(proj))) {
195
- stdx = s
196
- } else {
197
- eprintln("[cj-tauri] 告警: 未找到 stdx 目录,构建可能失败(可用 CANGJIE_STDX 指定)")
198
- }
199
- var cjpm = ""
200
- if (let Some(c) <- resolveCjpm()) {
201
- cjpm = c
202
- } else {
203
- eprintln("[cj-tauri] 错误: 找不到 cjpm")
204
- eprintln(" 请把仓颉 SDK 的 tools/bin 与 bin 加入 PATH,或设置 CANGJIE_HOME")
205
- return None
206
- }
207
- // 新旧形态判据:工程根有没有 cj-tauri.toml(RFC-002 §7 的单一真相文件)
208
- let newForm = exists(joinPath(proj, "cj-tauri.toml"))
209
- return Some(AppContext(proj, fw, stdx, cjpm, newForm))
210
- }
211
-
212
- /** 前端工程(Vite):新形态在 assets/ui、旧形态在 ui;有 package.json 才启用,没有整段跳过 */
213
- class WebUi {
214
- let dir: String
215
- let devUrl: String
216
- let logFile: String
217
- let port: UInt16
218
-
219
- init(dir: String) {
220
- this.dir = dir
221
- this.devUrl = "http://127.0.0.1:5173/"
222
- this.logFile = joinPath(dir, devLogName())
223
- this.port = 5173
224
- }
225
- }
226
-
227
- func detectWebUi(projectRoot: String, newForm: Bool): Option<WebUi> {
228
- let uiSub = if (newForm) { "assets/ui" } else { "ui" }
229
- let dir = joinPath(projectRoot, uiSub)
230
- if (exists(joinPath(dir, "package.json"))) {
231
- return Some(WebUi(dir))
232
- }
233
- return None
234
- }
235
-
236
- /**
237
- * dev server 的日志文件名:必须是**相对名**。
238
- * launch 会给整条命令行加引号,日志路径若再自己带引号,cmd.exe 会拿到嵌套引号并直接报
239
- * 「文件名、目录名或卷标语法不正确」。相对名 + 工作目录 = ui/ 即可,命令保持单层引号。
240
- */
241
- func devLogName(): String {
242
- return "dev-server.log"
243
- }
244
-
245
- /** 拼一条 npm 命令行;给了日志文件名就重定向过去 */
246
- func npmLine(parts: Array<String>, logName: String): String {
247
- var line = "npm"
248
- for (p in parts) {
249
- line = line + " " + p
250
- }
251
- if (logName.size > 0) {
252
- line = line + " > " + logName + " 2>&1"
253
- }
254
- return line
255
- }
256
-
257
- /** 阻塞跑一条 shell 命令行:npm 在两个平台上都是脚本,必须经 shell(cmd.exe /c 或 bash -lc) */
258
- func runShellLine(line: String, dir: String): Int64 {
259
- if (hostIsWindows()) {
260
- return execute("cmd.exe", ["/c", line], workingDirectory: Path(dir))
261
- }
262
- return execute("bash", ["-lc", line], workingDirectory: Path(dir))
263
- }
264
-
265
- /** 后台跑一条 shell 命令行,返回子进程(收尾时连同子进程一起收掉) */
266
- func launchShellLine(line: String, dir: String): SubProcess {
267
- if (hostIsWindows()) {
268
- return launch("cmd.exe", ["/c", line], workingDirectory: Path(dir))
269
- }
270
- // 刻意**不**用 setsid 另开会话:dev server 必须留在 CLI 的进程组里,
271
- // 终端 Ctrl-C 才能一并收掉它(实测 setsid 后 Ctrl-C 只杀 CLI,dev server 漏跑)。
272
- // npm → sh -c vite → node(vite) → esbuild 这条父子链改由 stopDevServer 递归收掉。
273
- return launch("bash", ["-lc", line], workingDirectory: Path(dir))
274
- }
275
-
276
- /**
277
- * 按「先子后父」递归收掉一棵进程树(对 rootPid 自身也发信号)。
278
- * npm → sh -c vite → node(vite) → esbuild 是一条父子链,只 terminate 直接子进程(bash)
279
- * 会留下后代继续跑(实测残留 vite + esbuild),所以收尾必须递归。用 shell 递归实现,
280
- * 省掉在仓颉侧解析 /proc 的复杂度。
281
- */
282
- func killProcessTree(rootPid: Int64, sig: String): Unit {
283
- let script = "t() { for c in $(pgrep -P \"$1\" 2>/dev/null); do t \"$c\"; done; kill -" +
284
- sig + " \"$1\" 2>/dev/null; }; t ${rootPid}"
285
- try {
286
- execute("bash", ["-lc", script])
287
- } catch (e: Exception) {
288
- () // 进程已自己退掉等情况不值得让收尾流程失败
289
- }
290
- }
291
-
292
- /** 前端依赖:没有 node_modules 就先 npm install */
293
- func ensureUiDeps(ui: WebUi): Bool {
294
- if (exists(joinPath(ui.dir, "node_modules"))) {
295
- return true
296
- }
297
- if (runShellLine("npm -v", ui.dir) != 0) {
298
- eprintln("[cj-tauri] 错误: ui/package.json 存在,但找不到 npm(需要 Node.js)")
299
- eprintln(" 装好 Node.js 再试,或删掉 ui/package.json 走内联 HTML 模式")
300
- return false
301
- }
302
- println("[cj-tauri] 安装前端依赖: npm install(首次会慢一些)")
303
- let code = runShellLine(npmLine(["install"], ""), ui.dir)
304
- if (code != 0) {
305
- eprintln("[cj-tauri] 错误: npm install 失败(exit=${code})")
306
- return false
307
- }
308
- return true
309
- }
310
-
311
- /** 前端发布构建:Vite + singlefile → ui/dist/index.html(后端 run(html) 读的就是它) */
312
- func buildUiBundle(ui: WebUi): Bool {
313
- println("[cj-tauri] 前端构建: npm run build(产物 ui/dist/index.html)")
314
- let code = runShellLine(npmLine(["run", "build"], ""), ui.dir)
315
- if (code != 0) {
316
- eprintln("[cj-tauri] 错误: npm run build 失败(exit=${code})")
317
- return false
318
- }
319
- if (!exists(joinPath(joinPath(ui.dir, "dist"), "index.html"))) {
320
- eprintln("[cj-tauri] 错误: npm run build 跑完仍没有 ui/dist/index.html")
321
- eprintln(" 检查 ui/vite.config.js 里是否装上了 vite-plugin-singlefile")
322
- return false
323
- }
324
- return true
325
- }
326
-
327
- /**
328
- * dev server 是否就绪:看 Vite 自己打在日志里的启动行(`Local: http://127.0.0.1:5173/`)。
329
- * 不用 TCP 探活是为了少一个平台差异点;探不到也不硬失败——调用方超时后会告警并继续。
330
- */
331
- func devServerReady(ui: WebUi): Bool {
332
- if (!exists(ui.logFile)) {
333
- return false
334
- }
335
- let txt = readText(ui.logFile)
336
- return txt.contains("Local:") && txt.contains("${ui.port}")
337
- }
338
-
339
- /** 等 dev server 就绪:200ms 一步,最多 timeoutSec 秒 */
340
- func waitDevServer(ui: WebUi, timeoutSec: Int64): Bool {
341
- var steps: Int64 = 0
342
- let limit = timeoutSec * 5
343
- while (steps < limit) {
344
- if (devServerReady(ui)) {
345
- return true
346
- }
347
- sleep(Duration.millisecond * 200)
348
- steps += 1
349
- }
350
- return false
351
- }
352
-
353
- /** 收掉 dev server 整棵进程树:Windows 用 taskkill /T;其他平台递归收掉父子链 */
354
- func stopDevServer(p: SubProcess): Unit {
355
- if (hostIsWindows()) {
356
- let code = execute("taskkill", ["/F", "/T", "/PID", "${p.pid}"])
357
- if (code == 0) {
358
- println("[cj-tauri] dev server 已收掉(pid=${p.pid})")
359
- } else {
360
- eprintln("[cj-tauri] 告警: 收 dev server 失败(pid=${p.pid}),可手动结束")
361
- }
362
- return
363
- }
364
- // 先 TERM 让 Vite 自己退,再 KILL 兜底;两趟都按「先子后父」收整棵树,
365
- // 免得只收掉直接子进程后,后代(vite / esbuild)继续占着 5173 端口。
366
- killProcessTree(p.pid, "TERM")
367
- sleep(Duration.millisecond * 500)
368
- killProcessTree(p.pid, "KILL")
369
- p.terminate() // 极端情况(pgrep 不可用)下至少收掉直接子进程
370
- println("[cj-tauri] dev server 已收掉(pid=${p.pid} 及后代)")
371
- }
372
-
373
- /** dev 模式:起前端 dev server → 注入 CJ_TAURI_DEV_URL → 启动应用 → 退出后收掉 dev server */
374
- func runWithDevServer(c: AppContext, ui: WebUi): Int64 {
375
- println("[cj-tauri] 启动前端 dev server: ${ui.dir}(npm run dev,日志 ${ui.logFile})")
376
- let devCmd = npmLine(["run", "dev"], devLogName())
377
- var dev: Option<SubProcess> = None
378
- try {
379
- dev = Some(launchShellLine(devCmd, ui.dir))
380
- } catch (e: Exception) {
381
- eprintln("[cj-tauri] 错误: 起 dev server 失败: ${e.message}")
382
- eprintln(" 命令: ${devCmd}(工作目录 ${ui.dir})")
383
- return 1
384
- }
385
- // 阈值给得宽松:本机实测 npm 把 Vite 拉起来能花到 85 秒(node/npm 冷启动偏慢),
386
- // 90 秒会贴着超时线走,宁可多等也不要误报「没就绪」。
387
- if (waitDevServer(ui, 180)) {
388
- println("[cj-tauri] dev server 就绪: ${ui.devUrl}")
389
- } else {
390
- eprintln("[cj-tauri] 告警: 180 秒内没在日志里看到就绪行,仍按 ${ui.devUrl} 启动")
391
- eprintln(" 日志: ${ui.logFile}")
392
- }
393
- setVariable("CJ_TAURI_DEV_URL", ui.devUrl)
394
- println("[cj-tauri] 注入 CJ_TAURI_DEV_URL=${ui.devUrl}")
395
- let code = runProject(c)
396
- if (let Some(p) <- dev) {
397
- stopDevServer(p)
398
- }
399
- return code
400
- }
401
-
402
- /** 构建:桥 + cjpm build;withUi=true 时先打前端发布包(有 Vite 前端才会触发) */
403
- func buildProject(c: AppContext, withUi: Bool): Bool {
404
- if (withUi) {
405
- if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
406
- if (!ensureUiDeps(ui)) {
407
- return false
408
- }
409
- if (!buildUiBundle(ui)) {
410
- return false
411
- }
412
- }
413
- }
414
- prepareEnv(c.frameworkRoot, c.stdx)
415
- if (!ensureBridge(c.frameworkRoot)) {
416
- return false
417
- }
418
- // 新形态:编 targets/desktop 骨架(path 依赖会连带编业务 static 包 + launcher 可执行)
419
- let buildDir = if (c.newForm) { joinPath(c.projectRoot, "targets/desktop") } else { c.projectRoot }
420
- if (!exists(joinPath(buildDir, "cjpm.toml"))) {
421
- eprintln("[cj-tauri] 错误: 骨架未生成:${buildDir}")
422
- eprintln(" 先运行 cj-tauri sync(按清单派生 targets/ 骨架)")
423
- return false
424
- }
425
- println("[cj-tauri] cjpm build ... (${baseName(buildDir)})")
426
- let code = execute(c.cjpm, ["build"], workingDirectory: Path(buildDir))
427
- if (code != 0) {
428
- eprintln("[cj-tauri] 构建失败(exit=${code})")
429
- return false
430
- }
431
- if (let Some(bin) <- findAppBinary(c.projectRoot, c.newForm)) {
432
- println("[cj-tauri] 构建完成: ${bin}")
433
- } else {
434
- println("[cj-tauri] 构建完成(未在 target/release/bin 找到可执行产物)")
435
- }
436
- return true
437
- }
438
-
439
- /** 运行已构建产物(App 以项目根为工作目录,页面/能力清单按相对路径读取) */
440
- func runProject(c: AppContext): Int64 {
441
- var bin = ""
442
- if (let Some(b) <- findAppBinary(c.projectRoot, c.newForm)) {
443
- bin = b
444
- } else {
445
- eprintln("[cj-tauri] 错误: 未找到可执行产物,请先 cj-tauri build")
446
- return 1
447
- }
448
- prepareEnv(c.frameworkRoot, c.stdx)
449
- if (!hostIsWindows()) {
450
- if (getVariable("DISPLAY").isNone()) {
451
- eprintln("[cj-tauri] 告警: 未设置 DISPLAY,需要可用的 X 显示环境(或 Wayland+XWayland)")
452
- }
453
- }
454
- println("[cj-tauri] 启动: ${bin}")
455
- let code = execute(bin, Array<String>(), workingDirectory: Path(c.projectRoot))
456
- println("[cj-tauri] 应用退出(exit=${code})")
457
- return code
458
- }
459
-
460
- /** 某个目录下是不是鸿蒙脚手架工程(判据:build-hap.sh + entry/cjpm.toml 同时在) */
461
- func isOhosProjectAt(dir: String): Bool {
462
- return exists(joinPath(dir, "build-hap.sh")) && exists(joinPath(joinPath(dir, "entry"), "cjpm.toml"))
463
- }
464
-
465
- /**
466
- * 当前工作目录属于哪个鸿蒙工程(也认「站在 entry/ 里」这种跑法)
467
- *
468
- * 不能复用 `findProjectRoot`:鸿蒙工程根**没有** cjpm.toml(那份在 entry/ 下),
469
- * 按「向上找 cjpm.toml」会在工程内一路找不到、反而跑到外面某个仓颉工程上去。
470
- */
471
- func findOhosProjectRoot(): Option<String> {
472
- let cur = stripTrailingSep(currentDir())
473
- if (isOhosProjectAt(cur)) {
474
- return Some(cur)
475
- }
476
- if (let Some(p) <- parentDir(cur)) {
477
- if (isOhosProjectAt(p)) {
478
- return Some(p)
479
- }
480
- }
481
- return None
482
- }
483
-
484
- /**
485
- * 鸿蒙工程:构建入口就是工程自带的 `build-hap.sh`
486
- *
487
- * 跑它而不是自己拼 hvigor 命令:DevEco 探测、cjc/opt 同代、stdx 联接、`--no-daemon`
488
- * 这些坑全在脚本里(见模板 build-hap.sh 的注释),CLI 只负责传参。
489
- * `--install` 走 `run`:编完装到设备并 `aa start`。
490
- */
491
- func buildOhos(root: String, install: Bool): Int64 {
492
- var argv = ArrayList<String>()
493
- argv.add("build-hap.sh")
494
- if (install) {
495
- argv.add("--install")
496
- }
497
- println("[cj-tauri] 鸿蒙工程:bash build-hap.sh${if (install) { " --install" } else { "" }}(${root})")
498
- let code = execute("bash", argv.toArray(), workingDirectory: Path(root))
499
- if (code != 0) {
500
- eprintln("[cj-tauri] 构建失败(exit=${code})")
501
- eprintln(" 日志: ${joinPath(root, ".buildlog")}/last-build.log")
502
- }
503
- return code
504
- }
505
-
506
- func cmdBuild(): Int64 {
507
- if (let Some(root) <- findOhosProjectRoot()) {
508
- return buildOhos(root, false)
509
- }
510
- if (let Some(c) <- loadContext()) {
511
- if (buildProject(c, true)) {
512
- return 0
513
- }
514
- return 1
515
- }
516
- return 1
517
- }
518
-
519
- func cmdDev(): Int64 {
520
- if (let Some(root) <- findOhosProjectRoot()) {
521
- // 鸿蒙没有「dev server 接管页面」这条路:页面是 hap 里的 rawfile,
522
- // 改一次要重编一次;dev 就等同于 build(再加上装到设备更常见,留给 run)。
523
- println("[cj-tauri] 鸿蒙工程:dev 等同于 build(页面在 hap 的 rawfile 里,没有 dev server)")
524
- return buildOhos(root, false)
525
- }
526
- if (let Some(c) <- loadContext()) {
527
- // 有 Vite 前端才走 Node 路径:dev 模式页面来自 dev server,不必先打发布包
528
- var withUi = true
529
- if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
530
- if (!ensureUiDeps(ui)) {
531
- return 1
532
- }
533
- withUi = false
534
- }
535
- if (!buildProject(c, withUi)) {
536
- return 1
537
- }
538
- if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
539
- return runWithDevServer(c, ui)
540
- }
541
- return runProject(c)
542
- }
543
- return 1
544
- }
545
-
546
- func cmdRun(): Int64 {
547
- if (let Some(root) <- findOhosProjectRoot()) {
548
- return buildOhos(root, true)
549
- }
550
- if (let Some(c) <- loadContext()) {
551
- return runProject(c)
552
- }
553
- return 1
554
- }
555
-
556
- /**
557
- * 环境自检:把解析结果打出来,排障用。
558
- *
559
- * `--json` 时只输出插件清单(机器可读,供工具 / CI 消费),stdout 保持是纯 JSON。
560
- */
561
- func cmdInfo(asJson: Bool): Int64 {
562
- if (asJson) {
563
- // 失败原因由 pluginManifest 打到 stderr:stdout 留给清单本身,管道里才是干净的 JSON
564
- match (pluginManifest("json")) {
565
- case Some(text) =>
566
- println(text)
567
- return 0
568
- case None =>
569
- return 1
570
- }
571
- }
572
- println("cj-tauri CLI ${cliVersion()}")
573
- println("")
574
- println(" 宿主平台 : ${hostOsName()} (${hostTriple()})")
575
- println(" 工作目录 : ${currentDir()}")
576
- if (let Some(exe) <- currentExePath()) {
577
- println(" CLI 可执行 : ${exe}")
578
- }
579
- if (let Some(fw) <- resolveFrameworkRoot()) {
580
- println(" 框架根目录 : ${fw}")
581
- let lib = joinPath(joinPath(fw, "native"), bridgeLibName())
582
- if (exists(lib)) {
583
- println(" C 桥 : ${lib} [已构建]")
584
- } else {
585
- println(" C 桥 : ${lib} [未构建,cj-tauri dev/build 会自动构建]")
586
- }
587
- } else {
588
- println(" 框架根目录 : 未找到(请设置 CJ_TAURI_ROOT)")
589
- }
590
- if (let Some(proj) <- findProjectRoot()) {
591
- let newForm = exists(joinPath(proj, "cj-tauri.toml"))
592
- println(" 项目根目录 : ${proj}${if (newForm) { "(新形态:cj-tauri.toml)" } else { "" }}")
593
- if (let Some(bin) <- findAppBinary(proj, newForm)) {
594
- println(" 可执行产物 : ${bin}")
595
- } else {
596
- println(" 可执行产物 : 未构建(cj-tauri sync + cj-tauri build)")
597
- }
598
- } else {
599
- println(" 项目根目录 : (当前不在 cj-tauri 项目内)")
600
- }
601
- if (let Some(fw) <- resolveFrameworkRoot()) {
602
- if (let Some(s) <- resolveStdx(fw, findProjectRoot())) {
603
- println(" stdx : ${s}")
604
- } else {
605
- println(" stdx : 未找到(请设置 CANGJIE_STDX)")
606
- }
607
- }
608
- if (let Some(home) <- resolveCangjieHome()) {
609
- println(" CANGJIE_HOME: ${home}")
610
- if (let Some(rt) <- findRuntimeLibDir(home)) {
611
- println(" 仓颉运行时 : ${rt}")
612
- }
613
- } else {
614
- println(" CANGJIE_HOME: 未找到(请设置 CANGJIE_HOME 或把 cjc 加入 PATH)")
615
- }
616
- if (let Some(cjpm) <- resolveCjpm()) {
617
- println(" cjpm : ${cjpm}")
618
- } else {
619
- println(" cjpm : 未找到")
620
- }
621
- if (hostIsWindows()) {
622
- println("")
623
- println(" 提示: Windows 需要 WebView2 Runtime(Win11 自带),且构建桥用的 WebView2 SDK")
624
- println(" 版本不得高于本机 Runtime 版本;详见框架 native/build_win.bat 头部注释。")
625
- }
626
- // 放在最后:取清单要跑一次应用(headless,不建窗口),自检结果先落屏,排障时更好读
627
- println("")
628
- println("插件清单(装配结果,取自应用 stdout):")
629
- match (pluginManifest("text")) {
630
- case Some(text) =>
631
- for (line in text.split("\n")) {
632
- if (line.size > 0) {
633
- println(" " + line)
634
- }
635
- }
636
- case None => println(" 不可用(原因见 stderr 的 [cj-tauri] 提示)")
637
- }
638
- return 0
639
- }
640
-
641
- /**
642
- * 取插件清单:让应用「跑到装配完成、但不创建窗口」一次,把它打到 stdout 的清单收回来。
643
- *
644
- * 为什么非跑一次应用不可:插件是应用 main() 里装配的,CLI 进程里没有那份 TauriApp;
645
- * 静态解析源码只能知道「插件声明了什么」,拿不到真实装配结果(前缀名、权限集展开、hasShim)。
646
- * 环境变量 CJ_TAURI_MANIFEST 让框架的 run() / runUrl() 装配完就打印并返回,因此不需要窗口——
647
- * 没有 X 显示、没装 WebView2 Runtime 的机器上照样能取到真值。
648
- *
649
- * 取不到时返回 None,并把原因打到 stderr(stdout 留给清单本身,`--json` 才不会被诊断污染)。
650
- */
651
- func pluginManifest(mode: String): Option<String> {
652
- var proj = ""
653
- if (let Some(p) <- findProjectRoot()) {
654
- proj = p
655
- } else {
656
- eprintln("[cj-tauri] 跳过插件清单: 当前目录不在 cj-tauri 项目内")
657
- return None
658
- }
659
- let newForm = exists(joinPath(proj, "cj-tauri.toml"))
660
- var bin = ""
661
- if (let Some(b) <- findAppBinary(proj, newForm)) {
662
- bin = b
663
- } else {
664
- eprintln("[cj-tauri] 跳过插件清单: 未找到可执行产物(先跑 cj-tauri build)")
665
- return None
666
- }
667
- var fw = ""
668
- if (let Some(f) <- frameworkFromProject(proj)) {
669
- fw = f
670
- } else if (let Some(f) <- resolveFrameworkRoot()) {
671
- fw = f
672
- }
673
- // 旧版框架认不出这个环境变量,真会去开窗口——那样 info 就卡在窗口上了,故先静态确认通道存在
674
- if (!frameworkSupportsManifestDump(fw)) {
675
- eprintln("[cj-tauri] 跳过插件清单: 框架 ${fw} 未提供 CJ_TAURI_MANIFEST 导出通道(该版本不支持)")
676
- return None
677
- }
678
- // 产物可能比框架源码旧(改了框架没重建应用):旧产物同样认不出该变量,跑起来就是真开窗口
679
- if (!appBinaryIsFresh(bin, fw)) {
680
- eprintln("[cj-tauri] 跳过插件清单: 应用产物早于框架源码,先 cj-tauri build 重建再跑 info")
681
- return None
682
- }
683
- var stdx = ""
684
- if (let Some(s) <- resolveStdx(fw, Some(proj))) {
685
- stdx = s
686
- }
687
- // 与 cj-tauri run 同款环境:桥、stdx、仓颉运行时都要在动态库搜索路径里
688
- prepareEnv(fw, stdx)
689
- setVariable("CJ_TAURI_MANIFEST", mode)
690
- let (code, out, err) = executeWithOutput(bin, Array<String>(), workingDirectory: Path(proj))
691
- removeVariable("CJ_TAURI_MANIFEST")
692
-
693
- let raw = String.fromUtf8(out)
694
- if (code != 0) {
695
- eprintln("[cj-tauri] 插件清单导出失败: 应用退出码 ${code}")
696
- relayLines(String.fromUtf8(err))
697
- return None
698
- }
699
- var picked = raw.trimAsciiEnd()
700
- if (mode == "json") {
701
- picked = lastJsonBlock(raw)
702
- }
703
- if (picked.size == 0) {
704
- eprintln("[cj-tauri] 插件清单导出失败: 应用没走到 run()/runUrl()(例如模板缺 ui/dist/index.html)")
705
- relayLines(String.fromUtf8(err))
706
- return None
707
- }
708
- return Some(picked)
709
- }
710
-
711
- /** 原样转发子进程的 stderr:失败时应用自己的话最能说明卡在哪 */
712
- func relayLines(text: String): Unit {
713
- for (line in text.split("\n")) {
714
- if (line.size > 0) {
715
- eprintln(" | " + line)
716
- }
717
- }
718
- }
719
-
720
- /**
721
- * 框架是否支持清单导出通道:读源码找那个环境变量名。
722
- * 比「跑一次看看」安全——旧框架不认这个变量,跑起来就是真开窗口。
723
- */
724
- func frameworkSupportsManifestDump(fwRoot: String): Bool {
725
- if (fwRoot.size == 0) {
726
- return false
727
- }
728
- let src = joinPath(joinPath(fwRoot, "src"), "app.cj")
729
- if (!exists(src)) {
730
- return false
731
- }
732
- return readText(src).contains("CJ_TAURI_MANIFEST")
733
- }
734
-
735
- /**
736
- * 应用产物是否不比框架源码旧:比源码旧说明改了框架却没重建应用,
737
- * 那种产物里没有清单导出通道——放它跑就会真去开窗口(无显示环境直接失败,有显示环境会把 info 卡住)。
738
- */
739
- func appBinaryIsFresh(bin: String, fwRoot: String): Bool {
740
- let src = joinPath(joinPath(fwRoot, "src"), "app.cj")
741
- if (!exists(src) || !exists(bin)) {
742
- return false
743
- }
744
- return !(FileInfo(bin).lastModificationTime < FileInfo(src).lastModificationTime)
745
- }
746
-
747
- /**
748
- * 取多行 JSON 正文:从**最后一个**以 '[' 开头的行起算。
749
- * 为什么不直接拿整段 stdout:应用自己的代码也可能往 stdout 打东西,而清单是装配完最后打印的一段。
750
- */
751
- func lastJsonBlock(text: String): String {
752
- let lines = text.split("\n")
753
- var start = -1
754
- for (i in 0..lines.size) {
755
- if (lines[i].trimAscii().startsWith("[")) {
756
- start = i
757
- }
758
- }
759
- if (start < 0) {
760
- return ""
761
- }
762
- var out = ""
763
- for (i in start..lines.size) {
764
- if (out.size > 0) {
765
- out += "\n"
766
- }
767
- out += lines[i]
768
- }
769
- return out.trimAscii()
770
- }
1
+ /**
2
+ * cj-tauri CLI:项目构建 / 运行 / 诊断(dev / build / run / info)
3
+ */
4
+ package cli
5
+
6
+ import std.env.*
7
+ import std.process.*
8
+ import std.fs.*
9
+ import std.collection.*
10
+
11
+ /** CLI 版本:须与 cli/cjpm.toml 的 version、CHANGELOG 顶部版本一致(scripts/check-version.sh 校验) */
12
+ func cliVersion(): String {
13
+ return "2.1.0"
14
+ }
15
+
16
+ /** 一次命令所需的路径上下文 */
17
+ public class AppContext {
18
+ let projectRoot: String
19
+ let frameworkRoot: String
20
+ let stdx: String
21
+ let cjpm: String
22
+ /** 新形态工程(工程根带 cj-tauri.toml 清单):可执行产物在 targets/desktop 骨架,前端在 assets/ui */
23
+ let newForm: Bool
24
+
25
+ public init(projectRoot: String, frameworkRoot: String, stdx: String, cjpm: String, newForm: Bool) {
26
+ this.projectRoot = projectRoot
27
+ this.frameworkRoot = frameworkRoot
28
+ this.stdx = stdx
29
+ this.cjpm = cjpm
30
+ this.newForm = newForm
31
+ }
32
+ }
33
+
34
+ /** 项目根目录:从当前目录向上找含 cjpm.toml 的目录 */
35
+ func findProjectRoot(): Option<String> {
36
+ var cur: Option<String> = Some(currentDir())
37
+ var guard = 0
38
+ while (guard < 64) {
39
+ guard++
40
+ if (let Some(d) <- cur) {
41
+ if (exists(joinPath(d, "cjpm.toml"))) {
42
+ return Some(d)
43
+ }
44
+ cur = parentDir(d)
45
+ } else {
46
+ return None
47
+ }
48
+ }
49
+ return None
50
+ }
51
+
52
+ /** 可执行产物:旧形态 = 工程根 target/release/bin/main[.exe];新形态 = targets/desktop 骨架 */
53
+ func findAppBinary(projectRoot: String, newForm: Bool): Option<String> {
54
+ let base = if (newForm) { joinPath(projectRoot, "targets/desktop") } else { projectRoot }
55
+ let binDir = joinPath(joinPath(joinPath(base, "target"), "release"), "bin")
56
+ if (!exists(binDir)) {
57
+ return None
58
+ }
59
+ let preferred = "main" + exeSuffix()
60
+ var fallback = ""
61
+ for (info in listDir(binDir)) {
62
+ if (info.isDirectory()) {
63
+ continue
64
+ }
65
+ let nm = baseName(info.path.toString())
66
+ if (nm.endsWith(".cjo") || nm.endsWith(".o") || nm.endsWith(".d") || nm.endsWith(".bc")) {
67
+ continue
68
+ }
69
+ if (hostIsWindows() && !nm.endsWith(".exe")) {
70
+ continue
71
+ }
72
+ if (nm == preferred) {
73
+ return Some(joinPath(binDir, nm))
74
+ }
75
+ fallback = joinPath(binDir, nm)
76
+ }
77
+ if (fallback.size > 0) {
78
+ return Some(fallback)
79
+ }
80
+ return None
81
+ }
82
+
83
+ /** 把若干目录前置到某个路径型环境变量(PATH / LD_LIBRARY_PATH) */
84
+ func prependPath(key: String, dirs: ArrayList<String>): Unit {
85
+ let sep = pathListSep()
86
+ var head = ""
87
+ for (d in dirs) {
88
+ if (d.size == 0) {
89
+ continue
90
+ }
91
+ head = head + d + sep
92
+ }
93
+ setVariable(key, head + envOr(key, ""))
94
+ }
95
+
96
+ /**
97
+ * 准备子进程运行环境:
98
+ * 动态库搜索路径需要「框架 native(含同步的 WebView2 loader)+ stdx + 仓颉运行时」,
99
+ * PATH 还需要「桥、cjpm、cjc」(Windows 下 DLL 也走 PATH)。
100
+ */
101
+ func prepareEnv(fwRoot: String, stdx: String): Unit {
102
+ let libDirs = ArrayList<String>()
103
+ libDirs.add(joinPath(fwRoot, "native"))
104
+ libDirs.add(joinPath(joinPath(fwRoot, "native"), "webview2"))
105
+ if (stdx.size > 0) {
106
+ libDirs.add(stdx)
107
+ }
108
+ let pathDirs = ArrayList<String>()
109
+ for (d in libDirs) {
110
+ pathDirs.add(d)
111
+ }
112
+ if (let Some(home) <- resolveCangjieHome()) {
113
+ if (let Some(rt) <- findRuntimeLibDir(home)) {
114
+ libDirs.add(rt)
115
+ pathDirs.add(rt)
116
+ }
117
+ pathDirs.add(joinPath(joinPath(home, "tools"), "bin")) // cjpm
118
+ pathDirs.add(joinPath(joinPath(home, "tools"), "lib")) // cjpm 依赖
119
+ pathDirs.add(joinPath(home, "bin")) // cjc
120
+ }
121
+ prependPath("PATH", pathDirs)
122
+ if (!hostIsWindows()) {
123
+ prependPath("LD_LIBRARY_PATH", libDirs)
124
+ }
125
+ }
126
+
127
+ /** 确保 C 桥已构建(缺失时调用平台构建脚本) */
128
+ func ensureBridge(fwRoot: String): Bool {
129
+ let nativeDir = joinPath(fwRoot, "native")
130
+ let lib = joinPath(nativeDir, bridgeLibName())
131
+ if (exists(lib)) {
132
+ return true
133
+ }
134
+ println("[cj-tauri] C 桥未构建,正在构建: ${lib}")
135
+ if (hostIsWindows()) {
136
+ let script = joinPath(nativeDir, "build_win.bat")
137
+ if (!exists(script)) {
138
+ eprintln("[cj-tauri] 错误: 缺少 ${script}")
139
+ return false
140
+ }
141
+ let code = execute("cmd.exe", ["/c", "\"" + script + "\""], workingDirectory: Path(nativeDir))
142
+ if (code != 0) {
143
+ eprintln("[cj-tauri] 错误: C 桥构建失败(exit=${code})")
144
+ return false
145
+ }
146
+ if (exists(lib)) {
147
+ return true
148
+ }
149
+ eprintln("[cj-tauri] 错误: 构建脚本执行完仍未生成 ${lib}")
150
+ return false
151
+ }
152
+ let script = joinPath(nativeDir, "build_linux.sh")
153
+ if (!exists(script)) {
154
+ eprintln("[cj-tauri] 错误: 缺少 ${script}")
155
+ return false
156
+ }
157
+ let code = execute("bash", [script], workingDirectory: Path(nativeDir))
158
+ if (code != 0) {
159
+ eprintln("[cj-tauri] 错误: C 桥构建失败(exit=${code})")
160
+ return false
161
+ }
162
+ if (exists(lib)) {
163
+ return true
164
+ }
165
+ eprintln("[cj-tauri] 错误: 构建脚本执行完仍未生成 ${lib}")
166
+ return false
167
+ }
168
+
169
+ /** 解析项目上下文(项目根 / 框架根 / stdx / cjpm) */
170
+ func loadContext(): Option<AppContext> {
171
+ var proj = ""
172
+ if (let Some(p) <- findProjectRoot()) {
173
+ proj = p
174
+ } else {
175
+ eprintln("[cj-tauri] 错误: 当前目录不在 cj-tauri 项目内(未找到 cjpm.toml)")
176
+ return None
177
+ }
178
+ var fw = ""
179
+ if (let Some(f) <- frameworkFromProject(proj)) {
180
+ fw = f
181
+ } else if (let Some(f) <- resolveFrameworkRoot()) {
182
+ fw = f
183
+ } else {
184
+ eprintln("[cj-tauri] 错误: 无法确定 cj-tauri 框架目录")
185
+ eprintln(" cjpm.toml 里的 cjTauri 依赖路径不可用,且未设置 CJ_TAURI_ROOT")
186
+ return None
187
+ }
188
+ if (!exists(fw)) {
189
+ eprintln("[cj-tauri] 错误: 框架目录不存在: ${fw}")
190
+ eprintln(" 请修改 cjpm.toml 的 cjTauri 依赖路径,或设置 CJ_TAURI_ROOT")
191
+ return None
192
+ }
193
+ var stdx = ""
194
+ if (let Some(s) <- resolveStdx(fw, Some(proj))) {
195
+ stdx = s
196
+ } else {
197
+ eprintln("[cj-tauri] 告警: 未找到 stdx 目录,构建可能失败(可用 CANGJIE_STDX 指定)")
198
+ }
199
+ var cjpm = ""
200
+ if (let Some(c) <- resolveCjpm()) {
201
+ cjpm = c
202
+ } else {
203
+ eprintln("[cj-tauri] 错误: 找不到 cjpm")
204
+ eprintln(" 请把仓颉 SDK 的 tools/bin 与 bin 加入 PATH,或设置 CANGJIE_HOME")
205
+ return None
206
+ }
207
+ // 新旧形态判据:工程根有没有 cj-tauri.toml(RFC-002 §7 的单一真相文件)
208
+ let newForm = exists(joinPath(proj, "cj-tauri.toml"))
209
+ return Some(AppContext(proj, fw, stdx, cjpm, newForm))
210
+ }
211
+
212
+ /** 前端工程(Vite):新形态在 assets/ui、旧形态在 ui;有 package.json 才启用,没有整段跳过 */
213
+ class WebUi {
214
+ let dir: String
215
+ let devUrl: String
216
+ let logFile: String
217
+ let port: UInt16
218
+
219
+ init(dir: String) {
220
+ this.dir = dir
221
+ this.devUrl = "http://127.0.0.1:5173/"
222
+ this.logFile = joinPath(dir, devLogName())
223
+ this.port = 5173
224
+ }
225
+ }
226
+
227
+ func detectWebUi(projectRoot: String, newForm: Bool): Option<WebUi> {
228
+ let uiSub = if (newForm) { "assets/ui" } else { "ui" }
229
+ let dir = joinPath(projectRoot, uiSub)
230
+ if (exists(joinPath(dir, "package.json"))) {
231
+ return Some(WebUi(dir))
232
+ }
233
+ return None
234
+ }
235
+
236
+ /**
237
+ * dev server 的日志文件名:必须是**相对名**。
238
+ * launch 会给整条命令行加引号,日志路径若再自己带引号,cmd.exe 会拿到嵌套引号并直接报
239
+ * 「文件名、目录名或卷标语法不正确」。相对名 + 工作目录 = ui/ 即可,命令保持单层引号。
240
+ */
241
+ func devLogName(): String {
242
+ return "dev-server.log"
243
+ }
244
+
245
+ /** 拼一条 npm 命令行;给了日志文件名就重定向过去 */
246
+ func npmLine(parts: Array<String>, logName: String): String {
247
+ var line = "npm"
248
+ for (p in parts) {
249
+ line = line + " " + p
250
+ }
251
+ if (logName.size > 0) {
252
+ line = line + " > " + logName + " 2>&1"
253
+ }
254
+ return line
255
+ }
256
+
257
+ /** 阻塞跑一条 shell 命令行:npm 在两个平台上都是脚本,必须经 shell(cmd.exe /c 或 bash -lc) */
258
+ func runShellLine(line: String, dir: String): Int64 {
259
+ if (hostIsWindows()) {
260
+ return execute("cmd.exe", ["/c", line], workingDirectory: Path(dir))
261
+ }
262
+ return execute("bash", ["-lc", line], workingDirectory: Path(dir))
263
+ }
264
+
265
+ /** 后台跑一条 shell 命令行,返回子进程(收尾时连同子进程一起收掉) */
266
+ func launchShellLine(line: String, dir: String): SubProcess {
267
+ if (hostIsWindows()) {
268
+ return launch("cmd.exe", ["/c", line], workingDirectory: Path(dir))
269
+ }
270
+ // 刻意**不**用 setsid 另开会话:dev server 必须留在 CLI 的进程组里,
271
+ // 终端 Ctrl-C 才能一并收掉它(实测 setsid 后 Ctrl-C 只杀 CLI,dev server 漏跑)。
272
+ // npm → sh -c vite → node(vite) → esbuild 这条父子链改由 stopDevServer 递归收掉。
273
+ return launch("bash", ["-lc", line], workingDirectory: Path(dir))
274
+ }
275
+
276
+ /**
277
+ * 按「先子后父」递归收掉一棵进程树(对 rootPid 自身也发信号)。
278
+ * npm → sh -c vite → node(vite) → esbuild 是一条父子链,只 terminate 直接子进程(bash)
279
+ * 会留下后代继续跑(实测残留 vite + esbuild),所以收尾必须递归。用 shell 递归实现,
280
+ * 省掉在仓颉侧解析 /proc 的复杂度。
281
+ */
282
+ func killProcessTree(rootPid: Int64, sig: String): Unit {
283
+ let script = "t() { for c in $(pgrep -P \"$1\" 2>/dev/null); do t \"$c\"; done; kill -" +
284
+ sig + " \"$1\" 2>/dev/null; }; t ${rootPid}"
285
+ try {
286
+ execute("bash", ["-lc", script])
287
+ } catch (e: Exception) {
288
+ () // 进程已自己退掉等情况不值得让收尾流程失败
289
+ }
290
+ }
291
+
292
+ /** 前端依赖:没有 node_modules 就先 npm install */
293
+ func ensureUiDeps(ui: WebUi): Bool {
294
+ if (exists(joinPath(ui.dir, "node_modules"))) {
295
+ return true
296
+ }
297
+ if (runShellLine("npm -v", ui.dir) != 0) {
298
+ eprintln("[cj-tauri] 错误: ui/package.json 存在,但找不到 npm(需要 Node.js)")
299
+ eprintln(" 装好 Node.js 再试,或删掉 ui/package.json 走内联 HTML 模式")
300
+ return false
301
+ }
302
+ println("[cj-tauri] 安装前端依赖: npm install(首次会慢一些)")
303
+ let code = runShellLine(npmLine(["install"], ""), ui.dir)
304
+ if (code != 0) {
305
+ eprintln("[cj-tauri] 错误: npm install 失败(exit=${code})")
306
+ return false
307
+ }
308
+ return true
309
+ }
310
+
311
+ /** 前端发布构建:Vite + singlefile → ui/dist/index.html(后端 run(html) 读的就是它) */
312
+ func buildUiBundle(ui: WebUi): Bool {
313
+ println("[cj-tauri] 前端构建: npm run build(产物 ui/dist/index.html)")
314
+ let code = runShellLine(npmLine(["run", "build"], ""), ui.dir)
315
+ if (code != 0) {
316
+ eprintln("[cj-tauri] 错误: npm run build 失败(exit=${code})")
317
+ return false
318
+ }
319
+ if (!exists(joinPath(joinPath(ui.dir, "dist"), "index.html"))) {
320
+ eprintln("[cj-tauri] 错误: npm run build 跑完仍没有 ui/dist/index.html")
321
+ eprintln(" 检查 ui/vite.config.js 里是否装上了 vite-plugin-singlefile")
322
+ return false
323
+ }
324
+ return true
325
+ }
326
+
327
+ /**
328
+ * dev server 是否就绪:看 Vite 自己打在日志里的启动行(`Local: http://127.0.0.1:5173/`)。
329
+ * 不用 TCP 探活是为了少一个平台差异点;探不到也不硬失败——调用方超时后会告警并继续。
330
+ */
331
+ func devServerReady(ui: WebUi): Bool {
332
+ if (!exists(ui.logFile)) {
333
+ return false
334
+ }
335
+ let txt = readText(ui.logFile)
336
+ return txt.contains("Local:") && txt.contains("${ui.port}")
337
+ }
338
+
339
+ /** 等 dev server 就绪:200ms 一步,最多 timeoutSec 秒 */
340
+ func waitDevServer(ui: WebUi, timeoutSec: Int64): Bool {
341
+ var steps: Int64 = 0
342
+ let limit = timeoutSec * 5
343
+ while (steps < limit) {
344
+ if (devServerReady(ui)) {
345
+ return true
346
+ }
347
+ sleep(Duration.millisecond * 200)
348
+ steps += 1
349
+ }
350
+ return false
351
+ }
352
+
353
+ /** 收掉 dev server 整棵进程树:Windows 用 taskkill /T;其他平台递归收掉父子链 */
354
+ func stopDevServer(p: SubProcess): Unit {
355
+ if (hostIsWindows()) {
356
+ let code = execute("taskkill", ["/F", "/T", "/PID", "${p.pid}"])
357
+ if (code == 0) {
358
+ println("[cj-tauri] dev server 已收掉(pid=${p.pid})")
359
+ } else {
360
+ eprintln("[cj-tauri] 告警: 收 dev server 失败(pid=${p.pid}),可手动结束")
361
+ }
362
+ return
363
+ }
364
+ // 先 TERM 让 Vite 自己退,再 KILL 兜底;两趟都按「先子后父」收整棵树,
365
+ // 免得只收掉直接子进程后,后代(vite / esbuild)继续占着 5173 端口。
366
+ killProcessTree(p.pid, "TERM")
367
+ sleep(Duration.millisecond * 500)
368
+ killProcessTree(p.pid, "KILL")
369
+ p.terminate() // 极端情况(pgrep 不可用)下至少收掉直接子进程
370
+ println("[cj-tauri] dev server 已收掉(pid=${p.pid} 及后代)")
371
+ }
372
+
373
+ /** dev 模式:起前端 dev server → 注入 CJ_TAURI_DEV_URL → 启动应用 → 退出后收掉 dev server */
374
+ func runWithDevServer(c: AppContext, ui: WebUi): Int64 {
375
+ println("[cj-tauri] 启动前端 dev server: ${ui.dir}(npm run dev,日志 ${ui.logFile})")
376
+ let devCmd = npmLine(["run", "dev"], devLogName())
377
+ var dev: Option<SubProcess> = None
378
+ try {
379
+ dev = Some(launchShellLine(devCmd, ui.dir))
380
+ } catch (e: Exception) {
381
+ eprintln("[cj-tauri] 错误: 起 dev server 失败: ${e.message}")
382
+ eprintln(" 命令: ${devCmd}(工作目录 ${ui.dir})")
383
+ return 1
384
+ }
385
+ // 阈值给得宽松:本机实测 npm 把 Vite 拉起来能花到 85 秒(node/npm 冷启动偏慢),
386
+ // 90 秒会贴着超时线走,宁可多等也不要误报「没就绪」。
387
+ if (waitDevServer(ui, 180)) {
388
+ println("[cj-tauri] dev server 就绪: ${ui.devUrl}")
389
+ } else {
390
+ eprintln("[cj-tauri] 告警: 180 秒内没在日志里看到就绪行,仍按 ${ui.devUrl} 启动")
391
+ eprintln(" 日志: ${ui.logFile}")
392
+ }
393
+ setVariable("CJ_TAURI_DEV_URL", ui.devUrl)
394
+ println("[cj-tauri] 注入 CJ_TAURI_DEV_URL=${ui.devUrl}")
395
+ let code = runProject(c)
396
+ if (let Some(p) <- dev) {
397
+ stopDevServer(p)
398
+ }
399
+ return code
400
+ }
401
+
402
+ /** 构建:桥 + cjpm build;withUi=true 时先打前端发布包(有 Vite 前端才会触发) */
403
+ func buildProject(c: AppContext, withUi: Bool): Bool {
404
+ if (withUi) {
405
+ if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
406
+ if (!ensureUiDeps(ui)) {
407
+ return false
408
+ }
409
+ if (!buildUiBundle(ui)) {
410
+ return false
411
+ }
412
+ }
413
+ }
414
+ prepareEnv(c.frameworkRoot, c.stdx)
415
+ if (!ensureBridge(c.frameworkRoot)) {
416
+ return false
417
+ }
418
+ // 新形态:编 targets/desktop 骨架(path 依赖会连带编业务 static 包 + launcher 可执行)
419
+ let buildDir = if (c.newForm) { joinPath(c.projectRoot, "targets/desktop") } else { c.projectRoot }
420
+ if (!exists(joinPath(buildDir, "cjpm.toml"))) {
421
+ eprintln("[cj-tauri] 错误: 骨架未生成:${buildDir}")
422
+ eprintln(" 先运行 cj-tauri sync(按清单派生 targets/ 骨架)")
423
+ return false
424
+ }
425
+ println("[cj-tauri] cjpm build ... (${baseName(buildDir)})")
426
+ let code = execute(c.cjpm, ["build"], workingDirectory: Path(buildDir))
427
+ if (code != 0) {
428
+ eprintln("[cj-tauri] 构建失败(exit=${code})")
429
+ return false
430
+ }
431
+ if (let Some(bin) <- findAppBinary(c.projectRoot, c.newForm)) {
432
+ println("[cj-tauri] 构建完成: ${bin}")
433
+ } else {
434
+ println("[cj-tauri] 构建完成(未在 target/release/bin 找到可执行产物)")
435
+ }
436
+ return true
437
+ }
438
+
439
+ /** 运行已构建产物(App 以项目根为工作目录,页面/能力清单按相对路径读取) */
440
+ func runProject(c: AppContext): Int64 {
441
+ var bin = ""
442
+ if (let Some(b) <- findAppBinary(c.projectRoot, c.newForm)) {
443
+ bin = b
444
+ } else {
445
+ eprintln("[cj-tauri] 错误: 未找到可执行产物,请先 cj-tauri build")
446
+ return 1
447
+ }
448
+ prepareEnv(c.frameworkRoot, c.stdx)
449
+ if (!hostIsWindows()) {
450
+ if (getVariable("DISPLAY").isNone()) {
451
+ eprintln("[cj-tauri] 告警: 未设置 DISPLAY,需要可用的 X 显示环境(或 Wayland+XWayland)")
452
+ }
453
+ }
454
+ println("[cj-tauri] 启动: ${bin}")
455
+ let code = execute(bin, Array<String>(), workingDirectory: Path(c.projectRoot))
456
+ println("[cj-tauri] 应用退出(exit=${code})")
457
+ return code
458
+ }
459
+
460
+ /** 某个目录下是不是鸿蒙脚手架工程(判据:build-hap.sh + entry/cjpm.toml 同时在) */
461
+ func isOhosProjectAt(dir: String): Bool {
462
+ return exists(joinPath(dir, "build-hap.sh")) && exists(joinPath(joinPath(dir, "entry"), "cjpm.toml"))
463
+ }
464
+
465
+ /**
466
+ * 当前工作目录属于哪个鸿蒙工程(也认「站在 entry/ 里」这种跑法)
467
+ *
468
+ * 不能复用 `findProjectRoot`:鸿蒙工程根**没有** cjpm.toml(那份在 entry/ 下),
469
+ * 按「向上找 cjpm.toml」会在工程内一路找不到、反而跑到外面某个仓颉工程上去。
470
+ */
471
+ func findOhosProjectRoot(): Option<String> {
472
+ let cur = stripTrailingSep(currentDir())
473
+ if (isOhosProjectAt(cur)) {
474
+ return Some(cur)
475
+ }
476
+ if (let Some(p) <- parentDir(cur)) {
477
+ if (isOhosProjectAt(p)) {
478
+ return Some(p)
479
+ }
480
+ }
481
+ return None
482
+ }
483
+
484
+ /**
485
+ * 鸿蒙工程:构建入口就是工程自带的 `build-hap.sh`
486
+ *
487
+ * 跑它而不是自己拼 hvigor 命令:DevEco 探测、cjc/opt 同代、stdx 联接、`--no-daemon`
488
+ * 这些坑全在脚本里(见模板 build-hap.sh 的注释),CLI 只负责传参。
489
+ * `--install` 走 `run`:编完装到设备并 `aa start`。
490
+ */
491
+ func buildOhos(root: String, install: Bool): Int64 {
492
+ var argv = ArrayList<String>()
493
+ argv.add("build-hap.sh")
494
+ if (install) {
495
+ argv.add("--install")
496
+ }
497
+ println("[cj-tauri] 鸿蒙工程:bash build-hap.sh${if (install) { " --install" } else { "" }}(${root})")
498
+ let code = execute("bash", argv.toArray(), workingDirectory: Path(root))
499
+ if (code != 0) {
500
+ eprintln("[cj-tauri] 构建失败(exit=${code})")
501
+ eprintln(" 日志: ${joinPath(root, ".buildlog")}/last-build.log")
502
+ }
503
+ return code
504
+ }
505
+
506
+ func cmdBuild(): Int64 {
507
+ if (let Some(root) <- findOhosProjectRoot()) {
508
+ return buildOhos(root, false)
509
+ }
510
+ if (let Some(c) <- loadContext()) {
511
+ if (buildProject(c, true)) {
512
+ return 0
513
+ }
514
+ return 1
515
+ }
516
+ return 1
517
+ }
518
+
519
+ func cmdDev(): Int64 {
520
+ if (let Some(root) <- findOhosProjectRoot()) {
521
+ // 鸿蒙没有「dev server 接管页面」这条路:页面是 hap 里的 rawfile,
522
+ // 改一次要重编一次;dev 就等同于 build(再加上装到设备更常见,留给 run)。
523
+ println("[cj-tauri] 鸿蒙工程:dev 等同于 build(页面在 hap 的 rawfile 里,没有 dev server)")
524
+ return buildOhos(root, false)
525
+ }
526
+ if (let Some(c) <- loadContext()) {
527
+ // 有 Vite 前端才走 Node 路径:dev 模式页面来自 dev server,不必先打发布包
528
+ var withUi = true
529
+ if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
530
+ if (!ensureUiDeps(ui)) {
531
+ return 1
532
+ }
533
+ withUi = false
534
+ }
535
+ if (!buildProject(c, withUi)) {
536
+ return 1
537
+ }
538
+ if (let Some(ui) <- detectWebUi(c.projectRoot, c.newForm)) {
539
+ return runWithDevServer(c, ui)
540
+ }
541
+ return runProject(c)
542
+ }
543
+ return 1
544
+ }
545
+
546
+ func cmdRun(): Int64 {
547
+ if (let Some(root) <- findOhosProjectRoot()) {
548
+ return buildOhos(root, true)
549
+ }
550
+ if (let Some(c) <- loadContext()) {
551
+ return runProject(c)
552
+ }
553
+ return 1
554
+ }
555
+
556
+ /**
557
+ * 环境自检:把解析结果打出来,排障用。
558
+ *
559
+ * `--json` 时只输出插件清单(机器可读,供工具 / CI 消费),stdout 保持是纯 JSON。
560
+ */
561
+ func cmdInfo(asJson: Bool): Int64 {
562
+ if (asJson) {
563
+ // 失败原因由 pluginManifest 打到 stderr:stdout 留给清单本身,管道里才是干净的 JSON
564
+ match (pluginManifest("json")) {
565
+ case Some(text) =>
566
+ println(text)
567
+ return 0
568
+ case None =>
569
+ return 1
570
+ }
571
+ }
572
+ println("cj-tauri CLI ${cliVersion()}")
573
+ println("")
574
+ println(" 宿主平台 : ${hostOsName()} (${hostTriple()})")
575
+ println(" 工作目录 : ${currentDir()}")
576
+ if (let Some(exe) <- currentExePath()) {
577
+ println(" CLI 可执行 : ${exe}")
578
+ }
579
+ if (let Some(fw) <- resolveFrameworkRoot()) {
580
+ println(" 框架根目录 : ${fw}")
581
+ let lib = joinPath(joinPath(fw, "native"), bridgeLibName())
582
+ if (exists(lib)) {
583
+ println(" C 桥 : ${lib} [已构建]")
584
+ } else {
585
+ println(" C 桥 : ${lib} [未构建,cj-tauri dev/build 会自动构建]")
586
+ }
587
+ } else {
588
+ println(" 框架根目录 : 未找到(请设置 CJ_TAURI_ROOT)")
589
+ }
590
+ if (let Some(proj) <- findProjectRoot()) {
591
+ let newForm = exists(joinPath(proj, "cj-tauri.toml"))
592
+ println(" 项目根目录 : ${proj}${if (newForm) { "(新形态:cj-tauri.toml)" } else { "" }}")
593
+ if (let Some(bin) <- findAppBinary(proj, newForm)) {
594
+ println(" 可执行产物 : ${bin}")
595
+ } else {
596
+ println(" 可执行产物 : 未构建(cj-tauri sync + cj-tauri build)")
597
+ }
598
+ } else {
599
+ println(" 项目根目录 : (当前不在 cj-tauri 项目内)")
600
+ }
601
+ if (let Some(fw) <- resolveFrameworkRoot()) {
602
+ if (let Some(s) <- resolveStdx(fw, findProjectRoot())) {
603
+ println(" stdx : ${s}")
604
+ } else {
605
+ println(" stdx : 未找到(请设置 CANGJIE_STDX)")
606
+ }
607
+ }
608
+ if (let Some(home) <- resolveCangjieHome()) {
609
+ println(" CANGJIE_HOME: ${home}")
610
+ if (let Some(rt) <- findRuntimeLibDir(home)) {
611
+ println(" 仓颉运行时 : ${rt}")
612
+ }
613
+ } else {
614
+ println(" CANGJIE_HOME: 未找到(请设置 CANGJIE_HOME 或把 cjc 加入 PATH)")
615
+ }
616
+ if (let Some(cjpm) <- resolveCjpm()) {
617
+ println(" cjpm : ${cjpm}")
618
+ } else {
619
+ println(" cjpm : 未找到")
620
+ }
621
+ if (hostIsWindows()) {
622
+ println("")
623
+ println(" 提示: Windows 需要 WebView2 Runtime(Win11 自带),且构建桥用的 WebView2 SDK")
624
+ println(" 版本不得高于本机 Runtime 版本;详见框架 native/build_win.bat 头部注释。")
625
+ }
626
+ // 放在最后:取清单要跑一次应用(headless,不建窗口),自检结果先落屏,排障时更好读
627
+ println("")
628
+ println("插件清单(装配结果,取自应用 stdout):")
629
+ match (pluginManifest("text")) {
630
+ case Some(text) =>
631
+ for (line in text.split("\n")) {
632
+ if (line.size > 0) {
633
+ println(" " + line)
634
+ }
635
+ }
636
+ case None => println(" 不可用(原因见 stderr 的 [cj-tauri] 提示)")
637
+ }
638
+ return 0
639
+ }
640
+
641
+ /**
642
+ * 取插件清单:让应用「跑到装配完成、但不创建窗口」一次,把它打到 stdout 的清单收回来。
643
+ *
644
+ * 为什么非跑一次应用不可:插件是应用 main() 里装配的,CLI 进程里没有那份 TauriApp;
645
+ * 静态解析源码只能知道「插件声明了什么」,拿不到真实装配结果(前缀名、权限集展开、hasShim)。
646
+ * 环境变量 CJ_TAURI_MANIFEST 让框架的 run() / runUrl() 装配完就打印并返回,因此不需要窗口——
647
+ * 没有 X 显示、没装 WebView2 Runtime 的机器上照样能取到真值。
648
+ *
649
+ * 取不到时返回 None,并把原因打到 stderr(stdout 留给清单本身,`--json` 才不会被诊断污染)。
650
+ */
651
+ func pluginManifest(mode: String): Option<String> {
652
+ var proj = ""
653
+ if (let Some(p) <- findProjectRoot()) {
654
+ proj = p
655
+ } else {
656
+ eprintln("[cj-tauri] 跳过插件清单: 当前目录不在 cj-tauri 项目内")
657
+ return None
658
+ }
659
+ let newForm = exists(joinPath(proj, "cj-tauri.toml"))
660
+ var bin = ""
661
+ if (let Some(b) <- findAppBinary(proj, newForm)) {
662
+ bin = b
663
+ } else {
664
+ eprintln("[cj-tauri] 跳过插件清单: 未找到可执行产物(先跑 cj-tauri build)")
665
+ return None
666
+ }
667
+ var fw = ""
668
+ if (let Some(f) <- frameworkFromProject(proj)) {
669
+ fw = f
670
+ } else if (let Some(f) <- resolveFrameworkRoot()) {
671
+ fw = f
672
+ }
673
+ // 旧版框架认不出这个环境变量,真会去开窗口——那样 info 就卡在窗口上了,故先静态确认通道存在
674
+ if (!frameworkSupportsManifestDump(fw)) {
675
+ eprintln("[cj-tauri] 跳过插件清单: 框架 ${fw} 未提供 CJ_TAURI_MANIFEST 导出通道(该版本不支持)")
676
+ return None
677
+ }
678
+ // 产物可能比框架源码旧(改了框架没重建应用):旧产物同样认不出该变量,跑起来就是真开窗口
679
+ if (!appBinaryIsFresh(bin, fw)) {
680
+ eprintln("[cj-tauri] 跳过插件清单: 应用产物早于框架源码,先 cj-tauri build 重建再跑 info")
681
+ return None
682
+ }
683
+ var stdx = ""
684
+ if (let Some(s) <- resolveStdx(fw, Some(proj))) {
685
+ stdx = s
686
+ }
687
+ // 与 cj-tauri run 同款环境:桥、stdx、仓颉运行时都要在动态库搜索路径里
688
+ prepareEnv(fw, stdx)
689
+ setVariable("CJ_TAURI_MANIFEST", mode)
690
+ let (code, out, err) = executeWithOutput(bin, Array<String>(), workingDirectory: Path(proj))
691
+ removeVariable("CJ_TAURI_MANIFEST")
692
+
693
+ let raw = String.fromUtf8(out)
694
+ if (code != 0) {
695
+ eprintln("[cj-tauri] 插件清单导出失败: 应用退出码 ${code}")
696
+ relayLines(String.fromUtf8(err))
697
+ return None
698
+ }
699
+ var picked = raw.trimAsciiEnd()
700
+ if (mode == "json") {
701
+ picked = lastJsonBlock(raw)
702
+ }
703
+ if (picked.size == 0) {
704
+ eprintln("[cj-tauri] 插件清单导出失败: 应用没走到 run()/runUrl()(例如模板缺 ui/dist/index.html)")
705
+ relayLines(String.fromUtf8(err))
706
+ return None
707
+ }
708
+ return Some(picked)
709
+ }
710
+
711
+ /** 原样转发子进程的 stderr:失败时应用自己的话最能说明卡在哪 */
712
+ func relayLines(text: String): Unit {
713
+ for (line in text.split("\n")) {
714
+ if (line.size > 0) {
715
+ eprintln(" | " + line)
716
+ }
717
+ }
718
+ }
719
+
720
+ /**
721
+ * 框架是否支持清单导出通道:读源码找那个环境变量名。
722
+ * 比「跑一次看看」安全——旧框架不认这个变量,跑起来就是真开窗口。
723
+ */
724
+ func frameworkSupportsManifestDump(fwRoot: String): Bool {
725
+ if (fwRoot.size == 0) {
726
+ return false
727
+ }
728
+ let src = joinPath(joinPath(fwRoot, "src"), "app.cj")
729
+ if (!exists(src)) {
730
+ return false
731
+ }
732
+ return readText(src).contains("CJ_TAURI_MANIFEST")
733
+ }
734
+
735
+ /**
736
+ * 应用产物是否不比框架源码旧:比源码旧说明改了框架却没重建应用,
737
+ * 那种产物里没有清单导出通道——放它跑就会真去开窗口(无显示环境直接失败,有显示环境会把 info 卡住)。
738
+ */
739
+ func appBinaryIsFresh(bin: String, fwRoot: String): Bool {
740
+ let src = joinPath(joinPath(fwRoot, "src"), "app.cj")
741
+ if (!exists(src) || !exists(bin)) {
742
+ return false
743
+ }
744
+ return !(FileInfo(bin).lastModificationTime < FileInfo(src).lastModificationTime)
745
+ }
746
+
747
+ /**
748
+ * 取多行 JSON 正文:从**最后一个**以 '[' 开头的行起算。
749
+ * 为什么不直接拿整段 stdout:应用自己的代码也可能往 stdout 打东西,而清单是装配完最后打印的一段。
750
+ */
751
+ func lastJsonBlock(text: String): String {
752
+ let lines = text.split("\n")
753
+ var start = -1
754
+ for (i in 0..lines.size) {
755
+ if (lines[i].trimAscii().startsWith("[")) {
756
+ start = i
757
+ }
758
+ }
759
+ if (start < 0) {
760
+ return ""
761
+ }
762
+ var out = ""
763
+ for (i in start..lines.size) {
764
+ if (out.size > 0) {
765
+ out += "\n"
766
+ }
767
+ out += lines[i]
768
+ }
769
+ return out.trimAscii()
770
+ }