@synopackageland/cli 0.9.7 → 0.10.1
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.
- package/CHANGELOG.md +146 -0
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +14 -91
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +69 -18
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
|
@@ -13,7 +13,7 @@ export function readBackground(value, fail) {
|
|
|
13
13
|
const ok = typeof value === "object" && value !== null && !Array.isArray(value)
|
|
14
14
|
&& Object.keys(value).length === 1 && value.worker === "sha256-v1";
|
|
15
15
|
if (!ok) {
|
|
16
|
-
fail({ code: "STANDALONE_BACKGROUND_INVALID", message: "background
|
|
16
|
+
fail({ code: "STANDALONE_BACKGROUND_INVALID", message: "background accepts only { worker: sha256-v1 }; background processes always run as the package account and cannot specify a user, command or privileges." });
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
19
|
return { worker: "sha256-v1" };
|
|
@@ -22,13 +22,15 @@ export function readBackground(value, fail) {
|
|
|
22
22
|
export function renderBackgroundResource() {
|
|
23
23
|
return JSON.stringify({ "systemd-user-unit": {} });
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/** The worker's arguments: none for a Go App's own worker, `--worker` for the bridge runtime (spec/bridge/v1). */
|
|
26
|
+
export const BRIDGE_WORKER_ARGS = ["--worker"];
|
|
27
|
+
export function renderBackgroundUnit(pkg, args = []) {
|
|
26
28
|
return `[Unit]
|
|
27
29
|
Description=${pkg} background worker
|
|
28
30
|
|
|
29
31
|
[Service]
|
|
30
32
|
Type=simple
|
|
31
|
-
ExecStart
|
|
33
|
+
ExecStart=${[`/var/packages/${pkg}/target/${BACKGROUND_WORKER_PAYLOAD}`, ...args].join(" ")}
|
|
32
34
|
UMask=0077
|
|
33
35
|
KillMode=control-group
|
|
34
36
|
TimeoutStopSec=10
|
|
@@ -80,7 +82,9 @@ export function renderUserManagerRecovery() {
|
|
|
80
82
|
echo "package user manager $pid has no socket in /run/user/$uid; stopping it so DSM starts a fresh one" >&2
|
|
81
83
|
kill -TERM "$pid" || return 1
|
|
82
84
|
i=0
|
|
83
|
-
|
|
85
|
+
# DSM's bash logs a failed kill to syslog even with 2>/dev/null, so the
|
|
86
|
+
# exit is looked up in /proc.
|
|
87
|
+
while [ -d "/proc/$pid" ]; do
|
|
84
88
|
[ "$i" -lt 100 ] || { echo "package user manager $pid did not exit" >&2; return 1; }
|
|
85
89
|
sleep 0.1; i=$((i + 1))
|
|
86
90
|
done
|
|
@@ -21,17 +21,17 @@ export function checkCgiBinary(projectRoot, file, arch, interpreter) {
|
|
|
21
21
|
stat = lstatSync(file);
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
|
-
return fail("STANDALONE_BINARY_MISSING",
|
|
24
|
+
return fail("STANDALONE_BINARY_MISSING", `Backend executable for ${arch} not found: ${file}`);
|
|
25
25
|
}
|
|
26
26
|
if (!stat.isFile()) {
|
|
27
|
-
return fail("STANDALONE_BINARY_NOT_REGULAR", `${file}
|
|
27
|
+
return fail("STANDALONE_BINARY_NOT_REGULAR", `${file} must be a regular file (not a symlink or directory).`);
|
|
28
28
|
}
|
|
29
29
|
const rel = relative(realpathSync(projectRoot), realpathSync(file));
|
|
30
30
|
if (rel.startsWith("..")) {
|
|
31
|
-
return fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${file}
|
|
31
|
+
return fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${file} actually lives outside the project directory.`);
|
|
32
32
|
}
|
|
33
33
|
if (stat.size > MAX_BINARY_BYTES) {
|
|
34
|
-
return fail("STANDALONE_BINARY_TOO_LARGE", `${file}
|
|
34
|
+
return fail("STANDALONE_BINARY_TOO_LARGE", `${file} exceeds 256 MiB.`);
|
|
35
35
|
}
|
|
36
36
|
return checkElf(readHeader(file), file, arch, interpreter);
|
|
37
37
|
}
|
|
@@ -61,14 +61,14 @@ function readElfHeader(buf) {
|
|
|
61
61
|
}
|
|
62
62
|
function checkElf(buf, file, arch, interpreter) {
|
|
63
63
|
const bits = ELF_CLASS64[arch] ? 64 : 32;
|
|
64
|
-
const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file}
|
|
64
|
+
const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file} is not the ${bits}-bit little-endian Linux ELF executable ${arch} needs.`);
|
|
65
65
|
const header = readElfHeader(buf);
|
|
66
66
|
if (!header || header.class64 !== ELF_CLASS64[arch]) {
|
|
67
67
|
return notElf;
|
|
68
68
|
}
|
|
69
69
|
const { machine, phoff, phentsize, phnum } = header;
|
|
70
70
|
if (machine !== ELF_MACHINE[arch]) {
|
|
71
|
-
return fail("STANDALONE_BINARY_ARCH_MISMATCH",
|
|
71
|
+
return fail("STANDALONE_BINARY_ARCH_MISMATCH", `The ELF machine of ${file} is ${machine}, not ${arch}.`);
|
|
72
72
|
}
|
|
73
73
|
if (phentsize < 4 || phoff + phentsize * phnum > buf.length) {
|
|
74
74
|
return notElf;
|
|
@@ -76,7 +76,7 @@ function checkElf(buf, file, arch, interpreter) {
|
|
|
76
76
|
for (let i = 0; i < phnum; i++) {
|
|
77
77
|
const entry = phoff + i * phentsize;
|
|
78
78
|
if (buf.readUInt32LE(entry) === PT_INTERP && (interpreter === undefined || interpreterOf(buf, entry, header.class64) !== interpreter)) {
|
|
79
|
-
return fail("STANDALONE_BINARY_DYNAMIC", `${file}
|
|
79
|
+
return fail("STANDALONE_BINARY_DYNAMIC", `${file} is a dynamically linked executable. Link statically (Go: CGO_ENABLED=0; Rust: *-unknown-linux-musl).`);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
return undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type StandaloneManifest } from "./manifest.js";
|
|
2
|
+
/** The Node SDK's package name, which the CLI places in the App's node_modules. */
|
|
3
|
+
export declare const NODE_SDK_PACKAGE = "@synopackageland/app";
|
|
4
|
+
export interface StagedApp {
|
|
5
|
+
/** target/app without bridge.json: the App's files and its SDK. */
|
|
6
|
+
dir: string;
|
|
7
|
+
methods: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Python and Node Apps are the same on every architecture: copy the App
|
|
11
|
+
* directory once, add the SDK from the App's SDK release, and take the methods
|
|
12
|
+
* from the manifest or, when it lists none, from the App's `--describe`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stageScriptApp(m: StandaloneManifest, work: string): Promise<StagedApp>;
|
|
15
|
+
/**
|
|
16
|
+
* Reads `--describe`: one JSON line `{"sdk","versions","methods"}` and exit 0
|
|
17
|
+
* (spec/bridge/v1 SDKs). Returns the methods, sorted.
|
|
18
|
+
*/
|
|
19
|
+
export declare function describeMethods(command: string, args: string[], cwd: string, env: Record<string, string>, timeoutMs?: number): Promise<string[]>;
|
|
20
|
+
/** The Node SDK as npm would install it: its package.json and its compiled dist/. */
|
|
21
|
+
export declare function copyNodeSdk(source: string, target: string): void;
|
|
22
|
+
/** Copies an App tree: regular files and directories only, without dot entries and Python bytecode. */
|
|
23
|
+
export declare function copyAppTree(source: string, target: string, skip?: readonly string[]): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { requiredSdk } from "../runtime/sdk-source.js";
|
|
5
|
+
import { METHOD } from "./bridge-backend.js";
|
|
6
|
+
import { PYTHON_VENDOR_DIR } from "./bridge-json.js";
|
|
7
|
+
import { sdkBackend } from "./manifest.js";
|
|
8
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
9
|
+
/** The Node SDK's package name, which the CLI places in the App's node_modules. */
|
|
10
|
+
export const NODE_SDK_PACKAGE = "@synopackageland/app";
|
|
11
|
+
const PYTHON_SDK = "synopkgland_app";
|
|
12
|
+
const MAX_APP_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
const DESCRIBE_TIMEOUT_MS = 10_000;
|
|
14
|
+
/**
|
|
15
|
+
* Python and Node Apps are the same on every architecture: copy the App
|
|
16
|
+
* directory once, add the SDK from the App's SDK release, and take the methods
|
|
17
|
+
* from the manifest or, when it lists none, from the App's `--describe`.
|
|
18
|
+
*/
|
|
19
|
+
export async function stageScriptApp(m, work) {
|
|
20
|
+
const backend = m.backend;
|
|
21
|
+
const sdk = requiredSdk(sdkBackend(m));
|
|
22
|
+
const dir = join(work, "app");
|
|
23
|
+
copyAppTree(backend.dir, dir);
|
|
24
|
+
if (backend.kind === "python") {
|
|
25
|
+
const vendor = join(dir, PYTHON_VENDOR_DIR, PYTHON_SDK);
|
|
26
|
+
rmSync(vendor, { recursive: true, force: true });
|
|
27
|
+
copyAppTree(join(sdk.goDir, "python", PYTHON_SDK), vendor);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const modules = join(dir, "node_modules", NODE_SDK_PACKAGE);
|
|
31
|
+
rmSync(modules, { recursive: true, force: true });
|
|
32
|
+
copyNodeSdk(join(sdk.goDir, "node"), modules);
|
|
33
|
+
checkNodeDependencies(dir);
|
|
34
|
+
}
|
|
35
|
+
return { dir, methods: backend.methods ?? await describe(m, dir) };
|
|
36
|
+
}
|
|
37
|
+
/** Runs `<entry> --describe` on the build machine, from the staged directory, with the SDK it ships. */
|
|
38
|
+
async function describe(m, dir) {
|
|
39
|
+
const backend = m.backend;
|
|
40
|
+
const env = { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", SYNOPKGLAND_PACKAGE: m.package };
|
|
41
|
+
if (backend.kind === "python") {
|
|
42
|
+
Object.assign(env, { PYTHONPATH: PYTHON_VENDOR_DIR, PYTHONUTF8: "1", PYTHONDONTWRITEBYTECODE: "1", PYTHONNOUSERSITE: "1" });
|
|
43
|
+
return describeMethods("python3", [backend.entry, "--describe"], dir, env);
|
|
44
|
+
}
|
|
45
|
+
return describeMethods("node", [backend.entry, "--describe"], dir, env);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads `--describe`: one JSON line `{"sdk","versions","methods"}` and exit 0
|
|
49
|
+
* (spec/bridge/v1 SDKs). Returns the methods, sorted.
|
|
50
|
+
*/
|
|
51
|
+
export function describeMethods(command, args, cwd, env, timeoutMs = DESCRIBE_TIMEOUT_MS) {
|
|
52
|
+
const shown = [command, ...args].join(" ");
|
|
53
|
+
const fail = (why) => new StandaloneBuildError("STANDALONE_BRIDGE_DESCRIBE_FAILED", `There is no backend.methods, so build reads the App's methods with ${shown}, but ${why}`);
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = spawn(command, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
56
|
+
let stdout = "", stderr = "", done = false;
|
|
57
|
+
const finish = (error, methods) => {
|
|
58
|
+
if (done)
|
|
59
|
+
return;
|
|
60
|
+
done = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
try {
|
|
63
|
+
process.kill(-child.pid, "SIGKILL");
|
|
64
|
+
}
|
|
65
|
+
catch { /* already gone */ }
|
|
66
|
+
if (error)
|
|
67
|
+
reject(error);
|
|
68
|
+
else
|
|
69
|
+
resolve(methods);
|
|
70
|
+
};
|
|
71
|
+
const timer = setTimeout(() => finish(fail(` it did not finish within ${timeoutMs} ms.\n${stderr}`)), timeoutMs);
|
|
72
|
+
child.on("error", (error) => finish(fail(error.code === "ENOENT" ? `${command} was not found; install it, or list backend.methods in dsm-package.yaml.` : error.message)));
|
|
73
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; });
|
|
74
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
75
|
+
child.on("close", (code) => {
|
|
76
|
+
if (code !== 0)
|
|
77
|
+
return finish(fail(`exit ${code}; the SDK version must support --describe, otherwise list backend.methods.\n${stderr}`));
|
|
78
|
+
const line = stdout.split("\n")[0];
|
|
79
|
+
let described;
|
|
80
|
+
try {
|
|
81
|
+
described = JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return finish(fail(`the output is not JSON: ${line}`));
|
|
85
|
+
}
|
|
86
|
+
const methods = described.methods;
|
|
87
|
+
if (!Array.isArray(described.versions) || !described.versions.includes(1) || !Array.isArray(methods) || !methods.every(name => typeof name === "string" && METHOD.test(name))) {
|
|
88
|
+
return finish(fail(`the output is not a v1 describe: ${line}`));
|
|
89
|
+
}
|
|
90
|
+
finish(undefined, [...methods].sort());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** The Node SDK as npm would install it: its package.json and its compiled dist/. */
|
|
95
|
+
export function copyNodeSdk(source, target) {
|
|
96
|
+
if (!existsSync(join(source, "dist/index.js"))) {
|
|
97
|
+
throw new StandaloneBuildError("STANDALONE_NODE_SDK_NOT_BUILT", `The Node SDK has no dist/: ${source}. Run npm ci && npm run build in node/ of the SYNOPKGLAND_SDK_DIR checkout first; a release SDK must contain node/dist.`);
|
|
98
|
+
}
|
|
99
|
+
mkdirSync(target, { recursive: true });
|
|
100
|
+
copyFileSync(join(source, "package.json"), join(target, "package.json"));
|
|
101
|
+
chmodSync(join(target, "package.json"), 0o644);
|
|
102
|
+
copyAppTree(join(source, "dist"), join(target, "dist"));
|
|
103
|
+
}
|
|
104
|
+
/** Every package.json dependency must be installed in the App's node_modules; the SDK itself comes from the CLI. */
|
|
105
|
+
function checkNodeDependencies(dir) {
|
|
106
|
+
const manifest = join(dir, "package.json");
|
|
107
|
+
if (!existsSync(manifest))
|
|
108
|
+
return;
|
|
109
|
+
const dependencies = JSON.parse(readFileSync(manifest, "utf8")).dependencies ?? {};
|
|
110
|
+
const missing = Object.keys(dependencies).filter(name => name !== NODE_SDK_PACKAGE && !existsSync(join(dir, "node_modules", name, "package.json")));
|
|
111
|
+
if (missing.length) {
|
|
112
|
+
throw new StandaloneBuildError("STANDALONE_NODE_DEPENDENCY_MISSING", `The package.json dependencies are not installed in the App's node_modules: ${missing.join(", ")}. Run npm ci --omit=dev in the App directory and build again.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Copies an App tree: regular files and directories only, without dot entries and Python bytecode. */
|
|
116
|
+
export function copyAppTree(source, target, skip = []) {
|
|
117
|
+
let total = 0;
|
|
118
|
+
const walk = (from, to) => {
|
|
119
|
+
mkdirSync(to, { recursive: true });
|
|
120
|
+
chmodSync(to, 0o755);
|
|
121
|
+
for (const name of readdirSync(from).sort()) {
|
|
122
|
+
if (name.startsWith(".") || name === "__pycache__" || skip.includes(name))
|
|
123
|
+
continue;
|
|
124
|
+
const src = join(from, name);
|
|
125
|
+
const stat = lstatSync(src);
|
|
126
|
+
if (stat.isDirectory())
|
|
127
|
+
walk(src, join(to, name));
|
|
128
|
+
else if (stat.isFile()) {
|
|
129
|
+
total += stat.size;
|
|
130
|
+
if (total > MAX_APP_BYTES)
|
|
131
|
+
throw new StandaloneBuildError("STANDALONE_BRIDGE_APP_INVALID", `The App directory exceeds 256 MiB: ${source}`);
|
|
132
|
+
copyFileSync(src, join(to, name));
|
|
133
|
+
chmodSync(join(to, name), stat.mode & 0o111 ? 0o755 : 0o644);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
throw new StandaloneBuildError("STANDALONE_BRIDGE_APP_INVALID", `The App directory may contain only regular files and directories (no symlinks): ${src}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
walk(source, target);
|
|
141
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A backend in another language behind the SDK's Go runtime (spec/bridge/v1):
|
|
3
|
+
* the CLI builds the runtime as the package CGI and ships the App in target/app.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BRIDGE_RUNTIMES: readonly ["python", "node", "rust"];
|
|
6
|
+
export type BridgeRuntime = (typeof BRIDGE_RUNTIMES)[number];
|
|
7
|
+
/** The Package Center Node.js packages an App may depend on. */
|
|
8
|
+
export declare const NODE_PACKAGES: readonly ["Node.js_v18", "Node.js_v20", "Node.js_v22"];
|
|
9
|
+
export interface BridgeBackend {
|
|
10
|
+
kind: BridgeRuntime;
|
|
11
|
+
/** The App's sources: python and node ship this directory, rust is a Cargo project. */
|
|
12
|
+
dir: string;
|
|
13
|
+
/** python: the script; node: the module, both relative to dir; rust: the Cargo binary. */
|
|
14
|
+
entry: string;
|
|
15
|
+
/** The App's methods; absent (python and node only), the build reads them from the App's hello. */
|
|
16
|
+
methods?: string[];
|
|
17
|
+
/** python and node: the SDK release the runtime and the vendored SDK come from; rust reads Cargo.lock. */
|
|
18
|
+
sdk?: string;
|
|
19
|
+
node?: {
|
|
20
|
+
package: (typeof NODE_PACKAGES)[number];
|
|
21
|
+
};
|
|
22
|
+
/** Methods the browser calls as an HTTP upload or download (spec/bridge/v1 Transfers). */
|
|
23
|
+
transfers?: Record<string, "upload" | "download">;
|
|
24
|
+
/** The upload limit; absent, the runtime's 16 GiB. */
|
|
25
|
+
maxUploadBytes?: number;
|
|
26
|
+
/** The request deadline in seconds; absent, DEFAULT_REQUEST_TIMEOUT_SECONDS. */
|
|
27
|
+
timeoutSeconds?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function isBridgeBackend(backend: {
|
|
30
|
+
kind: string;
|
|
31
|
+
}): backend is BridgeBackend;
|
|
32
|
+
export declare const METHOD: RegExp;
|
|
33
|
+
/** filestation.MaxUploadBytes, the runtime's default and ceiling. */
|
|
34
|
+
export declare const MAX_UPLOAD_BYTES: number;
|
|
35
|
+
type Raw = Record<string, unknown>;
|
|
36
|
+
/** Reads a python, node or rust backend; `dir` resolves a project-relative path, or reports and returns undefined. */
|
|
37
|
+
export declare function readBridgeBackend(value: Raw, dir: (value: unknown) => string | undefined, fail: (code: string, message: string) => undefined): BridgeBackend | undefined;
|
|
38
|
+
/** Whether one directory is the other or holds it. */
|
|
39
|
+
export declare function overlaps(a: string, b: string): boolean;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import { TIMEOUT_USAGE, validTimeoutSeconds } from "./request-timeout.js";
|
|
4
|
+
/**
|
|
5
|
+
* A backend in another language behind the SDK's Go runtime (spec/bridge/v1):
|
|
6
|
+
* the CLI builds the runtime as the package CGI and ships the App in target/app.
|
|
7
|
+
*/
|
|
8
|
+
export const BRIDGE_RUNTIMES = ["python", "node", "rust"];
|
|
9
|
+
/** The Package Center Node.js packages an App may depend on. */
|
|
10
|
+
export const NODE_PACKAGES = ["Node.js_v18", "Node.js_v20", "Node.js_v22"];
|
|
11
|
+
export function isBridgeBackend(backend) {
|
|
12
|
+
return BRIDGE_RUNTIMES.includes(backend.kind);
|
|
13
|
+
}
|
|
14
|
+
/** spec/bridge/v1 manifest.schema.json: entry and method names. */
|
|
15
|
+
const ENTRY = /^(?!\/)(?!.*(^|\/)\.\.?(\/|$))[A-Za-z0-9._/-]{1,255}$/;
|
|
16
|
+
export const METHOD = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
|
|
17
|
+
const CARGO_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
18
|
+
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
19
|
+
const TRANSFER_FIELDS = ["transfers", "max_upload_bytes", "timeout_seconds"];
|
|
20
|
+
const FIELDS = {
|
|
21
|
+
python: ["runtime", "dir", "entry", "methods", "sdk", ...TRANSFER_FIELDS],
|
|
22
|
+
node: ["runtime", "dir", "entry", "methods", "sdk", "node", ...TRANSFER_FIELDS],
|
|
23
|
+
rust: ["runtime", "dir", "entry", "methods", ...TRANSFER_FIELDS],
|
|
24
|
+
};
|
|
25
|
+
/** filestation.MaxUploadBytes, the runtime's default and ceiling. */
|
|
26
|
+
export const MAX_UPLOAD_BYTES = 16 * 1024 ** 3;
|
|
27
|
+
const USAGE = {
|
|
28
|
+
python: "backend.runtime: python needs dir, entry (a .py inside the App directory) and sdk (the SDK version), with optional methods.",
|
|
29
|
+
node: "backend.runtime: node needs dir, entry (a module inside the App directory), sdk (the SDK version) and node.package (Node.js_v18, Node.js_v20 or Node.js_v22), with optional methods.",
|
|
30
|
+
rust: "backend.runtime: rust needs dir (a Cargo project), entry (the Cargo binary name) and methods (a cross-compiled binary cannot run on the build machine).",
|
|
31
|
+
};
|
|
32
|
+
/** Reads a python, node or rust backend; `dir` resolves a project-relative path, or reports and returns undefined. */
|
|
33
|
+
export function readBridgeBackend(value, dir, fail) {
|
|
34
|
+
const runtime = value.runtime;
|
|
35
|
+
const invalid = (why = "") => fail("STANDALONE_BACKEND_INVALID", `${USAGE[runtime]}${why}`);
|
|
36
|
+
const unknown = Object.keys(value).filter(key => !FIELDS[runtime].includes(key));
|
|
37
|
+
if (unknown.length)
|
|
38
|
+
return invalid(`Unsupported ${unknown.map(key => `backend.${key}`).join(", ")}.`);
|
|
39
|
+
const entry = value.entry;
|
|
40
|
+
if (typeof entry !== "string" || !(runtime === "rust" ? CARGO_NAME : ENTRY).test(entry))
|
|
41
|
+
return invalid();
|
|
42
|
+
const methods = value.methods;
|
|
43
|
+
if (methods !== undefined && !(Array.isArray(methods) && methods.length > 0 && methods.length <= 1024
|
|
44
|
+
&& methods.every(method => typeof method === "string" && METHOD.test(method)) && new Set(methods).size === methods.length)) {
|
|
45
|
+
return invalid("methods must be unique API method names (for example hello.greet), 1-1024 of them.");
|
|
46
|
+
}
|
|
47
|
+
if (runtime === "rust" && methods === undefined)
|
|
48
|
+
return invalid();
|
|
49
|
+
const sdk = value.sdk;
|
|
50
|
+
if (runtime !== "rust" && (typeof sdk !== "string" || !VERSION.test(sdk)))
|
|
51
|
+
return invalid();
|
|
52
|
+
const node = value.node;
|
|
53
|
+
if (runtime === "node" && !(node && typeof node === "object" && Object.keys(node).join() === "package" && NODE_PACKAGES.includes(node.package)))
|
|
54
|
+
return invalid();
|
|
55
|
+
const transfers = value.transfers;
|
|
56
|
+
if (transfers !== undefined && !(transfers && typeof transfers === "object" && !Array.isArray(transfers) && Object.keys(transfers).length > 0
|
|
57
|
+
&& Object.entries(transfers).every(([method, use]) => METHOD.test(method) && (use === "upload" || use === "download") && (!methods || methods.includes(method))))) {
|
|
58
|
+
return invalid("transfers is keyed by a method in methods, with the value upload or download.");
|
|
59
|
+
}
|
|
60
|
+
const maxUpload = value.max_upload_bytes;
|
|
61
|
+
if (maxUpload !== undefined && !(Number.isSafeInteger(maxUpload) && maxUpload >= 1 && maxUpload <= MAX_UPLOAD_BYTES)) {
|
|
62
|
+
return invalid("max_upload_bytes is an integer from 1 to 17179869184 (16 GiB).");
|
|
63
|
+
}
|
|
64
|
+
const timeout = value.timeout_seconds;
|
|
65
|
+
if (timeout !== undefined && !validTimeoutSeconds(timeout))
|
|
66
|
+
return invalid(TIMEOUT_USAGE);
|
|
67
|
+
const root = dir(value.dir);
|
|
68
|
+
if (!root)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (runtime !== "rust" && !isFileInside(root, entry))
|
|
71
|
+
return invalid(`entry not found: ${entry}.`);
|
|
72
|
+
return {
|
|
73
|
+
kind: runtime,
|
|
74
|
+
dir: root,
|
|
75
|
+
entry,
|
|
76
|
+
...(runtime !== "rust" ? { sdk: sdk } : {}),
|
|
77
|
+
...(methods ? { methods: methods } : {}),
|
|
78
|
+
...(runtime === "node" ? { node: { package: node.package } } : {}),
|
|
79
|
+
...(transfers ? { transfers: transfers } : {}),
|
|
80
|
+
...(maxUpload !== undefined ? { maxUploadBytes: maxUpload } : {}),
|
|
81
|
+
...(timeout !== undefined ? { timeoutSeconds: timeout } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isFileInside(root, entry) {
|
|
85
|
+
const path = join(root, entry);
|
|
86
|
+
return existsSync(path) && lstatSync(path).isFile();
|
|
87
|
+
}
|
|
88
|
+
/** Whether one directory is the other or holds it. */
|
|
89
|
+
export function overlaps(a, b) {
|
|
90
|
+
const inside = (outer, inner) => {
|
|
91
|
+
const rel = relative(outer, inner);
|
|
92
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
93
|
+
};
|
|
94
|
+
return inside(a, b) || inside(b, a);
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/** What every architecture shares: the methods and, for Python and Node, the staged App directory. */
|
|
5
|
+
export interface PreparedBridge {
|
|
6
|
+
methods: string[];
|
|
7
|
+
staged?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Stages a Python or Node App once; a Rust App is compiled per architecture in `buildBridge`. */
|
|
10
|
+
export declare function prepareBridge(m: StandaloneManifest, work: string): Promise<PreparedBridge | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* The runtime CGI of one architecture and the target/app directory whose
|
|
13
|
+
* digests it binds. bridge.json is per architecture: delegate pins the
|
|
14
|
+
* architecture's executor. With capabilities.jobs (delegate implies it) the
|
|
15
|
+
* same runtime is also the background worker, run with --worker.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildBridge(m: StandaloneManifest, prepared: PreparedBridge, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<{
|
|
18
|
+
cgi: string;
|
|
19
|
+
app: string;
|
|
20
|
+
worker?: string;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { copyFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { API_PERMISSIONS_FILE, readApiPermissions } from "./api-permissions.js";
|
|
5
|
+
import { copyAppTree, stageScriptApp } from "./bridge-app.js";
|
|
6
|
+
import { isBridgeBackend } from "./bridge-backend.js";
|
|
7
|
+
import { BRIDGE_JSON, bridgeManifestProblems, renderBridgeJson } from "./bridge-json.js";
|
|
8
|
+
import { buildRustApp } from "./bridge-rust.js";
|
|
9
|
+
import { buildBridgeRuntime } from "./bridge-runtime.js";
|
|
10
|
+
import { bridgeMethodProblems, sdkBackend } from "./manifest.js";
|
|
11
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
12
|
+
/** Stages a Python or Node App once; a Rust App is compiled per architecture in `buildBridge`. */
|
|
13
|
+
export async function prepareBridge(m, work) {
|
|
14
|
+
const backend = m.backend;
|
|
15
|
+
if (!isBridgeBackend(backend))
|
|
16
|
+
return undefined;
|
|
17
|
+
if (backend.kind === "rust")
|
|
18
|
+
return { methods: backend.methods };
|
|
19
|
+
const staged = await stageScriptApp(m, work);
|
|
20
|
+
return { methods: staged.methods, staged: staged.dir };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The runtime CGI of one architecture and the target/app directory whose
|
|
24
|
+
* digests it binds. bridge.json is per architecture: delegate pins the
|
|
25
|
+
* architecture's executor. With capabilities.jobs (delegate implies it) the
|
|
26
|
+
* same runtime is also the background worker, run with --worker.
|
|
27
|
+
*/
|
|
28
|
+
export async function buildBridge(m, prepared, arch, work, run) {
|
|
29
|
+
let app;
|
|
30
|
+
if (prepared.staged) {
|
|
31
|
+
app = join(work, "app");
|
|
32
|
+
copyAppTree(prepared.staged, app);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
app = await buildRustApp(m, arch, work, run);
|
|
36
|
+
}
|
|
37
|
+
const digests = finishApp(m, app, prepared.methods);
|
|
38
|
+
const cgi = await buildBridgeRuntime(m, arch, work, run, digests);
|
|
39
|
+
return { cgi, app, ...(m.capabilities.jobs ? { worker: cgi } : {}) };
|
|
40
|
+
}
|
|
41
|
+
/** Adds permissions.json and bridge.json to a staged App directory and returns the digests the runtime binds. */
|
|
42
|
+
function finishApp(m, dir, methods) {
|
|
43
|
+
const backend = m.backend;
|
|
44
|
+
const permissions = readApiPermissions(sdkBackend(m), m.capabilities);
|
|
45
|
+
// UI build hooks run after validation; the runtime must bind the policy the manifest checked.
|
|
46
|
+
if (permissions.digest !== m.apiPermissionsDigest)
|
|
47
|
+
throw new StandaloneBuildError("STANDALONE_API_PERMISSIONS_CHANGED", "API permissions changed during build; check and rebuild.");
|
|
48
|
+
const transfers = Object.keys(backend.transfers ?? {}).filter(method => !methods.includes(method));
|
|
49
|
+
const problems = [
|
|
50
|
+
...bridgeMethodProblems(methods, permissions, API_PERMISSIONS_FILE[backend.kind]),
|
|
51
|
+
...(transfers.length ? [`backend.transfers lists methods the App does not have: ${transfers.join(", ")}.`] : []),
|
|
52
|
+
];
|
|
53
|
+
if (problems.length)
|
|
54
|
+
throw new StandaloneBuildError("STANDALONE_BRIDGE_METHODS_MISMATCH", problems.join("\n"));
|
|
55
|
+
copyFileSync(join(backend.dir, API_PERMISSIONS_FILE[backend.kind]), join(dir, "permissions.json"));
|
|
56
|
+
const bridge = renderBridgeJson(m, methods);
|
|
57
|
+
const invalid = bridgeManifestProblems(bridge);
|
|
58
|
+
if (invalid.length)
|
|
59
|
+
throw new StandaloneBuildError("STANDALONE_BRIDGE_MANIFEST_INVALID", invalid.join("\n"));
|
|
60
|
+
writeFileSync(join(dir, BRIDGE_JSON), bridge, { mode: 0o644 });
|
|
61
|
+
return { manifest: createHash("sha256").update(bridge).digest("hex"), permissions: permissions.digest };
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StandaloneManifest } from "./manifest.js";
|
|
2
|
+
import type { BridgeBackend } from "./bridge-backend.js";
|
|
3
|
+
/** The App manifest the runtime reads: spec/bridge/v1 bridge.json, in target/app. */
|
|
4
|
+
export declare const BRIDGE_JSON = "bridge.json";
|
|
5
|
+
/** A copy of the SDK's frozen spec/bridge/v1/manifest.schema.json. */
|
|
6
|
+
export declare const BRIDGE_SCHEMA_PATH: string;
|
|
7
|
+
/** The vendored Python SDK's directory in target/app, the runtime's working directory, so PYTHONPATH is relative. */
|
|
8
|
+
export declare const PYTHON_VENDOR_DIR = "vendor";
|
|
9
|
+
/** Where a Rust App's binary goes in target/app. */
|
|
10
|
+
export declare const RUST_BIN_DIR = "bin";
|
|
11
|
+
/** bridge.json's entry: the script or module for python and node, `bin/<name>` for rust. */
|
|
12
|
+
export declare const bridgeEntry: (backend: BridgeBackend) => string;
|
|
13
|
+
/** bridge.json for these methods: sorted, two-space JSON, so the digest the runtime binds is reproducible. */
|
|
14
|
+
export declare function renderBridgeJson(m: Pick<StandaloneManifest, "package" | "capabilities" | "entry" | "backend" | "portal" | "executor">, methods: readonly string[]): string;
|
|
15
|
+
/** Why bridge.json would not validate against manifest.schema.json; empty when it does. */
|
|
16
|
+
export declare function bridgeManifestProblems(text: string): string[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
5
|
+
/** The App manifest the runtime reads: spec/bridge/v1 bridge.json, in target/app. */
|
|
6
|
+
export const BRIDGE_JSON = "bridge.json";
|
|
7
|
+
/** A copy of the SDK's frozen spec/bridge/v1/manifest.schema.json. */
|
|
8
|
+
export const BRIDGE_SCHEMA_PATH = fileURLToPath(new URL("../../schema/bridge-manifest.v1.schema.json", import.meta.url));
|
|
9
|
+
/** The vendored Python SDK's directory in target/app, the runtime's working directory, so PYTHONPATH is relative. */
|
|
10
|
+
export const PYTHON_VENDOR_DIR = "vendor";
|
|
11
|
+
/** Where a Rust App's binary goes in target/app. */
|
|
12
|
+
export const RUST_BIN_DIR = "bin";
|
|
13
|
+
/** bridge.json's entry: the script or module for python and node, `bin/<name>` for rust. */
|
|
14
|
+
export const bridgeEntry = (backend) => backend.kind === "rust" ? `${RUST_BIN_DIR}/${backend.entry}` : backend.entry;
|
|
15
|
+
/** bridge.json for these methods: sorted, two-space JSON, so the digest the runtime binds is reproducible. */
|
|
16
|
+
export function renderBridgeJson(m, methods) {
|
|
17
|
+
const { backend, capabilities } = m;
|
|
18
|
+
const delegate = capabilities.delegate;
|
|
19
|
+
const sdkRoutes = {
|
|
20
|
+
...(capabilities.files ? { files: "session" } : {}),
|
|
21
|
+
...(capabilities.identity ? { identity: "standalone" } : {}),
|
|
22
|
+
...(m.portal ? { portal: true } : {}),
|
|
23
|
+
...(capabilities.jobs ? { jobs: true } : {}),
|
|
24
|
+
...(capabilities.rootOps ? { root: true } : {}),
|
|
25
|
+
...(delegate ? { delegate: {
|
|
26
|
+
scope: delegate.scope,
|
|
27
|
+
destinations: [...delegate.destinations ?? []].sort(),
|
|
28
|
+
executor_digest: m.executor?.executorDigest,
|
|
29
|
+
policy_digest: m.executor?.policyDigest,
|
|
30
|
+
} } : {}),
|
|
31
|
+
};
|
|
32
|
+
return `${JSON.stringify({
|
|
33
|
+
schema: 1,
|
|
34
|
+
runtime: backend.kind,
|
|
35
|
+
entry: bridgeEntry(backend),
|
|
36
|
+
mode: "exec",
|
|
37
|
+
methods: [...methods].sort(),
|
|
38
|
+
...(m.entry ? { access: m.entry } : {}),
|
|
39
|
+
timeout_seconds: requestTimeoutSeconds(backend),
|
|
40
|
+
...(backend.kind === "python" ? { env: { PYTHONPATH: PYTHON_VENDOR_DIR } } : {}),
|
|
41
|
+
...(backend.node ? { node: { package: backend.node.package } } : {}),
|
|
42
|
+
...(backend.transfers ? { transfers: Object.fromEntries(Object.entries(backend.transfers).sort(([a], [b]) => a < b ? -1 : 1)) } : {}),
|
|
43
|
+
...(backend.maxUploadBytes ? { max_upload_bytes: backend.maxUploadBytes } : {}),
|
|
44
|
+
...(Object.keys(sdkRoutes).length ? { capabilities: sdkRoutes } : {}),
|
|
45
|
+
}, null, 2)}\n`;
|
|
46
|
+
}
|
|
47
|
+
let validator;
|
|
48
|
+
/** Why bridge.json would not validate against manifest.schema.json; empty when it does. */
|
|
49
|
+
export function bridgeManifestProblems(text) {
|
|
50
|
+
let value;
|
|
51
|
+
try {
|
|
52
|
+
value = JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return [`${BRIDGE_JSON} is not JSON: ${error.message}`];
|
|
56
|
+
}
|
|
57
|
+
validator ??= new Ajv2020({ allErrors: true, strict: false }).compile(JSON.parse(readFileSync(BRIDGE_SCHEMA_PATH, "utf8")));
|
|
58
|
+
return validator(value) ? [] : (validator.errors ?? []).map(e => `${BRIDGE_JSON}${e.instancePath || ""} ${e.message ?? e.keyword}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
/** The SDK's package CGI for an App in another language (spec/bridge/v1). */
|
|
5
|
+
export declare const RUNTIME_MAIN = "./bridge/cmd/runtime";
|
|
6
|
+
/** SHA-256 (hex) of the two files the runtime reads from target/app and refuses when they differ. */
|
|
7
|
+
export interface BridgeDigests {
|
|
8
|
+
manifest: string;
|
|
9
|
+
permissions: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds the runtime from the App's SDK release for one architecture, inside
|
|
13
|
+
* the SDK module (go build writes nothing there), with the pinned Go and the
|
|
14
|
+
* CLI's flags, so the same release and digests give the same bytes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildBridgeRuntime(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner, digests: BridgeDigests): Promise<string>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "../build-spk/go-toolchain.js";
|
|
3
|
+
import { requiredSdk, SDK_MODULE } from "../runtime/sdk-source.js";
|
|
4
|
+
import { checkCgiBinary } from "./binary.js";
|
|
5
|
+
import { sdkBackend } from "./manifest.js";
|
|
6
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
7
|
+
/** The SDK's package CGI for an App in another language (spec/bridge/v1). */
|
|
8
|
+
export const RUNTIME_MAIN = "./bridge/cmd/runtime";
|
|
9
|
+
/**
|
|
10
|
+
* Builds the runtime from the App's SDK release for one architecture, inside
|
|
11
|
+
* the SDK module (go build writes nothing there), with the pinned Go and the
|
|
12
|
+
* CLI's flags, so the same release and digests give the same bytes.
|
|
13
|
+
*/
|
|
14
|
+
export async function buildBridgeRuntime(m, arch, work, run, digests) {
|
|
15
|
+
let sdk;
|
|
16
|
+
try {
|
|
17
|
+
sdk = requiredSdk(sdkBackend(m));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new StandaloneBuildError("SDK_REQUIRED", error.message.replace(/^\[[A-Z_]+\] /, ""));
|
|
21
|
+
}
|
|
22
|
+
const out = join(work, `api-${arch}.cgi`);
|
|
23
|
+
const ldflags = [
|
|
24
|
+
"-s", "-w", "-buildid=",
|
|
25
|
+
`-X main.packageID=${m.package}`,
|
|
26
|
+
`-X main.manifestDigest=${digests.manifest}`,
|
|
27
|
+
`-X main.permissionsDigest=${digests.permissions}`,
|
|
28
|
+
`-X ${SDK_MODULE}/api.expectedSDKPermissionsDigest=${m.apiSDKPermissionsDigest}`,
|
|
29
|
+
].join(" ");
|
|
30
|
+
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, RUNTIME_MAIN], {
|
|
31
|
+
cwd: sdk.goDir,
|
|
32
|
+
env: { ...process.env, ...pinnedGoBuildEnv(arch) },
|
|
33
|
+
});
|
|
34
|
+
if (result.missing)
|
|
35
|
+
throw new StandaloneBuildError("STANDALONE_GO_MISSING", "The go command was not found. The Python, Node and Rust App runtime is compiled from the SDK's Go source; install Go 1.21 or later (https://go.dev/dl/).");
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
throw new StandaloneBuildError("STANDALONE_RUNTIME_BUILD_FAILED", `runtime (SDK ${sdk.version} ${RUNTIME_MAIN}, ${arch}) build failed; that SDK version must contain the bridge runtime. ${PINNED_GO_HINT}\n${result.stderr}`);
|
|
38
|
+
}
|
|
39
|
+
const problem = checkCgiBinary(work, out, arch);
|
|
40
|
+
if (problem)
|
|
41
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
42
|
+
const toolchain = pinnedGoProblem(out);
|
|
43
|
+
if (toolchain)
|
|
44
|
+
throw new StandaloneBuildError("STANDALONE_RUNTIME_BUILD_FAILED", toolchain);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
/** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust App may name. */
|
|
5
|
+
export declare const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
|
|
6
|
+
/**
|
|
7
|
+
* x86_64 and aarch64 are static musl binaries. armv7 links against glibc
|
|
8
|
+
* (2.17, older than any DSM 7), because musl's 64-bit time calls fail on
|
|
9
|
+
* DSM's ARMv7 kernels; zig provides both, so any host can build all three.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RUST_TARGETS: Record<NativeArchFamily, {
|
|
12
|
+
triple: string;
|
|
13
|
+
interpreter?: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Cross-compiles the App's binary for one architecture into `work/app-<arch>/bin`; returns that App directory. */
|
|
16
|
+
export declare function buildRustApp(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string>;
|