How to build, install, and operate the rusty-photon .deb / .rpm
packages on an observatory machine. Architecture decisions live in
ADR-012 (naming, config
model, shared user, unit classes) and
ADR-013 (native camera-SDK
payloads); the full design in
docs/plans/service-packaging.md; the
maintainer-script invariants in packaging/README.md.
Deployment is native packages by explicit decision — the drivers' USB / udev / firmware needs and ASCOM Alpaca's UDP discovery defeat containers (ADR-012).
Windows ships as one suite MSI instead of per-service packages — see
docs/packaging-windows.md (ADR-015). macOS ships
per-service Homebrew formulas from the rusty-photon/homebrew-rusty-photon
tap — see docs/packaging-macos.md.
Every package is named rusty-photon-<svc> and installs
/usr/bin/rusty-photon-<svc> plus a hardened
rusty-photon-<svc>.service unit that is enabled and started on install.
All daemons run as the shared system user rusty-photon (home
/var/lib/rusty-photon, no login shell), created by the first package
installed. (phd2-guider was originally the one plain CLI package; it
gained a unit when its HTTP service mode landed — issue #464. Its binary
doubles as the PHD2 CLI via subcommands.)
| Service | Port | Notes |
|---|---|---|
| filemonitor | 11111 | Alpaca SafetyMonitor |
| ppba-driver | 11112 | serial (dialout; deb adds plugdev) |
| qhy-focuser | 11113 | serial (dialout; deb adds plugdev) |
| sentinel | 11114 | dashboard: / |
| rp | 11115 | orchestrator API |
| sky-survey-camera | 11116 | config-gated (see below) |
| star-adventurer-gti | 11117 | serial (dialout; deb adds plugdev) |
| pa-falcon-rotator | 11118 | serial (dialout; deb adds plugdev) |
| dsd-fp2 | 11119 | serial (dialout; deb adds plugdev) |
| ui-htmx | 11120 | web config UI |
| qhy-camera | 11121 | USB camera; needs the firmware helper (below) |
| zwo-camera | 11122 | USB camera; its SDK blob bundled |
| pa-scops-oag | 11123 | serial (dialout; deb adds plugdev) |
| zwo-focuser | 11124 | USB focuser; its SDK blob bundled |
| planetarium-bridge | 11126 | virtual planetarium target-entry telescope (no hardware) |
| phd2-guider | 11130 | guider service wrapping PHD2 (PHD2 installed separately, below) |
| plate-solver | 11131 | config-gated; needs ASTAP (below) |
| calibrator-flats | 11170 | config-gated |
| session-runner | 11171 | config-gated |
| polar-align | 11172 | config-gated |
Alpaca UDP discovery is deliberately not served: with this many Alpaca
servers on one host they would collide on the discovery port. Point
clients (N.I.N.A. etc.) at host:port directly using the table above.
The sentinel package additionally carries the operator tool
/usr/bin/rusty-photon-doctor (diagnosis, --fix repair, and the TLS +
credential lifecycle — docs/services/doctor.md)
plus the TLS renewal units rusty-photon-renew.service /
rusty-photon-renew.timer. There is no separate doctor package:
sentinel is the always-installed supervisor, so its artifact is the
delivery vehicle (ADR-016).
Packages are built natively on the target architecture — nightly in CI on hosted x86_64 + arm64 runners (see Nightly channel), or on demand directly on the rig / a dev box. From a repo checkout on a Debian-family machine with Rust installed:
scripts/build-packages.sh # all services, .deb only
scripts/build-packages.sh --rpm # also .rpm
scripts/build-packages.sh --services qhy-camera,filemonitor
scripts/build-packages.sh --skip-sdk-staging # offline rebuild from cache
scripts/build-packages.sh --deb-version 0.1.0+nightly.202607120507.gba09dc9
# nightly version stamp (CI / rollback builds)With --rpm and --deb-version together, the rpm version is derived
from the deb stamp by rendering +nightly. as rpm's ^ snapshot
separator (0.1.0^202607120507.gba09dc9) — each packager renders its own
dialect of "sorts after the base release, before the next one".
The script installs apt build prerequisites, stages the pinned native
SDKs into ~/.cache/rusty-photon-pkg/ (QHYCCD static lib for the
link; per zwo service its ONE MIT blob, which also becomes that package's
payload per ADR-013 + ADR-014), then release-builds with the RUNPATH the
zwo packages need — the two zwo services each in their own cargo
invocation, so feature unification cannot re-union their per-device SDK
links — and runs cargo deb (and cargo generate-rpm with --rpm) per
service. Artifacts land in dist/<version>/ with a SHA256SUMS.txt.
The QHY SDK version/sha256 pins and the ZWO blob ref are pinned in the
script and cross-checked by scripts/check-pkg-assets.sh against the
firmware helper and the CI SDK action, so shipped and CI-linked SDK bits
cannot drift apart.
CI publishes a rolling nightly prerelease built from the HEAD of
main whenever it has changed since the last publish
(.github/workflows/nightly-packages.yml): every packaged service as a
.deb and .rpm for both amd64/x86_64 and arm64/aarch64, each
package lifecycle-verified in a systemd container (Debian for the debs,
Fedora for the rpms) before anything is published, plus the Windows
suite MSI (docs/packaging-windows.md)
and the macOS arm64 tarballs with their regenerated -nightly Homebrew
formulas (docs/packaging-macos.md)
— all-or-nothing across the legs, so the release is always one coherent
commit with a complete asset set. There is one release and one tag;
assets are replaced on each publish, with no dated history. The same
debs and rpms are additionally published as a real apt/dnf
repository (see Package repositories
below), which is the recommended way to consume the channel on Linux.
Nightly debs carry the version <base>+nightly.<datetime>.g<sha> (e.g.
0.1.0+nightly.202607120507.gba09dc9, UTC to the minute), which dpkg
sorts above the plain <base> release and below the next patch release —
apt upgrades a release install to a nightly in place, and the next
release upgrades over any nightly. The stamp carries the time, not just
the date, because it is the only ordered part of the version: the
g<sha> suffix compares as hex, so a second publish on the same day
must out-sort the first on the timestamp alone.
Nightly rpms carry <base>^<datetime>.g<sha> (e.g.
0.1.0^202607120507.gba09dc9); rpm's ^ separator sorts the same way,
so dnf upgrades in place identically. One wrinkle: GitHub rewrites ^
to . in uploaded asset names, so the file is called
…-0.1.0.<datetime>.g<sha>-1.<arch>.rpm while rpm -q after install
shows the true ^ version. SHA256SUMS.txt lists the dot-rendered names, so
checksums verify against the files as downloaded.
The channel is also served as a real apt/dnf repository at
pkg.rustyphoton.space (a Cloudflare R2 public bucket;
tools/rusty-photon-packages-r2/README.md documents the hosting), so a
machine set up once picks every nightly up with plain
apt upgrade / dnf upgrade. The repository is rolling-only — exactly
one version, the current nightly; the GitHub release assets below stay
as the manual path (and the downgrade/rollback path).
Clients verify the repo metadata against the signing key; its fingerprint is
C2BE 1E02 D49E 111B 6BEC 2882 51AA 3DE5 44C0 0B8F
(the same key as packaging/gpg/pubkey.asc in this repo — compare
before trusting the downloaded copy).
Debian-family:
sudo install -d /etc/apt/keyrings
sudo curl -fsSLo /etc/apt/keyrings/rusty-photon.asc https://pkg.rustyphoton.space/pubkey.asc
echo "deb [signed-by=/etc/apt/keyrings/rusty-photon.asc] https://pkg.rustyphoton.space/deb nightly main" \
| sudo tee /etc/apt/sources.list.d/rusty-photon-nightly.list
sudo apt update
sudo apt install rusty-photon-<svc> # thereafter: plain `apt upgrade`Fedora:
sudo tee /etc/yum.repos.d/rusty-photon-nightly.repo <<'EOF'
[rusty-photon-nightly]
name=Rusty Photon nightly
baseurl=https://pkg.rustyphoton.space/rpm/$basearch/
enabled=1
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkg.rustyphoton.space/pubkey.asc
EOF
sudo dnf install rusty-photon-<svc> # thereafter: plain `dnf upgrade`repo_gpgcheck=1 is what makes dnf verify the repo signature at all
(dnf imports the key on first contact — check the fingerprint it shows
against the one above); gpgcheck=0 because individual packages carry
no signature — the signed metadata's checksums cover them.
Filenames change nightly (they carry the version), so use
SHA256SUMS.txt — the one asset with a stable URL — as the index:
curl -fsSL https://github.com/rusty-photon/rusty-photon/releases/download/nightly/SHA256SUMS.txt
# pick the file for your service + arch, then:
curl -fLO "https://github.com/rusty-photon/rusty-photon/releases/download/nightly/<file>"
sha256sum -c --ignore-missing SHA256SUMS.txt
sudo apt-get install "./<file>" # Debian-family
sudo dnf install "./<file>" # Fedoraor, with the GitHub CLI (rpms: --pattern 'rusty-photon-<svc>-*.<arch>.rpm'
with <arch> = x86_64 or aarch64):
gh release download nightly --repo rusty-photon/rusty-photon \
--pattern 'rusty-photon-<svc>_*_arm64.deb'
sudo apt-get install ./rusty-photon-<svc>_*_arm64.debUpgrading is installing a newer nightly the same way; a running unit is restarted onto the new binary and the config untouched, as with any package upgrade.
Downgrades. Once a machine runs nightlies, anything older is a downgrade — an on-demand build stamped with the plain workspace version, or an older nightly — and needs:
sudo apt-get install --allow-downgrades ./rusty-photon-<svc>_0.1.0-1_arm64.deb
sudo dnf downgrade ./rusty-photon-<svc>-0.1.0-1.<arch>.rpm # FedoraRolling back. The channel keeps no history. To return to a
known-good state, downgrade to the plain release as above, or rebuild
the known-good commit on demand (add --rpm for the rpm set) and
install that the same downgrade way:
git checkout <known-good-sha>
scripts/build-packages.sh --deb-version "<base>+nightly.<datetime>.g<short-sha>"(<base> = the workspace version at that commit.)
sudo apt-get install ./rusty-photon-<svc>_*.debapt-get install ./<file> (not dpkg -i) resolves the runtime
dependencies. The unit is enabled and started immediately; on upgrade it is
restarted. On Fedora:
sudo dnf install ./rusty-photon-<svc>-*.rpm
sudo systemctl start rusty-photon-<svc>The rpm enables the unit but — Fedora convention — does not start it: start it once by hand (or reboot); upgrades restart a running unit and leave a stopped one alone. Verify with:
systemctl status rusty-photon-<svc>
curl http://localhost:<port>/management/apiversions # Alpaca servicesConfig-gated services (sky-survey-camera, plate-solver,
calibrator-flats, session-runner, polar-align) have no sensible default config, so their units carry
ConditionPathExists= on the config file: on a fresh install the unit
stays inactive (not failed) until you write
/etc/rusty-photon/<svc>.json, then systemctl start rusty-photon-<svc>.
Serial-device drivers (ppba-driver, qhy-focuser,
pa-falcon-rotator, pa-scops-oag, dsd-fp2, star-adventurer-gti) validate their
hardware eagerly at startup and exit if the device is missing — by design,
so a broken device is never advertised on the network. Until the device is
attached (and its path matches the config), the unit sits in a
restart-every-5s loop; it comes up by itself once the hardware appears.
The cameras and the network-only services serve with no hardware attached.
The installer puts bytes on disk; doctor wires the configs (ADR-016). After installing — or later adding — packages, run once:
sudo rusty-photon-doctor # diagnose (exit 0 clean, 1 = findings)
sudo rusty-photon-doctor --fix # converge; a re-run shows the clean bill--fix repairs what the checks flag (retired config keys, missing
joins, fixable shapes) and — see
doctor.md §Provisioning — can generate TLS
material and mint + distribute the observatory credential for every
installed service. Running it while services are live is fine
(warn-and-proceed, atomic writes); services pick fixed configs up on
their next restart: sudo systemctl restart 'rusty-photon-*'.
Everything doctor writes under sudo is chowned back to the
rusty-photon user, pki material included.
The sentinel package ships rusty-photon-renew.timer (daily, jittered,
Persistent=true), running rusty-photon-doctor tls renew as the
rusty-photon user — a no-op until certificates exist and are inside
their renewal window, so it is safe armed on every install. The deb
starts the timer on install; the rpm (Fedora convention) enables it to
arm on the next boot, or start it once by hand:
sudo systemctl start rusty-photon-renew.timer
systemctl list-timers rusty-photon-renew.timer # shows the next fireRunning services pick renewed certificates up without a restart (mtime-triggered in-process reload; ADR-002). Sentinel's watchdog deliberately ignores the renew unit — it is a scheduled job, not a daemon to supervise.
Packages ship no config files. Daemons self-create their config on first
start at /var/lib/rusty-photon/.config/rusty-photon/<svc>.json (the
shared user's XDG path), reachable via the /etc/rusty-photon symlink.
Exceptions: the config-gated five (above) never write one. To change
settings:
sudo -e /etc/rusty-photon/<svc>.json
sudo systemctl reload rusty-photon-<svc> # reload-capable services
sudo systemctl restart rusty-photon-<svc> # the restReload-capable (SIGHUP): filemonitor, ppba-driver, qhy-focuser,
sky-survey-camera, pa-falcon-rotator, pa-scops-oag, dsd-fp2,
star-adventurer-gti, qhy-camera, zwo-camera. Note that services with config.apply support
(via ui-htmx) rewrite these files at runtime — hand-edits and UI edits
share the same file.
Sentinel's restart endpoint, watchdog ladder, and health supervision shell
out to systemctl restart rusty-photon-<svc> as the unprivileged
rusty-photon user — its unit sets NoNewPrivileges=yes, so a sudo
prefix could never work. The sentinel package
therefore ships a scoped polkit rule,
/usr/share/polkit-1/rules.d/50-rusty-photon-sentinel.rules, granting that
user exactly the restart verb on rusty-photon-* units; other verbs and
non-prefixed units still require the usual authorization. polkitd picks the
rule up on install with no reload step. The restart commands themselves are
derived from the discovered unit names — the rule's scope and the discovery
scope are the same set (see
sentinel.md §Service discovery).
qhy-camera — QHYCCD's SDK is proprietary and never redistributed (ADR-013). After installing the package, run once, as root, with internet access:
sudo rusty-photon-qhy-firmware-installIt downloads the pinned SDK release from qhyccd.com, verifies a pinned
sha256, and installs the camera firmware images, QHYCCD's udev
firmware-upload rules, and their FX3-capable fxload. An already-plugged
cold camera is flashed immediately (the helper re-emits udev add events);
otherwise firmware uploads on the next plug-in. Offline installs work; the
camera just stays unusable until the helper has run.
zwo-camera / zwo-focuser — nothing to do: each package bundles its own
MIT-licensed SDK blob at /usr/lib/rusty-photon/ (libASICamera2.so /
libEAFFocuser.so; license in the package docdir), so the two co-install
cleanly (ADR-014). ZWO devices keep firmware in onboard flash.
svbony-camera — SVBony's SDK carries no license grant at all (ADR-018), so unlike ZWO it is never bundled. After installing the package, run once, as root, with internet access:
sudo rusty-photon-svbony-sdk-installIt downloads the pinned SDK library from the same indi-3rdparty commit CI
links against, verifies a pinned sha256, and installs libSVBCameraSDK.so
to /usr/lib/rusty-photon/ (the package's binary is linked with the
matching RUNPATH, mirroring zwo-camera's mechanism). Offline installs
work; the camera just stays unusable until the helper has run.
Until it has, the service cannot load the SDK and systemd keeps retrying it
(a loader error every few seconds in journalctl -u rusty-photon-svbony-camera) — that retry is the recovery path: the service
starts serving within seconds of the helper finishing, with no systemctl
step needed.
The same helper is the supported bootstrap on a machine with no
rusty-photon-svbony-camera package (a developer box running the binary
from a checkout): run it from the source tree
(sudo services/svbony-camera/pkg/rusty-photon-svbony-sdk-install) and it
installs a udev rule for VID f266 alongside the SDK, since nothing else
would grant USB access there. It never touches udev when the packaged rule
is already installed; --no-udev opts out entirely and --udev-group NAME picks the owning group. See
docs/services/svbony-camera.md.
Every camera package installs a udev rule assigning their USB VID's
device nodes to the rusty-photon service group (the account's own
primary group — no supplementary groups needed, and no reliance on
Debian's plugdev, which rpm-family hosts lack).
ASTAP is an external runtime dependency, deliberately not a package
dependency (bring-your-own, ADR-005):
you install the solver and a star database yourself and point the
service's config at them. The service is config-gated — the packaged
unit stays inert until plate-solver.json exists.
-
Install the solver binary. The wrapper drives
astap_cli, the command-line solver — the GUI program is not needed. Download the zip for your architecture from the ASTAP downloads (SourceForgelinux_installer/, e.g.astap_command-line_version_Linux_aarch64.zipon a Pi,…_amd64.zipon x86_64), then:unzip astap_command-line_version_Linux_*.zip sudo install -m 755 astap_cli /usr/local/bin/astap_cli -
Install a star database. Upstream's own rule: with a field of view of 0.6° or larger, any of D05/D20/D50/D80 works — they are all Gaia-derived to a similar depth, at increasing star density (and size: D05 ≈ 100 MB up to D80 ≈ 1.25 GB). D05 is plenty for typical deg-class refractor fields (the reference rig's 360 mm + IMX178 ≈ 1.2° × 0.8° solves with it; it is also what CI pins). Go denser (D50/D80) only below ~0.6°, and to W08 for very wide fields (> 20°). Either install the database
.debfrom the same site (confirm where it lands withdpkg -L) or unzip the database zip into a directory of your choice. Whichever way,astap_db_directorymust point at the specific directory holding that database's files — the examples here and in the service docs use/opt/astap/d05. -
Write the config. Create
/etc/rusty-photon/plate-solver.json(both keys are mandatory — there is no built-in default, which is exactly why the unit gates on the file):{ "server": { "port": 11131 }, "astap_binary_path": "/usr/local/bin/astap_cli", "astap_db_directory": "/opt/astap/d05" }The file (and both paths) must be readable by the
rusty-photonuser — the service validates them at startup and again on every/healthprobe. See docs/services/plate-solver.md for the full config surface (timeouts, concurrency, hints). -
Start and verify.
sudo systemctl start rusty-photon-plate-solver curl -s -w '\n%{http_code}\n' http://127.0.0.1:11131/health200with{"status":"ok"}means binary and database both check out;503carries{"status":"binary_unavailable"}or{"status":"db_unavailable"}, naming the check that failed, plus amessagewith the offending path. Sentinel shows a503as degraded (amber, message displayed) rather than restart-looping a service whose problem a restart cannot cure.
PHD2 is an external runtime dependency, deliberately not a package
dependency (bring-your-own, same posture as ASTAP above): phd2-guider
connects to an already-running PHD2 on localhost:4400 and never spawns
it — the operator owns the PHD2 process (see
phd2-guider.md §HTTP Service Mode).
The service itself needs no setup: it runs on built-in defaults and
keeps retrying the PHD2 connection in the background, so the install
order does not matter and no service restart is ever needed.
Debian and Raspberry Pi OS ship no phd2 package, so on a rig host you
build it from source (upstream's supported route — minutes, not hours,
on a Pi 5 class machine). PHD2 is a GUI application, so a headless host
also needs a persistent virtual display; TigerVNC provides that and
remote viewing in one.
-
Build and install PHD2.
sudo apt-get install -y build-essential git cmake pkg-config \ libwxgtk3.2-dev wx-common wx3.2-i18n gettext zlib1g-dev libx11-dev \ libcurl4-gnutls-dev libcfitsio-dev libnova-dev libusb-1.0-0-dev \ libeigen3-dev libopencv-dev libgtest-dev git clone --depth 1 --branch v2.6.14 https://github.com/OpenPHDGuiding/phd2.git cmake -S phd2 -B phd2/tmp -DCMAKE_BUILD_TYPE=Release make -C phd2/tmp -j"$(nproc)" sudo make -C phd2/tmp install # lands at /usr/bin/phd2 (PHD2's cmake sets the /usr prefix itself)
Do not pass
-DUSE_SYSTEM_LIBINDI=1on Debian or Raspberry Pi OS: theirlibindi-devis 1.9.x and PHD2 requires INDI ≥ 2.0, so the default configure — which downloads and builds PHD2's own INDI client — is the working path. The default build also includes the bundled vendor camera backends (QHY, ZWO, and friends), so guide cameras connect natively with no INDI server involved. -
Run PHD2 headless under TigerVNC.
sudo apt-get install -y tigervnc-standalone-server tigervncpasswd # one-time: set the viewing password printf '#!/bin/sh\nexport GDK_BACKEND=x11\nexec phd2\n' > ~/.vnc/xstartup chmod +x ~/.vnc/xstartup tigervncserver :1 -localhost yes -geometry 1280x800
PHD2 is the VNC session: quitting PHD2 ends the session, and
tigervncserver -kill :1is the other way down. TheGDK_BACKEND=x11line is load-bearing — on a host where any Wayland session exists (a Raspberry Pi OS console, for example), GTK prefers the Wayland socket overDISPLAYand PHD2 would come up invisible on the physical console instead of the VNC display. The password tool must betigervncpasswd: on Raspberry Pi OS plainvncpasswdis RealVNC's unrelated utility and writes a password file TigerVNC cannot read. Iftigervncserverreports the server "cleanly exited" right after starting, PHD2 crashed on launch (~/.vnc/<host>:1.logshows it) — a one-off on the very first start after installation has been observed; starting the session again recovers. -
Create the equipment profile (first run only). Tunnel the display to your workstation and point any VNC viewer at
localhost:5901:ssh -L 5901:127.0.0.1:5901 <rig-host>
Complete PHD2's first-light wizard. Equipment choices are site-specific and out of scope here; the built-in camera/mount simulator makes a fine smoke-test profile. Afterwards quit PHD2 cleanly once (File → Exit — this ends the VNC session) and start it again with the
tigervncserverline above: PHD2 persists its profile to~/.PHDGuidingV2on clean exit, not while running, and an unsaved profile would be lost to a power cut. -
Verify.
curl -s -w '\n%{http_code}\n' http://127.0.0.1:11130/health200with{"status":"ok"}means phd2-guider holds a live connection to PHD2.503with{"status":"unavailable"}(plus a human-readablemessage) means it does not — PHD2 is not running, or its event server is disabled (PHD2's Tools → Enable Server, a per-profile setting that defaults to on). The service reconnects on its own within seconds of PHD2 appearing.
Like every other daemon (see Configuration),
phd2-guider self-creates its config on first start, which also gives
sentinel a health-probe URL to derive — the guider is supervised from
then on. While PHD2 is off — the normal daytime state of a rig — the
guider's 503 counts as alive but degraded under
sentinel's health supervision:
no restarts, no notifications, an amber dashboard row showing the
guider's own message (issue #595). Supervision still restarts the
guider the moment it stops answering HTTP at all.
sudo apt-get remove rusty-photon-<svc> # keeps the service's config + state
sudo apt-get purge rusty-photon-<svc> # also deletes its config + state dirThe shared user, /var/lib/rusty-photon, and the /etc/rusty-photon
symlink are never removed (shared across packages, Debian convention for
system users). rpm has no purge lifecycle: erase behaves like remove;
to fully clean up after an erase, delete
/var/lib/rusty-photon/.config/rusty-photon/<svc>.json and
/var/lib/rusty-photon/<svc>/ by hand.
scripts/verify-packages.sh # all debs in dist/<version>/
scripts/verify-packages.sh --rpm # the rpms, in a Fedora container
scripts/verify-packages.sh --services filemonitor,zwo-camera --keepRuns a podman --systemd=always debian:trixie container and, per package:
install → unit active → config self-created → HTTP probe → remove (config
survives) → purge (config and state gone, shared pieces stay). The --rpm
flavor runs the same per-service checks in a Fedora container, adjusted
where rpm's lifecycle genuinely differs: it asserts the scriptlets'
enabled-but-not-started contract before starting each unit itself, its
dnf install doubles as the proof that every rpm's declared requires
resolve (nothing is preinstalled to compensate), and erase is verified as
remove-not-purge — config and state must survive. Gated
services verify enabled-but-inactive-and-not-failed instead; zwo-camera
additionally proves via ldd that each zwo binary resolves exactly its own
bundled blob through the RUNPATH — and does not link the other services'
SDKs (ADR-014). Rootless podman cannot apply the units' sandboxing, so
the script resets the hardening inside the container — hardening is
verified on real hosts with systemd-analyze security rusty-photon-<svc>.service.
Expected lintian findings (accepted, not bugs):
custom-library-search-path on every package (the RUNPATH is injected
uniformly; only the zwo packages use it); no-changelog / no-manual-page /
copyright-without-copyright-notice pre-1.0; unstripped-binary-or-object
and hardening-no-relro on the vendored ZWO blobs (shipped exactly as
published); embedded-library on qhy-camera's statically linked SDK;
appstream-metadata-missing-modalias-provide on the camera packages' udev
rules; empty-field Depends and unstripped-binary on our own binaries
only on ad-hoc builds from non-Debian hosts.