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:
| Approach | What goes in the SPK | Architectures | Size (measured) |
|---|---|---|---|
| A. Compile to one executable | One ELF that contains the runtime and your code | Bun and Deno: x86_64, armv8. No armv7. | Bun 78 MiB, Deno 103 MiB executable; SPK about 36 MiB |
| B. Bundle the interpreter | The interpreter binary plus your source files | Node.js: x86_64, armv8, armv7 | node 102–119 MiB; SPK 38–43 MiB |
| C. Run it in a container | A Compose file; the image is pulled at install | Container Manager models: x86_64, aarch64 | SPK 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.
Before you choose
Section titled “Before you choose”| Question | A | B | C |
|---|---|---|---|
| Needs Container Manager on the NAS | No | No | Yes; container apps need DSM 7.2.1-69057 or later |
| Needs the NAS glibc to be new enough | Yes | Yes | No |
Runs on armv7 | No (Bun, Deno) | Yes (Node.js) | No |
| Ships your source code readable in the SPK | No (bundled into the binary) | Yes | Only in the image |
| Works offline at install | Yes | Yes | No, the NAS pulls the image |
| Upgrade the runtime | Rebuild | Change the toolkit pin | Pin 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.
A. Compile to one executable
Section titled “A. Compile to one executable”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.yamlicon.png square PNG, 256×256 or largersrc/main.tspackage/compile.shspk/conf/privilegespk/scripts/start-stop-statusspk/scripts/preinst … the other six lifecycle scriptsdsm-package.yaml:
schema: 2backend: {kind: native}package: HelloBunversion: 1.0.0build: 1displayname: Hello Bundescription: A TypeScript worker compiled with bun build --compilemaintainer: Exampleos_min_ver: 7.0-40000arches: [x86_64, armv8]icon: icon.pnglayout: 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/shset -eucase "$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 ;;esacout="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/shPID="$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 ;;esacCreate the other lifecycle scripts and the privilege file, then build:
for hook in preinst postinst preuninst postuninst preupgrade postupgrade; do printf '#!/bin/sh\nexit 0\n' > "spk/scripts/$hook"donechmod 755 spk/scripts/*echo '{"defaults":{"run-as":"package"}}' > spk/conf/privilegesynopkgland checksynopkgland buildsynopkgland inspect dist/HelloBun-1.0.0-0001-x86_64.spkThe scripts must be executable in the metadata folder: the native layout copies their mode as it is.
B. Bundle the interpreter
Section titled “B. Bundle the interpreter”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:
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: 2backend: {kind: native}package: HelloNodeversion: 1.0.0build: 1displayname: Hello Nodedescription: A Node.js worker with its own Node.js runtimemaintainer: Exampleos_min_ver: 7.0-40000arches: [x86_64, armv8, armv7]icon: icon.pnglayout: 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/shset -euout="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.
| Note | Detail |
|---|---|
| Archive format | native.toolkits extracts .tar.xz archives only, with GNU tar. On macOS, put GNU tar (gtar) first in PATH under the name tar. |
| Python | Portable 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. |
| Size | node alone is 102–119 MiB. Keep each payload file under 256 MiB. |
C. Run it in a container
Section titled “C. Run it in a container”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: 1package: com.example.hello-pythonversion: 0.1.0displayname: Hello Pythondescription: A Python worker in a containermaintainer: Exampleos_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:
synopkgland images pinsynopkgland checksynopkgland buildsynopkgland inspect com.example.hello-python.spkimages 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.
Shell scripts without a build
Section titled “Shell scripts without a build”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:
set -euexport COPYFILE_DISABLE=1 # macOS: keep ._ files out of the archivesmkdir -p HelloSh/payload/bin HelloSh/spk/scripts HelloSh/spk/confcd HelloSh
cat > payload/bin/hello.sh <<'EOF'#!/bin/shwhile true; do date; sleep 60; doneEOFchmod 755 payload/bin/hello.shtar -czf spk/package.tgz -C payload .
cat > spk/scripts/start-stop-status <<'EOF'#!/bin/shPID="$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 ;;esacEOFfor hook in preinst postinst preuninst postuninst preupgrade postupgrade; do printf '#!/bin/sh\nexit 0\n' > "spk/scripts/$hook"donechmod 755 spk/scripts/*echo '{"defaults":{"run-as":"package"}}' > spk/conf/privilegecp ../icon-64.png spk/PACKAGE_ICON.PNG # your 64×64 PNGcp ../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 <<EOFpackage="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.PNGsynopkgland inspect HelloSh-1.0.0-0001-noarch.spksynopkgland deploy installs only projects it builds. Install a hand-packed SPK with Package Center > Manual Install.
Check the SPK
Section titled “Check the SPK”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.
synopkgland inspect HelloSh-1.0.0-0001-noarch.spksynopkgland inspect --json dist/HelloNode-1.0.0-0001-armv7.spkEach 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.