@synopackageland/cli 0.7.2 → 0.8.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.
- package/CHANGELOG.md +160 -0
- package/README.md +1 -1
- package/dist/build-spk/gateway.d.ts +2 -2
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.d.ts +1 -0
- package/dist/build-spk/generator.js +2 -2
- package/dist/build-spk/ui-texts.d.ts +3 -0
- package/dist/build-spk/ui-texts.js +7 -3
- package/dist/cli.js +107 -43
- package/dist/contract/web-launch.js +1 -4
- package/dist/error-registry.js +6 -1
- package/dist/reproducible-config.d.ts +8 -0
- package/dist/reproducible-config.js +32 -0
- package/dist/reproducible.d.ts +38 -0
- package/dist/reproducible.js +121 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +3 -3
- package/dist/spk/check-info.d.ts +5 -0
- package/dist/spk/check-info.js +123 -0
- package/dist/spk/check-metadata.d.ts +7 -0
- package/dist/spk/check-metadata.js +113 -0
- package/dist/spk/check-payload.d.ts +2 -0
- package/dist/spk/check-payload.js +156 -0
- package/dist/spk/dsm-spec.d.ts +42 -0
- package/dist/spk/dsm-spec.js +86 -0
- package/dist/spk/findings.d.ts +13 -0
- package/dist/spk/findings.js +8 -0
- package/dist/spk/inspect.d.ts +18 -0
- package/dist/spk/inspect.js +62 -0
- package/dist/spk/rules.d.ts +265 -0
- package/dist/spk/rules.js +53 -0
- package/dist/spk/validate-spk.d.ts +13 -0
- package/dist/spk/validate-spk.js +107 -0
- package/dist/standalone/arch-jobs.d.ts +11 -0
- package/dist/standalone/arch-jobs.js +54 -0
- package/dist/standalone/build.d.ts +11 -2
- package/dist/standalone/build.js +74 -31
- package/dist/standalone/daemon.d.ts +1 -1
- package/dist/standalone/daemon.js +2 -2
- package/dist/standalone/dsm-window.js +4 -5
- package/dist/standalone/inspect-spk.d.ts +1 -0
- package/dist/standalone/inspect-spk.js +1 -1
- package/dist/standalone/manifest.js +10 -1
- package/dist/standalone/native-layout.d.ts +3 -0
- package/dist/standalone/native-layout.js +94 -33
- package/package.json +1 -1
- package/schema/synology-app.schema.json +9 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `--jobs`: how many architectures build at once; 1 keeps the sequential build. */
|
|
2
|
+
export declare function checkJobs(jobs?: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Builds each architecture, at most `jobs` at a time, and returns the results in
|
|
5
|
+
* `arches` order. After a failure no further architecture starts; running ones
|
|
6
|
+
* finish so they clean up. The first failure in `arches` order is rethrown with
|
|
7
|
+
* `[arch]` in front of its message.
|
|
8
|
+
*/
|
|
9
|
+
export declare function forEachArch<A extends string, T>(arches: readonly A[], jobs: number, build: (arch: A) => Promise<T>): Promise<T[]>;
|
|
10
|
+
/** Builds every SPK in a private folder inside `outDir` and moves them into `outDir` only when all of them succeed. */
|
|
11
|
+
export declare function commitOutputs(outDir: string, build: (stage: string) => Promise<string[]>): Promise<string[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
/** `--jobs`: how many architectures build at once; 1 keeps the sequential build. */
|
|
4
|
+
export function checkJobs(jobs = 1) {
|
|
5
|
+
if (!Number.isSafeInteger(jobs) || jobs < 1) {
|
|
6
|
+
throw Object.assign(new Error(`[CLI_JOBS_INVALID] --jobs must be a positive integer, got ${jobs}.`), { code: "CLI_JOBS_INVALID" });
|
|
7
|
+
}
|
|
8
|
+
return jobs;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Builds each architecture, at most `jobs` at a time, and returns the results in
|
|
12
|
+
* `arches` order. After a failure no further architecture starts; running ones
|
|
13
|
+
* finish so they clean up. The first failure in `arches` order is rethrown with
|
|
14
|
+
* `[arch]` in front of its message.
|
|
15
|
+
*/
|
|
16
|
+
export async function forEachArch(arches, jobs, build) {
|
|
17
|
+
const results = [];
|
|
18
|
+
const failures = [];
|
|
19
|
+
let next = 0;
|
|
20
|
+
const worker = async () => {
|
|
21
|
+
while (next < arches.length && !failures.length) {
|
|
22
|
+
const index = next++;
|
|
23
|
+
try {
|
|
24
|
+
results[index] = await build(arches[index]);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
failures.push({ index, error });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
await Promise.all(Array.from({ length: Math.min(checkJobs(jobs), arches.length) }, worker));
|
|
32
|
+
const [first] = failures.sort((a, b) => a.index - b.index);
|
|
33
|
+
if (first) {
|
|
34
|
+
if (first.error instanceof Error)
|
|
35
|
+
first.error.message = `[${arches[first.index]}] ${first.error.message}`;
|
|
36
|
+
throw first.error;
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
/** Builds every SPK in a private folder inside `outDir` and moves them into `outDir` only when all of them succeed. */
|
|
41
|
+
export async function commitOutputs(outDir, build) {
|
|
42
|
+
mkdirSync(outDir, { recursive: true });
|
|
43
|
+
const stage = mkdtempSync(join(outDir, ".synopkgland-build-"));
|
|
44
|
+
try {
|
|
45
|
+
return (await build(stage)).map((staged) => {
|
|
46
|
+
const output = join(outDir, basename(staged));
|
|
47
|
+
renameSync(staged, output);
|
|
48
|
+
return output;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
rmSync(stage, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -4,11 +4,14 @@ export interface CommandResult {
|
|
|
4
4
|
stderr: string;
|
|
5
5
|
/** The program itself was not found. */
|
|
6
6
|
missing?: boolean;
|
|
7
|
+
/** Standard output, when the caller asked to capture it. */
|
|
8
|
+
stdout?: string;
|
|
7
9
|
}
|
|
8
10
|
export type CommandRunner = (command: string, args: string[], options: {
|
|
9
11
|
cwd: string;
|
|
10
12
|
env: Record<string, string | undefined>;
|
|
11
|
-
|
|
13
|
+
capture?: boolean;
|
|
14
|
+
}) => CommandResult | Promise<CommandResult>;
|
|
12
15
|
export interface StandaloneBuildOptions {
|
|
13
16
|
root: string;
|
|
14
17
|
/** Defaults to every arch in the manifest. */
|
|
@@ -17,10 +20,16 @@ export interface StandaloneBuildOptions {
|
|
|
17
20
|
outDir?: string;
|
|
18
21
|
/** INFO os_min_ver for every SPK (a release such as 7.1, or a full 7.x-build); defaults to the manifest's. */
|
|
19
22
|
minDsm?: string;
|
|
23
|
+
/** Architectures built at once; defaults to 1. */
|
|
24
|
+
jobs?: number;
|
|
20
25
|
run?: CommandRunner;
|
|
21
26
|
}
|
|
22
|
-
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept. */
|
|
27
|
+
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
|
|
23
28
|
export declare const runCommand: CommandRunner;
|
|
29
|
+
/** `runCommand` for concurrent builds: every output line goes to stderr behind `prefix`, so architectures stay apart. */
|
|
30
|
+
export declare function prefixedRunner(prefix: string): CommandRunner;
|
|
31
|
+
/** The runner one architecture uses: the caller's, or a labelled one while several architectures share the terminal. */
|
|
32
|
+
export declare function runnerFor(run: CommandRunner | undefined, jobs: number, arch: string): CommandRunner;
|
|
24
33
|
/** `build` for a Standalone Package: build the UI, compile the CGI, pack one SPK per arch. */
|
|
25
34
|
export declare function buildStandalone(options: StandaloneBuildOptions): Promise<{
|
|
26
35
|
outputs: string[];
|
package/dist/standalone/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { goBuildEnv } from "../build-spk/dsm-arch.js";
|
|
@@ -11,20 +11,61 @@ import { buildDaemonControl } from "./daemon.js";
|
|
|
11
11
|
import { buildExecutor } from "../runtime/executor-build.js";
|
|
12
12
|
import { BACKGROUND_WORKER_MAIN } from "./background.js";
|
|
13
13
|
import { readApiPermissions } from "./api-permissions.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import { checkJobs, commitOutputs, forEachArch } from "./arch-jobs.js";
|
|
15
|
+
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
|
|
16
|
+
export const runCommand = (command, args, options) => new Promise((done) => {
|
|
17
|
+
const child = spawn(command, args, {
|
|
17
18
|
cwd: options.cwd,
|
|
18
19
|
env: options.env,
|
|
19
|
-
stdio: ["ignore", process.stderr, "pipe"],
|
|
20
|
-
encoding: "utf8",
|
|
20
|
+
stdio: ["ignore", options.capture ? "pipe" : process.stderr, "pipe"],
|
|
21
21
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
let stderr = "";
|
|
23
|
+
let stdout = "";
|
|
24
|
+
child.stdout?.setEncoding("utf8");
|
|
25
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
26
|
+
child.stderr.setEncoding("utf8");
|
|
27
|
+
child.stderr.on("data", (chunk) => {
|
|
28
|
+
stderr += chunk;
|
|
29
|
+
process.stderr.write(chunk);
|
|
30
|
+
});
|
|
31
|
+
child.on("error", (error) => done({ status: null, stderr, missing: error.code === "ENOENT" }));
|
|
32
|
+
child.on("close", (status) => done({ status, stderr, missing: false, ...(options.capture ? { stdout } : {}) }));
|
|
33
|
+
});
|
|
34
|
+
/** `runCommand` for concurrent builds: every output line goes to stderr behind `prefix`, so architectures stay apart. */
|
|
35
|
+
export function prefixedRunner(prefix) {
|
|
36
|
+
return (command, args, options) => new Promise((done) => {
|
|
37
|
+
const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
38
|
+
let stderr = "";
|
|
39
|
+
let stdout = "";
|
|
40
|
+
if (options.capture) {
|
|
41
|
+
child.stdout.setEncoding("utf8");
|
|
42
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
43
|
+
}
|
|
44
|
+
const relay = (stream, keep) => {
|
|
45
|
+
let partial = "";
|
|
46
|
+
stream.setEncoding("utf8");
|
|
47
|
+
stream.on("data", (chunk) => {
|
|
48
|
+
if (keep)
|
|
49
|
+
stderr += chunk;
|
|
50
|
+
const lines = (partial + chunk).split("\n");
|
|
51
|
+
partial = lines.pop();
|
|
52
|
+
for (const line of lines)
|
|
53
|
+
process.stderr.write(`${prefix}${line}\n`);
|
|
54
|
+
});
|
|
55
|
+
stream.on("end", () => { if (partial)
|
|
56
|
+
process.stderr.write(`${prefix}${partial}\n`); });
|
|
57
|
+
};
|
|
58
|
+
if (!options.capture)
|
|
59
|
+
relay(child.stdout, false);
|
|
60
|
+
relay(child.stderr, true);
|
|
61
|
+
child.on("error", (error) => done({ status: null, stderr, missing: error.code === "ENOENT" }));
|
|
62
|
+
child.on("close", (status) => done({ status, stderr, missing: false, ...(options.capture ? { stdout } : {}) }));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** The runner one architecture uses: the caller's, or a labelled one while several architectures share the terminal. */
|
|
66
|
+
export function runnerFor(run, jobs, arch) {
|
|
67
|
+
return run ?? (jobs > 1 ? prefixedRunner(`[${arch}] `) : runCommand);
|
|
68
|
+
}
|
|
28
69
|
/** `build` for a Standalone Package: build the UI, compile the CGI, pack one SPK per arch. */
|
|
29
70
|
export async function buildStandalone(options) {
|
|
30
71
|
const loaded = loadStandaloneManifest(options.root);
|
|
@@ -41,45 +82,47 @@ export async function buildStandalone(options) {
|
|
|
41
82
|
if (manifest.portal && !isDsmAtLeast(manifest.os_min_ver, PORTAL_MIN_DSM)) {
|
|
42
83
|
throw new StandaloneBuildError("STANDALONE_PORTAL_MIN_DSM", `[STANDALONE_PORTAL_MIN_DSM] ${portalMinDsmMessage(manifest.os_min_ver, "portal")}`);
|
|
43
84
|
}
|
|
44
|
-
const
|
|
85
|
+
const jobs = checkJobs(options.jobs);
|
|
45
86
|
const arches = options.arches ?? manifest.arches;
|
|
46
87
|
for (const arch of arches) {
|
|
47
88
|
if (!manifest.arches.includes(arch)) {
|
|
48
89
|
throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch} 不在 dsm-package.yaml 的 arches 內;要建置它,請先把 ${arch} 加進 arches。`);
|
|
49
90
|
}
|
|
50
91
|
}
|
|
51
|
-
buildUi(manifest, run);
|
|
92
|
+
await buildUi(manifest, options.run ?? runCommand);
|
|
52
93
|
const outDir = options.outDir ?? join(options.root, "dist");
|
|
53
94
|
const work = mkdtempSync(join(tmpdir(), "syno-standalone-build-"));
|
|
54
95
|
try {
|
|
55
|
-
const outputs =
|
|
56
|
-
|
|
57
|
-
const
|
|
96
|
+
const outputs = await commitOutputs(outDir, (stage) => forEachArch(arches, jobs, async (arch) => {
|
|
97
|
+
const run = runnerFor(options.run, jobs, arch);
|
|
98
|
+
const archWork = join(work, arch);
|
|
99
|
+
mkdirSync(archWork);
|
|
100
|
+
const executor = await buildExecutor(manifest, arch, archWork, run);
|
|
58
101
|
const builtManifest = executor ? { ...manifest, executor } : manifest;
|
|
59
|
-
const cgiPath = backendFor(builtManifest, arch,
|
|
60
|
-
const problem = checkCgiBinary(manifest.backend.kind === "go" ?
|
|
102
|
+
const cgiPath = await backendFor(builtManifest, arch, archWork, run);
|
|
103
|
+
const problem = checkCgiBinary(manifest.backend.kind === "go" ? archWork : manifest.root, cgiPath, arch);
|
|
61
104
|
if (problem) {
|
|
62
105
|
throw new StandaloneBuildError(problem.code, problem.message);
|
|
63
106
|
}
|
|
64
|
-
const outputPath = join(
|
|
65
|
-
const daemonControl = buildDaemonControl(manifest, arch,
|
|
66
|
-
const backgroundWorker = buildBackgroundWorker(builtManifest, arch,
|
|
107
|
+
const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
108
|
+
const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
|
|
109
|
+
const backgroundWorker = await buildBackgroundWorker(builtManifest, arch, archWork, run);
|
|
67
110
|
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor });
|
|
68
|
-
|
|
69
|
-
}
|
|
111
|
+
return outputPath;
|
|
112
|
+
}));
|
|
70
113
|
return { outputs };
|
|
71
114
|
}
|
|
72
115
|
finally {
|
|
73
116
|
rmSync(work, { recursive: true, force: true });
|
|
74
117
|
}
|
|
75
118
|
}
|
|
76
|
-
function buildUi(manifest, run) {
|
|
119
|
+
async function buildUi(manifest, run) {
|
|
77
120
|
const build = manifest.ui.build;
|
|
78
121
|
if (!build) {
|
|
79
122
|
return;
|
|
80
123
|
}
|
|
81
124
|
const [command, ...args] = build.command;
|
|
82
|
-
const result = run(command, args, { cwd: build.cwd, env: { ...process.env } });
|
|
125
|
+
const result = await run(command, args, { cwd: build.cwd, env: { ...process.env } });
|
|
83
126
|
if (result.missing || result.status !== 0) {
|
|
84
127
|
const installHint = command === "npm" && existsSync(join(build.cwd, "package.json")) && !existsSync(join(build.cwd, "node_modules"))
|
|
85
128
|
? `\nUI 依賴尚未安裝。請在 ${build.cwd} 執行 npm ${existsSync(join(build.cwd, "package-lock.json")) ? "ci" : "install"},然後重新執行 build。`
|
|
@@ -87,7 +130,7 @@ function buildUi(manifest, run) {
|
|
|
87
130
|
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。${installHint}\n${result.stderr}`);
|
|
88
131
|
}
|
|
89
132
|
}
|
|
90
|
-
function backendFor(manifest, arch, work, run) {
|
|
133
|
+
async function backendFor(manifest, arch, work, run) {
|
|
91
134
|
const backend = manifest.backend;
|
|
92
135
|
if (backend.kind === "binary") {
|
|
93
136
|
return backend.files[arch];
|
|
@@ -113,7 +156,7 @@ function backendFor(manifest, arch, work, run) {
|
|
|
113
156
|
...(manifest.entry ? [`-X main.entry=${manifest.entry}`] : []),
|
|
114
157
|
...(manifest.portal ? ["-X main.portal=on"] : []),
|
|
115
158
|
].join(" ");
|
|
116
|
-
const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
|
|
159
|
+
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
|
|
117
160
|
cwd: backend.dir,
|
|
118
161
|
env: { ...process.env, ...goBuildEnv(arch) },
|
|
119
162
|
});
|
|
@@ -126,14 +169,14 @@ function backendFor(manifest, arch, work, run) {
|
|
|
126
169
|
return out;
|
|
127
170
|
}
|
|
128
171
|
/** The fixed worker is a second binary of the project's own Go module. */
|
|
129
|
-
function buildBackgroundWorker(manifest, arch, work, run) {
|
|
172
|
+
async function buildBackgroundWorker(manifest, arch, work, run) {
|
|
130
173
|
if (!manifest.capabilities.jobs || manifest.backend.kind !== "go")
|
|
131
174
|
return undefined;
|
|
132
175
|
const out = join(work, `background-worker-${arch}`);
|
|
133
176
|
const ldflags = ["-s", "-w", "-buildid=", `-X main.packageID=${manifest.package}`, ...delegateBuildFlags(manifest),
|
|
134
177
|
...(manifest.executor ? [`-X main.executorDigest=${manifest.executor.executorDigest}`, `-X main.policyDigest=${manifest.executor.policyDigest}`] : []),
|
|
135
178
|
].join(" ");
|
|
136
|
-
const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, BACKGROUND_WORKER_MAIN], {
|
|
179
|
+
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, BACKGROUND_WORKER_MAIN], {
|
|
137
180
|
cwd: manifest.backend.dir,
|
|
138
181
|
env: { ...process.env, ...goBuildEnv(arch) },
|
|
139
182
|
});
|
|
@@ -20,5 +20,5 @@ export interface Daemon {
|
|
|
20
20
|
}
|
|
21
21
|
export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
|
|
22
22
|
export declare function daemonConfig(manifest: StandaloneManifest): string;
|
|
23
|
-
export declare function buildDaemonControl(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): string | undefined
|
|
23
|
+
export declare function buildDaemonControl(manifest: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<string | undefined>;
|
|
24
24
|
export declare function copyDaemonPayload(manifest: StandaloneManifest, arch: NativeArchFamily, target: string, controller: string): void;
|
|
@@ -55,11 +55,11 @@ export function daemonConfig(manifest) {
|
|
|
55
55
|
const { exec, args, uninstall_args, env, timeouts } = manifest.daemon;
|
|
56
56
|
return JSON.stringify({ package: manifest.package, exec, args, ...(uninstall_args ? { uninstall_args } : {}), env, timeouts }) + "\n";
|
|
57
57
|
}
|
|
58
|
-
export function buildDaemonControl(manifest, arch, work, run) {
|
|
58
|
+
export async function buildDaemonControl(manifest, arch, work, run) {
|
|
59
59
|
if (!manifest.daemon)
|
|
60
60
|
return undefined;
|
|
61
61
|
const out = join(work, `daemon-control-${arch}`);
|
|
62
|
-
const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", "-s -w -buildid=", "-o", out, "."], {
|
|
62
|
+
const result = await run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", "-s -w -buildid=", "-o", out, "."], {
|
|
63
63
|
cwd: join(templatesRoot(), "_daemon"), env: { ...process.env, ...pinnedGoBuildEnv(arch) },
|
|
64
64
|
});
|
|
65
65
|
if (result.missing || result.status !== 0)
|
|
@@ -69,14 +69,12 @@ export function appTitle(manifest) {
|
|
|
69
69
|
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
70
70
|
export function renderUiConfig(manifest) {
|
|
71
71
|
const ns = `SYNO.SDS.${manifest.package}`;
|
|
72
|
-
const
|
|
72
|
+
const titleFields = uiTitleFields(appTitle(manifest));
|
|
73
73
|
return `${JSON.stringify({
|
|
74
74
|
[windowScriptName(manifest)]: {
|
|
75
75
|
[`${ns}.Instance`]: {
|
|
76
76
|
type: "app",
|
|
77
|
-
|
|
78
|
-
desc,
|
|
79
|
-
...(preloadTexts ? { preloadTexts } : {}),
|
|
77
|
+
...titleFields,
|
|
80
78
|
icon: "images/app_{0}.png",
|
|
81
79
|
appWindow: `${ns}.MainWindow`,
|
|
82
80
|
allowMultiInstance: false,
|
|
@@ -85,7 +83,8 @@ export function renderUiConfig(manifest) {
|
|
|
85
83
|
},
|
|
86
84
|
[`${ns}.MainWindow`]: {
|
|
87
85
|
type: "lib",
|
|
88
|
-
title,
|
|
86
|
+
title: titleFields.title,
|
|
87
|
+
texts: titleFields.texts,
|
|
89
88
|
icon: "images/app_{0}.png",
|
|
90
89
|
depend: [],
|
|
91
90
|
},
|
|
@@ -40,3 +40,4 @@ export interface SpkInspection {
|
|
|
40
40
|
export declare function inspectStandaloneSpk(spkPath: string): Promise<SpkInspection>;
|
|
41
41
|
/** Auto-detect the generated runtime; retain strict native-go inspection when requested. */
|
|
42
42
|
export declare function inspectSpk(spkPath: string, expectedKind?: SpkKind): Promise<SpkInspection>;
|
|
43
|
+
export declare function listPayload(packageTgz: string, findings: string[]): Promise<SpkInspection["payload"]>;
|
|
@@ -509,7 +509,7 @@ function checkUninstall(dir, findings) {
|
|
|
509
509
|
findings.push(`UNINSTALL_WIZARD_NOT_HONOURED WIZARD_UIFILES/${box.name} 提供 ${box.key} 勾選框,但 scripts/postuninst 沒有讀它`);
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
|
-
async function listPayload(packageTgz, findings) {
|
|
512
|
+
export async function listPayload(packageTgz, findings) {
|
|
513
513
|
if (!existsSync(packageTgz)) {
|
|
514
514
|
findings.push("缺少 package.tgz");
|
|
515
515
|
return [];
|
|
@@ -13,6 +13,7 @@ import { readSharedFolder } from "./shared-folder.js";
|
|
|
13
13
|
import { floorAtAdministrators, permitsOrdinaryUsers, readApiPermissions } from "./api-permissions.js";
|
|
14
14
|
import { readPortalDeclaration } from "../portal/alias.js";
|
|
15
15
|
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
16
|
+
import { validateReproducible } from "../reproducible-config.js";
|
|
16
17
|
/**
|
|
17
18
|
* `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
|
|
18
19
|
* runs as its own DSM package account with a CGI backend and a DSM window UI.
|
|
@@ -20,7 +21,7 @@ import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
|
20
21
|
export const STANDALONE_MANIFEST = "dsm-package.yaml";
|
|
21
22
|
export const KNOWN_FIELDS = new Set([
|
|
22
23
|
"schema", "package", "version", "build", "displayname", "description",
|
|
23
|
-
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "entry", "portal", "icon",
|
|
24
|
+
"metadata", "maintainer", "os_min_ver", "arches", "backend", "ui", "capabilities", "i18n", "daemon", "install", "entry", "portal", "icon", "reproducible",
|
|
24
25
|
]);
|
|
25
26
|
const I18N_FIELDS = new Set(["messages", "glossary", "info"]);
|
|
26
27
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
@@ -87,6 +88,14 @@ function readManifest(raw, c) {
|
|
|
87
88
|
c.fail("STANDALONE_FIELD_UNKNOWN", `不支援的欄位「${key}」。Standalone Package 不接受自訂 lifecycle scripts 或 DSM conf。`);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
if (raw.reproducible !== undefined) {
|
|
92
|
+
try {
|
|
93
|
+
validateReproducible(raw.reproducible);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
c.fail("REPRODUCIBLE_INVALID", error.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
90
99
|
if (raw.schema !== 2) {
|
|
91
100
|
c.fail("STANDALONE_SCHEMA_INVALID", "schema: 2 必填;請改成 schema: 2、backend.kind: go|binary,並將 privilege: root、background、fileStation 分別改為 capabilities.rootOps: true、capabilities.jobs: true、capabilities.files: session。");
|
|
92
101
|
}
|
|
@@ -63,6 +63,8 @@ export declare function loadNativeLayout(root: string): {
|
|
|
63
63
|
}[];
|
|
64
64
|
};
|
|
65
65
|
export declare function verifyToolkitArchive(path: string, expected: string): void;
|
|
66
|
+
/** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
|
|
67
|
+
export declare function toolkitCacheDir(env: Record<string, string | undefined>, platform: string, home: string): string;
|
|
66
68
|
export declare function layoutFiles(root: string): Array<{
|
|
67
69
|
path: string;
|
|
68
70
|
mode: number;
|
|
@@ -76,6 +78,7 @@ export declare function buildNativeLayout(options: {
|
|
|
76
78
|
outDir?: string;
|
|
77
79
|
build?: number;
|
|
78
80
|
minDsm?: string;
|
|
81
|
+
jobs?: number;
|
|
79
82
|
run?: CommandRunner;
|
|
80
83
|
}): Promise<{
|
|
81
84
|
outputs: string[];
|
|
@@ -2,14 +2,20 @@ 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
4
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
5
|
+
import { homedir, 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
|
-
import { runCommand } from './build.js';
|
|
9
|
+
import { runCommand, runnerFor } from './build.js';
|
|
10
|
+
import { checkJobs, commitOutputs, forEachArch } from './arch-jobs.js';
|
|
11
|
+
import { validateReproducible } from '../reproducible-config.js';
|
|
10
12
|
import { resolveMinDsm } from '../build-spk/dsm-version.js';
|
|
13
|
+
import { infoArchValue } from '../build-spk/dsm-arch.js';
|
|
14
|
+
import { LIFECYCLE_SCRIPTS, REPLACE_SCRIPTS } from '../spk/dsm-spec.js';
|
|
11
15
|
import { renderPackageIcons, writePackageCenterIcons } from '../build-spk/dsm-icons.js';
|
|
12
16
|
export const LAYOUT_ARCHES = { x86_64: [2, 62], aarch64: [2, 183], armv8: [2, 183], armv7: [1, 40] };
|
|
17
|
+
/** INFO arch: aarch64 is not an Appendix A value, so it lists the platforms as native-go and binary do. */
|
|
18
|
+
const layoutInfoArch = (arch) => arch === 'aarch64' ? infoArchValue('aarch64') : arch;
|
|
13
19
|
function fail(message) { throw Object.assign(new Error(`NATIVE_LAYOUT_INVALID: ${message}`), { code: 'NATIVE_LAYOUT_INVALID' }); }
|
|
14
20
|
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
15
21
|
function object(value, fields) {
|
|
@@ -84,7 +90,7 @@ export function isNativeLayout(root) {
|
|
|
84
90
|
}
|
|
85
91
|
export function loadNativeLayout(root) {
|
|
86
92
|
try {
|
|
87
|
-
const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native', 'icon']);
|
|
93
|
+
const m = object(parse(readFileSync(join(root, 'dsm-package.yaml'), 'utf8')), ['schema', 'backend', 'package', 'version', 'build', 'displayname', 'description', 'maintainer', 'metadata', 'os_min_ver', 'privilege', 'arches', 'layout', 'native', 'icon', 'reproducible']);
|
|
88
94
|
if (m.schema !== 2)
|
|
89
95
|
fail('schema 2 required; migrate schema: 1 to schema: 2 and add backend: {kind: native}');
|
|
90
96
|
const backend = object(m.backend, ['kind']);
|
|
@@ -105,6 +111,8 @@ export function loadNativeLayout(root) {
|
|
|
105
111
|
fail('invalid architecture list');
|
|
106
112
|
if (m.icon !== undefined)
|
|
107
113
|
m.icon = icon(root, m.icon);
|
|
114
|
+
if (m.reproducible !== undefined)
|
|
115
|
+
validateReproducible(m.reproducible);
|
|
108
116
|
const layout = object(m.layout, ['payload', 'metadata', 'binaries', 'anchors']);
|
|
109
117
|
layout.payload = pathText(layout.payload);
|
|
110
118
|
layout.metadata = pathText(layout.metadata);
|
|
@@ -142,11 +150,37 @@ export function verifyToolkitArchive(path, expected) {
|
|
|
142
150
|
if (!lstatSync(path).isFile() || digest(readFileSync(path)) !== expected)
|
|
143
151
|
fail('toolkit checksum mismatch');
|
|
144
152
|
}
|
|
153
|
+
/** One cache per user, shared by every project and worktree; archives are checked against sha256 on every use. */
|
|
154
|
+
export function toolkitCacheDir(env, platform, home) {
|
|
155
|
+
if (env.SYNOPKGLAND_CACHE_DIR)
|
|
156
|
+
return resolve(env.SYNOPKGLAND_CACHE_DIR);
|
|
157
|
+
if (platform === 'darwin')
|
|
158
|
+
return join(home, 'Library', 'Caches', 'synopkgland');
|
|
159
|
+
const xdg = env.XDG_CACHE_HOME;
|
|
160
|
+
return join(xdg && isAbsolute(xdg) ? xdg : join(home, '.cache'), 'synopkgland');
|
|
161
|
+
}
|
|
162
|
+
// Concurrent architectures that pin the same archive share one download.
|
|
163
|
+
const downloads = new Map();
|
|
164
|
+
async function download(m, cache, input, archive, run) {
|
|
165
|
+
const tmp = mkdtempSync(join(cache, '.download-'));
|
|
166
|
+
try {
|
|
167
|
+
const out = join(tmp, 'archive');
|
|
168
|
+
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 } });
|
|
169
|
+
if (r.status !== 0)
|
|
170
|
+
fail('toolkit download failed');
|
|
171
|
+
// Rename is atomic, so another build process sees either no archive or a verified one.
|
|
172
|
+
verifyToolkitArchive(out, input.sha256);
|
|
173
|
+
renameSync(out, archive);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
145
179
|
async function toolkitFor(m, arch, run) {
|
|
146
180
|
const inputs = m.native.toolkits[arch] ?? [];
|
|
147
181
|
if (!inputs.length)
|
|
148
182
|
return '';
|
|
149
|
-
const cache =
|
|
183
|
+
const cache = toolkitCacheDir(process.env, process.platform, homedir());
|
|
150
184
|
mkdirSync(cache, { recursive: true, mode: 0o700 });
|
|
151
185
|
if (lstatSync(cache).isSymbolicLink() || (lstatSync(cache).mode & 0o022))
|
|
152
186
|
fail('toolkit cache must be owned and private');
|
|
@@ -154,18 +188,12 @@ async function toolkitFor(m, arch, run) {
|
|
|
154
188
|
for (const input of inputs) {
|
|
155
189
|
const archive = join(cache, `${input.sha256}.txz`);
|
|
156
190
|
if (!existsSync(archive)) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (r.status !== 0)
|
|
162
|
-
fail('toolkit download failed');
|
|
163
|
-
verifyToolkitArchive(out, input.sha256);
|
|
164
|
-
renameSync(out, archive);
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
rmSync(download, { recursive: true, force: true });
|
|
191
|
+
let pending = downloads.get(archive);
|
|
192
|
+
if (!pending) {
|
|
193
|
+
pending = download(m, cache, input, archive, run).finally(() => downloads.delete(archive));
|
|
194
|
+
downloads.set(archive, pending);
|
|
168
195
|
}
|
|
196
|
+
await pending;
|
|
169
197
|
}
|
|
170
198
|
verifyToolkitArchive(archive, input.sha256);
|
|
171
199
|
archives.push(archive);
|
|
@@ -173,8 +201,11 @@ async function toolkitFor(m, arch, run) {
|
|
|
173
201
|
// Re-extract verified bytes: a package-writable marker never authenticates an extracted tree.
|
|
174
202
|
const stage = mkdtempSync(join(cache, `toolkit-${arch}-`));
|
|
175
203
|
try {
|
|
204
|
+
const flavor = await tarFlavor(m, run);
|
|
176
205
|
for (const archive of archives) {
|
|
177
|
-
|
|
206
|
+
if (flavor === 'bsdtar')
|
|
207
|
+
await refuseEscapingMembers(m, archive, run);
|
|
208
|
+
const r = await run('tar', flavor === 'gnu' ? ['-xJf', archive, '-C', stage, '--keep-directory-symlink'] : ['-xPJf', archive, '-C', stage], { cwd: m.root, env: { ...process.env } });
|
|
178
209
|
if (r.status !== 0)
|
|
179
210
|
fail('toolkit extraction failed');
|
|
180
211
|
}
|
|
@@ -185,9 +216,33 @@ async function toolkitFor(m, arch, run) {
|
|
|
185
216
|
throw e;
|
|
186
217
|
}
|
|
187
218
|
}
|
|
188
|
-
|
|
219
|
+
/**
|
|
220
|
+
* A later toolkit archive may write through a directory symlink an earlier one
|
|
221
|
+
* created. GNU tar keeps such a symlink only with --keep-directory-symlink;
|
|
222
|
+
* bsdtar only with -P, which also allows absolute and .. members, so those are
|
|
223
|
+
* refused from the listing first.
|
|
224
|
+
*/
|
|
225
|
+
async function tarFlavor(m, run) {
|
|
226
|
+
const r = await run('tar', ['--version'], { cwd: m.root, env: { ...process.env }, capture: true });
|
|
227
|
+
const version = r.status === 0 ? r.stdout ?? '' : '';
|
|
228
|
+
if (/\bGNU tar\b/.test(version))
|
|
229
|
+
return 'gnu';
|
|
230
|
+
if (/\bbsdtar\b/.test(version))
|
|
231
|
+
return 'bsdtar';
|
|
232
|
+
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' });
|
|
233
|
+
}
|
|
234
|
+
async function refuseEscapingMembers(m, archive, run) {
|
|
235
|
+
const r = await run('tar', ['-tJf', archive], { cwd: m.root, env: { ...process.env }, capture: true });
|
|
236
|
+
if (r.status !== 0)
|
|
237
|
+
fail('toolkit extraction failed');
|
|
238
|
+
for (const member of (r.stdout ?? '').split('\n').filter(Boolean)) {
|
|
239
|
+
if (member.startsWith('/') || member.split('/').includes('..'))
|
|
240
|
+
fail(`toolkit member escapes the toolkit folder: ${member}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function runHook(m, h, env, run) {
|
|
189
244
|
const [cmd, ...args] = h.command;
|
|
190
|
-
const r = run(cmd, args, { cwd: inside(m.root, h.cwd), env: { ...process.env, ...h.env, ...env } });
|
|
245
|
+
const r = await run(cmd, args, { cwd: inside(m.root, h.cwd), env: { ...process.env, ...h.env, ...env } });
|
|
191
246
|
if (r.status !== 0 || r.missing)
|
|
192
247
|
fail(`native hook failed: ${r.stderr}`);
|
|
193
248
|
}
|
|
@@ -255,6 +310,11 @@ export function checkLayoutMetadata(root, inspection = false, packageId) {
|
|
|
255
310
|
fail('native layout requires package-only lifecycle privilege');
|
|
256
311
|
if (!existsSync(join(root, 'scripts/start-stop-status')))
|
|
257
312
|
fail('start-stop-status missing');
|
|
313
|
+
for (const name of [...LIFECYCLE_SCRIPTS, ...REPLACE_SCRIPTS]) {
|
|
314
|
+
const path = join(root, 'scripts', name);
|
|
315
|
+
if (existsSync(path) && !(lstatSync(path).mode & 0o111))
|
|
316
|
+
throw Object.assign(new Error(`NATIVE_LAYOUT_SCRIPT_NOT_EXECUTABLE: scripts/${name} is not executable (mode ${(lstatSync(path).mode & 0o777).toString(8)}); DSM runs it directly and inspect reports SPK_SCRIPT_NOT_EXECUTABLE. Run chmod +x on it in the layout metadata folder and build again.`), { code: 'NATIVE_LAYOUT_SCRIPT_NOT_EXECUTABLE' });
|
|
317
|
+
}
|
|
258
318
|
if (existsSync(join(root, 'conf/resource'))) {
|
|
259
319
|
const resource = object(JSON.parse(readFileSync(join(root, 'conf/resource'), 'utf8')), ['systemd-unit']);
|
|
260
320
|
if (resource['systemd-unit'] !== undefined && Object.keys(object(resource['systemd-unit'], [])).length)
|
|
@@ -307,18 +367,20 @@ export async function buildNativeLayout(options) {
|
|
|
307
367
|
if (!arches.length || new Set(arches).size !== arches.length || arches.some(a => !m.arches.includes(a)))
|
|
308
368
|
fail('architecture not declared');
|
|
309
369
|
const icons = m.icon === undefined ? undefined : renderPackageIcons(inside(m.root, m.icon));
|
|
310
|
-
const
|
|
370
|
+
const jobs = checkJobs(options.jobs);
|
|
371
|
+
if (jobs > 1 && !m.layout.payload.includes('{arch}'))
|
|
372
|
+
fail('--jobs above 1 needs a layout.payload path with {arch}, so architectures do not overwrite one payload folder');
|
|
311
373
|
const version = `${m.version}-${String(m.build).padStart(4, '0')}`;
|
|
312
374
|
const env = { SYNOPKGLAND_PACKAGE: m.package, SYNOPKGLAND_VERSION: m.version, SYNOPKGLAND_BUILD: String(m.build), SYNOPKGLAND_SPK_VERSION: version, SYNOPKGLAND_PROJECT_DIR: m.root };
|
|
313
375
|
if (m.native.prepare)
|
|
314
|
-
runHook(m, m.native.prepare, env, run);
|
|
315
|
-
const outputs =
|
|
316
|
-
|
|
317
|
-
const arch = requested;
|
|
376
|
+
await runHook(m, m.native.prepare, env, options.run ?? runCommand);
|
|
377
|
+
const outputs = await commitOutputs(options.outDir ?? join(m.root, 'dist'), (outDir) => forEachArch(arches, jobs, async (arch) => {
|
|
378
|
+
const run = runnerFor(options.run, jobs, arch);
|
|
318
379
|
const toolkit = await toolkitFor(m, arch, run);
|
|
319
380
|
const stage = mkdtempSync(join(tmpdir(), 'synopkgland-layout-'));
|
|
381
|
+
const work = mkdtempSync(join(tmpdir(), 'synopkgland-work-'));
|
|
320
382
|
try {
|
|
321
|
-
runHook(m, m.native.hook, { ...env, SYNOPKGLAND_ARCH: arch, SYNOPKGLAND_TOOLKIT_DIR: toolkit }, run);
|
|
383
|
+
await runHook(m, m.native.hook, { ...env, SYNOPKGLAND_ARCH: arch, SYNOPKGLAND_TOOLKIT_DIR: toolkit, SYNOPKGLAND_WORK_DIR: work }, run);
|
|
322
384
|
const payload = inside(m.root, m.layout.payload.replaceAll('{arch}', arch));
|
|
323
385
|
const metadata = inside(m.root, m.layout.metadata);
|
|
324
386
|
checkLayoutMetadata(metadata, false, m.package);
|
|
@@ -339,23 +401,22 @@ export async function buildNativeLayout(options) {
|
|
|
339
401
|
const files = copyTree(payload, target);
|
|
340
402
|
await createReproducibleTar(target, join(stage, 'package.tgz'), { gzip: true });
|
|
341
403
|
rmSync(target, { recursive: true });
|
|
342
|
-
const fields = { package: m.package, version, arch, os_min_ver: m.os_min_ver, displayname: m.displayname, description: m.description, maintainer: m.maintainer, dsmuidir: 'ui', dsmappname: `SYNO.SDS.${m.package}.Instance`, startable: 'yes', thirdparty: 'yes', checksum: createHash('md5').update(readFileSync(join(stage, 'package.tgz'))).digest('hex') };
|
|
404
|
+
const fields = { package: m.package, version, arch: layoutInfoArch(arch), os_min_ver: m.os_min_ver, displayname: m.displayname, description: m.description, maintainer: m.maintainer, dsmuidir: 'ui', dsmappname: `SYNO.SDS.${m.package}.Instance`, startable: 'yes', thirdparty: 'yes', checksum: createHash('md5').update(readFileSync(join(stage, 'package.tgz'))).digest('hex') };
|
|
343
405
|
const info = [...Object.entries(fields), ...metadataInfoFields(m.metadata), ...anchorFields(m.layout.anchors ?? {}, files)];
|
|
344
406
|
if (new Set(info.map(([k]) => k)).size !== info.length)
|
|
345
407
|
fail('INFO anchor collides with an INFO field');
|
|
346
408
|
writeFileSync(join(stage, 'INFO'), info.map(([k, v]) => `${k}="${v.replace(/([\\"$`])/g, '\\$1')}"\n`).join(''));
|
|
347
409
|
writeFileSync(join(stage, 'native-layout.json'), JSON.stringify({ format: 1, arch, binaries: binaries.map(({ path, linkage }) => ({ path, linkage })), files, ...(m.layout.anchors ? { anchors: m.layout.anchors } : {}) }));
|
|
348
|
-
const output = join(
|
|
349
|
-
mkdirSync(dirname(output), { recursive: true });
|
|
410
|
+
const output = join(outDir, `${m.package}-${version}-${arch}.spk`);
|
|
350
411
|
await createReproducibleTar(stage, output);
|
|
351
|
-
|
|
412
|
+
return output;
|
|
352
413
|
}
|
|
353
414
|
finally {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
415
|
+
for (const dir of [stage, work, toolkit])
|
|
416
|
+
if (dir)
|
|
417
|
+
rmSync(dir, { recursive: true, force: true });
|
|
357
418
|
}
|
|
358
|
-
}
|
|
419
|
+
}));
|
|
359
420
|
return { outputs };
|
|
360
421
|
}
|
|
361
422
|
export function inspectNativeLayout(dir, payload, info) {
|
|
@@ -363,7 +424,7 @@ export function inspectNativeLayout(dir, payload, info) {
|
|
|
363
424
|
if (!/^[A-Za-z][A-Za-z0-9]{1,63}$/.test(info.package ?? '') || !/^\d+(\.\d+){0,3}-\d+$/.test(info.version ?? '') || !/^7\.\d+(\.\d+)?-\d+$/.test(info.os_min_ver ?? ''))
|
|
364
425
|
fail('invalid native INFO identity/version/DSM');
|
|
365
426
|
const inv = object(JSON.parse(readFileSync(join(dir, 'native-layout.json'), 'utf8')), ['format', 'arch', 'binaries', 'files', 'anchors']);
|
|
366
|
-
if (inv.format !== 1 ||
|
|
427
|
+
if (inv.format !== 1 || !Object.hasOwn(LAYOUT_ARCHES, inv.arch) || layoutInfoArch(inv.arch) !== info.arch)
|
|
367
428
|
fail('invalid native layout inventory');
|
|
368
429
|
checkLayoutMetadata(dir, true, info.package);
|
|
369
430
|
const actual = layoutFiles(payload);
|