cj-tauri 1.1.2 → 2.0.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 (101) hide show
  1. package/README.md +64 -18
  2. package/cjpm.toml +52 -52
  3. package/cli/cjpm.toml +10 -1
  4. package/cli/src/doctor.cj +4 -2
  5. package/cli/src/main.cj +11 -3
  6. package/cli/src/manifest.cj +558 -0
  7. package/cli/src/project.cj +38 -22
  8. package/cli/src/scaffold.cj +369 -361
  9. package/cli/src/sync.cj +526 -0
  10. package/cli/src/util.cj +168 -159
  11. package/cli/templates/app/README.md +48 -80
  12. package/cli/templates/app/{capabilities → assets/capabilities}/default.json +7 -7
  13. package/cli/templates/app/{ui → assets/ui}/index.html +118 -118
  14. package/cli/templates/app/cj-tauri.toml +22 -0
  15. package/cli/templates/app/cjpm.toml +34 -17
  16. package/cli/templates/app/src/main.cj +90 -88
  17. package/cli/templates/app-react/README.md +56 -67
  18. package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +7 -7
  19. package/cli/templates/app-react/{ui → assets/ui}/package.json +19 -19
  20. package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +6 -6
  21. package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +24 -24
  22. package/cli/templates/app-react/cj-tauri.toml +23 -0
  23. package/cli/templates/app-react/cjpm.toml +34 -17
  24. package/cli/templates/app-react/src/main.cj +91 -102
  25. package/cli/templates/app-vue/README.md +56 -60
  26. package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +7 -7
  27. package/cli/templates/app-vue/{ui → assets/ui}/package.json +18 -18
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +4 -4
  29. package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +24 -24
  30. package/cli/templates/app-vue/cj-tauri.toml +23 -0
  31. package/cli/templates/app-vue/cjpm.toml +34 -17
  32. package/cli/templates/app-vue/src/main.cj +91 -102
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +13 -13
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +8 -8
  35. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +6 -6
  36. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +79 -79
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +46 -46
  38. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +26 -26
  39. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +43 -43
  40. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +6 -6
  41. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +9 -9
  42. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +17 -17
  43. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +94 -94
  44. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/index.cj +167 -167
  45. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +139 -139
  46. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +49 -49
  47. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +8 -8
  48. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +16 -16
  49. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +6 -6
  50. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +2 -2
  51. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +6 -6
  52. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +92 -92
  53. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +5 -5
  54. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +6 -6
  55. package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +10 -10
  56. package/package.json +4 -2
  57. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  58. package/src/app.cj +49 -20
  59. package/src/app_spec.cj +76 -0
  60. package/src/arkweb_script.cj +142 -154
  61. package/src/host.cj +20 -17
  62. package/src/host_arkweb.cj +778 -740
  63. package/src/host_webkit.cj +22 -0
  64. package/src/host_webview2.cj +22 -0
  65. package/src/launch.cj +62 -0
  66. package/src/platform/platform_app_context.cj +77 -0
  67. package/src/platform/platform_assets.cj +33 -0
  68. package/src/platform/platform_discover.cj +176 -0
  69. package/src/platform/platform_env.cj +29 -0
  70. package/src/platform/platform_feature.cj +31 -0
  71. package/src/platform/platform_page_source.cj +87 -0
  72. package/src/platform/platform_target_info.cj +32 -0
  73. package/src/platform_host_provider.cj +87 -0
  74. package/src/plugin.cj +36 -2
  75. package/src/plugins/clipboard/plugin_clipboard.cj +8 -0
  76. package/src/plugins/menu/plugin_menu.cj +158 -150
  77. package/src/plugins/notification/plugin_notification.cj +8 -0
  78. package/src/plugins/serial/plugin_serial.cj +454 -442
  79. package/src/plugins/serial/serial_bridge.cj +194 -186
  80. package/src/plugins/serial/serial_ohos_stub.cj +61 -0
  81. package/src/plugins/shell/plugin_shell.cj +258 -230
  82. package/src/plugins/sqlite/plugin_sqlite.cj +710 -697
  83. package/src/plugins/sqlite/sqlite_ffi.cj +211 -206
  84. package/src/plugins/sqlite/sqlite_ohos_stub.cj +91 -0
  85. package/src/tests/app_spec_test.cj +127 -0
  86. package/src/tests/fake_host.cj +23 -8
  87. package/src/tests/platform_contract_test.cj +251 -0
  88. package/src/tests/plugin_test.cj +585 -542
  89. package/src/version.cj +1 -1
  90. package/src/host_factory.cj +0 -48
  91. /package/cli/templates/app-react/{ui → assets/ui}/index.html +0 -0
  92. /package/cli/templates/app-react/{ui → assets/ui}/src/App.css +0 -0
  93. /package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +0 -0
  94. /package/cli/templates/app-vue/{ui → assets/ui}/index.html +0 -0
  95. /package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +0 -0
  96. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  97. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  98. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +0 -0
  99. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  100. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  101. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
@@ -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
+ }