ADR 0031: Distribution — apt and containers first, from one build, with a system profile
- Status: accepted
- Date: 2026-09-28
- Supersedes: —
Context
Section titled “Context”M2.5 delivers Fjarr to robots. docs/26 already planned an apt repository, container images, a CMake package for embedding and an install script, but not how they are built, hosted or versioned. The customers Fjarr is for install software in five different ways: apt, Docker, Pixi (conda-forge/RoboStack), Yocto and Nix, depending on the company.
Two different things get installed:
- The product: the agent, its desktop modules, the session helper (ADR-0028), the viewer, the driver catalog. Every ecosystem can package these.
- System integration: the service unit and its user and groups, udev rules, the tunnel device created before the robot’s software (the ordering rule, ADR-0023), and for desktop robots the auto-login account, the GDM watchdog, the helper’s user unit and a forced connector on the kernel command line (ADR-0006). Here the ecosystems genuinely differ. apt lets a tool change a system imperatively; Nix and Yocto forbid it (configuration is generated, or the image is read-only); Docker expresses it in compose.
Pixi, Nix and Yocto also bring their own GStreamer, not Ubuntu 26.04’s 1.28, which ADR-0022 made the baseline. Supporting them means supporting a range of GStreamer versions with CI for each. That is the real cost of breadth, larger than any packaging.
Options considered
Section titled “Options considered”- apt and containers now, the rest per customer later — with integration described as data so later channels render it rather than rewrite it.
- All five channels from the start. It reaches every customer on day one, but it multiplies the GStreamer versions to support before there is a single design partner, and turns M2.5 into a platform-matrix milestone.
- Pixi as the one channel (it spans Linux, macOS and Windows). It suits user tools and embedding. It cannot express a system service and its integration, and its conda-forge graphics stack has to match the host’s drivers.
For the apt repository itself:
- (a) debhelper packages built in CI, a signed static repository on
Cloudflare R2 at
apt.fjarr.io. - (b) A hosted service (Cloudsmith, packagecloud): signing, hosting and private repositories handled for us, but a paid dependency tied to their domain.
- (c) A Launchpad PPA: free and trusted builds from source, but Ubuntu only, slow queues, and no room for vendor or commercial packages (ADR-0011).
Decision
Section titled “Decision”Option 1, with repository (a).
- Channels in M2.5: the apt repository and container images. Pixi/conda-forge, a NixOS module and a Yocto layer follow customer by customer. Each is a rendering of the same system profile, and each takes on its GStreamer version as a supported, CI-tested row in docs/04.
- Packages are debhelper packages built inside Ubuntu 26.04 containers on
native amd64 and arm64 runners.
dpkg-shlibdepsderives dependencies from what the binaries actually link. The repository is generated byreprepro/aptly, signed with a Fjarr key held in CI secrets, served from R2 atapt.fjarr.io, and hasstableandtestingcomponents. - Container images are built from those packages, so both channels ship the same bits. They are multi-arch, published to GHCR, and signed with cosign (keyless) with an SBOM.
- The system profile is data in the repository: per feature (core, net,
desktop-wayland, desktop-x11, headless), the users, groups, units, udev
rules, kernel arguments and GDM settings the feature needs. Package scripts
and
setupread it.--checkverifies the installed copy on every robot, however it was installed. Later channels render it natively. - Packages install;
setupswitches on;--checkverifies. Nothing invasive (GDM, the kernel command line) happens as a side effect ofapt install.setuprecords each change andsetup --undo <feature>reverses it. - One product version for every artifact, from one tag: packages, images,
fjarr-connect, thefjarr-serverimage, crates and npm packages. The wire protocol keeps its own major. 0.x until the extension API is stable (M6). - Releases: tag → full CI →
testing→ a manual, protected promotion tostablethat copies the same artifacts and never rebuilds them.
Consequences
Section titled “Consequences”- M2.5 owns a signing key and its rotation, an R2 bucket, and two release environments. The key’s custody and rotation are documented in docs/26.
- Containerized robots still install one host piece for the desktop:
fjarr-desktop-sessionhas to run inside the desktop user’s session, and its socket is bind-mounted into the container. The helper’s uid check requires no user-namespace remapping, or a configured mapped uid. fjarr-inputdis not packaged in M2.5: no chosen backend needs it.- Revisit when a customer needs a third channel. That is expected rather than a failure of this decision: the work is a new rendering of the profile and a new GStreamer row, and ADR-0022’s baseline becomes the first of several.