Skip to content
Syno Package Land
Language: English

Package scripts and interpreted languages

synopkgland does not build an SPK from scripts alone. The native layout needs at least one ELF binary for every architecture and has no noarch option. native-go and binary packages run a Go or static ELF CGI. So a program written in a script language needs one of these:

ApproachWhat goes in the SPKArchitecturesSize (measured)
A. Compile to one executableOne ELF that contains the runtime and your codeBun and Deno: x86_64, armv8. No armv7.Bun 78 MiB, Deno 103 MiB executable; SPK about 36 MiB
B. Bundle the interpreterThe interpreter binary plus your source filesNode.js: x86_64, armv8, armv7node 102–119 MiB; SPK 38–43 MiB
C. Run it in a containerA Compose file; the image is pulled at installContainer Manager models: x86_64, aarch64SPK about 80 KB, plus the image

Sizes are for a one-line program built with Bun 1.4.2, Deno 2.9.4 and Node.js 22.23.3. The native layout allows 256 MiB per file and 1 GiB for the whole payload, so each executable fits.

Whatever you build, check it with synopkgland inspect <spk> before you install it.

QuestionABC
Needs Container Manager on the NASNoNoYes; container apps need DSM 7.2.1-69057 or later
Needs the NAS glibc to be new enoughYesYesNo
Runs on armv7No (Bun, Deno)Yes (Node.js)No
Ships your source code readable in the SPKNo (bundled into the binary)YesOnly in the image
Works offline at installYesYesNo, the NAS pulls the image
Upgrade the runtimeRebuildChange the toolkit pinPin a new image digest

All three binaries here are dynamically linked against glibc, so declare linkage: dynamic. The newest glibc symbol each one needs is 2.17 for Bun, 2.27 for Deno and 2.28 for Node.js 22. Check the glibc version on your target NAS, for example with ldd --version over SSH. This guide does not list which DSM models meet each version.

Node.js and Python packages from Package Center

Section titled “Node.js and Python packages from Package Center”

Package Center offers Node.js and Python packages from Synology. An SPK can depend on them with the INFO field install_dep_packages. synopkgland does not support this: the native layout writes INFO itself and has no field for it, and inspect reports every package dependency except Web Station or Container Manager on a container app. If you need this dependency, pack the SPK by hand (see Shell scripts without a build) and add install_dep_packages to INFO. inspect accepts the field on an SPK it did not build, but does not check that the package exists.

bun build --compile and deno compile produce one Linux executable from your TypeScript or JavaScript, and they can cross-compile from macOS or Linux. Neither has a 32-bit ARM target. PyInstaller does the same for Python but does not cross-compile, so run it on a Linux machine of each target architecture.

Project layout:

dsm-package.yaml
icon.png square PNG, 256×256 or larger
src/main.ts
package/compile.sh
spk/conf/privilege
spk/scripts/start-stop-status
spk/scripts/preinst … the other six lifecycle scripts

dsm-package.yaml:

schema: 2
backend: {kind: native}
package: HelloBun
version: 1.0.0
build: 1
displayname: Hello Bun
description: A TypeScript worker compiled with bun build --compile
maintainer: Example
os_min_ver: 7.0-40000
arches: [x86_64, armv8]
icon: icon.png
layout:
payload: target/payload/{arch}
metadata: spk
binaries:
- {path: bin/hello, linkage: dynamic}
native:
hook: {command: [sh, package/compile.sh]}

Use armv8 for 64-bit ARM here: the native layout writes it into INFO arch as it is, and it names the whole family in the Developer Guide’s arch table. aarch64 also works and writes the platforms native-go uses, armada37xx rtd1296 rtd1619 rtd1619b.

package/compile.sh runs once for every architecture:

#!/bin/sh
set -eu
case "$SYNOPKGLAND_ARCH" in
x86_64) target=bun-linux-x64 ;;
armv8) target=bun-linux-arm64 ;;
*) echo "bun has no target for $SYNOPKGLAND_ARCH" >&2; exit 1 ;;
esac
out="target/payload/$SYNOPKGLAND_ARCH/bin"
rm -rf "$out" && mkdir -p "$out"
bun build --compile --target="$target" src/main.ts --outfile "$out/hello"

For Deno, replace the last line with deno compile --target x86_64-unknown-linux-gnu -o "$out/hello" src/main.ts, or aarch64-unknown-linux-gnu for armv8.

spk/scripts/start-stop-status starts the program as the package account and keeps its PID in the package’s var folder:

#!/bin/sh
PID="$SYNOPKG_PKGVAR/hello.pid"
case "$1" in
start) "$SYNOPKG_PKGDEST/bin/hello" >>"$SYNOPKG_PKGVAR/hello.log" 2>&1 &
echo $! >"$PID" ;;
stop) [ -f "$PID" ] && kill "$(cat "$PID")"; rm -f "$PID" ;;
status) [ -f "$PID" ] && kill -0 "$(cat "$PID")" 2>/dev/null ;;
esac

Create the other lifecycle scripts and the privilege file, then build:

Terminal window
for hook in preinst postinst preuninst postuninst preupgrade postupgrade; do
printf '#!/bin/sh\nexit 0\n' > "spk/scripts/$hook"
done
chmod 755 spk/scripts/*
echo '{"defaults":{"run-as":"package"}}' > spk/conf/privilege
synopkgland check
synopkgland build
synopkgland inspect dist/HelloBun-1.0.0-0001-x86_64.spk

The scripts must be executable in the metadata folder: the native layout copies their mode as it is.

Put the interpreter in the payload and start your source with it. Use the same spk folder as approach A, and change the start line of start-stop-status to:

Terminal window
start) "$SYNOPKG_PKGDEST/bin/node" "$SYNOPKG_PKGDEST/app/main.mjs" >>"$SYNOPKG_PKGVAR/hello.log" 2>&1 &

native.toolkits downloads the official Node.js build for each architecture and checks its SHA-256, so the build is reproducible:

schema: 2
backend: {kind: native}
package: HelloNode
version: 1.0.0
build: 1
displayname: Hello Node
description: A Node.js worker with its own Node.js runtime
maintainer: Example
os_min_ver: 7.0-40000
arches: [x86_64, armv8, armv7]
icon: icon.png
layout:
payload: target/payload/{arch}
metadata: spk
binaries:
- {path: bin/node, linkage: dynamic}
native:
hook: {command: [sh, package/collect.sh]}
toolkits:
x86_64:
- {url: 'https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-x64.tar.xz', sha256: df450af89261115ef9f9e3830c3eeb2cc9213b63c720b1af623cb5dcbe2e02de}
armv8:
- {url: 'https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-arm64.tar.xz', sha256: a44aeb94849a299b22df10b9e622ec2f605c2183501bc40590705131de7c740f}
armv7:
- {url: 'https://nodejs.org/dist/v22.23.3/node-v22.23.3-linux-armv7l.tar.xz', sha256: 590a2768199bdd0b848648a81cd7f07f88a5ceb27949f0c70d5e6652c3eaca69}

Take the digests from the release’s SHASUMS256.txt. package/collect.sh copies the runtime and your source into the payload:

#!/bin/sh
set -eu
out="target/payload/$SYNOPKGLAND_ARCH"
rm -rf "$out" && mkdir -p "$out/bin"
cp "$SYNOPKGLAND_TOOLKIT_DIR"/node-v*/bin/node "$out/bin/node"
cp -R app "$out/app"

Only the node binary is copied, which is enough for a program without npm packages. For dependencies, run npm ci --omit=dev in app before the build and copy node_modules with it. A dependency with a native addon must be built for each architecture.

NoteDetail
Archive formatnative.toolkits extracts .tar.xz archives only, with GNU tar. On macOS, put GNU tar (gtar) first in PATH under the name tar.
PythonPortable Python builds such as python-build-standalone ship .tar.gz or .tar.zst, not .tar.xz. Repack the archive as .tar.xz and pin your copy, or collect the interpreter in native.prepare. This guide has not built a Python example.
Sizenode alone is 102–119 MiB. Keep each payload file under 256 MiB.

On a NAS with Container Manager, a container app runs the official runtime image, and the SPK holds only the Compose file. The CLI rejects Compose build:, so your code must be in a published image or in a mounted folder. This example runs a short inline program:

synology-app.yaml:

schema: 1
package: com.example.hello-python
version: 0.1.0
displayname: Hello Python
description: A Python worker in a container
maintainer: Example
os_min_ver: "7.2.1-69057"

compose.yaml:

services:
worker:
image: python:3.13-alpine
command: ["python", "-u", "-c", "import time\nwhile True:\n print('hello from HelloPython')\n time.sleep(60)"]

Then run:

Terminal window
synopkgland images pin
synopkgland check
synopkgland build
synopkgland inspect com.example.hello-python.spk

images pin replaces the tag with its digest, so every install gets the same image. Without a digest, build sets force_pull and each install resolves the tag again. The SPK is noarch; the NAS pulls the image for its own CPU during install.

A package of shell scripts only needs no ELF and can be noarch, but synopkgland build cannot produce it. Pack it with tar and check it with inspect:

Terminal window
set -eu
export COPYFILE_DISABLE=1 # macOS: keep ._ files out of the archives
mkdir -p HelloSh/payload/bin HelloSh/spk/scripts HelloSh/spk/conf
cd HelloSh
cat > payload/bin/hello.sh <<'EOF'
#!/bin/sh
while true; do date; sleep 60; done
EOF
chmod 755 payload/bin/hello.sh
tar -czf spk/package.tgz -C payload .
cat > spk/scripts/start-stop-status <<'EOF'
#!/bin/sh
PID="$SYNOPKG_PKGVAR/hello.pid"
case "$1" in
start) "$SYNOPKG_PKGDEST/bin/hello.sh" >>"$SYNOPKG_PKGVAR/hello.log" 2>&1 &
echo $! >"$PID" ;;
stop) [ -f "$PID" ] && kill "$(cat "$PID")"; rm -f "$PID" ;;
status) [ -f "$PID" ] && kill -0 "$(cat "$PID")" 2>/dev/null ;;
esac
EOF
for hook in preinst postinst preuninst postuninst preupgrade postupgrade; do
printf '#!/bin/sh\nexit 0\n' > "spk/scripts/$hook"
done
chmod 755 spk/scripts/*
echo '{"defaults":{"run-as":"package"}}' > spk/conf/privilege
cp ../icon-64.png spk/PACKAGE_ICON.PNG # your 64×64 PNG
cp ../icon-256.png spk/PACKAGE_ICON_256.PNG # your 256×256 PNG
checksum=$( (md5sum spk/package.tgz 2>/dev/null || md5 -r spk/package.tgz) | cut -d' ' -f1)
cat > spk/INFO <<EOF
package="HelloSh"
version="1.0.0-0001"
os_min_ver="7.0-40000"
displayname="Hello Shell"
description="A shell script worker"
maintainer="Example"
arch="noarch"
checksum="$checksum"
EOF
tar -cf HelloSh-1.0.0-0001-noarch.spk -C spk INFO package.tgz scripts conf PACKAGE_ICON.PNG PACKAGE_ICON_256.PNG
synopkgland inspect HelloSh-1.0.0-0001-noarch.spk

synopkgland deploy installs only projects it builds. Install a hand-packed SPK with Package Center > Manual Install.

synopkgland inspect <spk> checks any SPK against the DSM 7 package format, whoever built it: the archive layout, INFO fields and checksum, paths and links in package.tgz, ELF files against arch, scripts and their execute bit, conf/privilege and conf/resource, icons and wizards. It does not install or run anything.

Terminal window
synopkgland inspect HelloSh-1.0.0-0001-noarch.spk
synopkgland inspect --json dist/HelloNode-1.0.0-0001-armv7.spk

Each finding has a stable code from the SPK rules. An error, such as an ELF file in a noarch package, exits 1 with SPK_INSPECTION_FAILED. A warning, such as a missing preinst, exits 0. An SPK built by synopkgland, such as the native layout and Compose ones above, also gets a second section that checks the synopkgland build contract; a hand-packed SPK gets only the DSM format section. inspect does not prove that the program runs on a NAS. Install it on a test NAS before you share it.