Skip to content

ADR 0031: Distribution — apt and containers first, from one build, with a system profile

  • Status: accepted
  • Date: 2026-09-28
  • Supersedes: —

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:

  1. The product: the agent, its desktop modules, the session helper (ADR-0028), the viewer, the driver catalog. Every ecosystem can package these.
  2. 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.

  1. apt and containers now, the rest per customer later — with integration described as data so later channels render it rather than rewrite it.
  2. 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.
  3. 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).

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-shlibdeps derives dependencies from what the binaries actually link. The repository is generated by reprepro/aptly, signed with a Fjarr key held in CI secrets, served from R2 at apt.fjarr.io, and has stable and testing components.
  • 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 setup read it. --check verifies the installed copy on every robot, however it was installed. Later channels render it natively.
  • Packages install; setup switches on; --check verifies. Nothing invasive (GDM, the kernel command line) happens as a side effect of apt install. setup records each change and setup --undo <feature> reverses it.
  • One product version for every artifact, from one tag: packages, images, fjarr-connect, the fjarr-server image, 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 to stable that copies the same artifacts and never rebuilds them.
  • 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-session has 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-inputd is 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.