Package an existing native product
Use the native layout when you already have a product that builds its own binaries, start script and DSM window, and you only need an SPK around it. synopkgland runs your build hooks, checks the result and writes one SPK per architecture. It does not generate application code, a backend or a UI.
If you want a new app with generated code, start from Build a native app instead.
Set it up
Section titled “Set it up”Write dsm-package.yaml by hand. The native layout is not an init template.
schema: 2backend: {kind: native}package: ExampleNativeversion: 1.2.3build: 8displayname: Example Nativedescription: Existing native productmaintainer: Exampleos_min_ver: 7.0-40000arches: [x86_64, armv8, armv7]layout: payload: target/native-layout/{arch}/payload metadata: target/spk-metadata binaries: - {path: bin/app, linkage: static, arches: [x86_64, armv8]} - {path: bin/app, linkage: dynamic, arches: [armv7]}native: prepare: {cwd: ., command: [python3, package/prepare.py]} hook: {cwd: ., command: [python3, package/compile.py]} toolkits: x86_64: - {url: 'https://example.com/toolkit.txz', sha256: '<64 lowercase hex digits>'}Replace the toolkit URL and digest with your own, or remove toolkits if your compile hook needs none.
Then run:
synopkgland checksynopkgland build --arch x86_64,armv8,armv7synopkgland inspect dist/ExampleNative-1.2.3-0008-x86_64.spkbuild writes dist/<package>-<version>-<build padded to 4 digits>-<arch>.spk. Without --arch it builds every architecture in arches.
To install it on a NAS, run synopkgland deploy --host <nas>. It builds only the NAS architecture, using armv8 on an aarch64 NAS when you declare that name, and installs the SPK. Increase build before every deploy. synopkgland status --host <nas> then reports the installed state.
Manifest fields
Section titled “Manifest fields”| Field | Rule |
|---|---|
| Allowed keys | schema, backend, package, version, build, displayname, description, maintainer, metadata, icon, os_min_ver, privilege, arches, layout, native |
schema | 2 |
backend | {kind: native} |
package | ^[A-Za-z][A-Za-z0-9]{1,63}$ |
version | ^\d+(\.\d+){0,3}$ |
build | Integer from 1 to 2147483647 |
os_min_ver | 7.x-<build>, for example 7.0-40000 |
privilege | none, or omit it |
arches | Non-empty, no duplicates: x86_64, aarch64, armv8, armv7 |
layout.payload | Directory with the files that go into the package. May contain {arch}. |
layout.metadata | Directory with the SPK metadata files (see below) |
layout.binaries | One or more {path, linkage, arches?}. linkage is static or dynamic. arches must be a subset of the manifest arches. Every architecture needs at least one binary. |
icon | Optional. Square PNG of at least 256×256 in the project, or an icon_<size>.png directory with icon_256.png. build writes PACKAGE_ICON.PNG and PACKAGE_ICON_256.PNG from it; the metadata folder must not also contain them. Window icons stay in your payload. |
layout.anchors | Optional. <key>_sha256: <payload path>. Writes the file’s SHA-256 into INFO for every architecture. See Product root helpers and upgrades. |
native.hook | Required. Compiles or collects the payload for one architecture. |
native.prepare | Optional. Runs once before the per-architecture hooks. |
native.toolkits | Optional. <arch>: [{url, sha256}]. url is HTTPS. sha256 is 64 lowercase hex digits. |
reproducible | Optional. {inputs?: [file...], clean?: [path...]} for build --verify-reproducible. List build outputs such as target/{arch} under clean so the second build compiles from scratch. See reproducible builds. |
Paths in the manifest are relative to the project, never absolute, and never contain .. or {/} other than {arch}. A compose.yaml in the same project is rejected.
A hook is {command: [argv...], cwd?: '.', env?: {...}}. The command runs with arguments and without a shell, inside the project. A hook must exit with status 0. env keys match ^[A-Z_][A-Z0-9_]*$ and must not start with SYNOPKGLAND_.
| Variable | Meaning |
|---|---|
SYNOPKGLAND_PACKAGE | Package name |
SYNOPKGLAND_VERSION, SYNOPKGLAND_BUILD | Manifest version and build number |
SYNOPKGLAND_SPK_VERSION | Version plus the build padded to 4 digits |
SYNOPKGLAND_PROJECT_DIR | Absolute project root |
SYNOPKGLAND_ARCH | Current architecture. Compile hook only. |
SYNOPKGLAND_TOOLKIT_DIR | Freshly extracted toolkit tree. Compile hook only. |
SYNOPKGLAND_WORK_DIR | Empty scratch folder for this architecture, removed after the build. Compile hook only. |
synopkgland build --jobs <n> runs the compile hook for up to n architectures at the same time. Use it only when your hook keeps architectures apart: layout.payload must contain {arch} (the CLI refuses --jobs above 1 otherwise), and the hook must not write to a shared folder such as a single build/ or out/ directory. Put scratch files in SYNOPKGLAND_WORK_DIR. Cargo is safe when each architecture uses its own --target, because it locks the shared target/ folder, but the builds then wait for each other there. native.prepare still runs once, before any architecture starts.
Toolkit archives are downloaded over HTTPS into a cache and checked against sha256 on every build. Set SYNOPKGLAND_CACHE_DIR to choose the cache folder. The default is a per-user cache shared by every project and worktree: $XDG_CACHE_HOME/synopkgland or ~/.cache/synopkgland on Linux, and ~/Library/Caches/synopkgland on macOS. Several builds can use the cache at the same time: each download goes to a private temporary file that is checked and then renamed into place, and each build extracts its own toolkit copy. The cache folder must not be a symlink or be writable by other users.
The archives are extracted with the tar on PATH, which must be GNU tar or bsdtar (the macOS default). A later archive can write through a folder symlink an earlier archive created. With bsdtar the CLI first refuses an archive that has an absolute or .. member.
Layout rules
Section titled “Layout rules”check and build verify the payload and the metadata folder.
| Area | Rule |
|---|---|
| Files | No symlinks and no special files. No setuid or setgid bits. |
| Size | Each file at most 256 MiB, all files together at most 1 GiB. |
| Binaries | Each listed binary is an executable ELF file for the architecture, and its linkage matches linkage. |
metadata top level | Only conf, scripts, WIZARD_UIFILES, PACKAGE_ICON.PNG and PACKAGE_ICON_256.PNG (not both with manifest icon) |
scripts/start-stop-status | Required |
scripts/* | Must not contain sudo, chown root or chmod +s |
| Lifecycle scripts | Executable (chmod +x): preinst, postinst, preuninst, postuninst, preupgrade, postupgrade, start-stop-status, prereplace, postreplace |
conf/privilege | JSON with defaults.run-as set to package. username and groupname are optional and must equal the package name. |
conf/resource | Optional. Only an empty systemd-unit object is accepted. |
conf/systemd | Only top-level *.service files. Each has exactly one User= equal to the package name. Group= equals the package name if present. |
| Service units | No line continuations. User, Group and Exec* lines only in [Service]. No AmbientCapabilities, SupplementaryGroups, PermissionsStartOnly, RootDirectory or RootImage. No + or ! prefix on an Exec* command. |
The package always runs as its own package account. The native layout has no root option.
Product root helpers and upgrades
Section titled “Product root helpers and upgrades”If your product ships its own root helper with an administrator grant, bind the grant to what the administrator approved: the capabilities, the package account and the pinned task command. Do not bind it to a build, manifest or file-tree digest. To let a new SPK change root code without a new approval, declare each root payload in layout.anchors:
layout: anchors: example_helper_sha256: bin/helper example_bootstrap_sha256: bin/bootstrapFor every architecture, build writes the SHA-256 of that architecture’s payload file into INFO, for example example_helper_sha256="<64 lowercase hex>". Package Center writes /var/packages/<package>/INFO as root, so the package account cannot change it.
| Rule | Value |
|---|---|
| Key | ^[a-z][a-z0-9_]{0,48}_sha256$, not starting with synopkgland_, at most 16 keys |
| Path | A regular file in the payload, relative to the payload folder, without {arch} |
build | Fails when the file is missing or a key collides with another INFO field |
inspect | Refuses an SPK whose INFO anchor is changed, duplicated, missing or undeclared |
Your root helper reads exactly one valid line for each key from the root-owned INFO, copies the payload into its protected folder, checks the copy against the value and only then runs it. Keep the pinned task command free of build-specific digests. A missing, duplicated or different value keeps the grant inactive until an administrator approves. Revoke the grant on upgrade only when the capabilities change, and answer authorization.status with reason: upgrade_reapproval then. The SDK executor follows the same rule with synopkgland_executor_sha256.
Architectures
Section titled “Architectures”arches value | ELF check | Notes |
|---|---|---|
x86_64 | 64-bit, machine 62 | |
aarch64 | 64-bit, machine 183 | INFO arch lists the aarch64 platforms (armada37xx rtd1296 rtd1619 rtd1619b), as native-go and binary packages do. |
armv8 | 64-bit, machine 183 | Same binary format as aarch64. The SPK keeps the armv8 name. |
armv7 | 32-bit, machine 40 |
dynamic linkage means your binary depends on libraries of the target DSM. Pin the matching DSM toolkit in native.toolkits.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Commands that work | check, build, inspect, deploy, status |
| Commands that reject it | logs |
build range | 1 to 2147483647 |
| Privilege | none (package account) |
| Architectures | x86_64, aarch64, armv8, armv7 |
| File size | 256 MiB each, 1 GiB total |
inspect checks the archive without installing or running anything. It does not prove the product works on a device.
Troubleshooting
Section titled “Troubleshooting”| Code or message | Cause | Fix |
|---|---|---|
NATIVE_LAYOUT_INVALID: schema 2 required | schema is not 2, or backend is missing. | Set schema: 2 and backend: {kind: native}. |
NATIVE_LAYOUT_INVALID: unknown field | A key is not in the allowed list. | Remove it. |
NATIVE_LAYOUT_INVALID: path must stay inside project | A path is absolute, has .., or has { other than {arch}. | Use a relative path inside the project. |
NATIVE_LAYOUT_INVALID: ELF architecture mismatch | The binary is not for the architecture being built. | Build the right binary in the hook. |
NATIVE_LAYOUT_INVALID: ELF linkage mismatch | linkage does not match the binary. | Fix linkage or the build. |
NATIVE_LAYOUT_INVALID: native hook failed | A hook exited with a non-zero status. | Run the hook command by hand. |
NATIVE_LAYOUT_INVALID: toolkit checksum mismatch | The download does not match sha256. | Fix the URL or the digest. |
NATIVE_LAYOUT_INVALID: native layout requires package-only lifecycle privilege | conf/privilege asks for more than the package account. | Set defaults.run-as to package. |
NATIVE_LAYOUT_INVALID: start-stop-status missing | scripts/start-stop-status is not in metadata. | Add it. |
NATIVE_TOOLKIT_TAR_UNSUPPORTED | The tar on PATH is neither GNU tar nor bsdtar. | Install one and put it first on PATH. |
NATIVE_LAYOUT_SCRIPT_NOT_EXECUTABLE | A lifecycle script in metadata/scripts is not executable. | Run chmod +x on it. |
See Errors for every code.