cj-tauri 1.1.2 → 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 (162) hide show
  1. package/README.md +137 -77
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +13 -2
  4. package/cli/cjpm.toml +22 -13
  5. package/cli/src/doctor.cj +308 -304
  6. package/cli/src/main.cj +129 -121
  7. package/cli/src/manifest.cj +632 -0
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -754
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +46 -38
  12. package/cli/src/sync.cj +526 -0
  13. package/cli/src/util.cj +9 -0
  14. package/cli/templates/app/README.md +30 -59
  15. package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
  16. package/cli/templates/app/cj-tauri.toml +32 -0
  17. package/cli/templates/app/cjpm.toml +21 -4
  18. package/cli/templates/app/src/main.cj +29 -16
  19. package/cli/templates/app-react/README.md +34 -45
  20. package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
  21. package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
  22. package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
  23. package/cli/templates/app-react/cj-tauri.toml +33 -0
  24. package/cli/templates/app-react/cjpm.toml +21 -4
  25. package/cli/templates/app-react/src/main.cj +18 -29
  26. package/cli/templates/app-vue/README.md +34 -38
  27. package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
  29. package/cli/templates/app-vue/cj-tauri.toml +33 -0
  30. package/cli/templates/app-vue/cjpm.toml +21 -4
  31. package/cli/templates/app-vue/src/main.cj +18 -29
  32. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
  35. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  36. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
  38. package/native/build_win.bat +62 -62
  39. package/native/core/bridge_core.c +1681 -1626
  40. package/native/core/bridge_core.h +675 -618
  41. package/native/core/bridge_js.h +108 -108
  42. package/native/platforms/harmonyos/README.md +115 -6
  43. package/native/platforms/linux/bridge_linux.c +2787 -2485
  44. package/native/platforms/windows/bridge_win.c +3740 -3248
  45. package/native/tests/gtk_threading_probe.c +143 -143
  46. package/native/tests/test_bridge_core.c +1339 -1228
  47. package/package.json +53 -51
  48. package/prebuilt/linux-x64/cj-tauri +0 -0
  49. package/src/api_system.cj +67 -67
  50. package/src/app.cj +325 -940
  51. package/src/app_events.cj +166 -0
  52. package/src/app_host_ops.cj +287 -0
  53. package/src/app_run.cj +283 -0
  54. package/src/app_spec.cj +76 -0
  55. package/src/arkweb_menu.cj +216 -0
  56. package/src/arkweb_script.cj +0 -12
  57. package/src/capability.cj +409 -409
  58. package/src/clipboard_bridge.cj +178 -0
  59. package/src/dialog_spec.cj +79 -0
  60. package/src/dragdrop.cj +26 -26
  61. package/src/embedded_assets.cj +98 -98
  62. package/src/host.cj +752 -604
  63. package/src/host_arkweb.cj +201 -17
  64. package/src/host_webkit.cj +663 -548
  65. package/src/host_webview2.cj +663 -548
  66. package/src/hotkey.cj +66 -66
  67. package/src/ipc_hub.cj +337 -337
  68. package/src/ipc_message.cj +286 -286
  69. package/src/launch.cj +62 -0
  70. package/src/logging.cj +127 -127
  71. package/src/menu.cj +197 -197
  72. package/src/platform/platform_app_context.cj +77 -0
  73. package/src/platform/platform_assets.cj +33 -0
  74. package/src/platform/platform_discover.cj +176 -0
  75. package/src/platform/platform_env.cj +29 -0
  76. package/src/platform/platform_feature.cj +31 -0
  77. package/src/platform/platform_page_source.cj +87 -0
  78. package/src/platform/platform_target_info.cj +32 -0
  79. package/src/platform_host_provider.cj +87 -0
  80. package/src/plugin.cj +333 -299
  81. package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
  82. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  83. package/src/plugins/fs/plugin_fs.cj +105 -1
  84. package/src/plugins/http/http_backend.cj +131 -0
  85. package/src/plugins/http/http_desktop.cj +195 -0
  86. package/src/plugins/http/http_ohos.cj +204 -0
  87. package/src/plugins/http/plugin_http.cj +310 -459
  88. package/src/plugins/log/plugin_log.cj +105 -105
  89. package/src/plugins/menu/plugin_menu.cj +8 -0
  90. package/src/plugins/notification/plugin_notification.cj +154 -146
  91. package/src/plugins/serial/plugin_serial.cj +17 -1
  92. package/src/plugins/serial/serial_bridge.cj +9 -1
  93. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  94. package/src/plugins/serial/serial_ohos.cj +529 -0
  95. package/src/plugins/shell/plugin_shell.cj +29 -1
  96. package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
  97. package/src/plugins/ws/plugin_ws.cj +488 -470
  98. package/src/process_hidden.cj +174 -174
  99. package/src/scope.cj +391 -391
  100. package/src/shell_event.cj +69 -69
  101. package/src/tests/app_spec_test.cj +126 -0
  102. package/src/tests/capability_test.cj +243 -243
  103. package/src/tests/dialog_spec_test.cj +73 -0
  104. package/src/tests/fake_host.cj +201 -186
  105. package/src/tests/ipc_hub_test.cj +487 -487
  106. package/src/tests/logging_test.cj +62 -62
  107. package/src/tests/platform_contract_test.cj +251 -0
  108. package/src/tests/plugin_clipboard_test.cj +222 -222
  109. package/src/tests/plugin_dialog_test.cj +237 -237
  110. package/src/tests/plugin_fs_test.cj +207 -0
  111. package/src/tests/plugin_http_test.cj +124 -124
  112. package/src/tests/plugin_notification_test.cj +205 -205
  113. package/src/tests/plugin_serial_test.cj +48 -0
  114. package/src/tests/plugin_test.cj +66 -3
  115. package/src/tests/plugin_ws_test.cj +40 -40
  116. package/src/tests/process_hidden_test.cj +129 -129
  117. package/src/tests/scope_test.cj +350 -350
  118. package/src/tests/system_host_test.cj +99 -81
  119. package/src/tests/urlutil_test.cj +53 -53
  120. package/src/tests/version_test.cj +75 -75
  121. package/src/tests/window_registry_test.cj +123 -123
  122. package/src/tray.cj +90 -90
  123. package/src/urlutil.cj +159 -159
  124. package/src/version.cj +44 -44
  125. package/src/window.cj +51 -51
  126. package/src/window_registry.cj +116 -116
  127. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
  128. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  129. package/src/host_factory.cj +0 -48
  130. /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
  131. /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
  132. /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
  133. /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
  134. /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
  135. /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
  136. /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
  137. /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
  138. /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
  139. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
  140. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
  141. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  142. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  143. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
  144. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
  145. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
  146. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
  147. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
  148. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
  149. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
  150. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
  151. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
  152. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
  153. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  154. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  155. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
  156. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
  157. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
  158. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
  159. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
  160. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
  161. /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
  162. /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
@@ -2,7 +2,9 @@
2
2
  * cj-tauri CLI:项目脚手架(create)
3
3
  *
4
4
  * 模板位于 <框架根>/cli/templates/<模板目录>,占位符形如 {{PROJECT_NAME}};
5
- * 递归拷贝 + 文本渲染,生成一个可直接 `cj-tauri dev`(桌面)或 `bash build-hap.sh`(鸿蒙)的应用。
5
+ * 递归拷贝 + 文本渲染,生成「新形态」工程(cj-tauri.toml 清单 + 业务 static 包 + assets/),
6
+ * 骨架由 cj-tauri sync 派生(RFC-002 §7):桌面 cj-tauri dev,鸿蒙 targets/ohos 的 build-hap.sh。
7
+ * ohos-skeleton 目录是 cj-tauri sync ohos 的 DevEco 骨架源,不是 create 模板。
6
8
  */
7
9
  package cli
8
10
 
@@ -186,15 +188,21 @@ func templateDirName(tpl: String): Option<String> {
186
188
  if (tpl == "react") {
187
189
  return Some("app-react") // React 18 + Vite
188
190
  }
191
+ // 鸿蒙生态(PC / 手机 / 平板):新形态是统一工程,模板内容同 app——
192
+ // 清单决定目标端,骨架由 cj-tauri sync ohos 派生;旧 ohos-pc 命名保留兼容
189
193
  if (tpl == "ohos" || tpl == "ohos-pc" || tpl == "harmonyos" || tpl == "harmonyos-pc") {
190
- return Some("ohos-pc") // 鸿蒙 PC(2in1):ArkWeb 宿主 + build-hap.sh
194
+ return Some("app")
195
+ }
196
+ // 内部用:cj-tauri sync ohos 的骨架源(DevEco 工程,不是 create 模板)
197
+ if (tpl == "ohos-skeleton") {
198
+ return Some("ohos-skeleton")
191
199
  }
192
200
  return None
193
201
  }
194
202
 
195
- /** 该项目是不是鸿蒙脚手架(模板目录名即判据,别名在 templateDirName 里已归一到 ohos-pc) */
196
- func isOhosScaffoldTemplate(tplDir: String): Bool {
197
- return baseName(tplDir) == "ohos-pc"
203
+ /** create --template 参数是否鸿蒙生态(下一步提示用;新形态下不能按目录名判) */
204
+ func isOhosTemplate(tpl: String): Bool {
205
+ return tpl == "ohos" || tpl == "ohos-pc" || tpl == "harmonyos" || tpl == "harmonyos-pc"
198
206
  }
199
207
 
200
208
  /**
@@ -287,26 +295,25 @@ func cmdCreate(nameArg: String, tpl: String, bundleArg: String, labelArg: String
287
295
  eprintln(" 请设置环境变量 CJ_TAURI_ROOT 指向 cj-tauri 仓库根目录")
288
296
  return 1
289
297
  }
290
- // 模板先解析:鸿蒙模板**不吃桌面 stdx**(编 hap 时由 build-hap.sh 用 CANGJIE_STDX_OHOS),
291
- // 所以它不能因为本机没配 CANGJIE_STDX 就被拦下。
298
+ // 模板先解析。新形态业务 toml 的 stdx 走 ${CANGJIE_STDX} 环境变量
299
+ // (cjpm 构建期替换),create 不再需要解析 stdx
292
300
  var tplDir = ""
301
+ var dirName = ""
293
302
  if (let Some(d) <- resolveTemplateDir(fwRoot, tpl)) {
294
303
  tplDir = d
304
+ dirName = baseName(d)
295
305
  } else {
296
306
  eprintln("[cj-tauri] 错误: 未知模板 '${tpl}'")
297
307
  eprintln(" 可用模板: ${availableTemplates(fwRoot)}")
298
308
  eprintln(" 别名: app(内联单文件,缺省)/ vue / react / ohos")
299
309
  return 1
300
310
  }
301
- let ohos = isOhosScaffoldTemplate(tplDir)
302
- var stdx = ""
303
- if (let Some(s) <- resolveStdx(fwRoot, None)) {
304
- stdx = s
305
- } else if (!ohos) {
306
- eprintln("[cj-tauri] 错误: 找不到 stdx 目录")
307
- eprintln(" 请设置环境变量 CANGJIE_STDX 指向 stdx 动态库目录(含 stdx.encoding.json)")
311
+ if (dirName == "ohos-skeleton") {
312
+ eprintln("[cj-tauri] 错误: ohos-skeleton 是 cj-tauri sync ohos 的骨架源,不能用于 create")
313
+ eprintln(" 创建鸿蒙工程请用: cj-tauri create ${nameArg} --template ohos")
308
314
  return 1
309
315
  }
316
+ let ohos = isOhosTemplate(tpl)
310
317
  warnIfSpacey("框架", fwRoot)
311
318
 
312
319
  var bundle = bundleArg
@@ -318,44 +325,45 @@ func cmdCreate(nameArg: String, tpl: String, bundleArg: String, labelArg: String
318
325
  label = projectName
319
326
  }
320
327
 
321
- let v = TemplateVars(projectName, pkg, fwRoot, stdx, bundle, label)
328
+ let v = TemplateVars(projectName, pkg, fwRoot, "", bundle, label)
322
329
  ensureDir(targetDir)
323
330
  let count = copyTree(tplDir, targetDir, v)
324
331
 
325
332
  println("[cj-tauri] 已创建项目 ${targetDir}/(${count} 个文件)")
326
333
  println("")
327
334
  println(" 项目名 : ${projectName}")
328
- println(" 模板 : ${baseName(tplDir)}")
335
+ println(" 模板 : ${tpl}")
336
+ println(" 仓颉包名 : ${pkg}")
337
+ println(" 框架依赖 : ${fwRoot}")
329
338
  if (ohos) {
330
339
  println(" 应用名 : ${label}")
331
340
  println(" bundle : ${bundle}")
332
- println(" 框架依赖 : ${fwRoot}")
333
- println("")
334
- println("下一步:")
335
- println(" cd ${targetDir}")
336
- println(" export CANGJIE_STDX_OHOS=<鸿蒙版 stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx")
337
- println(" bash build-hap.sh # 编 hap")
338
- println(" bash build-hap.sh --install # 编完装到设备并拉起")
339
- println("")
340
- println("提示: 模板不带签名证书——首次在 DevEco 里 File → Project Structure → Signing Configs")
341
- println(" 勾「Automatically generate signature」(要登录华为账号);不签名只能编出")
342
- println(" *-unsigned.hap,装不进设备。")
343
- return 0
344
341
  }
345
- println(" 仓颉包名 : ${pkg}")
346
- println(" 框架依赖 : ${fwRoot}")
347
- println(" stdx : ${stdx}")
348
342
  println("")
349
343
  println("下一步:")
350
344
  println(" cd ${targetDir}")
351
- println(" cj-tauri dev # 构建 C 桥 + 编译 + 启动窗口")
352
- println(" cj-tauri build # 仅构建")
353
- println("")
354
- println("提示: cjpm.toml 里的路径是本机绝对路径;换机器需重新执行 create 或手改这两处。")
355
- if (exists(joinPath(joinPath(targetDir, "ui"), "package.json"))) {
345
+ println(" cj-tauri sync # 按清单派生 targets/ 骨架(desktop + ohos)")
346
+ if (dirName == "app-vue" || dirName == "app-react") {
347
+ println(" cd assets/ui && npm install && cd ../.. # 前端依赖(需 Node.js)")
348
+ println(" cj-tauri dev # 起 Vite dev server(热更新)+ 编译 + 启动窗口")
349
+ } else {
350
+ println(" cj-tauri dev # 构建 C 桥 + 编译 + 启动窗口")
351
+ }
352
+ if (ohos) {
353
+ println(" 鸿蒙端: export CANGJIE_STDX_OHOS=<鸿蒙版 stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx")
354
+ println(" cd targets/ohos && bash build-hap.sh [--install]")
355
+ println("")
356
+ println(" 提示: 模板不带签名证书——首次在 DevEco 里 File → Project Structure → Signing Configs")
357
+ println(" 勾「Automatically generate signature」(要登录华为账号);不签名只能编出")
358
+ println(" *-unsigned.hap,装不进设备。")
359
+ } else {
360
+ println("")
361
+ println(" 提示: cjpm.toml 里的框架路径是本机绝对路径;换机器需重新执行 create 或手改这一处。")
362
+ }
363
+ if (exists(joinPath(joinPath(joinPath(targetDir, "assets"), "ui"), "package.json"))) {
356
364
  println("")
357
- println("提示: 该模板前端用 Vite;首次 cj-tauri dev 会自动 npm install 并起 dev server")
358
- println(" (页面走 CJ_TAURI_DEV_URL,改 ui/src 下的前端源码不重启应用就能看到效果)。")
365
+ println(" 提示: 该模板前端用 Vite;首次 cj-tauri dev 会自动 npm install 并起 dev server")
366
+ println(" (页面走 CJ_TAURI_DEV_URL,改 assets/ui/src 下的前端源码不重启应用就能看到效果)。")
359
367
  }
360
368
  return 0
361
369
  }
@@ -0,0 +1,526 @@
1
+ /**
2
+ * 批次 2(RFC-002 §7.2):cj-tauri check / sync
3
+ *
4
+ * check:清单静态校验 + 文件系统检查(entry/资产根/page)+ 骨架漂移检测(desktop/ohos);
5
+ * sync:按清单派生 targets/desktop + targets/ohos 骨架(幂等:重跑只覆盖生成物)。
6
+ * 清单不设 [plugins] 段——插件真相是业务 build() 装配(RFC §4.6.7),插件依赖生成随批次 3。
7
+ */
8
+ package cli
9
+
10
+ import std.collection.*
11
+ import std.env.*
12
+ import std.fs.*
13
+ import std.process.*
14
+ import stdx.encoding.json.*
15
+
16
+ /** 读清单并校验;失败打印原因并返回 None */
17
+ func loadValidatedManifest(): Option<Manifest> {
18
+ let source = "cj-tauri.toml"
19
+ var text = ""
20
+ try {
21
+ text = readText(source)
22
+ } catch (e: Exception) {
23
+ eprintln("[cj-tauri] 当前目录没有 ${source}(cwd=${currentDir()})")
24
+ return None
25
+ }
26
+ var mOpt: Option<Manifest> = None
27
+ try {
28
+ mOpt = Some(Manifest.fromToml(text, source))
29
+ } catch (e: Exception) {
30
+ eprintln("[cj-tauri] 清单解析失败:${e.message}")
31
+ return None
32
+ }
33
+ let m = mOpt.getOrThrow()
34
+ var failed = false
35
+ for (err in m.validate()) {
36
+ eprintln("[cj-tauri] 清单错误:${err}")
37
+ failed = true
38
+ }
39
+ if (failed) {
40
+ return None
41
+ }
42
+ return Some(m)
43
+ }
44
+
45
+ /** 文件系统层检查(entry / 资产根 / page / capabilities);返回是否失败 */
46
+ func checkManifestOnDisk(m: Manifest): Bool {
47
+ var failed = false
48
+ if (!exists(m.entry)) {
49
+ eprintln("[cj-tauri] 清单错误:entry 不存在:${m.entry}")
50
+ failed = true
51
+ }
52
+ if (!exists(m.assetsRoot)) {
53
+ eprintln("[cj-tauri] 清单错误:[assets] root 不存在:${m.assetsRoot}")
54
+ failed = true
55
+ } else {
56
+ let pagePath = m.assetsRoot + "/" + m.page
57
+ if (!exists(pagePath)) {
58
+ eprintln("[cj-tauri] 清单错误:[assets] page 不存在:${pagePath}")
59
+ failed = true
60
+ }
61
+ }
62
+ if (!exists(m.capabilitiesDir)) {
63
+ eprintln("[cj-tauri] 提示:capabilities 目录不存在(${m.capabilitiesDir})——没有命令权限声明")
64
+ }
65
+ return failed
66
+ }
67
+
68
+ /** 骨架漂移:缺骨架=提示(引导 sync),内容不一致=失败 */
69
+ func checkDesktopDrift(m: Manifest, fwRoot: String): Bool {
70
+ let tomlPath = "targets/desktop/cjpm.toml"
71
+ let mainPath = "targets/desktop/src/main.cj"
72
+ let expectToml = m.renderDesktopToml(fwRoot, cliVersion())
73
+ let expectMain = m.renderDesktopMain(cliVersion())
74
+ var failed = false
75
+ if (!exists(tomlPath)) {
76
+ eprintln("[cj-tauri] 提示:骨架尚未生成(${tomlPath})——运行 cj-tauri sync desktop")
77
+ } else if (readText(tomlPath) != expectToml) {
78
+ eprintln("[cj-tauri] 骨架漂移:${tomlPath} 与生成器输出不一致(运行 cj-tauri sync desktop 重新生成)")
79
+ failed = true
80
+ }
81
+ if (exists(mainPath) && readText(mainPath) != expectMain) {
82
+ eprintln("[cj-tauri] 骨架漂移:${mainPath} 与生成器输出不一致(运行 cj-tauri sync desktop 重新生成)")
83
+ failed = true
84
+ }
85
+ return failed
86
+ }
87
+
88
+ func cmdCheck(): Int64 {
89
+ let mOpt = loadValidatedManifest()
90
+ if (let None <- mOpt) {
91
+ return 1
92
+ }
93
+ let m = mOpt.getOrThrow()
94
+ var failed = checkManifestOnDisk(m)
95
+ match (resolveFrameworkRoot()) {
96
+ case Some(fw) =>
97
+ let drift = checkDesktopDrift(m, fw)
98
+ failed = drift || failed
99
+ failed = checkOhosDrift(m, fw) || failed
100
+ case None =>
101
+ eprintln("[cj-tauri] 提示:未定位到框架根(CJ_TAURI_ROOT),跳过骨架漂移检查")
102
+ }
103
+ // 三方比对(RFC §4.6.7):骨架未构建时提示跳过,不阻塞
104
+ failed = checkThreeWay(m) || failed
105
+ if (failed) {
106
+ eprintln("[cj-tauri] check 未通过:${m.appId}@${m.version}")
107
+ return 1
108
+ }
109
+ println("[cj-tauri] check 通过:${m.appId}@${m.version}(${m.name})")
110
+ return 0
111
+ }
112
+
113
+ /**
114
+ * 三方比对(批次 3,RFC §4.6.7):① 能力声明 × ② 插件 describe() 自描述 × ③ 无头导出(实际装配真相)。
115
+ * ②③ 由 CJ_TAURI_MANIFEST=json 无头导出一起带出(插件数组,与启动日志同款形态)。
116
+ *
117
+ * 规则:
118
+ * - 已注册的插件命令未在 capabilities 声明 → 失败(「漏声明编不过」的运行时兜底);
119
+ * capabilities 的 permissions 命名权限集按插件自描述单层展开后与明文 commands 同权(对齐运行时)
120
+ * - requiredCapability 不在 capabilities 目录 → 失败
121
+ * - supportedHosts 不含当前宿主 → 提示(P-2:该端应以桩如实失败「端不支持」)
122
+ * - 骨架未构建 → 提示跳过(不阻塞,构建后重跑即生效)
123
+ *
124
+ * 边界如实说:应用自身命令(SystemCommands / wire 注册)不在无头导出里(该通道只带插件),
125
+ * 它们的漏声明照旧由运行时能力门禁拒绝;导出形态将来再扩展。
126
+ */
127
+ func checkThreeWay(m: Manifest): Bool {
128
+ // cjpm 可执行工程的二进制恒名 main(music / CLI 同口径),与模块名无关;
129
+ // 但 Windows 产物带 `.exe`(cjc 不补后缀,cmd 也不认无后缀名),裸名判定会让
130
+ // 三方比对在 Windows 上恒被跳过——后缀与构建侧同源取(AGENTS §4「三处同源」)
131
+ let binPath = "targets/desktop/target/release/bin/main" + exeSuffix()
132
+ if (!exists(binPath)) {
133
+ eprintln("[cj-tauri] 提示:跳过三方比对(桌面骨架未构建:${binPath};cd targets/desktop && cjpm build 后重跑)")
134
+ return false
135
+ }
136
+ // ① 能力声明:commands 全名集合 + identifier 集合 + permissions 引用的集名
137
+ let declared = ArrayList<String>()
138
+ let identifiers = ArrayList<String>()
139
+ let permSetNames = ArrayList<String>()
140
+ if (exists(m.capabilitiesDir)) {
141
+ var entries: Array<FileInfo>
142
+ try {
143
+ entries = Directory.readFrom(m.capabilitiesDir)
144
+ } catch (e: Exception) {
145
+ eprintln("[cj-tauri] 三方比对:读 capabilities 目录失败 ${m.capabilitiesDir}: ${e.message}")
146
+ return true
147
+ }
148
+ for (info in entries) {
149
+ if (info.isDirectory()) {
150
+ continue
151
+ }
152
+ let path = info.path.toString()
153
+ if (!path.endsWith(".json")) {
154
+ continue
155
+ }
156
+ var text = ""
157
+ try {
158
+ text = String.fromUtf8(File.readFrom(path))
159
+ } catch (e: Exception) {
160
+ eprintln("[cj-tauri] 三方比对:读 ${path} 失败: ${e.message}")
161
+ return true
162
+ }
163
+ var obj: JsonObject
164
+ try {
165
+ obj = JsonValue.fromStr(text).asObject()
166
+ } catch (e: Exception) {
167
+ eprintln("[cj-tauri] 三方比对:${path} 不是合法 JSON: ${e.message}")
168
+ return true
169
+ }
170
+ if (let Some(idVal) <- obj.get("identifier")) {
171
+ let id = jsonStringOf(idVal)
172
+ if (id.size > 0) {
173
+ identifiers.add(id)
174
+ }
175
+ }
176
+ for (c in jsonStrArray(obj.get("commands"))) {
177
+ declared.add(c)
178
+ }
179
+ for (s in jsonStrArray(obj.get("permissions"))) {
180
+ permSetNames.add(s)
181
+ }
182
+ }
183
+ }
184
+ // ②③ 无头导出(子进程继承环境;json 模式只打印插件数组后退出)
185
+ setVariable("CJ_TAURI_MANIFEST", "json")
186
+ var outText = ""
187
+ try {
188
+ let (code, raw, _) = executeWithOutput(binPath, [])
189
+ outText = String.fromUtf8(raw)
190
+ if (code != 0) {
191
+ eprintln("[cj-tauri] 三方比对:无头导出退出异常(code=${code}):${outText}")
192
+ return true
193
+ }
194
+ } catch (e: Exception) {
195
+ eprintln("[cj-tauri] 三方比对:运行 ${binPath} 失败:${e.message}")
196
+ return true
197
+ }
198
+ var pluginsArr: JsonArray
199
+ try {
200
+ pluginsArr = JsonValue.fromStr(outText).asArray()
201
+ } catch (e: Exception) {
202
+ eprintln("[cj-tauri] 三方比对:无头导出输出解析失败:${e.message}")
203
+ return true
204
+ }
205
+ let hostKind = localHostKind()
206
+ // 权限集单层展开(与运行时 CapabilityRegistry 同语义,见 src/capability.cj「单层展开」注释):
207
+ // 清单引用的每个集名,到插件自描述的 permissions(集全名 → 成员全名)里按名取成员并入 declared。
208
+ // 成员若又是集名不再展开(避免环)。
209
+ for (setName in permSetNames) {
210
+ for (i in 0..pluginsArr.size()) {
211
+ var pobj: JsonObject
212
+ try {
213
+ pobj = pluginsArr.get(i).getOrThrow().asObject()
214
+ } catch (e: Exception) {
215
+ continue
216
+ }
217
+ if (let Some(permVal) <- pobj.get("permissions")) {
218
+ let permObj = permVal.asObject()
219
+ if (let Some(membersVal) <- permObj.get(setName)) {
220
+ for (mm in jsonStrArray(membersVal)) {
221
+ if (!declared.contains(mm)) {
222
+ declared.add(mm)
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ var failed = false
230
+ for (i in 0..pluginsArr.size()) {
231
+ var obj: JsonObject
232
+ try {
233
+ obj = pluginsArr.get(i).getOrThrow().asObject()
234
+ } catch (e: Exception) {
235
+ continue
236
+ }
237
+ if (let Some(nameVal) <- obj.get("name")) {
238
+ let pname = jsonStringOf(nameVal)
239
+ if (pname.size > 0) {
240
+ for (c in jsonStrArray(obj.get("commands"))) {
241
+ if (!declared.contains(c)) {
242
+ eprintln("[cj-tauri] 三方比对:已注册命令 ${c} 未在 capabilities 声明(补进 ${m.capabilitiesDir}/*.json 的 commands)")
243
+ failed = true
244
+ }
245
+ }
246
+ for (r in jsonStrArray(obj.get("requiredCapabilities"))) {
247
+ if (!identifiers.contains(r)) {
248
+ eprintln("[cj-tauri] 三方比对:插件 ${pname} 要求的能力 ${r} 不在 ${m.capabilitiesDir} 中")
249
+ failed = true
250
+ }
251
+ }
252
+ let hosts = jsonStrArray(obj.get("supportedHosts"))
253
+ if (hosts.size > 0 && !hosts.contains(hostKind)) {
254
+ eprintln("[cj-tauri] 提示:插件 ${pname} 声明不支持当前宿主(${hostKind},声明 ${joinComma(hosts)})——该端应以桩如实失败「端不支持」(P-2)")
255
+ }
256
+ // ohos 端可用性汇总(P-2,批次 3-PR3):check 跑在桌面,但要为「sync ohos 之后
257
+ // 这些插件会变成桩」提前给一句可照做的提示——编译能过,运行期调用报「端不支持」
258
+ if (hosts.size > 0 && !hosts.contains("arkweb")) {
259
+ eprintln("[cj-tauri] 提示:插件 ${pname} 无 ohos 端实现(声明 ${joinComma(hosts)})——sync ohos 后为 P-1 桩,运行期调用将报「端不支持」")
260
+ }
261
+ }
262
+ }
263
+ }
264
+ return failed
265
+ }
266
+
267
+ /** JsonValue → 字符串(非字符串形态返回空串;用法与 cspub 先例一致:先过 kind 再转换) */
268
+ func jsonStringOf(v: JsonValue): String {
269
+ match (v.kind()) {
270
+ case JsonKind.JsString => return v.asString().getValue()
271
+ case _ => return ""
272
+ }
273
+ }
274
+
275
+ /** Option<JsonValue> → 字符串数组(缺失或非数组形态返回空) */
276
+ func jsonStrArray(v: Option<JsonValue>): ArrayList<String> {
277
+ let out = ArrayList<String>()
278
+ if (let Some(val) <- v) {
279
+ try {
280
+ let arr = val.asArray()
281
+ for (i in 0..arr.size()) {
282
+ if (let Some(item) <- arr.get(i)) {
283
+ let s = jsonStringOf(item)
284
+ if (s.size > 0) {
285
+ out.add(s)
286
+ }
287
+ }
288
+ }
289
+ } catch (e: Exception) {
290
+ // 非数组形态按空处理:上层消息会指出具体字段
291
+ }
292
+ }
293
+ return out
294
+ }
295
+
296
+ /** 开发机 hostKind(CLI 只跑在桌面,映射与框架 discover 一致):linux→webkitgtk、windows→webview2 */
297
+ func localHostKind(): String {
298
+ if (let Some(v) <- getVariable("OS")) {
299
+ if (v.contains("Windows")) {
300
+ return "webview2"
301
+ }
302
+ }
303
+ return "webkitgtk"
304
+ }
305
+
306
+ /** "a,b" 拼接,供提示消息 */
307
+ func joinComma(items: ArrayList<String>): String {
308
+ var s = ""
309
+ for (x in items) {
310
+ if (s.size > 0) {
311
+ s += ","
312
+ }
313
+ s += x
314
+ }
315
+ return s
316
+ }
317
+
318
+ func cmdSync(args: Array<String>): Int64 {
319
+ var target = ""
320
+ var fwOverride: Option<String> = None
321
+ var i: Int64 = 1
322
+ while (i < args.size) {
323
+ let a = args[i]
324
+ if (a == "--framework") {
325
+ if (i + 1 >= args.size) {
326
+ eprintln("用法: cj-tauri sync [desktop|ohos] [--framework <框架根路径>]")
327
+ return 1
328
+ }
329
+ i += 1
330
+ fwOverride = Some(args[i])
331
+ } else {
332
+ target = a
333
+ }
334
+ i += 1
335
+ }
336
+ if (target != "" && target != "desktop" && target != "ohos") {
337
+ eprintln("用法: cj-tauri sync [desktop|ohos] [--framework <框架根路径>]")
338
+ return 1
339
+ }
340
+ let mOpt = loadValidatedManifest()
341
+ if (let None <- mOpt) {
342
+ return 1
343
+ }
344
+ let m = mOpt.getOrThrow()
345
+ var fwRoot = ""
346
+ match (fwOverride) {
347
+ case Some(f) => fwRoot = f
348
+ case None =>
349
+ match (resolveFrameworkRoot()) {
350
+ case Some(f) => fwRoot = f
351
+ case None =>
352
+ eprintln("[cj-tauri] 未定位到框架根:请 export CJ_TAURI_ROOT=<框架仓库根>,或 --framework <路径>")
353
+ return 1
354
+ }
355
+ }
356
+ var code = 0
357
+ if (target == "" || target == "desktop") {
358
+ ensureDir("targets/desktop/src")
359
+ writeText("targets/desktop/cjpm.toml", m.renderDesktopToml(fwRoot, cliVersion()))
360
+ writeText("targets/desktop/src/main.cj", m.renderDesktopMain(cliVersion()))
361
+ println("[cj-tauri] 已生成 targets/desktop/cjpm.toml 与 targets/desktop/src/main.cj(launcher 调 ${m.pkg}.build())")
362
+ println("[cj-tauri] 下一步:cd targets/desktop && cjpm build(需 CANGJIE_STDX 指向 stdx dynamic/stdx)")
363
+ }
364
+ if (target == "" || target == "ohos") {
365
+ code = syncOhos(m, fwRoot)
366
+ }
367
+ return code
368
+ }
369
+
370
+ // ---------- ohos 骨架(批次 3-PR3,RFC §7.3) ----------
371
+
372
+ /** 能力清单文件名单:[capabilities] 目录下 .json 的 basename(sync 生成与 check 漂移共用) */
373
+ func listCapsFiles(dir: String): ArrayList<String> {
374
+ let out = ArrayList<String>()
375
+ if (!exists(dir)) {
376
+ return out
377
+ }
378
+ var entries: Array<FileInfo>
379
+ try {
380
+ entries = Directory.readFrom(dir)
381
+ } catch (e: Exception) {
382
+ return out
383
+ }
384
+ for (info in entries) {
385
+ if (info.isDirectory()) {
386
+ continue
387
+ }
388
+ let path = info.path.toString()
389
+ if (path.endsWith(".json")) {
390
+ out.add(baseName(path))
391
+ }
392
+ }
393
+ return out
394
+ }
395
+
396
+ /** 资产递归拷贝:资产根目录整体拷入 rawfile 目录(字节级、二进制安全——RFC §7.3 的 rawfile 资产注入口) */
397
+ func copyAssetsToRawfile(srcRoot: String, dstRoot: String): Int64 {
398
+ if (!exists(srcRoot)) {
399
+ eprintln("[cj-tauri] 错误:资产根不存在:${srcRoot}")
400
+ return 0
401
+ }
402
+ ensureDir(dstRoot)
403
+ var count = 0
404
+ for (info in listDir(srcRoot)) {
405
+ let nm = baseName(info.path.toString())
406
+ if (nm.size == 0) {
407
+ continue
408
+ }
409
+ let s = joinPath(srcRoot, nm)
410
+ let d = joinPath(dstRoot, nm)
411
+ if (info.isDirectory()) {
412
+ count = count + copyAssetsToRawfile(s, d)
413
+ } else {
414
+ writeBytes(d, File.readFrom(s))
415
+ count = count + 1
416
+ }
417
+ }
418
+ return count
419
+ }
420
+
421
+ /**
422
+ * ohos 骨架派生:ohos-skeleton 模板整体拷渲染(静态物 + 图标)+ 4 个清单派生文件覆盖 + rawfile 资产注入。
423
+ * 幂等对齐 desktop:重跑覆盖生成物,rawfile 每次从 assets/ 全量重建。
424
+ */
425
+ func syncOhos(m: Manifest, fwRoot: String): Int64 {
426
+ let tplDirOpt = resolveTemplateDir(fwRoot, "ohos-skeleton")
427
+ match (tplDirOpt) {
428
+ case None =>
429
+ eprintln("[cj-tauri] 未找到 ohos 骨架模板:${fwRoot}/cli/templates/ohos-skeleton(框架根定位有误?)")
430
+ return 1
431
+ case Some(tplDir) =>
432
+ // 模板占位符取清单值:bundle=id、label=name;stdx 不参与(build-hap.sh 用 CANGJIE_STDX_OHOS)
433
+ let v = TemplateVars(m.name, m.pkg, fwRoot, "", m.appId, m.name)
434
+ ensureDir("targets/ohos")
435
+ let count = copyTree(tplDir, "targets/ohos", v)
436
+ // 4 个清单派生文件覆盖模板(check 的漂移检测复用同一批渲染器,二者必然一致)
437
+ writeText("targets/ohos/AppScope/app.json5", m.renderOhosAppJson(cliVersion()))
438
+ writeText("targets/ohos/entry/cjpm.toml", m.renderOhosEntryToml(fwRoot, cliVersion()))
439
+ let capsFiles = listCapsFiles(m.capabilitiesDir)
440
+ writeText("targets/ohos/entry/src/main/cangjie/app_host.cj", m.renderOhosAppHost(cliVersion(), capsFiles))
441
+ writeText("targets/ohos/entry/src/main/cangjie/main_ability.cj", m.renderOhosMainAbility(cliVersion()))
442
+ // rawfile 资产注入口:清掉模板自带的演示资产,按 assets/ 原样拷入(字节级)
443
+ let rawfileDir = "targets/ohos/entry/src/main/resources/rawfile"
444
+ if (exists(rawfileDir + "/index.html")) {
445
+ remove(rawfileDir + "/index.html")
446
+ }
447
+ if (exists(rawfileDir + "/capabilities.json")) {
448
+ remove(rawfileDir + "/capabilities.json")
449
+ }
450
+ let copied = copyAssetsToRawfile(m.assetsRoot, rawfileDir)
451
+ println("[cj-tauri] 已生成 targets/ohos:模板 ${count} 文件 + 清单派生 4 文件 + rawfile ${copied} 资产")
452
+ println("[cj-tauri] 下一步:export CANGJIE_STDX_OHOS=<鸿蒙版 stdx>/linux_ohos_aarch64_cjnative/dynamic/stdx")
453
+ println(" cd targets/ohos && bash build-hap.sh")
454
+ return 0
455
+ }
456
+ }
457
+
458
+ /** ohos 骨架漂移:targets/ohos 存在才查(opt-in,不阻塞纯桌面应用);4 派生文件 + rawfile 资产一致性 */
459
+ func checkOhosDrift(m: Manifest, fwRoot: String): Bool {
460
+ if (!exists("targets/ohos/entry/cjpm.toml")) {
461
+ eprintln("[cj-tauri] 提示:ohos 骨架未生成(应用要接鸿蒙端时运行 cj-tauri sync ohos)")
462
+ return false
463
+ }
464
+ let ver = cliVersion()
465
+ let capsFiles = listCapsFiles(m.capabilitiesDir)
466
+ var failed = false
467
+ failed = sameGeneratedFile("targets/ohos/AppScope/app.json5", m.renderOhosAppJson(ver), failed)
468
+ failed = sameGeneratedFile("targets/ohos/entry/cjpm.toml", m.renderOhosEntryToml(fwRoot, ver), failed)
469
+ failed = sameGeneratedFile("targets/ohos/entry/src/main/cangjie/app_host.cj", m.renderOhosAppHost(ver, capsFiles), failed)
470
+ failed = sameGeneratedFile("targets/ohos/entry/src/main/cangjie/main_ability.cj", m.renderOhosMainAbility(ver), failed)
471
+ // rawfile 资产:page 必须与资产根字节一致;能力文件名单必须与清单目录一致
472
+ let rawfileDir = "targets/ohos/entry/src/main/resources/rawfile"
473
+ let pageSrc = m.assetsRoot + "/" + m.page
474
+ let pageDst = rawfileDir + "/" + m.page
475
+ if (exists(pageSrc)) {
476
+ if (!exists(pageDst) || !bytesEqual(File.readFrom(pageSrc), File.readFrom(pageDst))) {
477
+ eprintln("[cj-tauri] 骨架漂移:${pageDst} 与 ${pageSrc} 不一致(重跑 cj-tauri sync ohos)")
478
+ failed = true
479
+ }
480
+ }
481
+ let capsDstDir = rawfileDir + "/capabilities"
482
+ for (f in capsFiles) {
483
+ if (!exists(capsDstDir + "/" + f)) {
484
+ eprintln("[cj-tauri] 骨架漂移:缺 ${capsDstDir}/${f}(重跑 cj-tauri sync ohos)")
485
+ failed = true
486
+ }
487
+ }
488
+ if (exists(capsDstDir)) {
489
+ for (info in listDir(capsDstDir)) {
490
+ if (info.isDirectory()) {
491
+ continue
492
+ }
493
+ let nm = baseName(info.path.toString())
494
+ if (!capsFiles.contains(nm)) {
495
+ eprintln("[cj-tauri] 骨架漂移:rawfile/capabilities 有残留文件 ${nm}(重跑 cj-tauri sync ohos)")
496
+ failed = true
497
+ }
498
+ }
499
+ }
500
+ return failed
501
+ }
502
+
503
+ /** 生成物比对:缺失 = 漂移,内容不一致 = 漂移 */
504
+ func sameGeneratedFile(path: String, expect: String, failed: Bool): Bool {
505
+ if (!exists(path)) {
506
+ eprintln("[cj-tauri] 骨架漂移:缺生成文件 ${path}(重跑 cj-tauri sync ohos)")
507
+ return true
508
+ }
509
+ if (readText(path) != expect) {
510
+ eprintln("[cj-tauri] 骨架漂移:${path} 与生成器输出不一致(重跑 cj-tauri sync ohos)")
511
+ return true
512
+ }
513
+ return failed
514
+ }
515
+
516
+ func bytesEqual(a: Array<UInt8>, b: Array<UInt8>): Bool {
517
+ if (a.size != b.size) {
518
+ return false
519
+ }
520
+ for (i in 0..a.size) {
521
+ if (a[i] != b[i]) {
522
+ return false
523
+ }
524
+ }
525
+ return true
526
+ }
package/cli/src/util.cj CHANGED
@@ -157,3 +157,12 @@ func listDir(p: String): Array<FileInfo> {
157
157
  func escapeTomlString(p: String): String {
158
158
  return p.replace("\\", "\\\\").replace("\"", "\\\"")
159
159
  }
160
+
161
+ /**
162
+ * 写进生成 TOML 的路径一律转正斜杠。
163
+ * 为什么:Windows 的 `E:\a\b` 在 TOML 基本字符串里是转义序列,`\a` / `\t` 会让 cjpm 解析期直接
164
+ * `errLexEscape` 失败(AGENTS §4「生成 toml 的脚本用正斜杠」);正斜杠 cjpm 与 gcc/ld 都认。
165
+ */
166
+ func toTomlPath(p: String): String {
167
+ return p.replace("\\", "/")
168
+ }