@synopackageland/cli 0.13.1 → 0.15.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 +18 -0
- package/README.md +44 -1
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -850
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/app-launch.d.ts +11 -0
- package/dist/contract/app-launch.js +18 -0
- package/dist/contract/web-launch.d.ts +3 -13
- package/dist/contract/web-launch.js +14 -81
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/error-registry.js +1020 -831
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +55 -9
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +15 -0
- package/dist/standalone/inspect-spk.js +714 -128
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +10 -1
- package/dist/standalone/manifest.js +308 -87
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +139 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synology-app.schema.json +1 -1
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +34 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +26 -4
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -138
- package/skills/reference/portal.md +31 -0
- package/skills/reference/ui-vue.md +31 -0
- package/skills/reference/wrap.md +10 -30
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -1,856 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
5
|
import * as tar from "tar";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { classifyComposeRoute, loadCompose } from "../compose/analyze.js";
|
|
12
|
-
import { scanComposePrivilege } from "../compose/privilege.js";
|
|
13
|
-
import { validateProject } from "../validate.js";
|
|
14
|
-
import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
15
|
-
import { contractIconPath } from "../contract/icon.js";
|
|
16
|
-
import { infoArchValue, isContainerArchFamily } from "./dsm-arch.js";
|
|
17
|
-
import { assertContainerMinDsm, resolveMinDsm } from "./dsm-version.js";
|
|
18
|
-
import { createReproducibleTar } from "./reproducible-archive.js";
|
|
19
|
-
import { copyProjectNotices } from "./notices.js";
|
|
20
|
-
import { renderDeleteAppDataBlock } from "./delete-app-data.js";
|
|
21
|
-
import { renderUninstallWizardFiles } from "./uninstall-wizard.js";
|
|
22
|
-
import { APP_SCRIPTS_DIR, APP_SCRIPTS_SPK_DIR, composeAppScript, readAppScripts } from "../standalone/app-scripts.js";
|
|
23
|
-
import { renderInstallWizardFiles } from "./install-wizard.js";
|
|
24
|
-
import { renderWizardFiles, writeWizardUiFiles } from "./wizard-i18n.js";
|
|
25
|
-
import { sharedFolderCombobox } from "./share-combobox.js";
|
|
26
|
-
import { writeUiTexts } from "./ui-texts.js";
|
|
27
|
-
import { DSM_LANGS, LOCALES } from "../standalone/i18n.js";
|
|
28
|
-
import { contractAppTitle } from "../contract/i18n.js";
|
|
29
|
-
import { renderHeadlessResourceMarker, renderResourceConfig, renderResourceWorkerStartStopStatus, } from "./resource-worker.js";
|
|
30
|
-
const FILE_MODE = 0o644;
|
|
31
|
-
const SCRIPT_MODE = 0o755;
|
|
32
|
-
const INSTALL_UIFILE = "install_uifile";
|
|
33
|
-
const UPGRADE_UIFILE = "upgrade_uifile";
|
|
34
|
-
const UNINSTALL_UIFILE = "uninstall_uifile";
|
|
35
|
-
const HEADLESS_PROBE_VOLUME = "./.synopkgland:/run/synopkgland:ro";
|
|
36
|
-
const HEADLESS_PROBE_README = "Reserved bind target for package-user headless status probing.\n";
|
|
37
|
-
const SCRIPT_NAMES = [
|
|
38
|
-
"preinst",
|
|
39
|
-
"postinst",
|
|
40
|
-
"preuninst",
|
|
41
|
-
"postuninst",
|
|
42
|
-
"preupgrade",
|
|
43
|
-
"postupgrade",
|
|
44
|
-
"start-stop-status",
|
|
45
|
-
];
|
|
46
|
-
export async function buildSpk(options) {
|
|
47
|
-
if (options.archFamily && !isContainerArchFamily(options.archFamily)) {
|
|
48
|
-
return { ok: false, errors: ["CONTAINER_ARCH_UNSUPPORTED"] };
|
|
49
|
-
}
|
|
50
|
-
const validation = validateProject(options.project);
|
|
51
|
-
if (!validation.ok) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
errors: validation.errors.map((e) => e.code),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const parsed = loadContract(options.project.contractPath);
|
|
58
|
-
const contract = { ...parsed.contract };
|
|
59
|
-
if (options.packageIdentity) {
|
|
60
|
-
contract.package = options.packageIdentity;
|
|
61
|
-
}
|
|
62
|
-
if (options.displayname) {
|
|
63
|
-
contract.displayname = options.displayname;
|
|
64
|
-
}
|
|
65
|
-
if (options.spkVersion) {
|
|
66
|
-
contract.version = options.spkVersion;
|
|
67
|
-
}
|
|
68
|
-
if (options.minDsm !== undefined) {
|
|
69
|
-
contract.os_min_ver = resolveMinDsm(options.minDsm);
|
|
70
|
-
}
|
|
71
|
-
if (options.minDsm !== undefined) {
|
|
72
|
-
assertContainerMinDsm(String(contract.os_min_ver));
|
|
73
|
-
}
|
|
74
|
-
const parsedCompose = loadCompose(options.project.composePath);
|
|
75
|
-
const route = parsedCompose?.compose
|
|
76
|
-
? classifyComposeRoute(contract, parsedCompose)
|
|
77
|
-
: undefined;
|
|
78
|
-
if (route && route.errors.length > 0) {
|
|
79
|
-
return { ok: false, errors: route.errors.map((error) => error.code) };
|
|
80
|
-
}
|
|
81
|
-
if (!route || route.route === "reject") {
|
|
82
|
-
return { ok: false, errors: ["COMPOSE_ROUTE_UNSUPPORTED"] };
|
|
83
|
-
}
|
|
84
|
-
const rwEligibility = route.eligibility;
|
|
85
|
-
const gateway = rwEligibility?.adminport !== undefined && usesAuthGateway(contract)
|
|
86
|
-
? gatewayConfig(contract, rwEligibility.adminport)
|
|
87
|
-
: undefined;
|
|
88
|
-
if (gateway && !options.archFamily) {
|
|
89
|
-
return { ok: false, errors: ["GATEWAY_ARCH_FAMILY_REQUIRED"] };
|
|
90
|
-
}
|
|
91
|
-
const isDynamicRoute = route?.route === "dynamic";
|
|
92
|
-
const isResourceWorkerRoute = route?.route === "static-zero-config" ||
|
|
93
|
-
isDynamicRoute;
|
|
94
|
-
const icons = packageIcons(contractIconPath(contract, options.project.root));
|
|
95
|
-
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
96
|
-
const gatewayWork = mkdtempSync(join(tmpdir(), "syno-gateway-"));
|
|
97
|
-
try {
|
|
98
|
-
let gatewayBinary;
|
|
99
|
-
if (gateway) {
|
|
100
|
-
const built = await buildGatewayBinary({
|
|
101
|
-
packageId: String(contract.package),
|
|
102
|
-
arch: options.archFamily,
|
|
103
|
-
config: gateway,
|
|
104
|
-
work: gatewayWork,
|
|
105
|
-
run: options.gatewayRun,
|
|
106
|
-
});
|
|
107
|
-
if ("error" in built) {
|
|
108
|
-
console.error(built.detail);
|
|
109
|
-
return { ok: false, errors: [built.error] };
|
|
110
|
-
}
|
|
111
|
-
gatewayBinary = built.binary;
|
|
112
|
-
}
|
|
113
|
-
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily, isResourceWorkerRoute), "utf8");
|
|
114
|
-
writePackageCenterIcons(staging, icons);
|
|
115
|
-
mkdirSync(join(staging, "conf"), { recursive: true });
|
|
116
|
-
writeFileSync(join(staging, "conf", "privilege"), renderPrivilege(), "utf8");
|
|
117
|
-
writeFileSync(join(staging, "privilege"), renderPrivilege(), "utf8");
|
|
118
|
-
if (isResourceWorkerRoute) {
|
|
119
|
-
const resourceConfig = renderResourceConfig(contract, rwEligibility, parsedCompose.compose);
|
|
120
|
-
writeFileSync(join(staging, "conf", "resource"), resourceConfig, "utf8");
|
|
121
|
-
chmodSync(join(staging, "conf", "resource"), FILE_MODE);
|
|
122
|
-
}
|
|
123
|
-
writeWizards(staging, contract, parsedCompose?.compose, "container");
|
|
124
|
-
mkdirSync(join(staging, "scripts"), { recursive: true });
|
|
125
|
-
// validateProject already refused a bad scripts/ entry.
|
|
126
|
-
const appScripts = readAppScripts(options.project.root, () => { }) ?? [];
|
|
127
|
-
for (const name of SCRIPT_NAMES) {
|
|
128
|
-
const scriptPath = join(staging, "scripts", name);
|
|
129
|
-
const script = renderManagedScript(name, contract, isResourceWorkerRoute, rwEligibility?.adminport, isDynamicRoute, rwEligibility?.headless === true);
|
|
130
|
-
writeFileSync(scriptPath, appScripts.includes(name) ? composeAppScript(name, script) : script, "utf8");
|
|
131
|
-
chmodSync(scriptPath, SCRIPT_MODE);
|
|
132
|
-
}
|
|
133
|
-
for (const name of appScripts) {
|
|
134
|
-
mkdirSync(join(staging, APP_SCRIPTS_SPK_DIR), { recursive: true });
|
|
135
|
-
copyFileSync(join(options.project.root, APP_SCRIPTS_DIR, name), join(staging, APP_SCRIPTS_SPK_DIR, name));
|
|
136
|
-
chmodSync(join(staging, APP_SCRIPTS_SPK_DIR, name), SCRIPT_MODE);
|
|
137
|
-
}
|
|
138
|
-
mkdirSync(join(staging, "package"), { recursive: true });
|
|
139
|
-
copyFileSync(options.project.composePath, join(staging, "package", "compose.yaml"));
|
|
140
|
-
copyFileSync(options.project.contractPath, join(staging, "package", "synology-app.yaml"));
|
|
141
|
-
if (isResourceWorkerRoute) {
|
|
142
|
-
const resourceProjectDir = join(staging, "package", "compose");
|
|
143
|
-
mkdirSync(resourceProjectDir, { recursive: true });
|
|
144
|
-
const dsmUserParam = collectInstallParameters(contract).find((p) => p.param.type === "dsm_user");
|
|
145
|
-
let composeContent;
|
|
146
|
-
if (dsmUserParam || rwEligibility?.headless === true) {
|
|
147
|
-
const rawYaml = readFileSync(options.project.composePath, "utf8");
|
|
148
|
-
const doc = parseDocument(rawYaml);
|
|
149
|
-
const services = doc.get("services");
|
|
150
|
-
if (dsmUserParam && services && typeof services === "object" && typeof services.toJSON === "function") {
|
|
151
|
-
const serviceNames = Object.keys(services.toJSON());
|
|
152
|
-
for (const sName of serviceNames) {
|
|
153
|
-
const userVal = new Scalar(`\${${dsmUserParam.name}_UID}:\${${dsmUserParam.name}_GID}`);
|
|
154
|
-
userVal.type = Scalar.QUOTE_DOUBLE;
|
|
155
|
-
doc.setIn(["services", sName, "user"], userVal);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (rwEligibility?.headless === true) {
|
|
159
|
-
injectHeadlessProbeVolume(doc);
|
|
160
|
-
}
|
|
161
|
-
composeContent = doc.toString();
|
|
162
|
-
if (dsmUserParam) {
|
|
163
|
-
const escaped = `\\$\\{${dsmUserParam.name}_UID\\}:\\$\\{${dsmUserParam.name}_GID\\}`;
|
|
164
|
-
composeContent = composeContent.replace(new RegExp(`^([ \\t]*)user:\\s*"${escaped}"`, "gm"), `$1user: "\${${dsmUserParam.name}_UID}:\${${dsmUserParam.name}_GID}"\n$1# synopkgland:group_add ${dsmUserParam.name}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
composeContent = readFileSync(options.project.composePath, "utf8");
|
|
169
|
-
}
|
|
170
|
-
composeContent = injectPosixAllSharesMarkers(composeContent, contract);
|
|
171
|
-
writeFileSync(join(resourceProjectDir, "docker-compose.yml"), composeContent, "utf8");
|
|
172
|
-
chmodSync(join(resourceProjectDir, "docker-compose.yml"), FILE_MODE);
|
|
173
|
-
if (rwEligibility?.headless === true) {
|
|
174
|
-
const probePayloadDir = join(resourceProjectDir, ".synopkgland");
|
|
175
|
-
mkdirSync(probePayloadDir, { recursive: true });
|
|
176
|
-
writeFileSync(join(probePayloadDir, "README"), HEADLESS_PROBE_README, "utf8");
|
|
177
|
-
chmodSync(join(probePayloadDir, "README"), FILE_MODE);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const htmlDir = join(options.project.root, "html");
|
|
181
|
-
if (existsSync(htmlDir)) {
|
|
182
|
-
cpSync(htmlDir, join(staging, "package", "html"), { recursive: true });
|
|
183
|
-
if (isResourceWorkerRoute) {
|
|
184
|
-
cpSync(htmlDir, join(staging, "package", "compose", "html"), { recursive: true });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (contract.adminport !== undefined && resolvedWebLaunch(contract) === "dsm-window") {
|
|
188
|
-
const uiDir = join(staging, "package", "ui");
|
|
189
|
-
mkdirSync(uiDir, { recursive: true });
|
|
190
|
-
writeFileSync(join(uiDir, "config"), renderDsmWindowConfig(contract, icons), "utf8");
|
|
191
|
-
writeDsmWindowIcons(uiDir, icons);
|
|
192
|
-
writeUiTexts(uiDir, contractAppTitle(contract));
|
|
193
|
-
}
|
|
194
|
-
if (gateway && gatewayBinary) {
|
|
195
|
-
writeGatewayPayload(join(staging, "package"), gatewayBinary, gateway);
|
|
196
|
-
}
|
|
197
|
-
chmodSync(join(staging, "INFO"), FILE_MODE);
|
|
198
|
-
chmodSync(join(staging, "privilege"), FILE_MODE);
|
|
199
|
-
chmodSync(join(staging, "package", "compose.yaml"), FILE_MODE);
|
|
200
|
-
chmodSync(join(staging, "package", "synology-app.yaml"), FILE_MODE);
|
|
201
|
-
if (contract.adminport !== undefined && resolvedWebLaunch(contract) === "dsm-window") {
|
|
202
|
-
chmodSync(join(staging, "package", "ui", "config"), FILE_MODE);
|
|
203
|
-
}
|
|
204
|
-
const outputPath = options.outputPath ?? join(options.project.root, `${String(contract.package)}.spk`);
|
|
205
|
-
copyProjectNotices(options.project.root, join(staging, 'package'));
|
|
206
|
-
await createDeterministicSpk(staging, outputPath);
|
|
207
|
-
return { ok: true, outputPath, errors: [] };
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
rmSync(staging, { recursive: true, force: true });
|
|
211
|
-
rmSync(gatewayWork, { recursive: true, force: true });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
function quoteInfo(value) {
|
|
215
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
216
|
-
}
|
|
217
|
-
function spkVersion(version) {
|
|
218
|
-
return /-\d+$/.test(version) ? version : `${version}-0001`;
|
|
219
|
-
}
|
|
220
|
-
function renderInfo(contract, archFamily, isResourceWorker = false) {
|
|
221
|
-
const lines = [];
|
|
222
|
-
const add = (key, value) => {
|
|
223
|
-
lines.push(`${key}=${quoteInfo(String(value))}`);
|
|
224
|
-
};
|
|
225
|
-
add("package", String(contract.package));
|
|
226
|
-
add("version", spkVersion(String(contract.version)));
|
|
227
|
-
add("displayname", String(contract.displayname));
|
|
228
|
-
add("description", String(contract.description));
|
|
229
|
-
add("maintainer", String(contract.maintainer));
|
|
230
|
-
const info = contractAppTitle(contract).info;
|
|
231
|
-
for (const locale of LOCALES) {
|
|
232
|
-
const text = info?.[locale];
|
|
233
|
-
if (text?.displayname)
|
|
234
|
-
add(`displayname_${DSM_LANGS[locale]}`, text.displayname);
|
|
235
|
-
if (text?.description)
|
|
236
|
-
add(`description_${DSM_LANGS[locale]}`, text.description);
|
|
237
|
-
}
|
|
238
|
-
for (const [key, value] of metadataInfoFields(contract.metadata))
|
|
239
|
-
add(key, value);
|
|
240
|
-
add("arch", archFamily === undefined ? "noarch" : infoArchValue(archFamily));
|
|
241
|
-
add("os_min_ver", String(contract.os_min_ver));
|
|
242
|
-
const viaGateway = isResourceWorker && usesAuthGateway(contract);
|
|
243
|
-
if (isResourceWorker) {
|
|
244
|
-
// DSM reports Container Manager as "not ready" through the package
|
|
245
|
-
// dependency gate even while pkgctl-ContainerManager is running. The
|
|
246
|
-
// docker-project worker itself remains the authoritative live prerequisite
|
|
247
|
-
// (ContainerManager >= 1432); keep only the reliable Web Station package
|
|
248
|
-
// dependency here so a valid resource-worker SPK can reach acquisition.
|
|
249
|
-
// A gateway package has no Web Station portal, so it does not depend on it.
|
|
250
|
-
if (contract.adminport !== undefined && !viaGateway) {
|
|
251
|
-
add("install_dep_packages", "WebStation");
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
add("startable", "yes");
|
|
255
|
-
const launch = resolvedWebLaunch(contract);
|
|
256
|
-
if (contract.adminprotocol && !viaGateway) {
|
|
257
|
-
add("adminprotocol", String(contract.adminprotocol));
|
|
258
|
-
}
|
|
259
|
-
if (launch === "new-page" || launch === "dsm-window") {
|
|
260
|
-
// Package Center Open uses INFO.adminurl as a same-host path. Do not emit
|
|
261
|
-
// adminport — that would open the Compose published port (ADR-0053/0062).
|
|
262
|
-
add("adminurl", trustedAppEntryPath(contract));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
if (contract.adminport !== undefined) {
|
|
266
|
-
add("adminport", Number(contract.adminport));
|
|
267
|
-
// DSM checkport=yes treats INFO.adminport as exclusive. Compose also binds
|
|
268
|
-
// that host port, so a matching adminport yields synopkg 283 — including
|
|
269
|
-
// when the same App's container already holds the port. ADR-0053: the
|
|
270
|
-
// published port is the Web Station upstream, not Package Center Open.
|
|
271
|
-
add("checkport", "no");
|
|
272
|
-
}
|
|
273
|
-
if (contract.adminurl) {
|
|
274
|
-
add("adminurl", String(contract.adminurl));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (launch === "dsm-window") {
|
|
278
|
-
add("dsmuidir", "ui");
|
|
279
|
-
add("dsmappname", String(contract.package));
|
|
280
|
-
}
|
|
281
|
-
else if (viaGateway) {
|
|
282
|
-
// The gateway CGI is served from ui/ even when the App opens in a new page.
|
|
283
|
-
add("dsmuidir", "ui");
|
|
284
|
-
}
|
|
285
|
-
return `${lines.join("\n")}\n`;
|
|
286
|
-
}
|
|
287
|
-
function renderPrivilege() {
|
|
288
|
-
return `${JSON.stringify({
|
|
289
|
-
defaults: {
|
|
290
|
-
"run-as": "package",
|
|
291
|
-
},
|
|
292
|
-
}, null, 2)}\n`;
|
|
293
|
-
}
|
|
294
|
-
function volumeSources(compose) {
|
|
295
|
-
return volumeMounts(compose).map((mount) => mount.source);
|
|
296
|
-
}
|
|
297
|
-
function volumeMounts(compose) {
|
|
298
|
-
const mounts = [];
|
|
299
|
-
for (const service of Object.values(compose.services ?? {})) {
|
|
300
|
-
const volumes = service.volumes;
|
|
301
|
-
if (!volumes) {
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
const entries = Array.isArray(volumes) ? volumes : [volumes];
|
|
305
|
-
for (const entry of entries) {
|
|
306
|
-
if (typeof entry === "string") {
|
|
307
|
-
const parts = entry.split(":");
|
|
308
|
-
const source = parts[0]?.trim() ?? "";
|
|
309
|
-
if (source) {
|
|
310
|
-
const modes = parts
|
|
311
|
-
.slice(2)
|
|
312
|
-
.flatMap((part) => part.split(","))
|
|
313
|
-
.map((part) => part.trim());
|
|
314
|
-
mounts.push({ source, readOnly: modes.includes("ro") });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else if (entry && typeof entry === "object" && "source" in entry) {
|
|
318
|
-
const record = entry;
|
|
319
|
-
const source = String(record.source ?? "").trim();
|
|
320
|
-
if (source) {
|
|
321
|
-
mounts.push({ source, readOnly: record.read_only === true });
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return mounts;
|
|
327
|
-
}
|
|
328
|
-
// A Compose that mounts nothing reaches even less of the NAS than one that
|
|
329
|
-
// mounts only PKGVAR, so it earns the same consent line. Returning false for
|
|
330
|
-
// zero sources left the wizard step with no items at all, and DSM's Package
|
|
331
|
-
// Center rejects that SPK with "Invalid file format".
|
|
332
|
-
function composeUsesOnlyAppSpace(compose) {
|
|
333
|
-
const sources = volumeSources(compose);
|
|
334
|
-
return sources.every((source) => source.startsWith("${SYNOPKG_PKGVAR}") || source.startsWith("${SYNO_APP_SECRETS_DIR}"));
|
|
335
|
-
}
|
|
336
|
-
function injectHeadlessProbeVolume(doc) {
|
|
337
|
-
const compose = (doc.toJSON() ?? {});
|
|
338
|
-
const serviceName = Object.keys(compose.services ?? {})[0];
|
|
339
|
-
if (!serviceName) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const existingVolumes = compose.services?.[serviceName]?.volumes;
|
|
343
|
-
const volumes = existingVolumes == null
|
|
344
|
-
? []
|
|
345
|
-
: Array.isArray(existingVolumes)
|
|
346
|
-
? [...existingVolumes]
|
|
347
|
-
: [existingVolumes];
|
|
348
|
-
if (!volumes.some((volume) => volume === HEADLESS_PROBE_VOLUME)) {
|
|
349
|
-
volumes.push(HEADLESS_PROBE_VOLUME);
|
|
350
|
-
}
|
|
351
|
-
doc.setIn(["services", serviceName, "volumes"], volumes);
|
|
352
|
-
}
|
|
353
|
-
function injectPosixAllSharesMarkers(composeContent, contract) {
|
|
354
|
-
const names = collectInstallParameters(contract)
|
|
355
|
-
.filter(({ param }) => param.type === "posix_all_shares")
|
|
356
|
-
.map(({ name }) => name);
|
|
357
|
-
if (names.length === 0) {
|
|
358
|
-
return composeContent;
|
|
359
|
-
}
|
|
360
|
-
const declared = new Set(names);
|
|
361
|
-
const lines = composeContent.split("\n");
|
|
362
|
-
const output = [];
|
|
363
|
-
for (const line of lines) {
|
|
364
|
-
const match = line.match(/^(\s*)-\s*["']?\$\{([A-Za-z_][A-Za-z0-9_]*)\}:/);
|
|
365
|
-
const name = match?.[2];
|
|
366
|
-
if (match && name && declared.has(name)) {
|
|
367
|
-
const marker = `# synopkgland:posix_all_shares ${name}`;
|
|
368
|
-
if (output.at(-1)?.trim() !== marker) {
|
|
369
|
-
output.push(`${match[1]}${marker}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
output.push(line);
|
|
373
|
-
}
|
|
374
|
-
return output.join("\n");
|
|
375
|
-
}
|
|
376
|
-
function renderInstallWizard(t, contract, compose) {
|
|
377
|
-
const items = [];
|
|
378
|
-
const params = collectInstallParameters(contract);
|
|
379
|
-
const hasSharedFolder = params.some((item) => item.param.type === "shared_folder");
|
|
380
|
-
const hasHomes = params.some((item) => item.param.type === "homes");
|
|
381
|
-
const hasDsmUser = params.some((item) => item.param.type === "dsm_user");
|
|
382
|
-
if (compose && scanComposePrivilege(compose).length > 0) {
|
|
383
|
-
items.push({
|
|
384
|
-
desc: t("consent.privilege"),
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
if (hasSharedFolder) {
|
|
388
|
-
items.push({
|
|
389
|
-
desc: t("consent.sharedFolder"),
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
if (hasHomes) {
|
|
393
|
-
items.push({
|
|
394
|
-
desc: t("consent.homes"),
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
if (hasDsmUser) {
|
|
398
|
-
items.push({
|
|
399
|
-
desc: t("consent.dsmUser"),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
if (!hasSharedFolder && (!compose || composeUsesOnlyAppSpace(compose))) {
|
|
403
|
-
items.push({
|
|
404
|
-
desc: t("consent.appOwnSpace"),
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
for (const { name, param } of params) {
|
|
408
|
-
if (param.type === "secret" && param.generate === true) {
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
if (param.type === "string") {
|
|
412
|
-
items.push({
|
|
413
|
-
type: "textfield",
|
|
414
|
-
desc: t("param.install", { name }),
|
|
415
|
-
subitems: [{
|
|
416
|
-
key: name,
|
|
417
|
-
desc: name,
|
|
418
|
-
defaultValue: param.default ?? "",
|
|
419
|
-
validator: { allowBlank: false },
|
|
420
|
-
}],
|
|
421
|
-
});
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (param.type === "boolean") {
|
|
425
|
-
items.push({
|
|
426
|
-
type: "multiselect",
|
|
427
|
-
desc: t("param.install", { name }),
|
|
428
|
-
subitems: [{ key: name, desc: name, defaultValue: param.default ?? false }],
|
|
429
|
-
});
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (param.type === "secret") {
|
|
433
|
-
items.push({
|
|
434
|
-
type: "password",
|
|
435
|
-
desc: t("param.install", { name }),
|
|
436
|
-
subitems: [{ key: name, desc: name, validator: { allowBlank: false } }],
|
|
437
|
-
});
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
if (param.type === "shared_folder") {
|
|
441
|
-
items.push(sharedFolderCombobox(t, name));
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (param.type === "posix_all_shares") {
|
|
445
|
-
// A47 (install side): DSM's declarative wizard only ever renders a
|
|
446
|
-
// single-select combo here, so the former multiSelect control silently
|
|
447
|
-
// mounted exactly one Share instead of every Share. Package Center can
|
|
448
|
-
// therefore never carry the full Share list, and there is no honest
|
|
449
|
-
// control to render -- so this branch produces only an informational
|
|
450
|
-
// textblock. The full snapshot arrives exclusively via CLI
|
|
451
|
-
// extra_values (`synopkgland deploy`), which the preinst materializer
|
|
452
|
-
// expands. No subitem key is emitted, so the install uifile contains no
|
|
453
|
-
// ALL_SHARES and no CHANGE_ALL_SHARES.
|
|
454
|
-
items.push({
|
|
455
|
-
type: "textblock",
|
|
456
|
-
desc: t("consent.posixAllShares"),
|
|
457
|
-
});
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (param.type === "dsm_user") {
|
|
461
|
-
items.push({
|
|
462
|
-
type: "combobox",
|
|
463
|
-
desc: t("param.runAs", { name }),
|
|
464
|
-
subitems: [{
|
|
465
|
-
key: name,
|
|
466
|
-
desc: name,
|
|
467
|
-
displayField: "name",
|
|
468
|
-
valueField: "name",
|
|
469
|
-
editable: false,
|
|
470
|
-
forceSelection: true,
|
|
471
|
-
autoSelect: false,
|
|
472
|
-
mode: "remote",
|
|
473
|
-
api_store: {
|
|
474
|
-
api: "SYNO.Core.User",
|
|
475
|
-
method: "list",
|
|
476
|
-
version: 1,
|
|
477
|
-
baseParams: { limit: -1, offset: 0 },
|
|
478
|
-
root: "users",
|
|
479
|
-
idProperty: "name",
|
|
480
|
-
fields: ["name"],
|
|
481
|
-
},
|
|
482
|
-
validator: { allowBlank: false },
|
|
483
|
-
}],
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return [{
|
|
488
|
-
step_title: t("install.step"),
|
|
489
|
-
items,
|
|
490
|
-
}];
|
|
491
|
-
}
|
|
492
|
-
function renderUpgradeWizard(t, contract, compose) {
|
|
493
|
-
const installWizard = renderInstallWizard(t, contract, compose);
|
|
494
|
-
const params = collectInstallParameters(contract);
|
|
495
|
-
// Every type whose value must survive an upgrade the administrator did not
|
|
496
|
-
// deliberately change. Two mechanisms are needed because DSM renders them
|
|
497
|
-
// differently: a control that can be left blank just allows blank, while one
|
|
498
|
-
// that always carries a value needs an explicit "change this" gate.
|
|
499
|
-
// - dsm_user : blank in some layouts, but measured on DSM 7.4-90069 with
|
|
500
|
-
// Shared Folder controls beside it to come up pre-filled with a
|
|
501
|
-
// Shared Folder name, so it needs the same explicit gate
|
|
502
|
-
// - string : blank once the new version's default is withheld (A44)
|
|
503
|
-
// - shared_folder : DSM prefills the first store row
|
|
504
|
-
// - boolean : a checkbox has no blank state at all
|
|
505
|
-
// posix_all_shares is intentionally absent: it renders as a textblock with
|
|
506
|
-
// no subitem, so there is nothing to blank or gate on the upgrade wizard.
|
|
507
|
-
// Its value can only arrive via CLI extra_values, and the preinst
|
|
508
|
-
// change-gate shell (`upgrade_change_enabled`, `CHANGE_<name>`) still
|
|
509
|
-
// governs that path, reusing the canonical snapshot on an untouched upgrade.
|
|
510
|
-
const upgradeReuseTypes = new Map(params
|
|
511
|
-
.filter(({ param }) => param.type === "shared_folder" ||
|
|
512
|
-
param.type === "dsm_user" ||
|
|
513
|
-
param.type === "string" ||
|
|
514
|
-
param.type === "boolean")
|
|
515
|
-
.map(({ name, param }) => [name, param.type]));
|
|
516
|
-
const needsChangeGate = (type) => type === "shared_folder" || type === "dsm_user" || type === "boolean";
|
|
517
|
-
const upgradeItems = installWizard[0].items.flatMap((item) => {
|
|
518
|
-
if (!Array.isArray(item.subitems)) {
|
|
519
|
-
return [item];
|
|
520
|
-
}
|
|
521
|
-
const reuseKeys = [];
|
|
522
|
-
const subitems = item.subitems.map((subitem) => {
|
|
523
|
-
if (!subitem || typeof subitem !== "object") {
|
|
524
|
-
return subitem;
|
|
525
|
-
}
|
|
526
|
-
const record = subitem;
|
|
527
|
-
if (typeof record.key !== "string" || !upgradeReuseTypes.has(record.key)) {
|
|
528
|
-
return subitem;
|
|
529
|
-
}
|
|
530
|
-
reuseKeys.push(record.key);
|
|
531
|
-
const reused = {
|
|
532
|
-
...record,
|
|
533
|
-
validator: { allowBlank: true },
|
|
534
|
-
};
|
|
535
|
-
// Withholding the default is what makes an untouched upgrade blank, and
|
|
536
|
-
// blank is what lets preinst fall through to the canonical .env value.
|
|
537
|
-
// Install keeps the default -- only this upgrade copy drops it.
|
|
538
|
-
delete reused.defaultValue;
|
|
539
|
-
return reused;
|
|
540
|
-
});
|
|
541
|
-
if (reuseKeys.length === 0) {
|
|
542
|
-
return [item];
|
|
543
|
-
}
|
|
544
|
-
const control = {
|
|
545
|
-
...item,
|
|
546
|
-
desc: t("upgrade.reuseDesc", { desc: String(item.desc ?? "") }),
|
|
547
|
-
subitems,
|
|
548
|
-
};
|
|
549
|
-
const changeControls = reuseKeys
|
|
550
|
-
.filter((key) => needsChangeGate(upgradeReuseTypes.get(key)))
|
|
551
|
-
.map((key) => {
|
|
552
|
-
const type = upgradeReuseTypes.get(key);
|
|
553
|
-
const label = type === "shared_folder"
|
|
554
|
-
? t("label.sharedFolder")
|
|
555
|
-
: type === "dsm_user"
|
|
556
|
-
? t("label.runAs")
|
|
557
|
-
: key;
|
|
558
|
-
return {
|
|
559
|
-
type: "multiselect",
|
|
560
|
-
desc: t("upgrade.changeDesc", { label }),
|
|
561
|
-
subitems: [{
|
|
562
|
-
key: `CHANGE_${key}`,
|
|
563
|
-
desc: t("upgrade.changeSub", { label }),
|
|
564
|
-
defaultValue: false,
|
|
565
|
-
}],
|
|
566
|
-
};
|
|
567
|
-
});
|
|
568
|
-
return [...changeControls, control];
|
|
569
|
-
});
|
|
570
|
-
const sharedFolderNames = params
|
|
571
|
-
.filter((item) => item.param.type === "shared_folder")
|
|
572
|
-
.map((item) => item.name);
|
|
573
|
-
const hasSharedFolder = sharedFolderNames.length > 0;
|
|
574
|
-
const hasDsmUser = params.some((item) => item.param.type === "dsm_user");
|
|
575
|
-
const hasReadWriteSharedFolder = hasSharedFolder &&
|
|
576
|
-
compose !== undefined &&
|
|
577
|
-
volumeMounts(compose).some((mount) => !mount.readOnly &&
|
|
578
|
-
sharedFolderNames.some((name) => mount.source === `\${${name}}` || mount.source.startsWith(`\${${name}}/`)));
|
|
579
|
-
const upgradeConsent = [];
|
|
580
|
-
const posixAllSharesNames = params
|
|
581
|
-
.filter((item) => item.param.type === "posix_all_shares")
|
|
582
|
-
.map((item) => item.name);
|
|
583
|
-
if (posixAllSharesNames.length > 0) {
|
|
584
|
-
upgradeConsent.push({
|
|
585
|
-
desc: t("upgrade.posixNotice"),
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
if (hasReadWriteSharedFolder) {
|
|
589
|
-
upgradeConsent.push({
|
|
590
|
-
desc: t("upgrade.sharedFolderRw"),
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
if (hasDsmUser && hasSharedFolder) {
|
|
594
|
-
upgradeConsent.push({
|
|
595
|
-
desc: t("upgrade.dsmUser"),
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
if (upgradeConsent.length === 0) {
|
|
599
|
-
return [{
|
|
600
|
-
...installWizard[0],
|
|
601
|
-
step_title: t("upgrade.step"),
|
|
602
|
-
items: upgradeItems,
|
|
603
|
-
}];
|
|
604
|
-
}
|
|
605
|
-
const firstControl = upgradeItems.findIndex((item) => item.subitems !== undefined);
|
|
606
|
-
const insertAt = firstControl === -1 ? upgradeItems.length : firstControl;
|
|
607
|
-
const items = [...upgradeItems];
|
|
608
|
-
items.splice(insertAt, 0, ...upgradeConsent);
|
|
609
|
-
return [{
|
|
610
|
-
...installWizard[0],
|
|
611
|
-
step_title: t("upgrade.step"),
|
|
612
|
-
items,
|
|
613
|
-
}];
|
|
614
|
-
}
|
|
615
|
-
function writeWizards(staging, contract, compose, kind) {
|
|
616
|
-
const install = renderInstallWizardFiles(String(contract.package), (t) => renderInstallWizard(t, contract, compose));
|
|
617
|
-
writeWizardUiFiles(staging, {
|
|
618
|
-
...install.static,
|
|
619
|
-
...install.dynamic,
|
|
620
|
-
...renderWizardFiles(UPGRADE_UIFILE, (t) => renderUpgradeWizard(t, contract, compose)),
|
|
621
|
-
...renderUninstallWizardFiles(String(contract.package), kind),
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
function renderDynamicPreinstMaterializer(contract) {
|
|
625
|
-
const allParams = collectInstallParameters(contract);
|
|
626
|
-
const scalarParams = allParams.filter(({ param }) => param.type === "string" || param.type === "boolean");
|
|
627
|
-
const sharedFolderParams = allParams.filter(({ param }) => param.type === "shared_folder");
|
|
628
|
-
const posixAllSharesParams = allParams.filter(({ param }) => param.type === "posix_all_shares");
|
|
629
|
-
const dsmUserParam = allParams.find(({ param }) => param.type === "dsm_user");
|
|
630
|
-
const lines = [
|
|
631
|
-
"#!/bin/sh",
|
|
632
|
-
"set -eu",
|
|
633
|
-
"umask 077",
|
|
634
|
-
"# D3/D5.4 package-only carrier; values are wizard answers, canonical values, or defaults.",
|
|
635
|
-
"die() { echo \"dynamic route materialization failed: $1\" >&2; exit 1; }",
|
|
636
|
-
'T="${SYNOPKG_PKGINST_TEMP_DIR:?}"',
|
|
637
|
-
'STATUS="${SYNOPKG_PKG_STATUS:?}"',
|
|
638
|
-
'PROJECT="$T/compose"',
|
|
639
|
-
'RUNTIME_ENV="$PROJECT/.env"',
|
|
640
|
-
'CANONICAL_ENV="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}/.env"',
|
|
641
|
-
'PLATFORM_PKGVAR="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}"',
|
|
642
|
-
'PLATFORM_SECRETS="${SYNOPKG_PKGDEST_VOL:?}/@apphome/${SYNOPKG_PKGNAME:?}/secrets"',
|
|
643
|
-
'[ -d "$PROJECT" ] || die "compose directory is missing"',
|
|
644
|
-
'[ ! -L "$PROJECT" ] || die "compose directory is a symlink"',
|
|
645
|
-
'[ "$(stat -c %u "$PROJECT")" = "$(id -u)" ] || die "compose directory owner is not the package user"',
|
|
646
|
-
'[ ! -L "$RUNTIME_ENV" ] || die "runtime file is a symlink"',
|
|
647
|
-
'if [ -e "$RUNTIME_ENV" ] && [ ! -f "$RUNTIME_ENV" ]; then die "runtime path is not a regular file"; fi',
|
|
648
|
-
"read_env_value() {",
|
|
649
|
-
" file=\"$1\"",
|
|
650
|
-
" key=\"$2\"",
|
|
651
|
-
" awk -F= -v key=\"$key\" '$1 == key { value=substr($0, index($0,\"=\")+1); if (value ~ /^\".*\"$/) value=substr(value,2,length(value)-2); print value; found=1; exit } END { if (!found) exit 1 }' \"$file\"",
|
|
652
|
-
"}",
|
|
653
|
-
"append_env() {",
|
|
654
|
-
" key=\"$1\"",
|
|
655
|
-
" value=\"$2\"",
|
|
656
|
-
" type=\"${3:-string}\"",
|
|
657
|
-
" if printf '%s' \"$value\" | LC_ALL=C grep -q '[[:cntrl:]]'; then",
|
|
658
|
-
" die \"control character in environment value $key\"",
|
|
659
|
-
" fi",
|
|
660
|
-
" case \"$value\" in",
|
|
661
|
-
" *[[:cntrl:]]*) die \"control character in environment value $key\" ;;",
|
|
662
|
-
" esac",
|
|
663
|
-
" if [ \"$type\" = \"boolean\" ]; then",
|
|
664
|
-
" normalized=\"$(printf '%s' \"$value\" | LC_ALL=C tr '[:upper:]' '[:lower:]')\"",
|
|
665
|
-
" case \"$normalized\" in",
|
|
666
|
-
" true|false) value=\"$normalized\" ;;",
|
|
667
|
-
" *) die \"invalid boolean value $key\" ;;",
|
|
668
|
-
" esac",
|
|
669
|
-
" fi",
|
|
670
|
-
" case \"$value\" in",
|
|
671
|
-
" *[!A-Za-z0-9_./:@-]*)",
|
|
672
|
-
" escaped=\"$(printf '%s' \"$value\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g')\"",
|
|
673
|
-
" printf '%s=\"%s\"\\n' \"$key\" \"$escaped\"",
|
|
674
|
-
" ;;",
|
|
675
|
-
" *) printf '%s=%s\\n' \"$key\" \"$value\" ;;",
|
|
676
|
-
" esac",
|
|
677
|
-
"}",
|
|
678
|
-
];
|
|
679
|
-
// Emitted whenever anything reads a CHANGE_ gate. Boolean parameters (A44) use
|
|
680
|
-
// it too, and a package with only booleans has no Share resolvers -- leaving it
|
|
681
|
-
// behind that condition made `! upgrade_change_enabled ...` a missing command,
|
|
682
|
-
// which is true, so the gate silently always reused the canonical value.
|
|
683
|
-
const usesUpgradeChangeGate = sharedFolderParams.length > 0 ||
|
|
684
|
-
posixAllSharesParams.length > 0 ||
|
|
685
|
-
collectInstallParameters(contract).some(({ param }) => param.type === "boolean" || param.type === "dsm_user");
|
|
686
|
-
if (usesUpgradeChangeGate) {
|
|
687
|
-
lines.push("upgrade_change_enabled() {", " normalized=\"$(printf '%s' \"$1\" | LC_ALL=C tr '[:upper:]' '[:lower:]')\"", " case \"$normalized\" in", " true|1|yes|on) return 0 ;;", " *) return 1 ;;", " esac", "}");
|
|
688
|
-
}
|
|
689
|
-
if (sharedFolderParams.length > 0 || posixAllSharesParams.length > 0) {
|
|
690
|
-
lines.push("resolve_shared_folder() {", " share_name=\"$1\"", " case \"$share_name\" in", " */*|*..*) die \"invalid share name $share_name\" ;;", " '') die \"share name is empty\" ;;", " esac", " mounts_file=\"${D54_MOUNTS_FILE:-/proc/mounts}\"", " if [ ! -r \"$mounts_file\" ] && [ -r \"/etc/mtab\" ]; then", " mounts_file=\"/etc/mtab\"", " fi", " if [ ! -r \"$mounts_file\" ]; then", " die \"cannot read mount points from $mounts_file\"", " fi", " vols=\"$(awk '$2 ~ /^\\/volume[0-9]+$/ {print $2}' \"$mounts_file\" | sort -u)\"", ' if [ -z "$vols" ]; then', " vols=\"$(awk '{print $2}' \"$mounts_file\" | sort -u)\"", " fi", " matched=\"\"", " count=0", " for v in $vols; do", " if [ -d \"$v/$share_name\" ]; then", " matched=\"$v/$share_name\"", " count=$((count + 1))", " fi", " done", " if [ \"$count\" -eq 0 ]; then", " die \"share $share_name not found on any volume\"", " elif [ \"$count\" -gt 1 ]; then", " die \"share $share_name matched multiple volumes\"", " fi", " printf '%s' \"$matched\"", "}");
|
|
691
|
-
}
|
|
692
|
-
if (posixAllSharesParams.length > 0) {
|
|
693
|
-
lines.push("normalize_posix_all_shares() {", " value=\"$1\"", " case \"$value\" in", " \\[*\\]) value=\"$(printf '%s' \"$value\" | sed 's/^[[:space:]]*\\[//; s/\\][[:space:]]*$//; s/\"//g')\" ;;", " esac", " printf '%s' \"$value\" | awk -F, '{ for (i = 1; i <= NF; i++) { gsub(/^[ \\t]+|[ \\t]+$/, \"\", $i); if ($i != \"\") print $i } }'", "}", "resolve_posix_all_shares() {", " raw_list=\"$1\"", " normalized=\"$(normalize_posix_all_shares \"$raw_list\")\"", " [ -n \"$normalized\" ] || die \"POSIX_ALL_SHARES_EMPTY\"", " names=\"\"", " paths=\"\"", " count=0", " while IFS= read -r share_name; do", " case \"$share_name\" in", " */*|*..*|@*|'') die \"invalid Share name $share_name\" ;;", " *[[:cntrl:]]*) die \"invalid Share name $share_name\" ;;", " esac", " share_path=\"$(resolve_shared_folder \"$share_name\")\"", " names=\"${names}${names:+,}${share_name}\"", " paths=\"${paths}${paths:+,}${share_path}\"", " count=$((count + 1))", " done <<EOF", "$normalized", "EOF", " [ \"$count\" -gt 0 ] || die \"POSIX_ALL_SHARES_EMPTY\"", " printf '%s\\n%s\\n%s' \"$names\" \"$paths\" \"$count\"", "}", "rewrite_posix_all_shares() {", " p_name=\"$1\"", " share_names=\"$2\"", " share_paths=\"$3\"", " compose_file=\"$4\"", " [ -f \"$compose_file\" ] || die \"compose file missing at $compose_file\"", " [ ! -L \"$compose_file\" ] || die \"compose file is a symlink\"", " tmp_compose=\"$(mktemp \"${compose_file}.tmp.XXXXXX\")\"", " awk -v param=\"$p_name\" -v share_names=\"$share_names\" -v share_paths=\"$share_paths\" '", " BEGIN {", " name_count = split(share_names, names, \",\")", " path_count = split(share_paths, paths, \",\")", " marker = \"# synopkgland:posix_all_shares \" param", " pending = 0", " single_quote = sprintf(\"%c\", 39)", " }", " {", " if (index($0, marker) > 0 && $0 ~ /^[ \\t]*# synopkgland:posix_all_shares /) {", " print $0", " pending = 1", " next", " }", " if (!pending) { print $0; next }", " pending = 0", " line = $0", " indent = line", " sub(/[^ \\t].*$/, \"\", indent)", " entry = line", " sub(/^[ \\t]*-[ \\t]*/, \"\", entry)", " if (entry ~ /^\".*\"$/) entry = substr(entry, 2, length(entry) - 2)", " if (length(entry) >= 2 && substr(entry, 1, 1) == single_quote && substr(entry, length(entry), 1) == single_quote) entry = substr(entry, 2, length(entry) - 2)", " split(entry, fields, \":\")", " if (fields[1] != \"${\" param \"}\" || fields[2] == \"\") { print $0; next }", " mode = \"\"", " if (length(fields) >= 3) mode = \":\" fields[3]", " for (i = 1; i <= name_count && i <= path_count; i++) {", " print indent \"- \" paths[i] \":\" fields[2] \"/\" names[i] mode", " }", " }", " ' \"$compose_file\" > \"$tmp_compose\" || die \"failed to expand POSIX all-shares in $compose_file\"", " mv \"$tmp_compose\" \"$compose_file\" || die \"failed to replace $compose_file\"", "}");
|
|
694
|
-
}
|
|
695
|
-
if (dsmUserParam) {
|
|
696
|
-
lines.push("resolve_dsm_user() {", " user_name=\"$1\"", " case \"$user_name\" in", " */*|*..*|*[[:cntrl:]]*) die \"invalid user name $user_name\" ;;", " '') die \"user name is empty\" ;;", " esac", " u_uid=\"$(id -u -- \"$user_name\" 2>/dev/null || true)\"", " case \"$u_uid\" in", " ''|*[!0-9]*) die \"resolved uid for $user_name is not a valid integer\" ;;", " 0) die \"resolved uid for $user_name cannot be 0 (root)\" ;;", " esac", " u_gid=\"$(id -g -- \"$user_name\" 2>/dev/null || true)\"", " case \"$u_gid\" in", " ''|*[!0-9]*) die \"resolved gid for $user_name is not a valid integer\" ;;", " esac", " # id -G returns all group ids including primary", " raw_groups=\"$(id -G -- \"$user_name\" 2>/dev/null || true)\"", " # filter out primary gid, deduplicate, keep numeric only", " supp_groups=\"\"", " for g in $raw_groups; do", " case \"$g\" in", " ''|*[!0-9]*) continue ;;", " esac", " if [ \"$g\" != \"$u_gid\" ]; then", " already=0", " for sg in $supp_groups; do", " if [ \"$sg\" = \"$g\" ]; then", " already=1", " break", " fi", " done", " if [ \"$already\" -eq 0 ]; then", " supp_groups=\"${supp_groups}${supp_groups:+ }$g\"", " fi", " fi", " done", " printf '%s %s %s' \"$u_uid\" \"$u_gid\" \"$supp_groups\"", "}", "rewrite_group_add() {", " p_name=\"$1\"", " g_list=\"$2\"", " compose_file=\"$3\"", " [ -f \"$compose_file\" ] || die \"compose file missing at $compose_file\"", " [ ! -L \"$compose_file\" ] || die \"compose file is a symlink\"", " tmp_compose=\"$(mktemp \"${compose_file}.tmp.XXXXXX\")\"", " awk -v param=\"$p_name\" -v g_list=\"$g_list\" '", " BEGIN {", " n = split(g_list, g_arr, \" \")", " }", " {", " regex = \"^[ \\t]*# synopkgland:group_add \" param \"$\"", " if ($0 ~ regex) {", " match($0, /^[ \\t]*/)", " indent = substr($0, RSTART, RLENGTH)", " if (n > 0) {", " print indent \"group_add:\"", " for (i = 1; i <= n; i++) {", " print indent \" - ${\" param \"_GROUP_\" i \"}\"", " }", " }", " } else {", " print $0", " }", " }", " ' \"$compose_file\" > \"$tmp_compose\" || die \"failed to rewrite group_add in $compose_file\"", " mv \"$tmp_compose\" \"$compose_file\" || die \"failed to replace $compose_file\"", "}");
|
|
697
|
-
}
|
|
698
|
-
lines.push('tmp="$(mktemp "$PROJECT/.env.tmp.XXXXXX")"', 'chmod 600 "$tmp"', '[ "$(stat -c %a "$tmp")" = "600" ] || die "temporary env mode is not 0600"', '[ "$(stat -c %u "$tmp")" = "$(id -u)" ] || die "temporary env owner is not the package user"', "trap 'rm -f \"$tmp\"' EXIT HUP INT TERM", "{");
|
|
699
|
-
const valueVars = [];
|
|
700
|
-
for (const { name, param } of scalarParams) {
|
|
701
|
-
const type = param.type;
|
|
702
|
-
const valueVar = "D3_VALUE_" + name;
|
|
703
|
-
valueVars.push({ name, valueVar, type });
|
|
704
|
-
const fallback = param.default !== undefined
|
|
705
|
-
? String(param.default)
|
|
706
|
-
: type === "boolean"
|
|
707
|
-
? "false"
|
|
708
|
-
: "";
|
|
709
|
-
// A44: on UPGRADE the wizard always submits something for these controls, so
|
|
710
|
-
// "was it submitted" cannot mean "did the administrator choose it". A blank
|
|
711
|
-
// string, or a boolean whose CHANGE_ gate is unchecked, means "keep what is
|
|
712
|
-
// installed" -- otherwise the new version's default would silently replace a
|
|
713
|
-
// value chosen at install time. INSTALL is unaffected: STATUS is not UPGRADE,
|
|
714
|
-
// so the reuse branch is skipped and the default still applies.
|
|
715
|
-
const reuseGuard = type === "boolean"
|
|
716
|
-
? `! upgrade_change_enabled "\${CHANGE_${name}:-}"`
|
|
717
|
-
: `[ -z "\${${name}:-}" ]`;
|
|
718
|
-
lines.push(` if [ "$STATUS" = "UPGRADE" ] && ${reuseGuard} && [ -f "$CANONICAL_ENV" ] && ` +
|
|
719
|
-
`[ ! -L "$CANONICAL_ENV" ] && ${valueVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", ` elif [ "\${${name}+x}" = x ]; then`, ` ${valueVar}="\${${name}}"`, ' elif [ "$STATUS" = "UPGRADE" ] && [ -f "$CANONICAL_ENV" ] && [ ! -L "$CANONICAL_ENV" ] && ' +
|
|
720
|
-
`${valueVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", " else", " " + valueVar + "=" + shellQuote(fallback), " fi");
|
|
721
|
-
}
|
|
722
|
-
const shareVars = [];
|
|
723
|
-
for (const { name } of sharedFolderParams) {
|
|
724
|
-
const rawVar = "D54_RAW_SHARE_" + name;
|
|
725
|
-
const pathVar = "D54_RESOLVED_PATH_" + name;
|
|
726
|
-
const changeVar = "CHANGE_" + name;
|
|
727
|
-
shareVars.push({ name, pathVar });
|
|
728
|
-
lines.push(` if [ "$STATUS" = "UPGRADE" ] && upgrade_change_enabled "\${${changeVar}:-}"; then`, ` if [ "\${${name}+x}" = x ] && [ -n "\${${name}}" ]; then`, ` ${rawVar}="\${${name}}"`, " else", ` die "missing required shared folder parameter ${name}"`, " fi", ' elif [ "$STATUS" = "UPGRADE" ] && [ -f "$CANONICAL_ENV" ] && [ ! -L "$CANONICAL_ENV" ] && ' +
|
|
729
|
-
`${rawVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", ' elif [ "$STATUS" = "UPGRADE" ]; then', ` die "missing canonical shared folder parameter ${name}"`, ` elif [ "\${${name}+x}" = x ]; then`, ` ${rawVar}="\${${name}}"`, " else", ` die "missing required shared folder parameter ${name}"`, " fi", ` case "$${rawVar}" in`, " /*) " + pathVar + "=\"$" + rawVar + "\" ;;", " *) " + pathVar + "=\"$(resolve_shared_folder \"$" + rawVar + "\")\" ;;", " esac");
|
|
730
|
-
}
|
|
731
|
-
const posixVars = [];
|
|
732
|
-
for (const { name } of posixAllSharesParams) {
|
|
733
|
-
const rawVar = "D54_RAW_POSIX_" + name;
|
|
734
|
-
const namesVar = "D54_POSIX_NAMES_" + name;
|
|
735
|
-
const pathsVar = "D54_POSIX_PATHS_" + name;
|
|
736
|
-
const countVar = "D54_POSIX_COUNT_" + name;
|
|
737
|
-
const changeVar = "CHANGE_" + name;
|
|
738
|
-
posixVars.push({ name, namesVar, pathsVar, countVar });
|
|
739
|
-
lines.push(` if [ "$STATUS" = "UPGRADE" ] && upgrade_change_enabled "\${${changeVar}:-}"; then`, ` if [ "\${${name}+x}" = x ] && [ -n "\${${name}}" ]; then`, ` ${rawVar}="\${${name}}"`, " else", ` die "missing required POSIX all-shares parameter ${name}"`, " fi", ' elif [ "$STATUS" = "UPGRADE" ] && [ -f "$CANONICAL_ENV" ] && [ ! -L "$CANONICAL_ENV" ] && ' +
|
|
740
|
-
`${rawVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", ' elif [ "$STATUS" = "UPGRADE" ]; then', ` die "missing canonical POSIX all-shares parameter ${name}"`, ` elif [ "\${${name}+x}" = x ]; then`, ` ${rawVar}="\${${name}}"`, " else", ` die "missing required POSIX all-shares parameter ${name}"`, " fi", ` posix_info="$(resolve_posix_all_shares "$${rawVar}")"`, ` ${namesVar}="$(printf '%s\\n' "$posix_info" | sed -n '1p')"`, ` ${pathsVar}="$(printf '%s\\n' "$posix_info" | sed -n '2p')"`, ` ${countVar}="$(printf '%s\\n' "$posix_info" | sed -n '3p')"`, ` rewrite_posix_all_shares "${name}" "$${namesVar}" "$${pathsVar}" "$PROJECT/docker-compose.yml"`);
|
|
741
|
-
}
|
|
742
|
-
if (dsmUserParam) {
|
|
743
|
-
const name = dsmUserParam.name;
|
|
744
|
-
const rawUserVar = "D54_RAW_USER_" + name;
|
|
745
|
-
// Like the Shared Folder gate: on UPGRADE the submitted user only counts when
|
|
746
|
-
// CHANGE_<name> is ticked, because the control is not reliably blank.
|
|
747
|
-
lines.push(` if [ "$STATUS" = "UPGRADE" ] && upgrade_change_enabled "\${CHANGE_${name}:-}"; then`, ` if [ "\${${name}+x}" = x ] && [ -n "\${${name}}" ]; then`, ` ${rawUserVar}="\${${name}}"`, " else", ` die "missing required dsm user parameter ${name}"`, " fi", ' elif [ "$STATUS" = "UPGRADE" ] && [ -f "$CANONICAL_ENV" ] && [ ! -L "$CANONICAL_ENV" ] && ' +
|
|
748
|
-
`${rawUserVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", ` elif [ "\${${name}+x}" = x ]; then`, ` ${rawUserVar}="\${${name}}"`, " else", ` die "missing required dsm user parameter ${name}"`, " fi", ` resolved_user_info="$(resolve_dsm_user "$${rawUserVar}")"`, " set -- $resolved_user_info", ` ${name}_UID="$1"`, ` ${name}_GID="$2"`, " shift 2 || true", ` ${name}_GROUPS="$*"`, ` rewrite_group_add "${name}" "$${name}_GROUPS" "$PROJECT/docker-compose.yml"`);
|
|
749
|
-
}
|
|
750
|
-
lines.push(' append_env "SYNOPKG_PKGVAR" "$PLATFORM_PKGVAR"', ' append_env "SYNO_APP_SECRETS_DIR" "$PLATFORM_SECRETS"');
|
|
751
|
-
for (const { name, valueVar, type } of valueVars) {
|
|
752
|
-
lines.push(` append_env "${name}" "$${valueVar}" "${type}"`);
|
|
753
|
-
}
|
|
754
|
-
for (const { name, pathVar } of shareVars) {
|
|
755
|
-
lines.push(` append_env "${name}" "$${pathVar}" "string"`);
|
|
756
|
-
}
|
|
757
|
-
for (const { name, namesVar, countVar } of posixVars) {
|
|
758
|
-
lines.push(` append_env "${name}" "$${namesVar}" "string"`, ` append_env "${name}_COUNT" "$${countVar}" "string"`);
|
|
759
|
-
}
|
|
760
|
-
if (dsmUserParam) {
|
|
761
|
-
const name = dsmUserParam.name;
|
|
762
|
-
const rawUserVar = "D54_RAW_USER_" + name;
|
|
763
|
-
lines.push(` append_env "${name}" "$${rawUserVar}" "string"`, ` append_env "${name}_UID" "$${name}_UID" "string"`, ` append_env "${name}_GID" "$${name}_GID" "string"`, ` idx=1`, ` for g in $${name}_GROUPS; do`, ` append_env "${name}_GROUP_\${idx}" "$g" "string"`, ` idx=$((idx + 1))`, ` done`);
|
|
764
|
-
}
|
|
765
|
-
lines.push(" } > \"$tmp\"", " mv \"$tmp\" \"$RUNTIME_ENV\"", " trap - EXIT HUP INT TERM", " [ ! -L \"$RUNTIME_ENV\" ] || die \"runtime file became a symlink\"", " [ \"$(stat -c %a \"$RUNTIME_ENV\")\" = \"600\" ] || die \"runtime env mode is not 0600\"", "exit 0");
|
|
766
|
-
return lines.join("\n") + "\n";
|
|
767
|
-
}
|
|
768
|
-
function renderDynamicCanonicalMirror(headless = false) {
|
|
769
|
-
const lines = [
|
|
770
|
-
"#!/bin/sh",
|
|
771
|
-
"set -eu",
|
|
772
|
-
"umask 077",
|
|
773
|
-
"# D3 post-worker mirror; the runtime .env is the only source of truth here.",
|
|
774
|
-
"die() { echo \"dynamic route canonical mirror failed: $1\" >&2; exit 1; }",
|
|
775
|
-
'PKGVAR="${SYNOPKG_PKGVAR:?}"',
|
|
776
|
-
'PKGHOME="${SYNOPKG_PKGHOME:?}"',
|
|
777
|
-
'PKGDEST="${SYNOPKG_PKGDEST:?}"',
|
|
778
|
-
'PROJECT="${PKGDEST}/compose"',
|
|
779
|
-
'RUNTIME_ENV="${PROJECT}/.env"',
|
|
780
|
-
'CANONICAL_ENV="${PKGVAR}/.env"',
|
|
781
|
-
'SECRETS="${PKGHOME}/secrets"',
|
|
782
|
-
'mkdir -p "$PKGVAR"',
|
|
783
|
-
'[ -d "$PKGVAR" ] || die "PKGVAR is not a directory"',
|
|
784
|
-
'if [ -L "$SECRETS" ]; then die "secret root is a symlink"; fi',
|
|
785
|
-
'mkdir -p "$SECRETS"',
|
|
786
|
-
'[ -d "$SECRETS" ] || die "secret root is not a directory"',
|
|
787
|
-
'chmod 700 "$SECRETS"',
|
|
788
|
-
'[ "$(stat -c %a "$SECRETS")" = "700" ] || die "secret root mode is not 0700"',
|
|
789
|
-
'[ -d "$PROJECT" ] || die "project directory is missing"',
|
|
790
|
-
'[ ! -L "$PROJECT" ] || die "project directory is a symlink"',
|
|
791
|
-
'[ ! -L "$RUNTIME_ENV" ] || die "runtime file is a symlink"',
|
|
792
|
-
'[ -f "$RUNTIME_ENV" ] || die "runtime file is not a regular file"',
|
|
793
|
-
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file is a symlink"',
|
|
794
|
-
'if [ -e "$CANONICAL_ENV" ] && [ ! -f "$CANONICAL_ENV" ]; then die "canonical path is not a regular file"; fi',
|
|
795
|
-
'tmp="$(mktemp "$PKGVAR/.env.tmp.XXXXXX")"',
|
|
796
|
-
'chmod 600 "$tmp"',
|
|
797
|
-
'[ "$(stat -c %a "$tmp")" = "600" ] || die "temporary env mode is not 0600"',
|
|
798
|
-
'trap \'rm -f "$tmp"\' EXIT HUP INT TERM',
|
|
799
|
-
'cp "$RUNTIME_ENV" "$tmp"',
|
|
800
|
-
'chmod 600 "$tmp"',
|
|
801
|
-
'mv "$tmp" "$CANONICAL_ENV"',
|
|
802
|
-
'trap - EXIT HUP INT TERM',
|
|
803
|
-
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file became a symlink"',
|
|
804
|
-
'[ "$(stat -c %a "$CANONICAL_ENV")" = "600" ] || die "canonical env mode is not 0600"',
|
|
805
|
-
];
|
|
806
|
-
if (headless) {
|
|
807
|
-
lines.push('MARKER="${PKGVAR}/.docker-project-managed"', '[ ! -L "$MARKER" ] || die "docker-project marker is a symlink"', 'marker_tmp="$(mktemp "$PKGVAR/.docker-project-managed.tmp.XXXXXX")"', 'printf \'%s\\n\' managed > "$marker_tmp"', 'chmod 600 "$marker_tmp"', 'mv "$marker_tmp" "$MARKER"');
|
|
808
|
-
}
|
|
809
|
-
lines.push("exit 0");
|
|
810
|
-
return lines.join("\n") + "\n";
|
|
811
|
-
}
|
|
812
|
-
function shellQuote(value) {
|
|
813
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
814
|
-
}
|
|
815
|
-
function renderManagedScript(name, contract, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false, isHeadlessRoute = false) {
|
|
816
|
-
if (isResourceWorker) {
|
|
817
|
-
if (name === "start-stop-status") {
|
|
818
|
-
return renderResourceWorkerStartStopStatus(String(contract.package), resourceWorkerAdminport, isHeadlessRoute);
|
|
819
|
-
}
|
|
820
|
-
if (isDynamicRoute && name === "preinst") {
|
|
821
|
-
return renderDynamicPreinstMaterializer(contract);
|
|
822
|
-
}
|
|
823
|
-
if (isDynamicRoute && (name === "postinst" || name === "postupgrade")) {
|
|
824
|
-
return renderDynamicCanonicalMirror(isHeadlessRoute);
|
|
825
|
-
}
|
|
826
|
-
if (isHeadlessRoute && (name === "postinst" || name === "postupgrade")) {
|
|
827
|
-
return renderHeadlessResourceMarker();
|
|
828
|
-
}
|
|
829
|
-
if (name === "postuninst") {
|
|
830
|
-
const projectRoot = isDynamicRoute ? 'PROJECT="${SYNOPKG_PKGDEST:-}/compose"\n' : "";
|
|
831
|
-
const deleteRoots = [
|
|
832
|
-
{ variable: "PKGVAR", area: "appdata" },
|
|
833
|
-
{ variable: "PKGHOME", area: "apphome" },
|
|
834
|
-
...(isDynamicRoute ? [{ variable: "PROJECT", area: "appstore" }] : []),
|
|
835
|
-
];
|
|
836
|
-
return `#!/bin/sh
|
|
837
|
-
set -eu
|
|
838
|
-
PKGVAR="\${SYNOPKG_PKGVAR:-}"
|
|
839
|
-
PKGHOME="\${SYNOPKG_PKGHOME:-}"
|
|
840
|
-
${projectRoot}${renderDeleteAppDataBlock(String(contract.package), deleteRoots)}exit 0
|
|
841
|
-
`;
|
|
842
|
-
}
|
|
843
|
-
return `#!/bin/sh\nexit 0\n`;
|
|
844
|
-
}
|
|
845
|
-
throw new Error("Unsupported container route");
|
|
846
|
-
}
|
|
847
|
-
async function createDeterministicSpk(stagingDir, outputPath) {
|
|
848
|
-
const payloadDir = join(stagingDir, "package");
|
|
849
|
-
if (existsSync(payloadDir) && statSync(payloadDir).isDirectory()) {
|
|
850
|
-
await createReproducibleTar(payloadDir, join(stagingDir, "package.tgz"), { gzip: true });
|
|
851
|
-
rmSync(payloadDir, { recursive: true, force: true });
|
|
852
|
-
}
|
|
853
|
-
await createReproducibleTar(stagingDir, outputPath);
|
|
6
|
+
const INSTALL_UIFILE = "install_uifile", UPGRADE_UIFILE = "upgrade_uifile", UNINSTALL_UIFILE = "uninstall_uifile";
|
|
7
|
+
/** Legacy project detection supplies a migration error, never a second builder. */
|
|
8
|
+
export async function buildSpk(_options) {
|
|
9
|
+
return { ok: false, errors: ["COMPOSE_LEGACY_MANIFEST_REMOVED"] };
|
|
854
10
|
}
|
|
855
11
|
export function walkFiles(root, prefix = "") {
|
|
856
12
|
const files = [];
|