@synopackageland/cli 0.9.5 → 0.9.7

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 (58) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/assets/licenses/MUSL-COPYRIGHT +193 -0
  3. package/assets/licenses/RUST-COPYRIGHT-library.html +5556 -0
  4. package/dist/add.js +27 -27
  5. package/dist/build-spk/notices.d.ts +8 -1
  6. package/dist/build-spk/notices.js +13 -4
  7. package/dist/build-spk/uninstall-wizard.d.ts +9 -0
  8. package/dist/build-spk/uninstall-wizard.js +10 -3
  9. package/dist/build-spk/wizard-i18n.d.ts +1 -1
  10. package/dist/build-spk/wizard-i18n.js +1 -1
  11. package/dist/build-spk/wizard-messages.d.ts +33 -0
  12. package/dist/build-spk/wizard-messages.js +33 -0
  13. package/dist/capability-overlays.d.ts +10 -5
  14. package/dist/capability-overlays.js +32 -10
  15. package/dist/cli.js +5 -5
  16. package/dist/deploy/plan.d.ts +1 -1
  17. package/dist/deploy/plan.js +1 -1
  18. package/dist/error-registry.js +44 -3
  19. package/dist/info.js +2 -2
  20. package/dist/init.js +53 -28
  21. package/dist/runtime/executor-build.d.ts +1 -1
  22. package/dist/runtime/executor-build.js +2 -1
  23. package/dist/runtime/executor-digests.json +22 -0
  24. package/dist/runtime/executor-policy.d.ts +7 -0
  25. package/dist/runtime/executor-policy.js +5 -3
  26. package/dist/runtime/sdk-source.d.ts +14 -2
  27. package/dist/runtime/sdk-source.js +39 -4
  28. package/dist/standalone/api-permissions.d.ts +4 -2
  29. package/dist/standalone/api-permissions.js +6 -5
  30. package/dist/standalone/binary.d.ts +3 -2
  31. package/dist/standalone/binary.js +17 -5
  32. package/dist/standalone/build.js +13 -9
  33. package/dist/standalone/daemon.d.ts +6 -0
  34. package/dist/standalone/daemon.js +8 -4
  35. package/dist/standalone/inspect-spk.d.ts +1 -1
  36. package/dist/standalone/inspect-spk.js +39 -10
  37. package/dist/standalone/lifecycle.js +38 -4
  38. package/dist/standalone/manifest.d.ts +12 -0
  39. package/dist/standalone/manifest.js +55 -17
  40. package/dist/standalone/native-layout.d.ts +1 -7
  41. package/dist/standalone/native-layout.js +4 -97
  42. package/dist/standalone/root-ops.d.ts +2 -0
  43. package/dist/standalone/root-ops.js +2 -0
  44. package/dist/standalone/rust-build.d.ts +35 -0
  45. package/dist/standalone/rust-build.js +127 -0
  46. package/dist/standalone/sdk-check.d.ts +5 -3
  47. package/dist/standalone/sdk-check.js +21 -8
  48. package/dist/standalone/spk.js +14 -3
  49. package/dist/standalone/toolkit.d.ts +10 -0
  50. package/dist/standalone/toolkit.js +101 -0
  51. package/dist/standalone/uninstall-hook.d.ts +10 -0
  52. package/dist/standalone/uninstall-hook.js +71 -0
  53. package/dist/standalone/uninstall.d.ts +53 -0
  54. package/dist/standalone/uninstall.js +136 -0
  55. package/dist/types.d.ts +1 -1
  56. package/dist/yaml-edit.d.ts +6 -0
  57. package/dist/yaml-edit.js +31 -0
  58. package/package.json +2 -2
@@ -10,6 +10,9 @@ import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DS
10
10
  import { ELF_MACHINE } from "./binary.js";
11
11
  import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
12
12
  import { daemonConfig, readDaemon } from "./daemon.js";
13
+ import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
14
+ import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
15
+ import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
13
16
  import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit } from "./background.js";
14
17
  import { renderStandaloneResource } from "./resource.js";
15
18
  import { recoverSharedFolder, renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER } from "./shared-folder.js";
@@ -23,7 +26,6 @@ import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderP
23
26
  import { isPortalAlias } from "../portal/alias.js";
24
27
  import { defaultPackageIcons, MAX_ICON_BYTES } from "../build-spk/dsm-icons.js";
25
28
  import { decodePng } from "../build-spk/png.js";
26
- const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
27
29
  /** Reads an SPK without installing it and reports how it would run on DSM. */
28
30
  export async function inspectStandaloneSpk(spkPath) {
29
31
  return inspectSpk(spkPath, "native-go");
@@ -55,7 +57,7 @@ export async function inspectSpk(spkPath, expectedKind) {
55
57
  if (metadataText !== undefined) {
56
58
  try {
57
59
  const raw = JSON.parse(metadataText);
58
- if (raw.schema !== 2 || !["go", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
60
+ if (raw.schema !== 2 || !["go", "rust", "binary"].includes(raw.backend) || !Array.isArray(raw.capabilities) || raw.capabilities.some((name) => !["files", "jobs", "identity", "rootOps", "delegate"].includes(String(name))) || new Set(raw.capabilities).size !== raw.capabilities.length || (raw.entry !== undefined && !["admin-only", "app-privilege"].includes(raw.entry)))
59
61
  throw new Error();
60
62
  metadata = raw;
61
63
  }
@@ -63,12 +65,18 @@ export async function inspectSpk(spkPath, expectedKind) {
63
65
  findings.push("sdk-package.json 不符合 schema 2 capability inventory。");
64
66
  }
65
67
  }
68
+ const uninstallHook = metadata.uninstall === undefined ? undefined : readUninstallRecord(metadata.uninstall, metadata.capabilities?.includes("rootOps") === true);
69
+ if (metadata.uninstall !== undefined && !uninstallHook)
70
+ findings.push("UNINSTALL_RECORD_INVALID sdk-package.json 的 uninstall 不是 synopkgland build 寫出的 uninstall 設定。");
66
71
  const binaryBackend = metadata.backend === "binary";
67
- const kind = expectedKind ?? (paths.has("compose.yaml") ? "container" : binaryBackend ? "binary" : "native-go");
68
- const standalone = kind === "native-go" || kind === "binary";
72
+ // A strict standalone inspection names the language the CGI was built from.
73
+ const compiled = metadata.backend === "rust" ? "native-rust" : "native-go";
74
+ const kind = (expectedKind === "native-go" ? compiled : expectedKind) ?? (paths.has("compose.yaml") ? "container" : binaryBackend ? "binary" : compiled);
75
+ const compiledKind = kind === "native-go" || kind === "native-rust";
76
+ const standalone = compiledKind || kind === "binary";
69
77
  const info = readInfo(join(dir, "INFO"), findings, kind);
70
78
  await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
71
- const background = kind === "native-go" && paths.has(BACKGROUND_WORKER_PAYLOAD);
79
+ const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
72
80
  const sharedFolder = standalone ? recoverSharedFolder(readOptional(join(dir, "scripts/postinst"))) : undefined;
73
81
  // A Compose App has no executor, so its SPK never carries a portal.
74
82
  const portal = standalone ? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings) : undefined;
@@ -99,18 +107,20 @@ export async function inspectSpk(spkPath, expectedKind) {
99
107
  if (executor?.policy.portal && info.os_min_ver && OS_MIN_VER.test(info.os_min_ver) && !isDsmAtLeast(info.os_min_ver, PORTAL_EXECUTOR_MIN_DSM)) {
100
108
  findings.push(`INFO os_min_ver「${info.os_min_ver}」低於 executor 註冊 portal 需要的 DSM ${PORTAL_EXECUTOR_MIN_DSM}`);
101
109
  }
110
+ if (executor && executor.policy.purge !== uninstallHook?.uninstall.purge.root)
111
+ findings.push(`UNINSTALL_ROOT_POLICY_MISMATCH executor policy 的 purge「${executor.policy.purge ?? ""}」與 uninstall.purge.root「${uninstallHook?.uninstall.purge.root ?? ""}」不同。`);
102
112
  if (payload.find(p => p.path === "executor/executor")?.mode !== "0755")
103
113
  findings.push("Executor must be executable.");
104
114
  if (payload.find(p => p.path === "executor/lifecycle")?.mode !== "0755")
105
115
  findings.push("Executor lifecycle helper must be executable.");
106
116
  if (![...paths].some(path => path.startsWith("daemon/"))) {
107
- const lifecycleManifest = { package: info.package, capabilities: { rootOps: true, ...(background ? { jobs: { worker: "sha256-v1" } } : {}) } };
117
+ const lifecycleManifest = { package: info.package, capabilities: { rootOps: true, ...(background ? { jobs: { worker: "sha256-v1" } } : {}) }, ...(uninstallHook ? { uninstall: uninstallHook.uninstall } : {}) };
108
118
  for (const hook of ["preuninst", "preupgrade", "start-stop-status"]) {
109
119
  if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, lifecycleManifest))
110
120
  findings.push(`Executor scripts/${hook} differs from fixed lifecycle.`);
111
121
  }
112
122
  }
113
- for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true, portal: Boolean(portal), portalEntry: Boolean(executor?.policy.portal) }))) {
123
+ for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", { executor: true, portal: Boolean(portal), portalEntry: Boolean(executor?.policy.portal), ...uninstallWizardOptions(uninstallHook) }))) {
114
124
  if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
115
125
  findings.push(`Executor WIZARD_UIFILES/${name} is missing cleanup guidance.`);
116
126
  }
@@ -471,10 +481,10 @@ function checkScripts(dir, findings) {
471
481
  const DATA_KEYS = ["pkgwizard_remove_additional", "delete_app_data"];
472
482
  const DELETE_COMMAND = /\brm\s+-[a-zA-Z]*[rR]|\bfind\b[^\n]*(-delete|-exec\s+rm)/;
473
483
  const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]*!=\s*"?UPGRADE"?/;
474
- function uninstallBoxes(file) {
484
+ function uninstallBoxes(file, keys) {
475
485
  try {
476
486
  const steps = JSON.parse(readFileSync(file, "utf8"));
477
- return steps.flatMap(step => step.items ?? []).flatMap(item => item.subitems ?? []).filter(box => DATA_KEYS.includes(box.key));
487
+ return steps.flatMap(step => step.items ?? []).flatMap(item => item.subitems ?? []).filter(box => keys.includes(box.key));
478
488
  }
479
489
  catch {
480
490
  return [];
@@ -486,6 +496,13 @@ function uninstallBoxes(file) {
486
496
  * package's own `/volumeN/@app*` directories; the wizard and the script must
487
497
  * agree on the box that asks for it.
488
498
  */
499
+ /** The wizard texts of the App's uninstall hook, as build rendered them. */
500
+ function uninstallWizardOptions(hook) {
501
+ if (!hook)
502
+ return {};
503
+ const { uninstall, catalogs } = hook;
504
+ return { rootPurge: Boolean(uninstall.purge.root), ...(uninstall.state ? { state: { items: (locale) => stateItemTexts(uninstall, catalogs, locale) } } : {}) };
505
+ }
489
506
  function checkUninstall(dir, findings) {
490
507
  const script = join(dir, "scripts", "postuninst");
491
508
  if (!existsSync(script))
@@ -493,7 +510,8 @@ function checkUninstall(dir, findings) {
493
510
  const body = readFileSync(script, "utf8");
494
511
  const wizardDir = join(dir, "WIZARD_UIFILES");
495
512
  const wizards = existsSync(wizardDir) ? readdirSync(wizardDir).filter(name => name === "uninstall_uifile" || name.startsWith("uninstall_uifile_")).sort() : [];
496
- const boxes = wizards.flatMap(name => uninstallBoxes(join(wizardDir, name)).map(box => ({ ...box, name })));
513
+ const boxes = wizards.flatMap(name => uninstallBoxes(join(wizardDir, name), DATA_KEYS).map(box => ({ ...box, name })));
514
+ checkStateBoxes(dir, wizards.flatMap(name => uninstallBoxes(join(wizardDir, name), [DELETE_STATE_KEY]).map(box => ({ ...box, name }))), findings);
497
515
  const deletes = DELETE_COMMAND.test(body);
498
516
  if (deletes) {
499
517
  if (!UPGRADE_GUARD.test(body))
@@ -514,6 +532,17 @@ function checkUninstall(dir, findings) {
514
532
  findings.push(`UNINSTALL_WIZARD_NOT_HONOURED WIZARD_UIFILES/${box.name} 提供 ${box.key} 勾選框,但 scripts/postuninst 沒有讀它`);
515
533
  }
516
534
  }
535
+ /** The App's delete-state box keeps the state by default, and preuninst hands its answer to the App hook. */
536
+ function checkStateBoxes(dir, boxes, findings) {
537
+ const script = join(dir, "scripts", "preuninst");
538
+ const body = existsSync(script) ? readFileSync(script, "utf8") : "";
539
+ for (const box of boxes) {
540
+ if (box.defaultValue !== false)
541
+ findings.push(`UNINSTALL_STATE_DEFAULT_CHECKED WIZARD_UIFILES/${box.name} 的 ${box.key} 預設必須不勾(保留 App 狀態)`);
542
+ if (!body.includes(box.key))
543
+ findings.push(`UNINSTALL_STATE_NOT_HONOURED WIZARD_UIFILES/${box.name} 提供 ${box.key} 勾選框,但 scripts/preuninst 沒有讀它`);
544
+ }
545
+ }
517
546
  export async function listPayload(packageTgz, findings) {
518
547
  if (!existsSync(packageTgz)) {
519
548
  findings.push("缺少 package.tgz");
@@ -4,6 +4,8 @@ import { renderSharedFolderScript } from "./shared-folder.js";
4
4
  import { renderDelegateInstallDraft } from '../build-spk/delegate-scope.js';
5
5
  import { renderPortalLifecycle } from "../portal/package.js";
6
6
  import { nativePortal } from "./portal.js";
7
+ import { renderUninstallHook } from "./uninstall-hook.js";
8
+ const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
7
9
  /** Package-account lifecycle hooks for CGI, daemon and background packages. */
8
10
  export function renderStandaloneScript(name, manifest) {
9
11
  const portal = nativePortal(manifest);
@@ -43,12 +45,16 @@ function renderStandaloneScriptBody(name, manifest) {
43
45
  const stopDaemon = manifest.daemon ? `"/var/packages/${manifest.package}/target/daemon/control" stop "\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}" || exit $?\n` : "";
44
46
  return normal.replace("#!/bin/sh\n", `#!/bin/sh\n${workerStop}if [ "$1" = start ]; then\n${request("start")}fi\nif [ "$1" = stop ]; then\n${stopWorker}${stopDaemon}${request("stop")}${workerResult}fi\n`);
45
47
  }
46
- const action = name === "preuninst" ? '"$action"' : "stop";
47
- const choose = name === "preuninst" ? 'action=purge\n[ "${SYNOPKG_PKG_STATUS:-}" != UPGRADE ] || action=stop\n' : "";
48
- return `#!/bin/sh\n${workerStop}${stopWorker}${choose}${request(action)}${manifest.daemon ? renderDaemonScript(name, manifest).replace("#!/bin/sh\n", "") : workerResult}`;
48
+ if (name === "preuninst")
49
+ return renderExecutorPreuninst(manifest, workerStop, stopWorker, workerResult, request);
50
+ return `#!/bin/sh\n${workerStop}${stopWorker}${request("stop")}${manifest.daemon ? renderDaemonScript(name, manifest).replace("#!/bin/sh\n", "") : workerResult}`;
49
51
  }
50
52
  if (name === "postuninst")
51
53
  return renderPackagePostuninst(manifest.package);
54
+ if (name === "preuninst" && manifest.uninstall) {
55
+ const body = manifest.daemon ? renderDaemonPreuninst(manifest) : `${UNINSTALL_ONLY}exit 0\n`;
56
+ return `#!/bin/sh\n${renderUninstallHook(manifest)}${body}`;
57
+ }
52
58
  if ((manifest.capabilities?.rootOps || manifest.capabilities?.delegate) && ["postinst", "postupgrade"].includes(name)) {
53
59
  const normal = renderCapabilityScript(name, { ...manifest, capabilities: { ...manifest.capabilities, rootOps: undefined, delegate: undefined } });
54
60
  return normal.replace("#!/bin/sh\n", `#!/bin/sh\n"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}/executor/lifecycle" start --package ${manifest.package} || exit $?\n`);
@@ -84,7 +90,11 @@ function renderDelegatedDaemonScript(name, manifest) {
84
90
  if (name !== 'start-stop-status') {
85
91
  if (name === 'preupgrade')
86
92
  return `#!/bin/sh\n${workerStop}${stop('stop', 'stop')}`;
87
- return `#!/bin/sh\n${workerStop}if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n${stop('stop', 'stop')}fi\n${stop(manifest.daemon?.uninstall_args ? 'uninstall' : 'stop', 'purge')}`;
93
+ const purge = manifest.uninstall
94
+ ? stop(manifest.daemon?.uninstall_args ? 'uninstall' : 'stop', 'purge $synopkgland_purge_args')
95
+ .replace(executor('purge $synopkgland_purge_args'), () => `synopkgland_uninstall_hook\n${executor('purge $synopkgland_purge_args')}`)
96
+ : stop(manifest.daemon?.uninstall_args ? 'uninstall' : 'stop', 'purge');
97
+ return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n${stop('stop', 'stop')}fi\n${purge}`;
88
98
  }
89
99
  return `#!/bin/sh
90
100
  ${workerStop}${renderUserManagerRecovery()}${renderBackgroundWorkerStart(pkg)}case "$1" in
@@ -143,3 +153,27 @@ case "$1" in
143
153
  esac
144
154
  `;
145
155
  }
156
+ /**
157
+ * A real uninstall stops the daemon, runs the App hook, then asks the
158
+ * executor to purge; an upgrade only stops both.
159
+ */
160
+ function renderExecutorPreuninst(manifest, workerStop, stopWorker, workerResult, request) {
161
+ const pkg = manifest.package;
162
+ const choose = 'action=purge\n[ "${SYNOPKG_PKG_STATUS:-}" != UPGRADE ] || action=stop\n';
163
+ const hook = manifest.uninstall ? '[ "$action" = stop ] || synopkgland_uninstall_hook\n' : "";
164
+ const purge = request(manifest.uninstall ? '"$action" $synopkgland_purge_args' : '"$action"');
165
+ if (!manifest.daemon)
166
+ return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}${stopWorker}${choose}${hook}${purge}${workerResult}`;
167
+ const control = `"/var/packages/${pkg}/target/daemon/control"`;
168
+ const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}"`;
169
+ const daemonAction = manifest.daemon.uninstall_args ? 'daemon_action=stop\n[ "$action" = stop ] || daemon_action=uninstall\n' : "daemon_action=stop\n";
170
+ return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}${stopWorker}${choose}${daemonAction}daemon_result=0\n${control} "$daemon_action" ${paths} || daemon_result=$?\n${hook}${purge}exit "$daemon_result"\n`;
171
+ }
172
+ /** The daemon stops (or uninstalls) first; the App hook follows on a real uninstall. */
173
+ function renderDaemonPreuninst(manifest) {
174
+ const pkg = manifest.package;
175
+ const control = `"/var/packages/${pkg}/target/daemon/control"`;
176
+ const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}"`;
177
+ return `if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n exec ${control} stop ${paths}\nfi\n`
178
+ + `${control} ${manifest.daemon?.uninstall_args ? "uninstall" : "stop"} ${paths}\ndaemon_result=$?\nsynopkgland_uninstall_hook\nexit "$daemon_result"\n`;
179
+ }
@@ -6,10 +6,12 @@ import { type Catalogs, type InfoTranslations } from "./i18n.js";
6
6
  export type { InfoText } from "./i18n.js";
7
7
  import { type RootOp } from "./root-ops.js";
8
8
  import { type Daemon } from "./daemon.js";
9
+ import { type Uninstall } from "./uninstall.js";
9
10
  import { type Background } from "./background.js";
10
11
  import { type SharedFolder } from "./shared-folder.js";
11
12
  import { type ApiPermissions } from "./api-permissions.js";
12
13
  import { type PortalDeclaration } from "../portal/alias.js";
14
+ import { type SdkBackend } from "../runtime/sdk-source.js";
13
15
  /**
14
16
  * `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
15
17
  * runs as its own DSM package account with a CGI backend and a DSM window UI.
@@ -19,6 +21,12 @@ export type StandaloneBackend = {
19
21
  kind: "go";
20
22
  dir: string;
21
23
  main: string;
24
+ } | {
25
+ kind: "rust";
26
+ dir: string;
27
+ package: string;
28
+ bin: string;
29
+ worker: string;
22
30
  } | {
23
31
  kind: "binary";
24
32
  files: Partial<Record<NativeArchFamily, string>>;
@@ -63,6 +71,8 @@ export interface StandaloneManifest {
63
71
  tasks: ExecutorTask[];
64
72
  };
65
73
  daemon?: Daemon;
74
+ /** `uninstall`: the App's own purge hook and the state the wizard may delete. */
75
+ uninstall?: Uninstall;
66
76
  /** `install.sharedFolder`: the Shared Folder the install wizard selects or `data-share` creates. */
67
77
  sharedFolder?: SharedFolder;
68
78
  /** Who may use the app at all; absent keeps the route policy alone. */
@@ -97,6 +107,8 @@ export type ManifestResult = {
97
107
  };
98
108
  export declare const KNOWN_FIELDS: Set<string>;
99
109
  export declare function loadStandaloneManifest(root: string): ManifestResult;
110
+ /** Where an App's SDK version is declared: its Go or Rust backend; a prebuilt binary declares it in the project's go.mod. */
111
+ export declare function sdkBackend(m: Pick<StandaloneManifest, "backend" | "root">): SdkBackend;
100
112
  /**
101
113
  * The nearest directory at or above `start` that holds dsm-package.yaml,
102
114
  * unless a Managed Profile project (synology-app.yaml) is found first.
@@ -7,14 +7,15 @@ import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
7
7
  import { checkGlossary, loadCatalogs, readInfoTranslations } from "./i18n.js";
8
8
  import { loadRootOps, ROOT_OPS_DIR } from "./root-ops.js";
9
9
  import { readDaemon } from "./daemon.js";
10
+ import { readUninstall } from "./uninstall.js";
10
11
  import { readBackground } from "./background.js";
11
12
  import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
12
13
  import { readSharedFolder } from "./shared-folder.js";
13
- import { floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
14
+ import { API_PERMISSIONS_FILE, floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
14
15
  import { PORTAL_REQUIRES_EXECUTOR, portalEntryProblem, readPortalDeclaration } from "../portal/alias.js";
15
16
  import { renderPackageIcons } from "../build-spk/dsm-icons.js";
16
17
  import { validateReproducible } from "../reproducible-config.js";
17
- import { SDK_MODULE, sdkRequirement } from "../runtime/sdk-source.js";
18
+ import { appSdkVersion, SDK_CRATE, SDK_MODULE } from "../runtime/sdk-source.js";
18
19
  import { sdkProblems } from "./sdk-check.js";
19
20
  /**
20
21
  * `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
@@ -23,13 +24,15 @@ import { sdkProblems } from "./sdk-check.js";
23
24
  export const STANDALONE_MANIFEST = "dsm-package.yaml";
24
25
  export const KNOWN_FIELDS = new Set([
25
26
  "schema", "package", "version", "build", "displayname", "description",
26
- "metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "entry", "portal", "icon", "reproducible",
27
+ "metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "uninstall", "entry", "portal", "icon", "reproducible",
27
28
  ]);
28
29
  const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
29
30
  const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
30
31
  const VERSION = /^\d+(\.\d+){0,3}$/;
31
32
  const OS_MIN_VER = /^\d+\.\d+(\.\d+)?-\d+$/;
32
33
  const GO_MAIN = /^\.\/[A-Za-z0-9_./-]*$/;
34
+ /** A Cargo package or binary name. */
35
+ const CARGO_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
33
36
  function isRecord(value) {
34
37
  return typeof value === "object" && value !== null && !Array.isArray(value);
35
38
  }
@@ -132,17 +135,19 @@ function readManifest(raw, c) {
132
135
  const arches = readArches(raw.arches, c);
133
136
  const backend = arches && readBackend(raw.backend, arches, c);
134
137
  let permissions;
135
- if (backend?.kind === "go") {
138
+ if (backend && backend.kind !== "binary") {
136
139
  try {
137
- permissions = readApiPermissions(backend.dir, capabilities);
140
+ permissions = readApiPermissions(backend, capabilities);
138
141
  }
139
142
  catch (error) {
140
- // The SDK's own routes come from its module; a failed download is not a policy problem.
141
- const download = /^\[SDK_DOWNLOAD_FAILED\] ([\s\S]*)$/.exec(error.message);
143
+ // The SDK's own routes come from its module; a failed download or an
144
+ // untagged SDK (reported by sdkProblems) is not a policy problem.
145
+ const message = error.message;
146
+ const download = /^\[SDK_DOWNLOAD_FAILED\] ([\s\S]*)$/.exec(message);
142
147
  if (download)
143
148
  c.fail("SDK_DOWNLOAD_FAILED", download[1]);
144
- else
145
- c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/internal/api/permissions.json must define the centralized API policy: ${String(error)}`);
149
+ else if (!message.startsWith("[SDK_TAG_REQUIRED]"))
150
+ c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/${API_PERMISSIONS_FILE[backend.kind]} must define the centralized API policy: ${String(error)}`);
146
151
  }
147
152
  }
148
153
  const daemon = arches && readDaemon(raw.daemon, c.root, arches, e => c.fail(e.code, e.message));
@@ -169,12 +174,13 @@ function readManifest(raw, c) {
169
174
  c.fail("CAPABILITY_COMBINATION_INVALID", "capabilities.identity 需要獨立登入頁;plain UI 不支援,請改用 vue UI(init --ui vue)。");
170
175
  if (capabilities.delegate?.scope === "install-selected" && ui && !ui.build)
171
176
  c.fail("CAPABILITY_COMBINATION_INVALID", "delegate scope install-selected 只能在 Vue 面板確認;plain UI 不支援,請改用 vue UI(init --ui vue)。");
172
- if (backend?.kind === "go") {
173
- for (const problem of sdkProblems(backend.dir, ui?.build?.cwd, Boolean(capabilities.delegate || capabilities.rootOps)))
177
+ if (backend && backend.kind !== "binary") {
178
+ for (const problem of sdkProblems(backend, ui?.build?.cwd, Boolean(capabilities.delegate || capabilities.rootOps)))
174
179
  c.fail(problem.code, problem.message);
175
180
  }
176
181
  const i18n = raw.i18n === undefined ? undefined : readI18n(raw.i18n, c);
177
182
  const icon = raw.icon === undefined ? undefined : readIcon(raw.icon, c);
183
+ const uninstall = readUninstall(raw.uninstall, { rootOps: capabilities.rootOps === true, ops: rootOps, en: i18n?.catalogs?.en }, e => c.fail(e.code, e.message));
178
184
  const portal = raw.portal === undefined || !packageId ? undefined : readPortalDeclaration(raw.portal, packageId)
179
185
  ?? c.fail("STANDALONE_PORTAL_INVALID", "portal 必須是 {} 或 { alias: <小寫英數與 -,1–64 字,不可為 DSM 保留路徑> }。");
180
186
  if (portal && !capabilities.rootOps && !capabilities.delegate) {
@@ -210,12 +216,17 @@ function readManifest(raw, c) {
210
216
  capabilities,
211
217
  ...(rootOps ? { rootOps } : {}),
212
218
  ...(daemon ? { daemon } : {}),
219
+ ...(uninstall ? { uninstall } : {}),
213
220
  ...(sharedFolder ? { sharedFolder } : {}),
214
221
  ...(entry ? { entry } : {}),
215
222
  ...(portal ? { portal } : {}),
216
223
  ...(icon ? { icon } : {}),
217
224
  };
218
225
  }
226
+ /** Where an App's SDK version is declared: its Go or Rust backend; a prebuilt binary declares it in the project's go.mod. */
227
+ export function sdkBackend(m) {
228
+ return m.backend.kind === "binary" ? { kind: "go", dir: m.root } : m.backend;
229
+ }
219
230
  function readIcon(value, c) {
220
231
  const path = c.path(value, "icon");
221
232
  if (!path)
@@ -228,6 +239,20 @@ function readIcon(value, c) {
228
239
  return c.fail("STANDALONE_ICON_INVALID", `icon:${error.message}`);
229
240
  }
230
241
  }
242
+ /** Where each backend kind's CGI main wraps its authenticator in the SDK entry gate. */
243
+ const ENTRY_GATE = {
244
+ go: { main: b => b.kind === "go" ? join(b.dir, b.main, "main.go") : "", call: "dsm.NewEntryGate(", sdk: `backend/go.mod 要 require ${SDK_MODULE}` },
245
+ rust: { main: b => b.kind === "rust" ? join(b.dir, "src/bin", `${b.bin}.rs`) : "", call: "new_entry_gate(", sdk: `backend/Cargo.toml 要以 release tag 相依 ${SDK_CRATE}` },
246
+ };
247
+ /** The SDK version, or none when the backend has none or pins it wrongly (reported by sdkProblems). */
248
+ function sdkVersionOrUndefined(backend) {
249
+ try {
250
+ return appSdkVersion(backend);
251
+ }
252
+ catch {
253
+ return undefined;
254
+ }
255
+ }
231
256
  /**
232
257
  * The CGI enforces the entry mode only when the project's backend carries the
233
258
  * SDK gate and main wires it; `-X main.entry` on an older main would be
@@ -239,11 +264,12 @@ function readEntry(value, backend, policy, c) {
239
264
  }
240
265
  if (!backend)
241
266
  return undefined;
242
- const main = backend.kind === "go" ? join(backend.dir, backend.main, "main.go") : "";
243
- if (backend.kind !== "go" || !sdkRequirement(backend.dir)
244
- || !existsSync(main) || !readFileSync(main, "utf8").includes("dsm.NewEntryGate(")) {
267
+ const gate = ENTRY_GATE[backend.kind];
268
+ const main = gate?.main(backend) ?? "";
269
+ if (!gate || !sdkVersionOrUndefined(backend)
270
+ || !existsSync(main) || !readFileSync(main, "utf8").includes(gate.call)) {
245
271
  const shown = main ? relative(c.root, main) : "backend.main 的 main.go";
246
- return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry 需要 SDK 的 entry gate:backend/go.mod 要 require ${SDK_MODULE},並在 ${shown} 以 dsm.NewEntryGate 包住 authenticator。`);
272
+ return c.fail("STANDALONE_ENTRY_UNSUPPORTED", `entry 需要 SDK 的 entry gate:${gate?.sdk ?? `backend/go.mod 要 require ${SDK_MODULE}`},並在 ${shown} 以 ${gate?.call.slice(0, -1) ?? "dsm.NewEntryGate"} 包住 authenticator。`);
247
273
  }
248
274
  if (value === "app-privilege" && !permitsOrdinaryUsers(policy)) {
249
275
  return c.fail("STANDALONE_ENTRY_INVALID", "entry: app-privilege 需要至少一個 permissions.json 標為 authenticated 的 route,一般使用者才有功能可用。");
@@ -274,9 +300,11 @@ function readArches(value, c) {
274
300
  : c.fail("STANDALONE_ARCHES_INVALID", "arches 必須是不重複的 x86_64/aarch64/armv7 清單,至少一個。");
275
301
  }
276
302
  function readBackend(value, arches, c) {
277
- if (!isRecord(value) || !["go", "binary"].includes(String(value.kind))) {
278
- return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind 必須是 go 或 binary。");
303
+ if (!isRecord(value) || !["go", "rust", "binary"].includes(String(value.kind))) {
304
+ return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind 必須是 go、rust 或 binary。");
279
305
  }
306
+ if (value.kind === "rust")
307
+ return readRustBackend(value, c);
280
308
  if (value.kind === "go") {
281
309
  if (Object.keys(value).some(key => !["kind", "dir", "main"].includes(key)) || (value.main !== undefined && (typeof value.main !== "string" || !GO_MAIN.test(value.main) || value.main.includes("..")))) {
282
310
  return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: go 需要 dir,可選 main(預設 ./cmd/app)。");
@@ -303,6 +331,16 @@ function readBackend(value, arches, c) {
303
331
  }
304
332
  return Object.values(files).every(Boolean) ? { kind: "binary", files } : undefined;
305
333
  }
334
+ function readRustBackend(value, c) {
335
+ const { package: pkg, bin = "api", worker = "background-worker" } = value;
336
+ const names = [pkg, bin, worker];
337
+ if (Object.keys(value).some(key => !["kind", "dir", "package", "bin", "worker"].includes(key))
338
+ || !names.every(name => typeof name === "string" && CARGO_NAME.test(name)) || bin === worker) {
339
+ return c.fail("STANDALONE_BACKEND_INVALID", "backend.kind: rust 需要 dir 與 package(Cargo package),可選 bin(CGI,預設 api)與 worker(背景程序,預設 background-worker),兩者不可相同。");
340
+ }
341
+ const dir = c.path(value.dir, "backend.dir");
342
+ return dir ? { kind: "rust", dir, package: pkg, bin: bin, worker: worker } : undefined;
343
+ }
306
344
  function readUi(value, c) {
307
345
  if (!isRecord(value)) {
308
346
  return c.fail("STANDALONE_UI_INVALID", "ui 必須包含 dir。");
@@ -1,5 +1,6 @@
1
1
  import { type PackageMetadata } from "../package-metadata.js";
2
2
  import type { CommandRunner } from './build.js';
3
+ import { type Toolkit } from './toolkit.js';
3
4
  import { type PortalDeclaration } from '../portal/alias.js';
4
5
  export declare const LAYOUT_ARCHES: {
5
6
  readonly x86_64: readonly [2, 62];
@@ -13,10 +14,6 @@ type Hook = {
13
14
  cwd: string;
14
15
  env: Record<string, string>;
15
16
  };
16
- type Toolkit = {
17
- url: string;
18
- sha256: string;
19
- };
20
17
  type Binary = {
21
18
  path: string;
22
19
  linkage: 'static' | 'dynamic';
@@ -65,9 +62,6 @@ export declare function loadNativeLayout(root: string): {
65
62
  message: string;
66
63
  }[];
67
64
  };
68
- export declare function verifyToolkitArchive(path: string, expected: string): void;
69
- /** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
70
- export declare function toolkitCacheDir(env: Record<string, string | undefined>, platform: string, home: string): string;
71
65
  export declare function layoutFiles(root: string): Array<{
72
66
  path: string;
73
67
  mode: number;
@@ -1,13 +1,14 @@
1
1
  import { validateMetadata, metadataInfoFields } from "../package-metadata.js";
2
2
  /** Existing native products: the CLI owns archives/INFO, the project owns its layout. */
3
3
  import { createHash } from 'node:crypto';
4
- import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
5
- import { homedir, tmpdir } from 'node:os';
4
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
6
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
7
7
  import { parse } from 'yaml';
8
8
  import { createReproducibleTar } from '../build-spk/reproducible-archive.js';
9
9
  import { runCommand, runnerFor } from './build.js';
10
10
  import { checkJobs, commitOutputs, forEachArch } from './arch-jobs.js';
11
+ import { prepareToolkit } from './toolkit.js';
11
12
  import { validateReproducible } from '../reproducible-config.js';
12
13
  import { isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, resolveMinDsm } from '../build-spk/dsm-version.js';
13
14
  import { portalEntryProblem, readPortalDeclaration } from '../portal/alias.js';
@@ -164,100 +165,6 @@ export function loadNativeLayout(root) {
164
165
  return { ok: false, errors: [{ code: 'NATIVE_LAYOUT_INVALID', message: String(error) }] };
165
166
  }
166
167
  }
167
- export function verifyToolkitArchive(path, expected) {
168
- if (!lstatSync(path).isFile() || digest(readFileSync(path)) !== expected)
169
- fail('toolkit checksum mismatch');
170
- }
171
- /** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
172
- export function toolkitCacheDir(env, platform, home) {
173
- if (env.SYNOPKGLAND_CACHE_DIR)
174
- return resolve(env.SYNOPKGLAND_CACHE_DIR);
175
- if (platform === 'darwin')
176
- return join(home, 'Library', 'Caches', 'synopkgland');
177
- const xdg = env.XDG_CACHE_HOME;
178
- return join(xdg && isAbsolute(xdg) ? xdg : join(home, '.cache'), 'synopkgland');
179
- }
180
- // Concurrent architectures that pin the same archive share one download.
181
- const downloads = new Map();
182
- async function download(m, cache, input, archive, run) {
183
- const tmp = mkdtempSync(join(cache, '.download-'));
184
- try {
185
- const out = join(tmp, 'archive');
186
- const r = await run('curl', ['--fail', '--location', '--proto', '=https', '--proto-redir', '=https', '--retry', '2', '--retry-all-errors', '--connect-timeout', '20', '--max-time', '600', '--output', out, input.url], { cwd: m.root, env: { ...process.env } });
187
- if (r.status !== 0)
188
- fail('toolkit download failed');
189
- // Rename is atomic, so another build process sees either no archive or a verified one.
190
- verifyToolkitArchive(out, input.sha256);
191
- renameSync(out, archive);
192
- }
193
- finally {
194
- rmSync(tmp, { recursive: true, force: true });
195
- }
196
- }
197
- async function toolkitFor(m, arch, run) {
198
- const inputs = m.native.toolkits[arch] ?? [];
199
- if (!inputs.length)
200
- return '';
201
- const cache = toolkitCacheDir(process.env, process.platform, homedir());
202
- mkdirSync(cache, { recursive: true, mode: 0o700 });
203
- if (lstatSync(cache).isSymbolicLink() || (lstatSync(cache).mode & 0o022))
204
- fail('toolkit cache must be owned and private');
205
- const archives = [];
206
- for (const input of inputs) {
207
- const archive = join(cache, `${input.sha256}.txz`);
208
- if (!existsSync(archive)) {
209
- let pending = downloads.get(archive);
210
- if (!pending) {
211
- pending = download(m, cache, input, archive, run).finally(() => downloads.delete(archive));
212
- downloads.set(archive, pending);
213
- }
214
- await pending;
215
- }
216
- verifyToolkitArchive(archive, input.sha256);
217
- archives.push(archive);
218
- }
219
- // Re-extract verified bytes: a package-writable marker never authenticates an extracted tree.
220
- const stage = mkdtempSync(join(cache, `toolkit-${arch}-`));
221
- try {
222
- const flavor = await tarFlavor(m, run);
223
- for (const archive of archives) {
224
- if (flavor === 'bsdtar')
225
- await refuseEscapingMembers(m, archive, run);
226
- const r = await run('tar', flavor === 'gnu' ? ['-xJf', archive, '-C', stage, '--keep-directory-symlink'] : ['-xPJf', archive, '-C', stage], { cwd: m.root, env: { ...process.env } });
227
- if (r.status !== 0)
228
- fail('toolkit extraction failed');
229
- }
230
- return stage;
231
- }
232
- catch (e) {
233
- rmSync(stage, { recursive: true, force: true });
234
- throw e;
235
- }
236
- }
237
- /**
238
- * A later toolkit archive may write through a directory symlink an earlier one
239
- * created. GNU tar keeps such a symlink only with --keep-directory-symlink;
240
- * bsdtar only with -P, which also allows absolute and .. members, so those are
241
- * refused from the listing first.
242
- */
243
- async function tarFlavor(m, run) {
244
- const r = await run('tar', ['--version'], { cwd: m.root, env: { ...process.env }, capture: true });
245
- const version = r.status === 0 ? r.stdout ?? '' : '';
246
- if (/\bGNU tar\b/.test(version))
247
- return 'gnu';
248
- if (/\bbsdtar\b/.test(version))
249
- return 'bsdtar';
250
- throw Object.assign(new Error(`NATIVE_TOOLKIT_TAR_UNSUPPORTED: extracting native.toolkits needs GNU tar or bsdtar as \`tar\`; found ${r.missing ? 'no tar' : JSON.stringify(version.split('\n')[0] ?? '')}`), { code: 'NATIVE_TOOLKIT_TAR_UNSUPPORTED' });
251
- }
252
- async function refuseEscapingMembers(m, archive, run) {
253
- const r = await run('tar', ['-tJf', archive], { cwd: m.root, env: { ...process.env }, capture: true });
254
- if (r.status !== 0)
255
- fail('toolkit extraction failed');
256
- for (const member of (r.stdout ?? '').split('\n').filter(Boolean)) {
257
- if (member.startsWith('/') || member.split('/').includes('..'))
258
- fail(`toolkit member escapes the toolkit folder: ${member}`);
259
- }
260
- }
261
168
  async function runHook(m, h, env, run) {
262
169
  const [cmd, ...args] = h.command;
263
170
  const r = await run(cmd, args, { cwd: inside(m.root, h.cwd), env: { ...process.env, ...h.env, ...env } });
@@ -396,7 +303,7 @@ export async function buildNativeLayout(options) {
396
303
  await runHook(m, m.native.prepare, env, options.run ?? runCommand);
397
304
  const outputs = await commitOutputs(options.outDir ?? join(m.root, 'dist'), (outDir) => forEachArch(arches, jobs, async (arch) => {
398
305
  const run = runnerFor(options.run, jobs, arch);
399
- const toolkit = await toolkitFor(m, arch, run);
306
+ const toolkit = await prepareToolkit(m.root, arch, m.native.toolkits[arch] ?? [], run);
400
307
  const stage = mkdtempSync(join(tmpdir(), 'synopkgland-layout-'));
401
308
  const work = mkdtempSync(join(tmpdir(), 'synopkgland-work-'));
402
309
  try {
@@ -12,4 +12,6 @@ export type RootOpsResult = {
12
12
  errors: StandaloneError[];
13
13
  };
14
14
  export declare const ROOT_OPS_DIR = "root";
15
+ /** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
16
+ export declare const ROOT_SCRIPT_HINTS: RegExp[];
15
17
  export declare function loadRootOps(dir: string): RootOpsResult;
@@ -1,6 +1,8 @@
1
1
  import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  export const ROOT_OPS_DIR = "root";
4
+ /** Shell that tries to become root: sudo, chown root or setuid/setgid modes. */
5
+ export const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
4
6
  const MAX_ROOT_OP_BYTES = 16 * 1024;
5
7
  const OP_FILE = /^([a-z][a-z0-9_]{0,31})\.sh$/;
6
8
  export function loadRootOps(dir) {
@@ -0,0 +1,35 @@
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 {};