@synopackageland/cli 0.9.7 → 0.10.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 (111) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +5 -0
  3. package/dist/add.js +7 -8
  4. package/dist/build-spk/dsm-arch.js +1 -1
  5. package/dist/build-spk/dsm-icons.d.ts +8 -1
  6. package/dist/build-spk/dsm-icons.js +26 -12
  7. package/dist/build-spk/dsm-version.d.ts +1 -1
  8. package/dist/build-spk/dsm-version.js +4 -4
  9. package/dist/build-spk/gateway.js +2 -2
  10. package/dist/build-spk/generator.js +1 -1
  11. package/dist/build-spk/png.js +16 -16
  12. package/dist/capabilities.js +1 -1
  13. package/dist/capability-overlays.d.ts +16 -5
  14. package/dist/capability-overlays.js +30 -16
  15. package/dist/cli.js +107 -107
  16. package/dist/compose/analyze.js +23 -23
  17. package/dist/compose/privilege.js +12 -12
  18. package/dist/contract/i18n.js +3 -3
  19. package/dist/contract/icon.js +2 -2
  20. package/dist/contract/posix-all-shares.js +2 -2
  21. package/dist/contract/schema-validate.js +3 -3
  22. package/dist/contract/validate.js +16 -16
  23. package/dist/contract/web-launch.d.ts +2 -1
  24. package/dist/contract/web-launch.js +3 -2
  25. package/dist/deploy/deploy.js +8 -8
  26. package/dist/deploy/homes-values.js +1 -1
  27. package/dist/deploy/identity-shared.js +1 -1
  28. package/dist/deploy/native-install-values.js +1 -1
  29. package/dist/deploy/param-values.js +4 -4
  30. package/dist/deploy/plan.d.ts +1 -1
  31. package/dist/deploy/port-preflight.js +3 -3
  32. package/dist/deploy/run-as-values.js +2 -2
  33. package/dist/deploy/share-permission-preflight.js +6 -6
  34. package/dist/deploy/share-values.js +3 -3
  35. package/dist/error-registry.js +369 -346
  36. package/dist/errors.js +1 -1
  37. package/dist/host/dsm-port-profile.d.ts +1 -1
  38. package/dist/host/dsm-port-profile.js +1 -1
  39. package/dist/info.js +11 -11
  40. package/dist/init.d.ts +2 -0
  41. package/dist/init.js +85 -38
  42. package/dist/inspect.js +5 -5
  43. package/dist/portal/alias.d.ts +1 -1
  44. package/dist/portal/alias.js +4 -4
  45. package/dist/runtime/executor-build.js +12 -1
  46. package/dist/runtime/executor-digests.json +8 -96
  47. package/dist/runtime/executor-inspect.js +11 -4
  48. package/dist/runtime/executor-policy.d.ts +9 -2
  49. package/dist/runtime/executor-policy.js +27 -8
  50. package/dist/runtime/root-programs.d.ts +47 -0
  51. package/dist/runtime/root-programs.js +153 -0
  52. package/dist/runtime/sdk-source.d.ts +5 -4
  53. package/dist/runtime/sdk-source.js +8 -4
  54. package/dist/skill.js +14 -14
  55. package/dist/spk/check-info.js +17 -17
  56. package/dist/spk/check-metadata.js +18 -18
  57. package/dist/spk/check-payload.js +10 -10
  58. package/dist/spk/rules.d.ts +57 -57
  59. package/dist/spk/rules.js +50 -50
  60. package/dist/spk/validate-spk.js +5 -5
  61. package/dist/standalone/api-permissions.js +1 -1
  62. package/dist/standalone/background.d.ts +3 -1
  63. package/dist/standalone/background.js +8 -4
  64. package/dist/standalone/binary.js +7 -7
  65. package/dist/standalone/bridge-app.d.ts +23 -0
  66. package/dist/standalone/bridge-app.js +141 -0
  67. package/dist/standalone/bridge-backend.d.ts +40 -0
  68. package/dist/standalone/bridge-backend.js +95 -0
  69. package/dist/standalone/bridge-build.d.ts +21 -0
  70. package/dist/standalone/bridge-build.js +62 -0
  71. package/dist/standalone/bridge-json.d.ts +16 -0
  72. package/dist/standalone/bridge-json.js +59 -0
  73. package/dist/standalone/bridge-runtime.d.ts +16 -0
  74. package/dist/standalone/bridge-runtime.js +46 -0
  75. package/dist/standalone/bridge-rust.d.ts +16 -0
  76. package/dist/standalone/bridge-rust.js +89 -0
  77. package/dist/standalone/build.js +18 -14
  78. package/dist/standalone/daemon.js +6 -6
  79. package/dist/standalone/dsm-window.d.ts +10 -5
  80. package/dist/standalone/dsm-window.js +64 -16
  81. package/dist/standalone/i18n.js +18 -18
  82. package/dist/standalone/inspect-bridge.d.ts +31 -0
  83. package/dist/standalone/inspect-bridge.js +111 -0
  84. package/dist/standalone/inspect-spk.d.ts +1 -1
  85. package/dist/standalone/inspect-spk.js +77 -69
  86. package/dist/standalone/lifecycle.js +22 -3
  87. package/dist/standalone/manifest.d.ts +17 -9
  88. package/dist/standalone/manifest.js +143 -65
  89. package/dist/standalone/portal-window.js +14 -7
  90. package/dist/standalone/request-timeout.d.ts +9 -0
  91. package/dist/standalone/request-timeout.js +13 -0
  92. package/dist/standalone/root-ops.d.ts +23 -2
  93. package/dist/standalone/root-ops.js +118 -13
  94. package/dist/standalone/sdk-check.js +4 -4
  95. package/dist/standalone/shared-folder.js +7 -7
  96. package/dist/standalone/spk.d.ts +2 -0
  97. package/dist/standalone/spk.js +30 -11
  98. package/dist/standalone/uninstall-hook.js +22 -4
  99. package/dist/standalone/uninstall.js +7 -7
  100. package/dist/standalone/window-size.d.ts +17 -0
  101. package/dist/standalone/window-size.js +31 -0
  102. package/dist/types.d.ts +1 -1
  103. package/dist/validate.js +13 -13
  104. package/package.json +2 -2
  105. package/schema/bridge-manifest.v1.schema.json +86 -0
  106. package/skills/reference/create.md +76 -2
  107. package/skills/reference/ui-json.md +2 -1
  108. package/skills/reference/ui-vue.md +9 -3
  109. package/skills/reference/wrap.md +1 -1
  110. package/dist/standalone/rust-build.d.ts +0 -35
  111. package/dist/standalone/rust-build.js +0 -127
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.9.7",
3
+ "version": "0.10.0",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "@synopackageland/templates": "0.9.7",
27
+ "@synopackageland/templates": "0.10.0",
28
28
  "ajv": "^8.17.1",
29
29
  "commander": "^13.1.0",
30
30
  "tar": "^7.4.3",
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/synopackageland/syno-pkg-land-sdk/spec/bridge/v1/manifest.schema.json",
4
+ "title": "bridge.json: the App manifest the package build writes and the runtime reads",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema", "runtime", "entry", "mode", "methods"],
8
+ "properties": {
9
+ "schema": { "const": 1 },
10
+ "runtime": { "enum": ["python", "node", "rust"], "$comment": "The App's language. rust: a thin App on the syno-pkg-land-app crate, built for the package's architecture." },
11
+ "entry": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "maxLength": 255,
15
+ "pattern": "^(?!/)(?!.*(^|/)\\.\\.?(/|$))[A-Za-z0-9._/-]+$",
16
+ "$comment": "Relative to the App directory, inside it. python: the script, run as <python3> <entry>. node: the module, run as <node> <entry>. rust: the executable, run directly."
17
+ },
18
+ "mode": { "const": "exec", "$comment": "One process per call. \"socket\" is reserved for a later version and refused by v1 runtimes." },
19
+ "methods": {
20
+ "type": "array",
21
+ "minItems": 1,
22
+ "maxItems": 1024,
23
+ "uniqueItems": true,
24
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$" },
25
+ "$comment": "The App's methods. The App's hello MUST list exactly these. They MUST NOT be SDK methods (api/sdk-permissions.json)."
26
+ },
27
+ "access": { "enum": ["admin-only", "app-privilege"], "$comment": "Entry restriction (dsm.NewEntryGate). Absent: permissions.json alone decides." },
28
+ "timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 3600, "$comment": "Request deadline. Absent: api.DefaultRequestTimeout (60 s). The browser client must wait longer." },
29
+ "max_request_bytes": { "type": "integer", "minimum": 1, "maximum": 16777216, "$comment": "JSON request body limit. Absent: api.MaxRequestBytes." },
30
+ "env": {
31
+ "type": "object",
32
+ "maxProperties": 64,
33
+ "propertyNames": { "pattern": "^(?!HTTP_|SYNOPKGLAND_|LD_|DYLD_|PATH$)[A-Z_][A-Z0-9_]{0,63}$" },
34
+ "additionalProperties": { "type": "string", "maxLength": 4096, "pattern": "^[^\\u0000]*$" },
35
+ "$comment": "Extra environment for the App, on top of what the runtime sets (README, App manifest). Nothing is inherited from the CGI."
36
+ },
37
+ "node": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["package"],
41
+ "properties": {
42
+ "package": { "type": "string", "pattern": "^Node\\.js_v[0-9]{2}$", "$comment": "Package Center package the SPK depends on; the runtime runs /var/packages/<package>/target/usr/local/bin/node." }
43
+ },
44
+ "$comment": "Required when runtime is node, absent otherwise."
45
+ },
46
+ "transfers": {
47
+ "type": "object",
48
+ "maxProperties": 1024,
49
+ "propertyNames": { "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$" },
50
+ "additionalProperties": { "enum": ["upload", "download"] },
51
+ "$comment": "Added in v1 (additive). Methods of `methods` the browser calls as an HTTP transfer (?method=): upload streams the request body into the spool, download streams the file the App wrote there (README, Transfers)."
52
+ },
53
+ "max_upload_bytes": { "type": "integer", "minimum": 1, "maximum": 17179869184, "$comment": "Added in v1 (additive). Upload limit. Absent: 16 GiB (filestation.MaxUploadBytes)." },
54
+ "capabilities": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "files": { "const": "session", "$comment": "Serves the nas.* File Station routes with the caller's DSM session, and the svc files.*." },
59
+ "identity": { "const": "standalone", "$comment": "The SDK's App sign-in (identity.*) authenticates instead of DSM." },
60
+ "portal": { "type": "boolean", "$comment": "true serves portal.status, to the browser and as the svc portal.status." },
61
+ "jobs": { "type": "boolean", "$comment": "Added in v1 (additive). true offers the svc bg.submit, bg.get and bg.cancel on the package's job store." },
62
+ "root": { "type": "boolean", "$comment": "Added in v1 (additive). true offers the svc root.run, root.start, root.jobs.list, root.jobs.get and root.jobs.stop: the runtime forwards them to the package's executor with the request's DSM session, and the executor's policy and the App privilege decide." },
63
+ "delegate": {
64
+ "type": "object",
65
+ "additionalProperties": false,
66
+ "required": ["scope", "destinations", "executor_digest", "policy_digest"],
67
+ "properties": {
68
+ "scope": { "type": "string", "pattern": "^(home|install-selected|shared:[^/\\\\:\\u0000-\\u001f]{1,255})$" },
69
+ "destinations": { "type": "array", "minItems": 1, "maxItems": 64, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,63}$" } },
70
+ "executor_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
71
+ "policy_digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
72
+ },
73
+ "$comment": "Added in v1 (additive). The package's executor integration (lifetime until-revoked, operation files.moveFromStaging): serves the delegate.* routes and the svc delegate.*."
74
+ }
75
+ },
76
+ "$comment": "SDK routes the runtime serves in Go, never through the App. Later versions add capabilities as new members."
77
+ }
78
+ },
79
+ "allOf": [
80
+ {
81
+ "if": { "properties": { "runtime": { "const": "node" } } },
82
+ "then": { "required": ["node"] },
83
+ "else": { "not": { "required": ["node"] } }
84
+ }
85
+ ]
86
+ }
@@ -1,9 +1,10 @@
1
1
  # 建立(Create)
2
2
 
3
- 先選擇 `native-go`(Go CGI + DSM 視窗)、`binary`(預建服務程式)或 `compose`(上游容器)。`compose` 預設示範 Share 與固定 run-as 使用者,`--from <compose.yaml>` 匯入現有 Compose。
3
+ 先選擇 `native-go`(Go CGI + DSM 視窗)、`native-python`/`native-node`/`native-rust`(同一個 App,backend 換成 Python、Node.js(TypeScript)或 Rust)、`binary`(預建服務程式)或 `compose`(上游容器)。`compose` 預設示範 Share 與固定 run-as 使用者,`--from <compose.yaml>` 匯入現有 Compose。
4
4
 
5
5
  ```sh
6
6
  synopkgland init -t native-go --with files,jobs,login --package MyApp my-app
7
+ synopkgland init -t native-python --with jobs,delegate my-py-app
7
8
  synopkgland init -t binary service
8
9
  synopkgland init -t compose --from ./compose.yaml upstream-app
9
10
  ```
@@ -22,7 +23,7 @@ synopkgland init -t compose --from ./compose.yaml upstream-app
22
23
 
23
24
  執行 check、build、inspect,再看 deploy --dry-run。Go CGI 每包驗證 DSM session,root-ops 能力只提供固定管理員核准工作。容器必須使用已建置映像;不支援的特權 host 存取、secret/config 與 build stanza 會拒絕。
24
25
 
25
- 安裝前讀 deploy.md。需要資料或登入時讀 files.md、identity.md;不加入尚未實作的 bridge。
26
+ 安裝前讀 deploy.md。需要資料或登入時讀 files.md、identity.md。
26
27
 
27
28
  ## Schema 2 能力
28
29
 
@@ -33,11 +34,84 @@ check、inspect、build、deploy、status 支援 `--json`,stdout 只有單一
33
34
 
34
35
  rootOps 與 delegate 已共用管理員 PasswordConfirm 核准的 protected executor、boot/control tasks;精確 task readback 與 digest health 一致才 ready;policy(operations、scope、root 腳本)變更須重核准,policy 不變的 executor 更新會自動 repin,install-selected 的 scope 需管理員確認一次。Vue App 只在 SDK 的 ExecutorSettings 核准;plain UI 由 DSM 視窗面板核准,兩者都在面板內輸入密碼並走 PasswordConfirm。native 產品另可明確 opt-in authorization-sdk adapter,不與 generated executor tasks 混用。SDK off stub 與 routes_test.go 的預設 policy scaffold 只有內容仍等於 bundled base 時才會替換;App 自訂版本拒絕。
35
36
 
37
+ ## Python、Node、Rust App
38
+
39
+ backend 寫 `runtime: python|node|rust`(`runtime: go` 等同 `kind: go`)。CGI 是 SDK 的 Go runtime(`bridge/cmd/runtime`),它驗證 DSM session、entry 與 `permissions.json` 後,以套件帳號(不是 root)每個請求啟動一次 App。App 放在 ui 以外的目錄,打包成 `target/app/`,build 寫入 `bridge.json`(spec/bridge/v1)並把它與 `permissions.json` 的 sha256 編進 runtime。
40
+
41
+ ```yaml
42
+ backend: {runtime: python, dir: app, entry: main.py, sdk: X.Y.Z}
43
+ backend: {runtime: node, dir: app, entry: main.mjs, sdk: X.Y.Z, node: {package: Node.js_v20}}
44
+ backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet]}
45
+ ```
46
+
47
+ `init -t native-python|native-node|native-rust` 沿用 native-go 的 UI(`ui/`、`i18n/`,Vue 時是 `frontend/`)、jobs/delegate 的前端與 root-ops 的 `root/whoami.sh`;能力 overlay 換掉 backend 的 stub 模組。`--with jobs` 的範例讀取呼叫者選的檔案,所以同時宣告 `files: session`;delegate 讓登入使用者開啟 App 並存報告(`app.status`、`report.save` 為 `authenticated`);Rust 的 `backend.methods` 會加上能力的 methods。init 把測試要 import 的 App SDK 放進 `backend/vendor/`(`backend.sdk` 的 release,或 SYNOPKGLAND_SDK_DIR);build 另外打包它。`--shared-folder` 只適用於 native-go;`add` 仍只支援 Go backend。
48
+
49
+ - Python:DSM 的 `/usr/bin/python3`;SDK 放進 `app/vendor/synopkgland_app/`,`PYTHONPATH=vendor`。
50
+ - Node:Package Center 的 Node.js_v18/v20/v22 成為 INFO `install_dep_packages`;SDK 的 `package.json` 與 `dist/` 放進 `node_modules/@synopackageland/app`,其他 dependencies 先在 App 目錄 `npm ci --omit=dev`。
51
+ - Rust:Cargo.toml 以 git+tag 相依 `syno-pkg-land-app`;`cargo zigbuild` 交叉編譯(x86_64/aarch64 musl、armv7 glibc),`panic = "unwind"`;binary 在 `app/bin/<entry>`。需要 rustup 的 cargo、三個 target、cargo-zigbuild 與 zig。
52
+ - methods:沒寫時 build 在建置機執行 `<entry> --describe`;Rust 必須寫。`permissions.json` 只能設定 methods 內的 method。
53
+ - capabilities:`files`、`identity`、`jobs`、`rootOps`、`delegate`(僅 `until-revoked`+`files.moveFromStaging`+destinations)與 `portal`;daemon 需要 Go。jobs 或 delegate 時,同一個 runtime 另裝為 `target/background/worker`,DSM 以套件帳號執行 `worker --worker`;rootOps、delegate 附 executor,`bridge.json` 的 delegate digest 必須等於 SPK 的 executor。
54
+ - transfers:`backend.transfers: {<method>: upload|download}`(method 必須在 methods 內),`backend.max_upload_bytes` 上限 16 GiB。
55
+
56
+ ## 請求期限與視窗尺寸
57
+
58
+ 所有 App 的 backend 請求期限預設 60 秒;`backend.timeout_seconds`(1–3600 的整數)改成別的值,go、python、node、rust 都適用。到期時 App 回 `request_timeout`;DSM 視窗把同一值放進 `ctx.requestTimeout`(毫秒),`ctx.call` 與 SDK 的瀏覽器 client 多等 2 秒,讓 backend 的回覆先到。超過期限的工作改用 jobs 或 root job,不要拉長同步請求。
59
+
60
+ - python、node、rust:build 把它寫進 `bridge.json` 的 `timeout_seconds`;runtime 以它為期限,傳給 App 的 `deadline_ms` 是送出呼叫時剩下的時間。
61
+ - go:build 以 `-X main.requestTimeout=<秒>` 傳入。要寫 `timeout_seconds`,`main.go` 必須讀這個變數,否則 check 回 `STANDALONE_TIMEOUT_UNSUPPORTED`;main 沒有讀它時,CGI 維持 SDK 的 20 秒:
62
+
63
+ ```go
64
+ // Set by synopkgland build through -ldflags -X: backend.timeout_seconds, 60 unless set.
65
+ var requestTimeout = ""
66
+
67
+ func deadline() api.DeadlineOption {
68
+ seconds, err := strconv.Atoi(requestTimeout)
69
+ if err != nil || seconds <= 0 {
70
+ seconds = 60
71
+ }
72
+ return api.Timeout(time.Duration(seconds) * time.Second)
73
+ }
74
+
75
+ // main: cgi.Serve(api.WithRequestDeadline(handler, deadline()))
76
+ ```
77
+
78
+ - binary:預先建置的 CGI 自己決定期限,不接受 `timeout_seconds`;視窗仍等 62 秒。
79
+
80
+ DSM 視窗預設 960×640、最小 700×420;`ui.window` 只寫要改的欄位:
81
+
82
+ ```yaml
83
+ backend: {runtime: rust, dir: app, entry: hello, methods: [hello.greet], timeout_seconds: 120}
84
+ ui:
85
+ dir: ui/dist
86
+ window: {width: 1100, height: 720}
87
+ ```
88
+
89
+ width、minWidth 是 320–3840,height、minHeight 是 240–2160,最小值不可大於視窗;不合法時 check 與 build 回 `STANDALONE_UI_WINDOW_INVALID`。從 Portal 以 standalone 開啟時,最小寬度仍是 320。DSM 會以使用者上次留下的尺寸重開視窗,並縮進目前畫面。
90
+
36
91
  ## 兩種層級
37
92
 
38
93
  - 無升權 App(沒有 rootOps、delegate;含所有 Compose App):安裝、執行、解除安裝都只用套件帳號,任何階段都不用 root。不能宣告 portal(native `STANDALONE_PORTAL_REQUIRES_EXECUTOR`、Compose `CONTRACT_PORTAL_REQUIRES_EXECUTOR`);需要登入入口別名時由管理員在 DSM 手動設定反向代理。解除安裝後 DSM 保留的「控制台」>「應用程式權限」規則由管理員手動移除,精靈與 cleanup 清單會說明。
39
94
  - executor App(rootOps 或 delegate):解除安裝時 root purge 自動刪除 `SYNO.SDS.<package>.Instance` 的應用程式權限規則,宣告 portal 時在 DSM 移除別名後 reload nginx。精靈說明自動清除;cleanup 清單只在自動清除失敗時(系統日誌 `synopkgland-executor`)才需要管理員處理。
40
95
 
96
+ ## root 程式
97
+
98
+ `capabilities.rootOps: true` 時,`root/<名稱>.sh` 是 script;`root/<arch>/<名稱>` 是該架構的 ELF binary(每個 arches 都要有,各架構 SPK 只帶自己的,policy digest 因此依架構不同)。沒有設定的 script 維持 `root.run` 預設;`root` 設定其餘:
99
+
100
+ ```yaml
101
+ root:
102
+ state: true # root_state:/volumeN/@synopkgland/<package>,只有 root 可讀,升級保留
103
+ stop: prepare # 套件停止與升級時由 executor 執行(stop 或 prepare-upgrade)
104
+ programs:
105
+ scan:
106
+ kind: binary
107
+ schema: {type: object, additionalProperties: false, properties: {mode: {type: string, enum: [fast, full]}}}
108
+ run: {timeout_s: 60} # root.run:1–120 秒,max_output ≤ 64 MiB
109
+ job: {timeout_s: 3600, max_concurrent: 1} # root.start 與 root.jobs.*
110
+ prepare: {} # 沒有 run、job:只作為 stop 步驟
111
+ ```
112
+
113
+ schema 是 executor 的封閉子集(每個 object 都要 `additionalProperties: false`,key `[a-z0-9_]`,pattern 為 RE2)。這些設定需要支援它們的 SDK executor(`EXECUTOR_ROOT_PROGRAMS_UNSUPPORTED`)。preupgrade 呼叫 `executor/lifecycle stop --upgrade`;舊 SDK 的 helper 不認得此旗標時改用一般 stop。
114
+
41
115
  ## Permanent destination integration
42
116
 
43
117
  `init --with delegate` / `add delegate` default to `operations: [files.moveFromStaging]`,
@@ -76,7 +76,8 @@ brings the window to the front; below 600px they are bottom sheets. Reserve
76
76
  root sizing for the Vue/host wrapper; only use props exposed by the current JSON
77
77
  schema inside the spec. Reserve
78
78
  `modalScope: "document"` and `wideLayout: "web"` for whole-page web/Portal apps.
79
- Let the DSM host set AppWindow `minWidth` to 700px; do not force it through CSS.
79
+ Let the DSM host set AppWindow `minWidth` (700px, or `ui.window.minWidth` in
80
+ dsm-package.yaml); do not force it through CSS.
80
81
  Import only `@synopackageland/ui/themes/dsm7-embedded.css` (or its embedded
81
82
  reset/theme pair), and assign a unique `app.config.idPrefix` before each mount.
82
83
  Follow the existing template's mount, unmount and host subscription lifecycle.
@@ -45,9 +45,15 @@ do not guess APIs or copy a version-specific component inventory.
45
45
  The standalone `theme.css` and `dsm7.css` entrypoints affect the host page.
46
46
  - Set a unique `app.config.idPrefix` for every independent Vue app before
47
47
  `app.mount`. Preserve the existing template's per-mount prefix generation.
48
- - Keep `AppLayout wide-layout="window"` in DSM. JSON uses `AppShell` with
49
- `wideLayout: "window"`. Let the host set the 700px minimum window width;
50
- do not enforce a page-wide minimum in CSS.
48
+ - Keep `AppLayout wide-layout="window"` in a DSM desktop window. JSON uses
49
+ `AppShell` with `wideLayout: "window"`. Let the host set the minimum window
50
+ width (700px, or `ui.window.minWidth` in dsm-package.yaml); do not enforce a
51
+ page-wide minimum in CSS.
52
+ - `ctx.fullPage` is true when DSM launches the App standalone (Portal): the
53
+ window then owns the page and drops its minimum width to 320px. Use
54
+ `wide-layout="web"`, call the ui's `ensureDeviceViewport()` (DSM's page has
55
+ no device-width viewport, so a phone never reaches mobile without it) and
56
+ pass `ctx.appIcon` and `ctx.displayName` as `brand-icon` / `brand-name`.
51
57
  - Reserve `modalScope: "document"` / `modal-scope="document"` and
52
58
  `wide-layout="web"` for whole-page web/Portal applications.
53
59
 
@@ -24,7 +24,7 @@ synopkgland check # 契約、Compose 意圖與 Web entry 提示
24
24
 
25
25
  容器路徑跟映像/upstream 的 container target 走,原樣保留;只把 host source 換成 `${NAME}` 這類 install parameter。原有的 upstream named volume mount 與頂層 `volumes:` 宣告也保留,不要為了套模板改成 `/data`。需要既有 Share 時,管理員在安裝時選這台 NAS 的 Share。
26
26
 
27
- 修復方式:看 `check` 輸出的 `[錯誤碼]` 與「建議」行,對照 `diagnose.md`。**不要**為了過驗證而亂改 runtime 語意。
27
+ 修復方式:看 `check` 輸出的 `[錯誤碼]` 與 `Suggestion:` 行,對照 `diagnose.md`。**不要**為了過驗證而亂改 runtime 語意。
28
28
 
29
29
  ## 下一步
30
30
 
@@ -1,35 +0,0 @@
1
- import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
- import type { CommandRunner } from "./build.js";
3
- import type { StandaloneManifest } from "./manifest.js";
4
- import { type Toolkit } from "./toolkit.js";
5
- /** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust CGI may name. */
6
- export declare const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
7
- interface RustTarget {
8
- triple: string;
9
- /** The linker, given the extracted toolkit folder when the target pins one. */
10
- linker: (toolkit: string) => string;
11
- linkArgs: string[];
12
- toolkit?: Toolkit;
13
- interpreter?: string;
14
- }
15
- /**
16
- * x86_64 and aarch64 link static musl binaries with rust-lld. armv7 links
17
- * against the glibc of the DSM 7.0 toolkit, because musl's 64-bit time calls
18
- * fail on DSM's ARMv7 kernels; that gcc runs on Linux x86_64 only.
19
- */
20
- export declare const RUST_TARGETS: Record<NativeArchFamily, RustTarget>;
21
- /** Go's `-X` values for a Rust App, read with `option_env!`; every one is set, empty when unused. */
22
- export declare function rustBuildEnv(manifest: StandaloneManifest): Record<string, string>;
23
- export interface RustBuildDeps {
24
- host?: {
25
- platform: string;
26
- arch: string;
27
- };
28
- prepareToolkit?: (cwd: string, arch: string, inputs: Toolkit[], run: CommandRunner) => Promise<string>;
29
- }
30
- /** Builds the CGI bin, and the worker bin with capabilities.jobs, in one `cargo build` into `work`. */
31
- export declare function buildRustBackend(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner, { host, prepareToolkit: prepare }?: RustBuildDeps): Promise<{
32
- cgi: string;
33
- worker?: string;
34
- }>;
35
- export {};
@@ -1,127 +0,0 @@
1
- import { copyFileSync, rmSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { portalByExecutor } from "../build-spk/dsm-version.js";
5
- import { SDK_GIT_URL } from "../runtime/sdk-source.js";
6
- import { checkCgiBinary } from "./binary.js";
7
- import { StandaloneBuildError } from "./spk.js";
8
- import { prepareToolkit } from "./toolkit.js";
9
- /** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust CGI may name. */
10
- export const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
11
- const ARMV7_GCC = "usr/local/arm-unknown-linux-gnueabi/bin/arm-unknown-linux-gnueabi-gcc";
12
- /**
13
- * x86_64 and aarch64 link static musl binaries with rust-lld. armv7 links
14
- * against the glibc of the DSM 7.0 toolkit, because musl's 64-bit time calls
15
- * fail on DSM's ARMv7 kernels; that gcc runs on Linux x86_64 only.
16
- */
17
- export const RUST_TARGETS = {
18
- x86_64: { triple: "x86_64-unknown-linux-musl", linker: () => "rust-lld", linkArgs: ["--build-id=none"] },
19
- aarch64: { triple: "aarch64-unknown-linux-musl", linker: () => "rust-lld", linkArgs: ["--build-id=none"] },
20
- armv7: {
21
- triple: "armv7-unknown-linux-gnueabihf",
22
- linker: (toolkit) => join(toolkit, ARMV7_GCC),
23
- linkArgs: ["-Wl,--build-id=none", "-Wl,--discard-all"],
24
- toolkit: {
25
- url: "https://global.synologydownload.com/download/ToolChain/toolkit/7.0/alpine4k/ds.alpine4k-7.0.env.txz",
26
- sha256: "767cb27541e3c2aae5a07bc1078809956992c75a54e6bfca2275fa633e983407",
27
- },
28
- interpreter: ARMV7_INTERPRETER,
29
- },
30
- };
31
- /** Go's `-X` values for a Rust App, read with `option_env!`; every one is set, empty when unused. */
32
- export function rustBuildEnv(manifest) {
33
- const { capabilities, executor } = manifest;
34
- const delegate = capabilities.delegate;
35
- return {
36
- SYNOPKGLAND_PACKAGE: manifest.package,
37
- SYNOPKGLAND_VERSION: manifest.spkVersion,
38
- SYNOPKGLAND_PERMISSIONS_DIGEST: manifest.apiPermissionsDigest ?? "",
39
- SYNOPKGLAND_SDK_PERMISSIONS_DIGEST: manifest.apiSDKPermissionsDigest ?? "",
40
- SYNOPKGLAND_EXECUTOR_DIGEST: executor?.executorDigest ?? "",
41
- SYNOPKGLAND_POLICY_DIGEST: executor?.policyDigest ?? "",
42
- SYNOPKGLAND_DELEGATE_SCOPE: delegate?.scope ?? "",
43
- SYNOPKGLAND_DELEGATE_OPERATIONS: delegate ? JSON.stringify([...delegate.operations].sort()) : "",
44
- SYNOPKGLAND_DELEGATE_DESTINATIONS: delegate ? JSON.stringify(delegate.destinations ? [...delegate.destinations].sort() : []) : "",
45
- SYNOPKGLAND_DELEGATE_LIFETIME: delegate?.lifetime ?? "",
46
- SYNOPKGLAND_IDENTITY: capabilities.identity ?? "",
47
- SYNOPKGLAND_FILE_STATION: capabilities.files || delegate ? "session" : "",
48
- SYNOPKGLAND_ENTRY: manifest.entry ?? "",
49
- SYNOPKGLAND_PORTAL: manifest.portal ? (portalByExecutor(manifest.os_min_ver) ? "executor" : "on") : "",
50
- };
51
- }
52
- /** Builds the CGI bin, and the worker bin with capabilities.jobs, in one `cargo build` into `work`. */
53
- export async function buildRustBackend(manifest, arch, work, run, { host = { platform: process.platform, arch: process.arch }, prepareToolkit: prepare = prepareToolkit } = {}) {
54
- const backend = manifest.backend;
55
- if (backend.kind !== "rust")
56
- throw new Error("buildRustBackend needs backend.kind: rust");
57
- const target = RUST_TARGETS[arch];
58
- if (target.toolkit && (host.platform !== "linux" || host.arch !== "x64")) {
59
- fail(`${arch} links with the gcc of the DSM toolkit, which runs on Linux x86_64 only; build ${arch} on Linux x86_64 (for example in a container).`);
60
- }
61
- const bins = [backend.bin, ...(manifest.capabilities.jobs ? [backend.worker] : [])];
62
- const toolkit = target.toolkit ? await prepare(backend.dir, arch, [target.toolkit], run).catch((error) => fail(`DSM toolkit for ${arch}: ${error.message}`)) : "";
63
- try {
64
- const result = await run("cargo", [
65
- "build", "--release", "--locked", "--target", target.triple, "-p", backend.package,
66
- ...bins.flatMap(bin => ["--bin", bin]), "--message-format=json-render-diagnostics",
67
- ], { cwd: backend.dir, env: cargoEnv(manifest, backend.dir, target, toolkit), capture: true });
68
- if (result.missing)
69
- fail("找不到 cargo。請以 rustup 安裝 Rust(https://rustup.rs);backend/rust-toolchain.toml 會選定版本與 target。");
70
- if (result.status !== 0)
71
- fail(`cargo build(${arch})失敗。\n${result.stderr}`);
72
- const [cgi, worker] = bins.map((bin, i) => {
73
- const out = join(work, i === 0 ? `api-${arch}.cgi` : `background-worker-${arch}`);
74
- copyFileSync(artifact(result.stdout ?? "", bin), out);
75
- const problem = checkCgiBinary(work, out, arch, target.interpreter);
76
- if (problem)
77
- throw new StandaloneBuildError(problem.code, problem.message);
78
- return out;
79
- });
80
- return { cgi: cgi, ...(worker ? { worker } : {}) };
81
- }
82
- finally {
83
- if (toolkit)
84
- rmSync(toolkit, { recursive: true, force: true });
85
- }
86
- }
87
- /**
88
- * The environment of one reproducible cargo build: the CLI's flags replace any
89
- * inherited RUSTFLAGS, paths are remapped, symbols and build IDs dropped. Cargo
90
- * fetches the private SDK repository through git, with the user's credentials.
91
- */
92
- function cargoEnv(manifest, dir, target, toolkit) {
93
- const env = { ...process.env, ...rustBuildEnv(manifest) };
94
- delete env.RUSTFLAGS;
95
- const cargoHome = env.CARGO_HOME || join(homedir(), ".cargo");
96
- env.CARGO_ENCODED_RUSTFLAGS = [
97
- `--remap-path-prefix=${dir}=/build`,
98
- `--remap-path-prefix=${cargoHome}=/cargo`,
99
- "-Cstrip=symbols",
100
- ...target.linkArgs.map(arg => `-Clink-arg=${arg}`),
101
- ].join("\x1f");
102
- env[`CARGO_TARGET_${target.triple.toUpperCase().replaceAll("-", "_")}_LINKER`] = target.linker(toolkit);
103
- env.SOURCE_DATE_EPOCH = "0";
104
- env.CARGO_NET_GIT_FETCH_WITH_CLI = "true";
105
- if (env.SYNOPKGLAND_SDK_DIR) {
106
- // Development only: git reads the SDK repository from this checkout, which must hold the locked tag.
107
- const n = Number(env.GIT_CONFIG_COUNT ?? 0);
108
- env.GIT_CONFIG_COUNT = String(n + 1);
109
- env[`GIT_CONFIG_KEY_${n}`] = `url.file://${env.SYNOPKGLAND_SDK_DIR}.insteadOf`;
110
- env[`GIT_CONFIG_VALUE_${n}`] = SDK_GIT_URL;
111
- }
112
- return env;
113
- }
114
- /** The executable Cargo reported for `bin`. */
115
- function artifact(messages, bin) {
116
- for (const line of messages.split("\n")) {
117
- if (!line.startsWith("{"))
118
- continue;
119
- const message = JSON.parse(line);
120
- if (message.reason === "compiler-artifact" && message.target?.kind?.includes("bin") && message.target.name === bin && message.executable)
121
- return message.executable;
122
- }
123
- return fail(`cargo build 沒有產出 bin ${bin};確認 backend/src/bin/${bin}.rs 存在。`);
124
- }
125
- function fail(message) {
126
- throw new StandaloneBuildError("STANDALONE_RUST_BUILD_FAILED", message);
127
- }