@synopackageland/cli 0.1.0 → 0.2.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/dist/bin.js +0 -0
- package/dist/build-spk/broker-glue.d.ts +16 -0
- package/dist/build-spk/broker-glue.js +40 -0
- package/dist/build-spk/delete-app-data.d.ts +11 -0
- package/dist/build-spk/delete-app-data.js +56 -0
- package/dist/build-spk/generator.js +123 -70
- package/dist/build-spk/native-lifecycle.d.ts +51 -0
- package/dist/build-spk/native-lifecycle.js +205 -0
- package/dist/build-spk/native-payload.d.ts +15 -0
- package/dist/build-spk/native-payload.js +65 -39
- package/dist/cli.js +26 -3
- package/dist/deploy/deploy.d.ts +3 -0
- package/dist/deploy/deploy.js +13 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.js +2 -0
- package/dist/host/digests.js +9 -1
- package/dist/host/dsm-client.d.ts +7 -0
- package/dist/host/dsm-client.js +25 -0
- package/dist/host/types.d.ts +6 -0
- package/dist/init.js +1 -0
- package/dist/native/load.d.ts +8 -0
- package/dist/native/load.js +11 -0
- package/dist/native/types.d.ts +28 -0
- package/dist/native/types.js +3 -0
- package/dist/native/validate.d.ts +6 -0
- package/dist/native/validate.js +144 -0
- package/dist/publish/curated-release.js +277 -91
- package/dist/templates/native/native.yaml +16 -0
- package/dist/templates/native/synology-app.yaml +7 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +37 -4
- package/package.json +8 -8
- package/skills/reference/diagnose.md +15 -1
- package/templates/native/native.yaml +16 -0
- package/templates/native/synology-app.yaml +7 -0
- package/dist/deploy/package-source.d.ts +0 -7
- package/dist/deploy/package-source.js +0 -17
- package/dist/deploy/spk-info.d.ts +0 -12
- package/dist/deploy/spk-info.js +0 -41
- package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
package/dist/bin.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Broker installer glue every Managed Profile App carries, independent of
|
|
3
|
+
* runtime. Compose and native Apps install through the same socket, so the
|
|
4
|
+
* Local Attestation gate and the registration-status probe live here once.
|
|
5
|
+
*/
|
|
6
|
+
/** Shell variable preamble locating the Broker installer and its socket. */
|
|
7
|
+
export declare const BROKER_INSTALLER_VARS = "INSTALLER=\"${BROKER_INSTALLER:-/var/packages/SynoPkgLandService/target/bin/synopkgland-installer}\"\nSOCKET=\"${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}\"";
|
|
8
|
+
/**
|
|
9
|
+
* The `status` probe: asks the Broker whether this App is still registered.
|
|
10
|
+
* `removed` exits 150 so DSM shows the App as removed by the platform;
|
|
11
|
+
* `unknown` exits 4. Anything else falls through to the caller, which decides
|
|
12
|
+
* what "running" means for its runtime.
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderBrokerStatusProbe(packageIdentity: string): string;
|
|
15
|
+
/** `preinst` / `preupgrade`: refuse to install when the Broker socket is absent. */
|
|
16
|
+
export declare const BROKER_SOCKET_REQUIRED_SCRIPT = "#!/bin/sh\nSOCKET=\"${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}\"\nif [ ! -S \"$SOCKET\" ]; then\n echo \"SynoPkgLandService installer socket is not available\" >&2\n exit 1\nfi\nexit 0\n";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Broker installer glue every Managed Profile App carries, independent of
|
|
3
|
+
* runtime. Compose and native Apps install through the same socket, so the
|
|
4
|
+
* Local Attestation gate and the registration-status probe live here once.
|
|
5
|
+
*/
|
|
6
|
+
/** Shell variable preamble locating the Broker installer and its socket. */
|
|
7
|
+
export const BROKER_INSTALLER_VARS = `INSTALLER="\${BROKER_INSTALLER:-/var/packages/SynoPkgLandService/target/bin/synopkgland-installer}"
|
|
8
|
+
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"`;
|
|
9
|
+
/**
|
|
10
|
+
* The `status` probe: asks the Broker whether this App is still registered.
|
|
11
|
+
* `removed` exits 150 so DSM shows the App as removed by the platform;
|
|
12
|
+
* `unknown` exits 4. Anything else falls through to the caller, which decides
|
|
13
|
+
* what "running" means for its runtime.
|
|
14
|
+
*/
|
|
15
|
+
export function renderBrokerStatusProbe(packageIdentity) {
|
|
16
|
+
return ` if [ -x "$INSTALLER" ] && [ -S "$SOCKET" ]; then
|
|
17
|
+
"$INSTALLER" --socket "$SOCKET" --phase status --attempt-id "status-$$" \\
|
|
18
|
+
--package-identity "${packageIdentity}" --package-version "\${SYNOPKG_PKGVER}" \\
|
|
19
|
+
--package-path "\${SYNOPKG_PKGDEST}" --bundle-path "\${SYNOPKG_PKGDEST}" \\
|
|
20
|
+
--pkg-var "\${SYNOPKG_PKGVAR}" --pkg-home "\${SYNOPKG_PKGHOME}" >/tmp/synopkgland-status.$$ 2>/dev/null || true
|
|
21
|
+
if grep -q '"registrationStatus":"removed"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
22
|
+
rm -f /tmp/synopkgland-status.$$
|
|
23
|
+
exit 150
|
|
24
|
+
fi
|
|
25
|
+
if grep -q '"registrationStatus":"unknown"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
26
|
+
rm -f /tmp/synopkgland-status.$$
|
|
27
|
+
exit 4
|
|
28
|
+
fi
|
|
29
|
+
rm -f /tmp/synopkgland-status.$$
|
|
30
|
+
fi`;
|
|
31
|
+
}
|
|
32
|
+
/** `preinst` / `preupgrade`: refuse to install when the Broker socket is absent. */
|
|
33
|
+
export const BROKER_SOCKET_REQUIRED_SCRIPT = `#!/bin/sh
|
|
34
|
+
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"
|
|
35
|
+
if [ ! -S "$SOCKET" ]; then
|
|
36
|
+
echo "SynoPkgLandService installer socket is not available" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
exit 0
|
|
40
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Delete App Data block shared by the Compose and native postuninst scripts.
|
|
3
|
+
*
|
|
4
|
+
* The uninstall wizard sets `delete_app_data` (and DSM mirrors it as
|
|
5
|
+
* `wizard_delete_app_data`); unchecked is the default, so an absent variable
|
|
6
|
+
* must leave the data in place. `delete_root` refuses to follow a symlink out
|
|
7
|
+
* of `@appdata`/`@apphome`, empties the directory rather than removing the
|
|
8
|
+
* mount point DSM owns, and reports remnants instead of failing the script —
|
|
9
|
+
* a non-zero postuninst blocks uninstall.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderDeleteAppDataBlock(roots: readonly string[]): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Delete App Data block shared by the Compose and native postuninst scripts.
|
|
3
|
+
*
|
|
4
|
+
* The uninstall wizard sets `delete_app_data` (and DSM mirrors it as
|
|
5
|
+
* `wizard_delete_app_data`); unchecked is the default, so an absent variable
|
|
6
|
+
* must leave the data in place. `delete_root` refuses to follow a symlink out
|
|
7
|
+
* of `@appdata`/`@apphome`, empties the directory rather than removing the
|
|
8
|
+
* mount point DSM owns, and reports remnants instead of failing the script —
|
|
9
|
+
* a non-zero postuninst blocks uninstall.
|
|
10
|
+
*/
|
|
11
|
+
export function renderDeleteAppDataBlock(roots) {
|
|
12
|
+
const calls = roots.map((name) => ` delete_root "$${name}"`).join("\n");
|
|
13
|
+
return `should_delete=0
|
|
14
|
+
case "\${delete_app_data:-}" in
|
|
15
|
+
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
16
|
+
esac
|
|
17
|
+
case "\${wizard_delete_app_data:-}" in
|
|
18
|
+
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
19
|
+
esac
|
|
20
|
+
if [ "$should_delete" -eq 1 ]; then
|
|
21
|
+
delete_root() {
|
|
22
|
+
target="$1"
|
|
23
|
+
if [ ! -e "$target" ] && [ ! -L "$target" ]; then
|
|
24
|
+
return 0
|
|
25
|
+
fi
|
|
26
|
+
if [ -L "$target" ]; then
|
|
27
|
+
real=$(readlink -f "$target" 2>/dev/null || true)
|
|
28
|
+
case "$real" in
|
|
29
|
+
*/@appdata/*|*/@apphome/*)
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "Delete App Data skipped symlink: $target" >&2
|
|
33
|
+
return 0
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
else
|
|
37
|
+
real="$target"
|
|
38
|
+
fi
|
|
39
|
+
if [ ! -e "$real" ]; then
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
chmod -R u+w "$real" 2>/dev/null || true
|
|
43
|
+
if [ -d "$real" ]; then
|
|
44
|
+
find "$real" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
45
|
+
leftover=$(find "$real" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1 || true)
|
|
46
|
+
if [ -n "$leftover" ]; then
|
|
47
|
+
echo "Delete App Data remnant: $real" >&2
|
|
48
|
+
fi
|
|
49
|
+
else
|
|
50
|
+
rm -f "$real" || echo "Delete App Data remnant: $real" >&2
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
${calls}
|
|
54
|
+
fi
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
@@ -11,6 +11,11 @@ import { validateProject } from "../validate.js";
|
|
|
11
11
|
import { writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
12
12
|
import { infoArchValue } from "./dsm-arch.js";
|
|
13
13
|
import { createReproducibleTar } from "./reproducible-archive.js";
|
|
14
|
+
import { renderDeleteAppDataBlock } from "./delete-app-data.js";
|
|
15
|
+
import { BROKER_INSTALLER_VARS, BROKER_SOCKET_REQUIRED_SCRIPT, renderBrokerStatusProbe, } from "./broker-glue.js";
|
|
16
|
+
import { loadNative } from "../native/load.js";
|
|
17
|
+
import { materializeNativePayload } from "./native-payload.js";
|
|
18
|
+
import { renderNativePrivilege, renderNativeStartStopStatus, renderNativePostuninst, } from "./native-lifecycle.js";
|
|
14
19
|
const FILE_MODE = 0o644;
|
|
15
20
|
const SCRIPT_MODE = 0o755;
|
|
16
21
|
const INSTALL_UIFILE = "install_uifile";
|
|
@@ -48,6 +53,82 @@ export async function buildSpk(options) {
|
|
|
48
53
|
if (options.spkVersion) {
|
|
49
54
|
contract.version = options.spkVersion;
|
|
50
55
|
}
|
|
56
|
+
const nativePath = options.project.nativePath ?? join(options.project.root, "native.yaml");
|
|
57
|
+
const isNative = existsSync(nativePath);
|
|
58
|
+
if (isNative) {
|
|
59
|
+
if (!options.archFamily) {
|
|
60
|
+
return { ok: false, errors: ["NATIVE_ARCH_FAMILY_REQUIRED"] };
|
|
61
|
+
}
|
|
62
|
+
const parsedNative = loadNative(nativePath);
|
|
63
|
+
if (!parsedNative) {
|
|
64
|
+
return { ok: false, errors: ["NATIVE_LOAD_FAILED"] };
|
|
65
|
+
}
|
|
66
|
+
const targetSource = parsedNative.native.targets[options.archFamily];
|
|
67
|
+
if (!targetSource) {
|
|
68
|
+
return { ok: false, errors: ["NATIVE_TARGET_NOT_DECLARED"] };
|
|
69
|
+
}
|
|
70
|
+
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily), "utf8");
|
|
73
|
+
writePackageCenterIcons(staging);
|
|
74
|
+
mkdirSync(join(staging, "conf"), { recursive: true });
|
|
75
|
+
writeFileSync(join(staging, "conf", "privilege"), renderNativePrivilege(), "utf8");
|
|
76
|
+
writeFileSync(join(staging, "privilege"), renderNativePrivilege(), "utf8");
|
|
77
|
+
mkdirSync(join(staging, "WIZARD_UIFILES"), { recursive: true });
|
|
78
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", INSTALL_UIFILE), JSON.stringify(renderInstallWizard(contract, undefined), null, 2), "utf8");
|
|
79
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", UPGRADE_UIFILE), JSON.stringify(renderUpgradeWizard(contract, undefined), null, 2), "utf8");
|
|
80
|
+
writeFileSync(join(staging, "WIZARD_UIFILES", UNINSTALL_UIFILE), JSON.stringify(renderUninstallWizard(), null, 2), "utf8");
|
|
81
|
+
mkdirSync(join(staging, "scripts"), { recursive: true });
|
|
82
|
+
for (const name of SCRIPT_NAMES) {
|
|
83
|
+
const scriptPath = join(staging, "scripts", name);
|
|
84
|
+
writeFileSync(scriptPath, renderManagedScript(name, contract, { config: parsedNative.native }), "utf8");
|
|
85
|
+
chmodSync(scriptPath, SCRIPT_MODE);
|
|
86
|
+
}
|
|
87
|
+
const packageDir = join(staging, "package");
|
|
88
|
+
mkdirSync(packageDir, { recursive: true });
|
|
89
|
+
// 1. Materialize native payload
|
|
90
|
+
try {
|
|
91
|
+
await materializeNativePayload({
|
|
92
|
+
source: targetSource,
|
|
93
|
+
projectRoot: options.project.root,
|
|
94
|
+
destDir: packageDir,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const code = err && typeof err === "object" && "code" in err
|
|
99
|
+
? String(err.code)
|
|
100
|
+
: "NATIVE_PAYLOAD_ERROR";
|
|
101
|
+
return { ok: false, errors: [code] };
|
|
102
|
+
}
|
|
103
|
+
// Check collision with App Bundle members before copying
|
|
104
|
+
if (existsSync(join(packageDir, "synology-app.yaml")) ||
|
|
105
|
+
existsSync(join(packageDir, "native.yaml"))) {
|
|
106
|
+
return { ok: false, errors: ["NATIVE_PAYLOAD_BUNDLE_COLLISION"] };
|
|
107
|
+
}
|
|
108
|
+
copyFileSync(options.project.contractPath, join(packageDir, "synology-app.yaml"));
|
|
109
|
+
copyFileSync(nativePath, join(packageDir, "native.yaml"));
|
|
110
|
+
if (options.embeddedAttestationToken !== undefined) {
|
|
111
|
+
const attestationPath = join(packageDir, EMBEDDED_ATTESTATION_RELATIVE_PATH);
|
|
112
|
+
mkdirSync(join(packageDir, ".synopkgland"), { recursive: true });
|
|
113
|
+
writeFileSync(attestationPath, options.embeddedAttestationToken, "utf8");
|
|
114
|
+
chmodSync(attestationPath, FILE_MODE);
|
|
115
|
+
}
|
|
116
|
+
chmodSync(join(staging, "INFO"), FILE_MODE);
|
|
117
|
+
chmodSync(join(staging, "privilege"), FILE_MODE);
|
|
118
|
+
chmodSync(join(staging, "WIZARD_UIFILES", INSTALL_UIFILE), FILE_MODE);
|
|
119
|
+
chmodSync(join(staging, "WIZARD_UIFILES", UPGRADE_UIFILE), FILE_MODE);
|
|
120
|
+
chmodSync(join(staging, "WIZARD_UIFILES", UNINSTALL_UIFILE), FILE_MODE);
|
|
121
|
+
chmodSync(join(packageDir, "synology-app.yaml"), FILE_MODE);
|
|
122
|
+
chmodSync(join(packageDir, "native.yaml"), FILE_MODE);
|
|
123
|
+
const outputPath = options.outputPath ??
|
|
124
|
+
join(options.project.root, `${String(contract.package)}-${options.archFamily}.spk`);
|
|
125
|
+
await createDeterministicSpk(staging, outputPath);
|
|
126
|
+
return { ok: true, outputPath, errors: [] };
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
rmSync(staging, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
51
132
|
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
52
133
|
try {
|
|
53
134
|
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily), "utf8");
|
|
@@ -430,30 +511,52 @@ function renderUninstallWizard() {
|
|
|
430
511
|
],
|
|
431
512
|
}];
|
|
432
513
|
}
|
|
433
|
-
function renderManagedScript(name, contract) {
|
|
514
|
+
function renderManagedScript(name, contract, nativeContext) {
|
|
515
|
+
if (nativeContext) {
|
|
516
|
+
const lifecycleInput = {
|
|
517
|
+
packageIdentity: String(contract.package),
|
|
518
|
+
config: nativeContext.config,
|
|
519
|
+
};
|
|
520
|
+
if (name === "start-stop-status") {
|
|
521
|
+
return renderNativeStartStopStatus(lifecycleInput);
|
|
522
|
+
}
|
|
523
|
+
if (name === "preinst" || name === "preupgrade") {
|
|
524
|
+
return BROKER_SOCKET_REQUIRED_SCRIPT;
|
|
525
|
+
}
|
|
526
|
+
if (name === "postinst" || name === "postupgrade") {
|
|
527
|
+
const installerScript = renderInstallerCallScript(contract, name === "postupgrade" ? "update" : "install");
|
|
528
|
+
// The service process needs its scratch directory before it can start,
|
|
529
|
+
// and the installer call is the script's exit status, so widen the
|
|
530
|
+
// existing mkdir rather than appending after it.
|
|
531
|
+
return installerScript.replace('mkdir -p "${SYNOPKG_PKGVAR}" "${SYNOPKG_PKGHOME}/secrets"', 'mkdir -p "${SYNOPKG_PKGVAR}" "${SYNOPKG_PKGVAR}/tmp" "${SYNOPKG_PKGHOME}/secrets"');
|
|
532
|
+
}
|
|
533
|
+
if (name === "preuninst") {
|
|
534
|
+
// Compose installer uninstall (with UPGRADE early exit) + stop native process
|
|
535
|
+
const installerScript = renderInstallerCallScript(contract, "uninstall").replace("set -eu\n", 'set -eu\nif [ "${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ]; then\n exit 0\nfi\n');
|
|
536
|
+
// Stop the process BEFORE deregistering: `set -eu` aborts the script if
|
|
537
|
+
// the installer fails, so a stop appended after it would never run. The
|
|
538
|
+
// stop itself is best effort -- DSM already calls `start-stop-status
|
|
539
|
+
// stop` directly (N0), and a non-zero preuninst blocks uninstall.
|
|
540
|
+
const preuninstStopBlock = `CONTROL="/var/packages/\${SYNOPKG_PKGNAME:-${contract.package}}/scripts/start-stop-status"
|
|
541
|
+
if [ -x "$CONTROL" ]; then
|
|
542
|
+
"$CONTROL" stop 2>/dev/null || true
|
|
543
|
+
fi
|
|
544
|
+
`;
|
|
545
|
+
return installerScript.replace('"$INSTALLER"', `${preuninstStopBlock}"$INSTALLER"`);
|
|
546
|
+
}
|
|
547
|
+
if (name === "postuninst") {
|
|
548
|
+
return renderNativePostuninst(lifecycleInput);
|
|
549
|
+
}
|
|
550
|
+
return `#!/bin/sh\nexit 0\n`;
|
|
551
|
+
}
|
|
434
552
|
if (name === "start-stop-status") {
|
|
435
553
|
return `#!/bin/sh
|
|
436
|
-
|
|
437
|
-
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"
|
|
554
|
+
${BROKER_INSTALLER_VARS}
|
|
438
555
|
case "$1" in
|
|
439
556
|
start) exit 0 ;;
|
|
440
557
|
stop) exit 0 ;;
|
|
441
558
|
status)
|
|
442
|
-
|
|
443
|
-
"$INSTALLER" --socket "$SOCKET" --phase status --attempt-id "status-$$" \\
|
|
444
|
-
--package-identity "${contract.package}" --package-version "\${SYNOPKG_PKGVER}" \\
|
|
445
|
-
--package-path "\${SYNOPKG_PKGDEST}" --bundle-path "\${SYNOPKG_PKGDEST}" \\
|
|
446
|
-
--pkg-var "\${SYNOPKG_PKGVAR}" --pkg-home "\${SYNOPKG_PKGHOME}" >/tmp/synopkgland-status.$$ 2>/dev/null || true
|
|
447
|
-
if grep -q '"registrationStatus":"removed"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
448
|
-
rm -f /tmp/synopkgland-status.$$
|
|
449
|
-
exit 150
|
|
450
|
-
fi
|
|
451
|
-
if grep -q '"registrationStatus":"unknown"' /tmp/synopkgland-status.$$ 2>/dev/null; then
|
|
452
|
-
rm -f /tmp/synopkgland-status.$$
|
|
453
|
-
exit 4
|
|
454
|
-
fi
|
|
455
|
-
rm -f /tmp/synopkgland-status.$$
|
|
456
|
-
fi
|
|
559
|
+
${renderBrokerStatusProbe(String(contract.package))}
|
|
457
560
|
exit 0
|
|
458
561
|
;;
|
|
459
562
|
*) exit 1 ;;
|
|
@@ -461,14 +564,7 @@ esac
|
|
|
461
564
|
`;
|
|
462
565
|
}
|
|
463
566
|
if (name === "preinst" || name === "preupgrade") {
|
|
464
|
-
return
|
|
465
|
-
SOCKET="\${BROKER_SOCKET:-/var/packages/SynoPkgLandService/var/installer.sock}"
|
|
466
|
-
if [ ! -S "$SOCKET" ]; then
|
|
467
|
-
echo "SynoPkgLandService installer socket is not available" >&2
|
|
468
|
-
exit 1
|
|
469
|
-
fi
|
|
470
|
-
exit 0
|
|
471
|
-
`;
|
|
567
|
+
return BROKER_SOCKET_REQUIRED_SCRIPT;
|
|
472
568
|
}
|
|
473
569
|
if (name === "postinst" || name === "postupgrade") {
|
|
474
570
|
return renderInstallerCallScript(contract, name === "postupgrade" ? "update" : "install");
|
|
@@ -482,50 +578,7 @@ set -eu
|
|
|
482
578
|
PKGVAR="\${SYNOPKG_PKGVAR}"
|
|
483
579
|
PKGHOME="\${SYNOPKG_PKGHOME}"
|
|
484
580
|
SECRETS="\${PKGHOME}/secrets"
|
|
485
|
-
|
|
486
|
-
case "\${delete_app_data:-}" in
|
|
487
|
-
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
488
|
-
esac
|
|
489
|
-
case "\${wizard_delete_app_data:-}" in
|
|
490
|
-
true|True|TRUE|yes|1) should_delete=1 ;;
|
|
491
|
-
esac
|
|
492
|
-
if [ "$should_delete" -eq 1 ]; then
|
|
493
|
-
delete_root() {
|
|
494
|
-
target="$1"
|
|
495
|
-
if [ ! -e "$target" ] && [ ! -L "$target" ]; then
|
|
496
|
-
return 0
|
|
497
|
-
fi
|
|
498
|
-
if [ -L "$target" ]; then
|
|
499
|
-
real=$(readlink -f "$target" 2>/dev/null || true)
|
|
500
|
-
case "$real" in
|
|
501
|
-
*/@appdata/*|*/@apphome/*)
|
|
502
|
-
;;
|
|
503
|
-
*)
|
|
504
|
-
echo "Delete App Data skipped symlink: $target" >&2
|
|
505
|
-
return 0
|
|
506
|
-
;;
|
|
507
|
-
esac
|
|
508
|
-
else
|
|
509
|
-
real="$target"
|
|
510
|
-
fi
|
|
511
|
-
if [ ! -e "$real" ]; then
|
|
512
|
-
return 0
|
|
513
|
-
fi
|
|
514
|
-
chmod -R u+w "$real" 2>/dev/null || true
|
|
515
|
-
if [ -d "$real" ]; then
|
|
516
|
-
find "$real" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
517
|
-
leftover=$(find "$real" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1 || true)
|
|
518
|
-
if [ -n "$leftover" ]; then
|
|
519
|
-
echo "Delete App Data remnant: $real" >&2
|
|
520
|
-
fi
|
|
521
|
-
else
|
|
522
|
-
rm -f "$real" || echo "Delete App Data remnant: $real" >&2
|
|
523
|
-
fi
|
|
524
|
-
}
|
|
525
|
-
delete_root "$PKGVAR"
|
|
526
|
-
delete_root "$SECRETS"
|
|
527
|
-
fi
|
|
528
|
-
exit 0
|
|
581
|
+
${renderDeleteAppDataBlock(["PKGVAR", "SECRETS"])}exit 0
|
|
529
582
|
`;
|
|
530
583
|
}
|
|
531
584
|
return `#!/bin/sh
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { NativeConfig } from "../native/types.js";
|
|
2
|
+
export interface NativeLifecycleInput {
|
|
3
|
+
packageIdentity: string;
|
|
4
|
+
config: NativeConfig;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Renders `conf/privilege` content.
|
|
8
|
+
* For non-container native packages, DSM accepts `{"defaults":{"run-as":"package"}}`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderNativePrivilege(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Renders `scripts/start-stop-status`.
|
|
13
|
+
*
|
|
14
|
+
* Requirements & hard facts from N0:
|
|
15
|
+
* - Start must be backgrounded via `nohup setsid ... &` because Package Center uses a oneshot unit.
|
|
16
|
+
* - PID is tracked in `$PKGVAR/<pkg>.pid`.
|
|
17
|
+
* - Status exit codes: 0 = running, 1 = PID file present but process dead, 3 = stopped / no PID file.
|
|
18
|
+
* - Fallbacks for `SYNOPKG_PKGDEST` and `SYNOPKG_PKGVAR` via `:-/var/packages/<pkg>/target` and `:-/var/packages/<pkg>/var`.
|
|
19
|
+
* - `exec` is relative to `SYNOPKG_PKGDEST` (no `target/` subfolder).
|
|
20
|
+
* - Safe quoting for `exec`, `args`, and `env`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderNativeStartStopStatus(input: NativeLifecycleInput): string;
|
|
23
|
+
/**
|
|
24
|
+
* Renders `scripts/postinst`.
|
|
25
|
+
*
|
|
26
|
+
* Requirements from N0:
|
|
27
|
+
* - DSM creates the package user/group and sets up `SYNOPKG_PKGVAR` (drwxr-xr-x) before postinst runs.
|
|
28
|
+
* - Does NOT create any users (no synouser/useradd/adduser).
|
|
29
|
+
* - Does NOT chown.
|
|
30
|
+
* - Only ensures that subdirectories (like `$SYNOPKG_PKGVAR/tmp`) exist.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderNativePostinst(input: NativeLifecycleInput): string;
|
|
33
|
+
/**
|
|
34
|
+
* Renders `scripts/preuninst`.
|
|
35
|
+
*
|
|
36
|
+
* Requirements from N0:
|
|
37
|
+
* - Non-zero exit blocks uninstallation, so it must be best-effort (`|| true`).
|
|
38
|
+
* - Skips when `SYNOPKG_PKG_STATUS=UPGRADE`.
|
|
39
|
+
* - Calls stop via `/var/packages/${SYNOPKG_PKGNAME:-<pkg>}/scripts/start-stop-status stop`.
|
|
40
|
+
* - Does NOT use postfix stripping like `%/target`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderNativePreuninst(input: NativeLifecycleInput): string;
|
|
43
|
+
/**
|
|
44
|
+
* Renders `scripts/postuninst`.
|
|
45
|
+
*
|
|
46
|
+
* Shares `renderDeleteAppDataBlock` with the Compose generator so both runtimes
|
|
47
|
+
* honour the same unchecked-by-default wizard variable and the same refusal to
|
|
48
|
+
* follow a symlink out of `@appdata`. N0 showed DSM does not reclaim the
|
|
49
|
+
* package user on uninstall; deleting a system account is not this script's job.
|
|
50
|
+
*/
|
|
51
|
+
export declare function renderNativePostuninst(input: NativeLifecycleInput): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { renderDeleteAppDataBlock } from "./delete-app-data.js";
|
|
2
|
+
import { BROKER_INSTALLER_VARS, renderBrokerStatusProbe, } from "./broker-glue.js";
|
|
3
|
+
/**
|
|
4
|
+
* Escapes a literal string for safe inclusion in POSIX `/bin/sh` single quotes.
|
|
5
|
+
* A single quote is represented as `'\''` (close single quote, literal escaped single quote, reopen single quote).
|
|
6
|
+
*/
|
|
7
|
+
function quoteLiteral(str) {
|
|
8
|
+
return `'${str.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Escapes a token (exec path, arg, or env value) so that any `${SYNOPKG_PKGVAR}` placeholder
|
|
12
|
+
* expands safely to the shell variable `"${PKGVAR}"`, while all literal parts are securely
|
|
13
|
+
* quoted in single quotes, preventing shell injection or word splitting.
|
|
14
|
+
*/
|
|
15
|
+
function escapeShellTokenWithPkgvar(value, pkgvarExpr = '"${PKGVAR}"') {
|
|
16
|
+
if (value === "") {
|
|
17
|
+
return "''";
|
|
18
|
+
}
|
|
19
|
+
const placeholder = "${SYNOPKG_PKGVAR}";
|
|
20
|
+
const parts = value.split(placeholder);
|
|
21
|
+
const segments = [];
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
if (parts[i].length > 0) {
|
|
24
|
+
segments.push(quoteLiteral(parts[i]));
|
|
25
|
+
}
|
|
26
|
+
if (i < parts.length - 1) {
|
|
27
|
+
segments.push(pkgvarExpr);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return segments.length === 0 ? "''" : segments.join("");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Renders `conf/privilege` content.
|
|
34
|
+
* For non-container native packages, DSM accepts `{"defaults":{"run-as":"package"}}`.
|
|
35
|
+
*/
|
|
36
|
+
export function renderNativePrivilege() {
|
|
37
|
+
return `${JSON.stringify({
|
|
38
|
+
defaults: {
|
|
39
|
+
"run-as": "package",
|
|
40
|
+
},
|
|
41
|
+
}, null, 2)}\n`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Renders `scripts/start-stop-status`.
|
|
45
|
+
*
|
|
46
|
+
* Requirements & hard facts from N0:
|
|
47
|
+
* - Start must be backgrounded via `nohup setsid ... &` because Package Center uses a oneshot unit.
|
|
48
|
+
* - PID is tracked in `$PKGVAR/<pkg>.pid`.
|
|
49
|
+
* - Status exit codes: 0 = running, 1 = PID file present but process dead, 3 = stopped / no PID file.
|
|
50
|
+
* - Fallbacks for `SYNOPKG_PKGDEST` and `SYNOPKG_PKGVAR` via `:-/var/packages/<pkg>/target` and `:-/var/packages/<pkg>/var`.
|
|
51
|
+
* - `exec` is relative to `SYNOPKG_PKGDEST` (no `target/` subfolder).
|
|
52
|
+
* - Safe quoting for `exec`, `args`, and `env`.
|
|
53
|
+
*/
|
|
54
|
+
export function renderNativeStartStopStatus(input) {
|
|
55
|
+
const pkg = input.packageIdentity;
|
|
56
|
+
const service = input.config.service;
|
|
57
|
+
const pkgDestFallback = `\${SYNOPKG_PKGDEST:-/var/packages/${pkg}/target}`;
|
|
58
|
+
const pkgVarFallback = `\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}`;
|
|
59
|
+
const execToken = `"$PKGDEST/"${escapeShellTokenWithPkgvar(service.exec)}`;
|
|
60
|
+
const argTokens = (service.args ?? []).map((arg) => escapeShellTokenWithPkgvar(arg));
|
|
61
|
+
const envLines = [];
|
|
62
|
+
if (service.env) {
|
|
63
|
+
for (const [key, value] of Object.entries(service.env)) {
|
|
64
|
+
const escapedKey = quoteLiteral(key);
|
|
65
|
+
const escapedVal = escapeShellTokenWithPkgvar(value);
|
|
66
|
+
envLines.push(` export ${escapedKey}=${escapedVal}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const envBlock = envLines.length > 0 ? `\n${envLines.join("\n")}` : "";
|
|
70
|
+
const commandLine = [execToken, ...argTokens].join(" ");
|
|
71
|
+
return `#!/bin/sh
|
|
72
|
+
${BROKER_INSTALLER_VARS}
|
|
73
|
+
PKGDEST="${pkgDestFallback}"
|
|
74
|
+
PKGVAR="${pkgVarFallback}"
|
|
75
|
+
PIDFILE="$PKGVAR/${pkg}.pid"
|
|
76
|
+
LOGFILE="$PKGVAR/${pkg}.log"
|
|
77
|
+
|
|
78
|
+
pid_alive() {
|
|
79
|
+
if [ ! -f "$PIDFILE" ]; then
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
pid=$(cat "$PIDFILE" 2>/dev/null) || return 1
|
|
83
|
+
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "$1" in
|
|
87
|
+
start)
|
|
88
|
+
if pid_alive; then
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
mkdir -p "$PKGVAR"
|
|
92
|
+
rm -f "$PIDFILE"
|
|
93
|
+
(
|
|
94
|
+
cd /${envBlock}
|
|
95
|
+
nohup setsid ${commandLine} >> "$LOGFILE" 2>&1 </dev/null &
|
|
96
|
+
echo $! > "$PIDFILE"
|
|
97
|
+
)
|
|
98
|
+
# Verify the launch honestly: an exec that dies immediately (missing
|
|
99
|
+
# binary, wrong arch, bad loader) must not leave Package Center showing
|
|
100
|
+
# "started". N0 validated this re-check on hardware.
|
|
101
|
+
sleep 1
|
|
102
|
+
if pid_alive; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
rm -f "$PIDFILE"
|
|
106
|
+
exit 1
|
|
107
|
+
;;
|
|
108
|
+
stop)
|
|
109
|
+
if [ -f "$PIDFILE" ]; then
|
|
110
|
+
pid=$(cat "$PIDFILE" 2>/dev/null) || pid=""
|
|
111
|
+
if [ -n "$pid" ]; then
|
|
112
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
113
|
+
kill "$pid" 2>/dev/null || true
|
|
114
|
+
n=0
|
|
115
|
+
while [ "$n" -lt 30 ]; do
|
|
116
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
117
|
+
break
|
|
118
|
+
fi
|
|
119
|
+
sleep 0.2
|
|
120
|
+
n=$((n + 1))
|
|
121
|
+
done
|
|
122
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
123
|
+
kill -9 "$pid" 2>/dev/null || true
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
rm -f "$PIDFILE"
|
|
128
|
+
fi
|
|
129
|
+
exit 0
|
|
130
|
+
;;
|
|
131
|
+
status)
|
|
132
|
+
${renderBrokerStatusProbe(pkg)}
|
|
133
|
+
if pid_alive; then
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
if [ -f "$PIDFILE" ]; then
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
exit 3
|
|
140
|
+
;;
|
|
141
|
+
*)
|
|
142
|
+
exit 1
|
|
143
|
+
;;
|
|
144
|
+
esac
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Renders `scripts/postinst`.
|
|
149
|
+
*
|
|
150
|
+
* Requirements from N0:
|
|
151
|
+
* - DSM creates the package user/group and sets up `SYNOPKG_PKGVAR` (drwxr-xr-x) before postinst runs.
|
|
152
|
+
* - Does NOT create any users (no synouser/useradd/adduser).
|
|
153
|
+
* - Does NOT chown.
|
|
154
|
+
* - Only ensures that subdirectories (like `$SYNOPKG_PKGVAR/tmp`) exist.
|
|
155
|
+
*/
|
|
156
|
+
export function renderNativePostinst(input) {
|
|
157
|
+
const pkg = input.packageIdentity;
|
|
158
|
+
const pkgVarFallback = `\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}`;
|
|
159
|
+
return `#!/bin/sh
|
|
160
|
+
set -eu
|
|
161
|
+
PKGVAR="${pkgVarFallback}"
|
|
162
|
+
mkdir -p "$PKGVAR" "$PKGVAR/tmp"
|
|
163
|
+
exit 0
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Renders `scripts/preuninst`.
|
|
168
|
+
*
|
|
169
|
+
* Requirements from N0:
|
|
170
|
+
* - Non-zero exit blocks uninstallation, so it must be best-effort (`|| true`).
|
|
171
|
+
* - Skips when `SYNOPKG_PKG_STATUS=UPGRADE`.
|
|
172
|
+
* - Calls stop via `/var/packages/${SYNOPKG_PKGNAME:-<pkg>}/scripts/start-stop-status stop`.
|
|
173
|
+
* - Does NOT use postfix stripping like `%/target`.
|
|
174
|
+
*/
|
|
175
|
+
export function renderNativePreuninst(input) {
|
|
176
|
+
const pkg = input.packageIdentity;
|
|
177
|
+
return `#!/bin/sh
|
|
178
|
+
if [ "\${SYNOPKG_PKG_STATUS:-}" = "UPGRADE" ]; then
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
CONTROL="/var/packages/\${SYNOPKG_PKGNAME:-${pkg}}/scripts/start-stop-status"
|
|
183
|
+
if [ -x "$CONTROL" ]; then
|
|
184
|
+
"$CONTROL" stop 2>/dev/null || true
|
|
185
|
+
fi
|
|
186
|
+
exit 0
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Renders `scripts/postuninst`.
|
|
191
|
+
*
|
|
192
|
+
* Shares `renderDeleteAppDataBlock` with the Compose generator so both runtimes
|
|
193
|
+
* honour the same unchecked-by-default wizard variable and the same refusal to
|
|
194
|
+
* follow a symlink out of `@appdata`. N0 showed DSM does not reclaim the
|
|
195
|
+
* package user on uninstall; deleting a system account is not this script's job.
|
|
196
|
+
*/
|
|
197
|
+
export function renderNativePostuninst(input) {
|
|
198
|
+
const pkg = input.packageIdentity;
|
|
199
|
+
const pkgVarFallback = `\${SYNOPKG_PKGVAR:-/var/packages/${pkg}/var}`;
|
|
200
|
+
return `#!/bin/sh
|
|
201
|
+
set -eu
|
|
202
|
+
PKGVAR="${pkgVarFallback}"
|
|
203
|
+
${renderDeleteAppDataBlock(["PKGVAR"])}exit 0
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materializes one target's binary tree into `destDir`, which becomes the
|
|
3
|
+
* payload root (SYNOPKG_PKGDEST). Shared by `buildNativePayload` and by the SPK
|
|
4
|
+
* generator, which needs the tree in its own staging directory so the App
|
|
5
|
+
* Bundle files can sit alongside the binaries in a single package.tgz.
|
|
6
|
+
*/
|
|
7
|
+
export declare function materializeNativePayload(options: {
|
|
8
|
+
source: NativeSource;
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
destDir: string;
|
|
11
|
+
download?: (url: string) => Promise<Buffer>;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
sourceSha256: string;
|
|
14
|
+
entries: string[];
|
|
15
|
+
}>;
|
|
1
16
|
export type NativeArchiveKind = "tar.gz" | "tar.xz" | "zip" | "raw";
|
|
2
17
|
export interface NativeSource {
|
|
3
18
|
path?: string;
|