feat(packaging): a Synology DSM 7 package, the ARMv7 target it forced, and a desktop application - #1
Merged
Merged
Conversation
macOS writes an AppleDouble `._<name>` beside a file whose extended attributes the filesystem will not take. On an answers directory edited over SMB from a Mac, `._98-fa-9b-50-d8-10.toml` therefore appears next to the real file — with an extension that *is* on the allowlist, and a name that normalizes to the same identity, because normalization strips the leading `._` along with every other non-alphanumeric character. It then claims the very machine the real file was written for, with a body that is binary. The machine being configured receives a parse error instead of its answer, and `check` fails the group that claims it too. `.DS_Store` escapes only by luck: its extension is not on the list. A hidden file is never somebody's answer, so the file store now skips every entry whose name starts with a dot. Found on a real NAS whose share was mounted on a Mac, not by reading anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `.spk` is a release format, not a build: the same binary, wrapped for one platform's package manager. Nothing in `src/` learns that Synology exists. The two places DSM pressed back are answered in packaging — log rotation by a `copytruncate` stanza, and a CLI that cannot find its configuration by a three-line wrapper. `packaging/dsm/` carries the archive; `make-spk.sh` assembles it deterministically and `check-spk.sh` asserts its shape. Two harnesses test it without a NAS: `lifecycle-test.sh` drives the real lifecycle scripts against a fake tree, and `vm/` runs the rest on a DSM 7 virtual machine in Docker. Both run in CI. **armv7 is no longer musl, and that is the largest thing here.** Synology's ARMv7 kernels are 3.10 and answer the time64 syscalls with EINVAL rather than ENOSYS; musl 1.2 falls back to the 32-bit syscalls only on ENOSYS, so every clock_gettime, clock_nanosleep and timed futex fails. The symptom is a binary that answers `--version` and then panics at time.rs:131 the moment it wants a timestamp — which looks like an ABI problem and is not. Measured on a DS416j with a ten-line C probe. The target is now armv7-unknown-linux-gnueabihf with a 2.17 floor, which DSM has and newer ARMv7 Linux keeps working with. x86_64 and aarch64 are 64-bit, have no time32/time64 split, and stay static musl. What CI asserts for armv7 is therefore no longer "static" but "needs no glibc newer than the floor" — the old claim was repeated in six files and was only ever half true. Two defects the local harness could not see, both found by running the machine: the package root cannot be derived from SYNOPKG_PKGDEST, which resolves through a symlink into /volume1/@appstore; and SYNOPKG_TEMP_UPGRADE_FOLDER outlives the upgrade that created it, so a fresh install was restoring a removed installation's configuration, tokens and all. Installed, started, answered, upgraded and uninstalled on a DS416j. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rig and the lifecycle harness had only ever run on macOS and inside DSM. Run in a Debian container — which is what CI is — two of them broke, and one broke silently. `stat -f '%Lp'` is the format flag on BSD. On GNU it means "filesystem status", and with a separate argument it *succeeds*: the variable meant to hold a file mode filled with overlayfs trivia and the `||` fallback never fired, so two mode assertions compared a mode against a paragraph. Asking GNU first fails cleanly on macOS. (`build.sh`'s `stat -f%z` is safe by accident: bundled, GNU rejects it as an invalid option.) `shasum` is a Perl script. A minimal Debian does not have it; Ubuntu runners do, which is how a script like this ships broken to everyone else. `sha256sum` is coreutils. Also silences SC2087 in deploy.sh, where the client-side expansion in that heredoc is the point rather than an oversight. Verified by reproducing the CI job in a container: both .spk assembled with GNU tar, checked structurally with a real readelf and a real `--version`, the armv7 glibc floor asserted, shellcheck clean, and the lifecycle harness green at 34. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mments The configuration is environment variables, and on a deployment that reads them from a file there was no way to see or change one without an editor. `rescriptum config` prints every variable and — the part that matters — which of the file and the environment put it there: the file supplies defaults and the real environment wins, so a panel that offered to edit a value the environment overrides would be offering to change nothing. `config set` edits the file the way a person would want it edited. Comments survive, because on a packaged install they are the only documentation the configuration has; a commented-out setting is uncommented in place rather than appended below, leaving its explanation attached to the right line. The write preserves the file's owner as well as its mode — a root-owned rewrite of a 0600 file the service owns is a server that stops starting one restart later, for no reason anybody changed. Two refusals are deliberate: a misspelled variable, which written would be read back as a stranger and warned about only at the next start; and any change that would leave a server unable to start, refused whole before anything is written. The exit code says whether the configuration is one the server would start on, which makes it usable from a script the way `check` is. Unlike every other subcommand it is dispatched before `Config::from_env` and `validate`: a file that will not parse and a token one character short are the states people run it to get out of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyfAtgDRDguioTRDEqEDEm
DSM has no settings panel for a package, so changing a port or a log level meant SSH and vi on a 0600 file — not something a DS416j owner should have to do. The package now installs an application on the DSM desktop: the configuration as a form, the status, and the log. It is a real DSM application, built on the desktop's own framework, not a page of ours in a frame. Its backend is a CGI that authenticates against DSM's session, requires `administrators`, and refuses a write that does not carry a header a page on another origin cannot make a browser send. It edits the file rather than asking the running server, so it still works when the server will not start — which is when a settings panel earns its place. Underneath, it runs `rescriptum config`: the env-file semantics stay in Rust where they are tested rather than written a second time in shell. Nothing in src/ knows any of this exists. Four things were measured on a DSM 7.2.2 VM and a DS416j on 7.1.1, and none of them is in the developer guide. A CGI under webman runs as the owner of the script — the package user, not http and not root — which is what lets it read the file it owns. That path is not authenticated by DSM at all, so the checks inside the script are the only door. The guide's own ExtJS example does not run: it is ExtJS 3.4.1, `callParent` does not chain, and a method named `show` silently overrides the one DSM calls to display the window — which built, laid out, rendered its taskbar thumbnail and never appeared, without throwing on either version. And reproducible builds fight browser caches: fixed mtimes become `Last-Modified: 2019`, so the application's file carries the version in its name. ExtJS rather than DSM 7.2's Vue framework because the DS416j is capped at 7.1.1, where Vue is undefined; ExtJS is on both, so one application covers every DSM this package supports. os_min_ver is 7.1, and 7.0 is not claimed because nothing has run there. Also fixes build.sh, which still named the old ARMv7 musl target and so built the binary and then quietly produced no package at all — for the one machine this project exists for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyfAtgDRDguioTRDEqEDEm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
.spkfor DSM 7, the tooling that builds and checks it, and the rig thatproved it — and changes the ARMv7 target, which is the part worth reading first.
The ARMv7 target is no longer musl
Synology's ARMv7 kernels are 3.10 and answer the time64 syscalls with
EINVALrather than
ENOSYS. musl 1.2 falls back to the 32-bit syscalls only onENOSYS,so every
clock_gettime,clock_nanosleepand timed futex fails. The symptom is abinary that answers
--versionand then panics attime.rs:131withOs { code: 22, kind: InvalidInput }the moment it wants a timestamp — which lookslike an ABI problem and is not.
Measured on a DS416j (DSM 7.1, kernel 3.10.108) with a ten-line C probe:
The target is now
armv7-unknown-linux-gnueabihfwith a 2.17 floor, which DSM hasand which newer ARMv7 Linux keeps working with, glibc being backward compatible.
x86_64 and aarch64 are 64-bit, have no time32/time64 split, and stay static musl.
What CI asserts for armv7 is therefore no longer "static" but "needs no glibc newer
than the floor" — the old claim was repeated in six files and was only ever half true.
v0.1.0shipped the musl ARMv7 build, so that published artifact cannot run on aSynology. Worth a
v0.1.1and a note on the existing release; not done here.The package
A
.spkis a release format, not a build: the same binary wrapped for one platform'spackage manager. Nothing in
src/learns Synology exists. The two places DSM pressedback are answered in packaging — log rotation by a
copytruncatestanza, and a CLIthat cannot find its configuration by a three-line wrapper.
make-spk.shassembles it deterministically (uncompressed outer tar,ustar, fixedmtimes and ownership,
gzip -n);check-spk.shasserts its shape, down to thepackaged binary's own
--version.How it is tested
check-spk.shlifecycle-test.shvm/on-dsm.shRunning the machine found three defects a fake tree could not: the package root cannot
be derived from
SYNOPKG_PKGDEST(it resolves through a symlink into/volume1/@appstore);SYNOPKG_TEMP_UPGRADE_FOLDERoutlives its upgrade, so a freshinstall was restoring a removed installation's configuration, tokens and all; and a Mac
editing the answers share over SMB drops AppleDouble files that hijack a machine's
answer — fixed in the store, with a regression test.
Still open
aarch64and the remaining DSM 7-capable ARMv7 platforms (M6).🤖 Generated with Claude Code