@synopackageland/cli 0.7.3 → 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 +125 -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/cli.js +107 -43
- 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/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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `build --verify-reproducible`: build once more from a clean state and compare
|
|
3
|
+
* the SPKs. Projects may declare the dependency files whose fingerprint decides
|
|
4
|
+
* when the check needs to run again; the last passing one is recorded.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, lstatSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
|
+
import { extractSpk, walkFiles } from "./build-spk/generator.js";
|
|
11
|
+
import { invalid, readReproducible } from "./reproducible-config.js";
|
|
12
|
+
export { readReproducible, validateReproducible } from "./reproducible-config.js";
|
|
13
|
+
export const FINGERPRINT_RECORD = "reproducible-fingerprint.json";
|
|
14
|
+
const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
16
|
+
function insideProject(root, path) {
|
|
17
|
+
const rel = relative(realpathSync(root), realpathSync(path));
|
|
18
|
+
return rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel);
|
|
19
|
+
}
|
|
20
|
+
/** SHA-256 of each declared input plus the CLI version and `extra` pins, and one digest over all of them. */
|
|
21
|
+
export function dependencyFingerprint(root, config, extra = {}) {
|
|
22
|
+
const digests = { synopkgland: CLI_VERSION, ...extra };
|
|
23
|
+
for (const path of config.inputs) {
|
|
24
|
+
const full = join(root, path);
|
|
25
|
+
if (!existsSync(full) || !lstatSync(full).isFile() || !insideProject(root, full))
|
|
26
|
+
invalid(`reproducible input ${path} is not a regular file in the project.`);
|
|
27
|
+
digests[path] = sha256(readFileSync(full));
|
|
28
|
+
}
|
|
29
|
+
const inputs = Object.fromEntries(Object.entries(digests).sort(([a], [b]) => a.localeCompare(b)));
|
|
30
|
+
return { fingerprint: sha256(JSON.stringify(inputs)), inputs };
|
|
31
|
+
}
|
|
32
|
+
function readRecord(path) {
|
|
33
|
+
try {
|
|
34
|
+
const record = JSON.parse(readFileSync(path, "utf8"));
|
|
35
|
+
return typeof record.fingerprint === "string" && record.inputs && Array.isArray(record.arches) ? record : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function cleanPaths(root, clean, arches) {
|
|
42
|
+
for (const pattern of clean) {
|
|
43
|
+
for (const path of new Set(arches.length ? arches.map((arch) => pattern.replaceAll("{arch}", arch)) : [pattern])) {
|
|
44
|
+
const full = resolve(root, path);
|
|
45
|
+
if (!existsSync(dirname(full)) || !insideProject(root, dirname(full)))
|
|
46
|
+
continue;
|
|
47
|
+
rmSync(full, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function fileDigests(dir) {
|
|
52
|
+
return new Map(walkFiles(dir).map((file) => [file, sha256(readFileSync(join(dir, file)))]));
|
|
53
|
+
}
|
|
54
|
+
async function compareSpks(first, second) {
|
|
55
|
+
const digests = [sha256(readFileSync(first)), sha256(readFileSync(second))];
|
|
56
|
+
const result = { output: first, identical: digests[0] === digests[1], sha256: digests, payload: [], metadata: [] };
|
|
57
|
+
if (result.identical)
|
|
58
|
+
return result;
|
|
59
|
+
const work = mkdtempSync(join(tmpdir(), "synopkgland-compare-"));
|
|
60
|
+
try {
|
|
61
|
+
const [a, b] = [join(work, "1"), join(work, "2")];
|
|
62
|
+
await extractSpk(first, a);
|
|
63
|
+
await extractSpk(second, b);
|
|
64
|
+
const [left, right] = [fileDigests(a), fileDigests(b)];
|
|
65
|
+
for (const file of [...new Set([...left.keys(), ...right.keys()])].sort()) {
|
|
66
|
+
if (left.get(file) === right.get(file) || file === "package.tgz")
|
|
67
|
+
continue;
|
|
68
|
+
if (file.startsWith("package/"))
|
|
69
|
+
result.payload.push(file.slice("package/".length));
|
|
70
|
+
else
|
|
71
|
+
result.metadata.push(file);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rmSync(work, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Builds the project again with `rebuild` into a temporary folder, after removing
|
|
81
|
+
* the declared `clean` paths and with an empty GOCACHE, and compares each SPK with
|
|
82
|
+
* `outputs`, the SPKs of this build. With declared inputs, mode `changed` skips
|
|
83
|
+
* while the fingerprint and architectures match the last passing record, and a
|
|
84
|
+
* passing check updates that record.
|
|
85
|
+
*/
|
|
86
|
+
export async function checkReproducible(options) {
|
|
87
|
+
const { root, outputs, arches } = options;
|
|
88
|
+
const config = readReproducible(options.manifestPath);
|
|
89
|
+
const recordPath = join(root, FINGERPRINT_RECORD);
|
|
90
|
+
const current = config.inputs.length ? dependencyFingerprint(root, config, options.extraInputs) : undefined;
|
|
91
|
+
const recorded = current && readRecord(recordPath);
|
|
92
|
+
const changed = current && Object.keys({ ...current.inputs, ...recorded?.inputs }).filter((key) => current.inputs[key] !== recorded?.inputs[key]).sort();
|
|
93
|
+
if (current && options.mode === "changed" && recorded?.fingerprint === current.fingerprint && arches.every((arch) => recorded.arches.includes(arch))) {
|
|
94
|
+
return { skipped: true, identical: true, fingerprint: current.fingerprint, changed, results: [] };
|
|
95
|
+
}
|
|
96
|
+
cleanPaths(root, config.clean, arches);
|
|
97
|
+
const outDir = mkdtempSync(join(tmpdir(), "synopkgland-reproducible-"));
|
|
98
|
+
const gocache = mkdtempSync(join(tmpdir(), "synopkgland-gocache-"));
|
|
99
|
+
const saved = process.env.GOCACHE;
|
|
100
|
+
process.env.GOCACHE = gocache;
|
|
101
|
+
try {
|
|
102
|
+
const second = await options.rebuild(outDir);
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const [index, output] of outputs.entries())
|
|
105
|
+
results.push(await compareSpks(output, second[index]));
|
|
106
|
+
const identical = results.length === outputs.length && results.every((result) => result.identical);
|
|
107
|
+
if (current && identical) {
|
|
108
|
+
const known = recorded?.fingerprint === current.fingerprint ? recorded.arches : [];
|
|
109
|
+
writeFileSync(recordPath, `${JSON.stringify({ ...current, arches: [...new Set([...known, ...arches])].sort() }, null, 2)}\n`);
|
|
110
|
+
}
|
|
111
|
+
return { skipped: false, identical, ...(current ? { fingerprint: current.fingerprint, changed } : {}), results };
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (saved === undefined)
|
|
115
|
+
delete process.env.GOCACHE;
|
|
116
|
+
else
|
|
117
|
+
process.env.GOCACHE = saved;
|
|
118
|
+
for (const dir of [outDir, gocache])
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -11,7 +11,7 @@ export interface ExecutorArtifact extends ExecutorPins {
|
|
|
11
11
|
tasks: ExecutorTask[];
|
|
12
12
|
implementation: 'executor';
|
|
13
13
|
}
|
|
14
|
-
export declare function buildExecutor(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): ExecutorArtifact | undefined
|
|
14
|
+
export declare function buildExecutor(m: StandaloneManifest, arch: NativeArchFamily, work: string, run: CommandRunner): Promise<ExecutorArtifact | undefined>;
|
|
15
15
|
export declare function executorPlan(a: ExecutorArtifact): {
|
|
16
16
|
package: string;
|
|
17
17
|
executorDigest: string;
|
|
@@ -6,7 +6,7 @@ import { StandaloneBuildError } from '../standalone/spk.js';
|
|
|
6
6
|
import { checkCgiBinary } from '../standalone/binary.js';
|
|
7
7
|
import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js';
|
|
8
8
|
import { executorTasks } from './executor-tasks.js';
|
|
9
|
-
export function buildExecutor(m, arch, work, run) {
|
|
9
|
+
export async function buildExecutor(m, arch, work, run) {
|
|
10
10
|
if (!m.capabilities.delegate && !m.capabilities.rootOps)
|
|
11
11
|
return undefined;
|
|
12
12
|
const dir = join(work, `executor-${arch}`);
|
|
@@ -15,14 +15,14 @@ export function buildExecutor(m, arch, work, run) {
|
|
|
15
15
|
if (!existsSync(join(source, 'go.mod')))
|
|
16
16
|
throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', 'Executor sources are missing.');
|
|
17
17
|
const binary = join(dir, 'executor');
|
|
18
|
-
const result = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', binary, '.'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
|
|
18
|
+
const result = await run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', binary, '.'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
|
|
19
19
|
if (result.missing || result.status !== 0)
|
|
20
20
|
throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Executor build failed (${arch}). ${PINNED_GO_HINT}\n${result.stderr}`);
|
|
21
21
|
const problem = checkCgiBinary(work, binary, arch);
|
|
22
22
|
if (problem)
|
|
23
23
|
throw new StandaloneBuildError(problem.code, problem.message);
|
|
24
24
|
const lifecycleBinary = join(dir, 'lifecycle');
|
|
25
|
-
const lifecycleResult = run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
|
|
25
|
+
const lifecycleResult = await run('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o', lifecycleBinary, './cmd/lifecycle'], { cwd: source, env: { ...process.env, ...pinnedGoBuildEnv(arch) } });
|
|
26
26
|
if (lifecycleResult.missing || lifecycleResult.status !== 0)
|
|
27
27
|
throw new StandaloneBuildError('EXECUTOR_BUILD_FAILED', `Lifecycle helper build failed (${arch}). ${PINNED_GO_HINT}\n${lifecycleResult.stderr}`);
|
|
28
28
|
const lifecycleProblem = checkCgiBinary(work, lifecycleBinary, arch);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Findings } from "./findings.js";
|
|
2
|
+
/** INFO lines are `key="value"` (or unquoted `key=value`); quotes may escape `\"` and `\\`. */
|
|
3
|
+
export declare function parseInfo(text: string, findings?: Findings): Map<string, string>;
|
|
4
|
+
/** Checks INFO fields; `payloadPath` is package.tgz on disk, for the checksum. */
|
|
5
|
+
export declare function checkInfoFields(fields: Map<string, string>, payloadPath: string | undefined, findings: Findings): void;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { ARCH_ELF_MACHINE, BOOLEAN_INFO_FIELDS, DSM7_MIN_VER, EXTRA_INFO_FIELDS, INFO_LANGUAGES, OPTIONAL_INFO_FIELDS, REQUIRED_INFO_FIELDS, } from "./dsm-spec.js";
|
|
4
|
+
const KNOWN_FIELDS = new Set([...REQUIRED_INFO_FIELDS, ...OPTIONAL_INFO_FIELDS, ...EXTRA_INFO_FIELDS]);
|
|
5
|
+
const BOOLEAN_FIELDS = new Set(BOOLEAN_INFO_FIELDS);
|
|
6
|
+
const LANGUAGES = new Set(INFO_LANGUAGES);
|
|
7
|
+
const MAX_VERSION_PART = 2 ** 31 - 1;
|
|
8
|
+
/** X.Y-B as the guide states, and X.Y.Z-B as DSM itself reports (7.2.1-69057). */
|
|
9
|
+
const OS_VER = /^(\d+)\.(\d+)(?:\.(\d+))?-(\d+)$/;
|
|
10
|
+
/** INFO lines are `key="value"` (or unquoted `key=value`); quotes may escape `\"` and `\\`. */
|
|
11
|
+
export function parseInfo(text, findings) {
|
|
12
|
+
const fields = new Map();
|
|
13
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
14
|
+
if (/^\s*(#.*)?$/.test(line))
|
|
15
|
+
return;
|
|
16
|
+
const match = /^([A-Za-z0-9_]+)=(.*)$/.exec(line);
|
|
17
|
+
if (!match) {
|
|
18
|
+
findings?.add("SPK_INFO_LINE_INVALID", `INFO 第 ${index + 1} 行不是 key="value":${line.slice(0, 60)}`, "INFO");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const [, key, raw] = match;
|
|
22
|
+
let value = raw;
|
|
23
|
+
if (raw.startsWith('"')) {
|
|
24
|
+
const quoted = /^"((?:[^"\\]|\\.)*)"\s*$/.exec(raw);
|
|
25
|
+
if (!quoted) {
|
|
26
|
+
findings?.add("SPK_INFO_VALUE_UNTERMINATED", `INFO 第 ${index + 1} 行 ${key} 的引號沒有結束。`, "INFO");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
value = quoted[1].replace(/\\(.)/g, "$1");
|
|
30
|
+
}
|
|
31
|
+
if (fields.has(key))
|
|
32
|
+
findings?.add("SPK_INFO_FIELD_DUPLICATE", `INFO 重複設定 ${key};只有一個值會生效。`, "INFO");
|
|
33
|
+
fields.set(key, value);
|
|
34
|
+
});
|
|
35
|
+
return fields;
|
|
36
|
+
}
|
|
37
|
+
function compareOsVer(a, b) {
|
|
38
|
+
const parse = (value) => { const m = OS_VER.exec(value); return [Number(m[1]), Number(m[2]), Number(m[3] ?? 0), Number(m[4])]; };
|
|
39
|
+
const [x, y] = [parse(a), parse(b)];
|
|
40
|
+
for (let i = 0; i < 4; i++)
|
|
41
|
+
if (x[i] !== y[i])
|
|
42
|
+
return x[i] - y[i];
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
function checkVersion(version, findings) {
|
|
46
|
+
const parts = version.split(/[._-]/);
|
|
47
|
+
if (parts.some((part) => !/^\d+$/.test(part) || Number(part) > MAX_VERSION_PART)) {
|
|
48
|
+
findings.add("SPK_INFO_VERSION_INVALID", `INFO version「${version}」只能是以 .、-、_ 分隔的數字(每段 0–2147483647),例如 1.2.3-0001。`, "INFO");
|
|
49
|
+
}
|
|
50
|
+
else if (!version.includes("-")) {
|
|
51
|
+
findings.add("SPK_INFO_VERSION_NO_BUILD", `INFO version「${version}」沒有 -<build> 編號;指南格式為 [feature number]-[build number],例如 ${version}-0001。`, "INFO");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function checkArch(field, value, findings) {
|
|
55
|
+
const tokens = value.split(/\s+/).filter(Boolean);
|
|
56
|
+
for (const token of tokens.filter((t) => !ARCH_ELF_MACHINE.has(t))) {
|
|
57
|
+
findings.add("SPK_INFO_ARCH_UNKNOWN", `INFO ${field} 含有指南 Appendix A 與已知 DSM 7 平台都沒有的值「${token}」。`, "INFO");
|
|
58
|
+
}
|
|
59
|
+
if (field === "arch" && tokens.includes("noarch") && tokens.length > 1) {
|
|
60
|
+
findings.add("SPK_INFO_ARCH_NOARCH_MIXED", `INFO arch「${value}」同時有 noarch 與其他值;noarch 已代表所有平台。`, "INFO");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function checkTranslations(key, findings) {
|
|
64
|
+
const match = /^(displayname|description)_(.+)$/.exec(key);
|
|
65
|
+
if (!match)
|
|
66
|
+
return false;
|
|
67
|
+
if (!LANGUAGES.has(match[2])) {
|
|
68
|
+
findings.add("SPK_INFO_LANG_UNKNOWN", `INFO ${key} 的語言碼「${match[2]}」不在指南的清單(${INFO_LANGUAGES.join("、")})。`, "INFO");
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** Checks INFO fields; `payloadPath` is package.tgz on disk, for the checksum. */
|
|
73
|
+
export function checkInfoFields(fields, payloadPath, findings) {
|
|
74
|
+
for (const key of REQUIRED_INFO_FIELDS) {
|
|
75
|
+
if (!fields.get(key))
|
|
76
|
+
findings.add("SPK_INFO_FIELD_REQUIRED", `INFO 缺少必填欄位 ${key}。`, "INFO");
|
|
77
|
+
}
|
|
78
|
+
if (!fields.get("displayname")) {
|
|
79
|
+
findings.add("SPK_INFO_DISPLAYNAME_MISSING", "INFO 沒有 displayname;Package Center 會顯示 package 值。", "INFO");
|
|
80
|
+
}
|
|
81
|
+
const pkg = fields.get("package");
|
|
82
|
+
if (pkg && /[:/><|=]/.test(pkg)) {
|
|
83
|
+
findings.add("SPK_INFO_PACKAGE_INVALID", `INFO package「${pkg}」不能含有 : / > < | =。`, "INFO");
|
|
84
|
+
}
|
|
85
|
+
const version = fields.get("version");
|
|
86
|
+
if (version)
|
|
87
|
+
checkVersion(version, findings);
|
|
88
|
+
for (const key of ["os_min_ver", "os_max_ver"]) {
|
|
89
|
+
const value = fields.get(key);
|
|
90
|
+
if (!value)
|
|
91
|
+
continue;
|
|
92
|
+
if (!OS_VER.test(value)) {
|
|
93
|
+
findings.add("SPK_INFO_OS_VER_INVALID", `INFO ${key}「${value}」不是 <major>.<minor>-<build>(例如 7.0-40000)。`, "INFO");
|
|
94
|
+
}
|
|
95
|
+
else if (key === "os_min_ver" && compareOsVer(value, DSM7_MIN_VER) < 0) {
|
|
96
|
+
findings.add("SPK_INFO_OS_MIN_VER_LOW", `INFO os_min_ver「${value}」低於 ${DSM7_MIN_VER};DSM 7 套件應至少為 ${DSM7_MIN_VER}。`, "INFO");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const key of ["arch", "exclude_arch"]) {
|
|
100
|
+
const value = fields.get(key);
|
|
101
|
+
if (value)
|
|
102
|
+
checkArch(key, value, findings);
|
|
103
|
+
}
|
|
104
|
+
for (const [key, value] of fields) {
|
|
105
|
+
if (BOOLEAN_FIELDS.has(key) && value !== "yes" && value !== "no") {
|
|
106
|
+
findings.add("SPK_INFO_BOOLEAN_INVALID", `INFO ${key}「${value}」只能是 "yes" 或 "no"。`, "INFO");
|
|
107
|
+
}
|
|
108
|
+
if (!checkTranslations(key, findings) && !KNOWN_FIELDS.has(key)) {
|
|
109
|
+
findings.add("SPK_INFO_FIELD_UNKNOWN", `INFO ${key} 不是指南列出的欄位;DSM 不會使用它。`, "INFO");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const adminport = fields.get("adminport");
|
|
113
|
+
if (adminport !== undefined && (!/^\d+$/.test(adminport) || Number(adminport) > 65536)) {
|
|
114
|
+
findings.add("SPK_INFO_ADMINPORT_INVALID", `INFO adminport「${adminport}」必須是 0–65536 的整數。`, "INFO");
|
|
115
|
+
}
|
|
116
|
+
const checksum = fields.get("checksum");
|
|
117
|
+
if (checksum && payloadPath) {
|
|
118
|
+
const actual = createHash("md5").update(readFileSync(payloadPath)).digest("hex");
|
|
119
|
+
if (checksum.toLowerCase() !== actual) {
|
|
120
|
+
findings.add("SPK_INFO_CHECKSUM_MISMATCH", `INFO checksum「${checksum}」不是 package.tgz 的 MD5(${actual})。`, "INFO");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Findings } from "./findings.js";
|
|
2
|
+
/** The outer SPK files by exact path, and each entry's mode. */
|
|
3
|
+
export interface OuterFiles {
|
|
4
|
+
files: Map<string, Buffer>;
|
|
5
|
+
modes: Map<string, number>;
|
|
6
|
+
}
|
|
7
|
+
export declare function checkMetadata(outer: OuterFiles, findings: Findings): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CONF_ENTRIES, INFO_LANGUAGES, LIFECYCLE_SCRIPTS, MAX_LICENSE_BYTES, PACKAGE_ICONS, REPLACE_SCRIPTS, RESOURCE_WORKERS, } from "./dsm-spec.js";
|
|
2
|
+
const CONF = new Set(CONF_ENTRIES);
|
|
3
|
+
const WORKERS = new Set(RESOURCE_WORKERS);
|
|
4
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5
|
+
const WIZARD_NAME = new RegExp(`^(install|upgrade|uninstall)_uifile(_(${INFO_LANGUAGES.join("|")}))?(\\.sh)?$`);
|
|
6
|
+
const RUN_AS = ["package", "root"];
|
|
7
|
+
const executable = (mode) => ((mode ?? 0) & 0o111) !== 0;
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function parseJson(text) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(text?.toString("utf8") ?? "");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function checkScripts({ files, modes }, findings) {
|
|
20
|
+
for (const name of [...LIFECYCLE_SCRIPTS, ...REPLACE_SCRIPTS]) {
|
|
21
|
+
const path = `scripts/${name}`;
|
|
22
|
+
const body = files.get(path);
|
|
23
|
+
if (!body) {
|
|
24
|
+
if (name === "start-stop-status")
|
|
25
|
+
findings.add("SPK_SCRIPT_MISSING", `缺少 ${path};DSM 用它啟動、停止與查詢套件狀態。`, path);
|
|
26
|
+
else if (LIFECYCLE_SCRIPTS.includes(name))
|
|
27
|
+
findings.add("SPK_SCRIPT_HOOK_MISSING", `缺少 ${path};指南把它列為必備,沒有事要做時寫 #!/bin/sh 與 exit 0。`, path);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!executable(modes.get(path)))
|
|
31
|
+
findings.add("SPK_SCRIPT_NOT_EXECUTABLE", `${path} 沒有執行權限(mode ${(modes.get(path) ?? 0).toString(8)});打包前 chmod 755。`, path);
|
|
32
|
+
const head = body.subarray(0, 4).toString("latin1");
|
|
33
|
+
if (!head.startsWith("#!") && head !== "\x7fELF")
|
|
34
|
+
findings.add("SPK_SCRIPT_NO_INTERPRETER", `${path} 開頭沒有 #!(例如 #!/bin/sh)。`, path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function checkPrivilege(files, findings) {
|
|
38
|
+
const path = "conf/privilege";
|
|
39
|
+
if (!files.has(path)) {
|
|
40
|
+
findings.add("SPK_PRIVILEGE_MISSING", "缺少 conf/privilege;DSM 7 起每個套件都必須明確宣告執行身分。", path);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const privilege = parseJson(files.get(path));
|
|
44
|
+
const runAs = (value) => (isRecord(value) ? value["run-as"] : undefined);
|
|
45
|
+
const listed = isRecord(privilege)
|
|
46
|
+
? ["ctrl-script", "executable"].flatMap((key) => (Array.isArray(privilege[key]) ? privilege[key] : []).map(runAs))
|
|
47
|
+
: [];
|
|
48
|
+
const values = isRecord(privilege) ? [runAs(privilege.defaults), ...listed] : [];
|
|
49
|
+
if (!isRecord(privilege) || values.some((value) => typeof value !== "string" || !RUN_AS.includes(value))) {
|
|
50
|
+
findings.add("SPK_PRIVILEGE_INVALID", 'conf/privilege 必須是 JSON 物件,有 defaults.run-as,且每個 run-as 是 "package" 或 "root"。', path);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (values.includes("root"))
|
|
54
|
+
findings.add("SPK_PRIVILEGE_ROOT", "conf/privilege 要求以 root 執行;DSM 7 第三方套件應以 package 帳號執行。", path);
|
|
55
|
+
}
|
|
56
|
+
function checkConf({ files, modes }, findings) {
|
|
57
|
+
checkPrivilege(files, findings);
|
|
58
|
+
const resourcePath = "conf/resource";
|
|
59
|
+
if (files.has(resourcePath)) {
|
|
60
|
+
const resource = parseJson(files.get(resourcePath));
|
|
61
|
+
if (!isRecord(resource))
|
|
62
|
+
findings.add("SPK_RESOURCE_INVALID", "conf/resource 不是 JSON 物件。", resourcePath);
|
|
63
|
+
else
|
|
64
|
+
for (const id of Object.keys(resource).filter((key) => !WORKERS.has(key))) {
|
|
65
|
+
findings.add("SPK_RESOURCE_UNKNOWN", `conf/resource 的「${id}」不是指南列出的 resource worker。`, resourcePath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const names = new Set([...modes.keys()].filter((path) => path.startsWith("conf/")).map((path) => path.split("/")[1]).filter(Boolean));
|
|
69
|
+
for (const name of [...names].filter((n) => !CONF.has(n)))
|
|
70
|
+
findings.add("SPK_CONF_UNKNOWN", `conf/${name} 不是 DSM 讀取的設定檔。`, `conf/${name}`);
|
|
71
|
+
}
|
|
72
|
+
function checkIcons(files, findings) {
|
|
73
|
+
for (const [name, size] of PACKAGE_ICONS) {
|
|
74
|
+
const png = files.get(name);
|
|
75
|
+
if (!png) {
|
|
76
|
+
findings.add("SPK_ICON_MISSING", `缺少 ${name}(${size}×${size} PNG)。`, name);
|
|
77
|
+
}
|
|
78
|
+
else if (png.length < 24 || !png.subarray(0, 8).equals(PNG_SIGNATURE) || png.toString("latin1", 12, 16) !== "IHDR") {
|
|
79
|
+
findings.add("SPK_ICON_INVALID", `${name} 不是 PNG。`, name);
|
|
80
|
+
}
|
|
81
|
+
else if (png.readUInt32BE(16) !== size || png.readUInt32BE(20) !== size) {
|
|
82
|
+
findings.add("SPK_ICON_SIZE", `${name} 是 ${png.readUInt32BE(16)}×${png.readUInt32BE(20)},指南要求 ${size}×${size}。`, name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function checkWizards({ files, modes }, findings) {
|
|
87
|
+
for (const path of [...modes.keys()].filter((p) => p.startsWith("WIZARD_UIFILES/") && files.has(p))) {
|
|
88
|
+
const name = path.slice("WIZARD_UIFILES/".length);
|
|
89
|
+
if (!WIZARD_NAME.test(name)) {
|
|
90
|
+
findings.add("SPK_WIZARD_UNKNOWN", `${path} 不是 install/upgrade/uninstall_uifile[_<lang>][.sh];DSM 不會讀它。`, path);
|
|
91
|
+
}
|
|
92
|
+
else if (name.endsWith(".sh")) {
|
|
93
|
+
if (!executable(modes.get(path)))
|
|
94
|
+
findings.add("SPK_WIZARD_SCRIPT_NOT_EXECUTABLE", `${path} 沒有執行權限;DSM 執行它來產生精靈。`, path);
|
|
95
|
+
}
|
|
96
|
+
else if (!Array.isArray(parseJson(files.get(path)))) {
|
|
97
|
+
findings.add("SPK_WIZARD_INVALID", `${path} 不是 JSON 陣列(每個元素是一個精靈步驟)。`, path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function checkMetadata(outer, findings) {
|
|
102
|
+
checkScripts(outer, findings);
|
|
103
|
+
checkConf(outer, findings);
|
|
104
|
+
checkIcons(outer.files, findings);
|
|
105
|
+
checkWizards(outer, findings);
|
|
106
|
+
const license = outer.files.get("LICENSE");
|
|
107
|
+
if (license && license.length >= MAX_LICENSE_BYTES)
|
|
108
|
+
findings.add("SPK_LICENSE_TOO_LARGE", `LICENSE 有 ${license.length} bytes;指南要求小於 1 MB。`, "LICENSE");
|
|
109
|
+
const signature = outer.files.get("syno_signature.asc");
|
|
110
|
+
if (signature && !signature.toString("latin1").includes("-----BEGIN PGP SIGNATURE-----")) {
|
|
111
|
+
findings.add("SPK_SIGNATURE_INVALID", "syno_signature.asc 不是 ASCII-armored PGP 簽章。", "syno_signature.asc");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { openSync, readSync, closeSync } from "node:fs";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import * as tar from "tar";
|
|
4
|
+
import { ARCH_ELF_MACHINE } from "./dsm-spec.js";
|
|
5
|
+
const XZ_MAGIC = Buffer.from([0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]);
|
|
6
|
+
const ELF_MAGIC = Buffer.from("\x7fELF", "latin1");
|
|
7
|
+
const MAX_TEXT = 1024 * 1024;
|
|
8
|
+
const HEAD_BYTES = 20;
|
|
9
|
+
/** `section:key`: a DSM string reference rather than literal text. */
|
|
10
|
+
const STRING_REF = /^([A-Za-z0-9_]+):([A-Za-z0-9_]+)$/;
|
|
11
|
+
const at = (path) => `package.tgz:${path}`;
|
|
12
|
+
function startsWith(file, magic) {
|
|
13
|
+
const fd = openSync(file, "r");
|
|
14
|
+
try {
|
|
15
|
+
const head = Buffer.alloc(magic.length);
|
|
16
|
+
return readSync(fd, head, 0, magic.length, 0) === magic.length && head.equals(magic);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
closeSync(fd);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** `dsmuidir` is one path, or since DSM 7.0-40731 several `name:path` pairs. */
|
|
23
|
+
function uiDirs(dsmuidir) {
|
|
24
|
+
return (dsmuidir ?? "").split(/\s+/).filter(Boolean).map((item) => posix.normalize(item.slice(item.indexOf(":") + 1)).replace(/\/$/, ""));
|
|
25
|
+
}
|
|
26
|
+
/** Lists package.tgz without extracting it, keeping only file heads and the few texts the rules read. */
|
|
27
|
+
async function scanPayload(payloadPath, wanted, findings) {
|
|
28
|
+
const files = [];
|
|
29
|
+
let entries = 0;
|
|
30
|
+
try {
|
|
31
|
+
await tar.t({
|
|
32
|
+
file: payloadPath,
|
|
33
|
+
strict: true,
|
|
34
|
+
onReadEntry: (entry) => {
|
|
35
|
+
entries++;
|
|
36
|
+
const path = entry.path.replace(/^\.\//, "");
|
|
37
|
+
checkEntryPath(path, entry.type, entry.linkpath, findings);
|
|
38
|
+
if (entry.type !== "File")
|
|
39
|
+
return;
|
|
40
|
+
const file = { path, head: Buffer.alloc(0) };
|
|
41
|
+
const keep = wanted(path) && (entry.size ?? 0) <= MAX_TEXT;
|
|
42
|
+
const chunks = [];
|
|
43
|
+
entry.on("data", (chunk) => {
|
|
44
|
+
if (file.head.length < HEAD_BYTES)
|
|
45
|
+
file.head = Buffer.concat([file.head, chunk]).subarray(0, HEAD_BYTES);
|
|
46
|
+
if (keep)
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
});
|
|
49
|
+
entry.on("end", () => { if (keep)
|
|
50
|
+
file.text = Buffer.concat(chunks).toString("utf8"); });
|
|
51
|
+
files.push(file);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
entries = 0;
|
|
57
|
+
}
|
|
58
|
+
if (entries === 0) {
|
|
59
|
+
findings.add("SPK_PAYLOAD_UNREADABLE", "package.tgz 不是可讀的 tar、tar.gz 封存,或是空的。", "package.tgz");
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
function checkEntryPath(path, type, linkpath, findings) {
|
|
65
|
+
if (path.startsWith("/") || posix.normalize(path).split("/")[0] === "..") {
|
|
66
|
+
findings.add("SPK_PAYLOAD_PATH_UNSAFE", `${path} 會解開到套件目錄之外。`, at(path));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (type === "SymbolicLink" && linkpath) {
|
|
70
|
+
if (linkpath.startsWith("/")) {
|
|
71
|
+
findings.add("SPK_PAYLOAD_LINK_ABSOLUTE", `${path} 是指向 ${linkpath} 的絕對路徑 symlink;目標由 NAS 決定,不在套件內。`, at(path));
|
|
72
|
+
}
|
|
73
|
+
else if (posix.normalize(posix.join(posix.dirname(path), linkpath)).split("/")[0] === "..") {
|
|
74
|
+
findings.add("SPK_PAYLOAD_LINK_ESCAPE", `${path} 的 symlink 目標 ${linkpath} 跳出套件目錄。`, at(path));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (type === "Link" && linkpath && (linkpath.startsWith("/") || posix.normalize(linkpath).split("/")[0] === "..")) {
|
|
78
|
+
findings.add("SPK_PAYLOAD_LINK_ESCAPE", `${path} 的 hard link 目標 ${linkpath} 不在套件內。`, at(path));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Sections the package defines itself in its `texts/<lang>/strings` files. */
|
|
82
|
+
function ownSections(files) {
|
|
83
|
+
const sections = new Set();
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
if (!/(^|\/)texts\/[^/]+\/strings$/.test(file.path) || file.text === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
for (const match of file.text.matchAll(/^\s*\[([^\]]+)\]\s*$/gm))
|
|
88
|
+
sections.add(match[1]);
|
|
89
|
+
}
|
|
90
|
+
return sections;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* DSM resolves a `section:key` title only from the strings of an entry that
|
|
94
|
+
* declares `texts`; without it the menu shows the raw key. DSM's own
|
|
95
|
+
* sections need no `texts`, so only the package's own sections are errors.
|
|
96
|
+
*/
|
|
97
|
+
function checkUiConfig(file, sections, findings) {
|
|
98
|
+
let config;
|
|
99
|
+
try {
|
|
100
|
+
config = JSON.parse(file.text ?? "");
|
|
101
|
+
}
|
|
102
|
+
catch { /* reported below */ }
|
|
103
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
104
|
+
findings.add("SPK_UI_CONFIG_INVALID", `${file.path} 不是 JSON 物件。`, at(file.path));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const group of Object.values(config)) {
|
|
108
|
+
if (typeof group !== "object" || group === null)
|
|
109
|
+
continue;
|
|
110
|
+
for (const [id, entry] of Object.entries(group)) {
|
|
111
|
+
if (typeof entry !== "object" || entry === null || "texts" in entry)
|
|
112
|
+
continue;
|
|
113
|
+
for (const field of ["title", "desc"]) {
|
|
114
|
+
const value = entry[field];
|
|
115
|
+
const ref = typeof value === "string" ? STRING_REF.exec(value) : null;
|
|
116
|
+
if (!ref)
|
|
117
|
+
continue;
|
|
118
|
+
const own = ref[1] === "synopkgland" || sections.has(ref[1]);
|
|
119
|
+
findings.add(own ? "SPK_UI_TEXTS_MISSING" : "SPK_UI_TEXTS_UNDECLARED", `${file.path} 的 ${id}.${field}「${value}」是字串參照,但這個 entry 沒有宣告 "texts";${own ? "DSM 會直接顯示這個 key" : "除非是 DSM 內建的 section,否則 DSM 會直接顯示這個 key"}。`, at(file.path));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** ELF files must run on the platforms INFO arch names; a noarch package carries none. */
|
|
125
|
+
function checkElf(files, arch, findings) {
|
|
126
|
+
const tokens = (arch ?? "").split(/\s+/).filter(Boolean);
|
|
127
|
+
const noarch = tokens.length === 1 && tokens[0] === "noarch";
|
|
128
|
+
const machines = new Set(tokens.map((token) => ARCH_ELF_MACHINE.get(token)));
|
|
129
|
+
const checkable = !noarch && tokens.length > 0 && tokens.every((token) => ARCH_ELF_MACHINE.get(token) !== undefined);
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
if (file.head.length < HEAD_BYTES || !file.head.subarray(0, 4).equals(ELF_MAGIC))
|
|
132
|
+
continue;
|
|
133
|
+
const machine = file.head[5] === 2 ? file.head.readUInt16BE(18) : file.head.readUInt16LE(18);
|
|
134
|
+
if (noarch) {
|
|
135
|
+
findings.add("SPK_NOARCH_ELF", `${file.path} 是 ELF 執行檔,但 INFO arch 是 noarch;noarch 套件只能有腳本,否則在其他 CPU 的 NAS 上無法執行。`, at(file.path));
|
|
136
|
+
}
|
|
137
|
+
else if (checkable && !machines.has(machine)) {
|
|
138
|
+
findings.add("SPK_ELF_ARCH_MISMATCH", `${file.path} 的 ELF machine 是 ${machine},與 INFO arch「${arch}」的 CPU 不符。`, at(file.path));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export async function checkPayload(payloadPath, info, findings) {
|
|
143
|
+
if (startsWith(payloadPath, XZ_MAGIC)) {
|
|
144
|
+
findings.add("SPK_PAYLOAD_XZ", "package.tgz 是 xz 壓縮(Synology toolkit 的格式);inspect 讀不了 xz,沒有檢查 payload 內容。", "package.tgz");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const dirs = uiDirs(info.dsmuidir);
|
|
148
|
+
const configs = new Set(dirs.map((dir) => posix.join(dir, "config")));
|
|
149
|
+
const files = await scanPayload(payloadPath, (path) => configs.has(path) || (/\/texts\/[^/]+\/strings$/.test(path) && dirs.some((dir) => path.startsWith(`${dir}/`))), findings);
|
|
150
|
+
if (!files)
|
|
151
|
+
return;
|
|
152
|
+
const sections = ownSections(files);
|
|
153
|
+
for (const file of files.filter((f) => configs.has(f.path)))
|
|
154
|
+
checkUiConfig(file, sections, findings);
|
|
155
|
+
checkElf(files, info.arch, findings);
|
|
156
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of the Synology DSM 7 Developer Guide that `inspect` checks
|
|
3
|
+
* against, transcribed from the guide's INFO, Package Structure and
|
|
4
|
+
* Appendix A pages.
|
|
5
|
+
*/
|
|
6
|
+
/** INFO Necessary Fields. */
|
|
7
|
+
export declare const REQUIRED_INFO_FIELDS: readonly ["package", "version", "os_min_ver", "description", "arch", "maintainer"];
|
|
8
|
+
/** INFO Optional Fields, without the `displayname_<lang>` and `description_<lang>` families. */
|
|
9
|
+
export declare const OPTIONAL_INFO_FIELDS: readonly ["displayname", "maintainer_url", "distributor", "distributor_url", "support_url", "support_center", "model", "exclude_arch", "checksum", "adminport", "adminurl", "adminprotocol", "dsmuidir", "dsmappname", "dsmapppage", "dsmapplaunchname", "checkport", "startable", "ctl_stop", "ctl_uninstall", "precheckstartstop", "helpurl", "beta", "report_url", "install_reboot", "install_dep_packages", "install_conflict_packages", "install_break_packages", "install_replace_packages", "install_dep_services", "start_dep_services", "extractsize", "support_conf_folder", "install_type", "silent_install", "silent_upgrade", "silent_uninstall", "auto_upgrade_from", "offline_install", "thirdparty", "os_max_ver", "support_move", "exclude_model", "use_deprecated_replace_mechanism", "install_on_cold_storage"];
|
|
10
|
+
/**
|
|
11
|
+
* Fields outside the guide's list that are still expected: `changelog` is
|
|
12
|
+
* shown by Package Center and used by first-party packages; the others are
|
|
13
|
+
* written by this SDK and ignored by DSM.
|
|
14
|
+
*/
|
|
15
|
+
export declare const EXTRA_INFO_FIELDS: readonly ["changelog", "synopkgland_executor_sha256", "spk_land_metadata"];
|
|
16
|
+
/** Optional fields whose value is "yes" or "no". */
|
|
17
|
+
export declare const BOOLEAN_INFO_FIELDS: readonly ["support_center", "checkport", "startable", "ctl_stop", "ctl_uninstall", "precheckstartstop", "beta", "install_reboot", "support_conf_folder", "silent_install", "silent_upgrade", "silent_uninstall", "offline_install", "thirdparty", "support_move", "use_deprecated_replace_mechanism", "install_on_cold_storage"];
|
|
18
|
+
/** Language codes of `displayname_<lang>` and `description_<lang>`. */
|
|
19
|
+
export declare const INFO_LANGUAGES: readonly ["enu", "cht", "chs", "krn", "ger", "fre", "ita", "spn", "jpn", "dan", "nor", "sve", "nld", "rus", "plk", "ptb", "ptg", "hun", "trk", "csy"];
|
|
20
|
+
/** After DSM 7.0, os_min_ver "should be at least 7.0-40000". */
|
|
21
|
+
export declare const DSM7_MIN_VER = "7.0-40000";
|
|
22
|
+
export declare const ARCH_ELF_MACHINE: ReadonlyMap<string, number | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Top-level SPK entries of the Package Structure page, plus metadata this SDK
|
|
25
|
+
* adds; container SPKs also carry a top-level copy of conf/privilege.
|
|
26
|
+
*/
|
|
27
|
+
export declare const KNOWN_TOP_LEVEL: readonly ["INFO", "package.tgz", "scripts", "conf", "WIZARD_UIFILES", "LICENSE", "PACKAGE_ICON.PNG", "PACKAGE_ICON_256.PNG", "syno_signature.asc", "sdk-package.json", "native-layout.json", "privilege"];
|
|
28
|
+
/** scripts/ of the Package Structure page; DSM needs start-stop-status to start, stop and poll a package. */
|
|
29
|
+
export declare const LIFECYCLE_SCRIPTS: readonly ["preinst", "postinst", "preuninst", "postuninst", "preupgrade", "postupgrade", "start-stop-status"];
|
|
30
|
+
/** Optional scripts, run only with install_replace_packages. */
|
|
31
|
+
export declare const REPLACE_SCRIPTS: readonly ["prereplace", "postreplace"];
|
|
32
|
+
/** conf/ entries: the conf page's files, and the systemd unit folder of the systemd-user-unit worker. */
|
|
33
|
+
export declare const CONF_ENTRIES: readonly ["privilege", "resource", "PKG_DEPS", "PKG_CONX", "systemd"];
|
|
34
|
+
/**
|
|
35
|
+
* Resource ids of the guide's Available Workers pages; `systemd-unit` is not
|
|
36
|
+
* documented there but is used by packages that install on DSM 7.
|
|
37
|
+
*/
|
|
38
|
+
export declare const RESOURCE_WORKERS: readonly ["port-config", "usr-local-linker", "data-share", "systemd-user-unit", "systemd-unit", "web-config", "webservice", "docker-project", "docker", "apache22", "indexdb", "mariadb10", "php", "syslog-config", "sysnotify"];
|
|
39
|
+
/** PACKAGE_ICON.PNG is 64×64 on DSM 7; PACKAGE_ICON_256.PNG is 256×256. */
|
|
40
|
+
export declare const PACKAGE_ICONS: readonly [readonly ["PACKAGE_ICON.PNG", 64], readonly ["PACKAGE_ICON_256.PNG", 256]];
|
|
41
|
+
/** LICENSE "must be less than 1 MB". */
|
|
42
|
+
export declare const MAX_LICENSE_BYTES: number;
|