quine-linux/GUIDELINES.md
Philip Peterson 1a1b86b584 Use "Quine Linux" in the file-header example, not "Quine OS"
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-08 05:17:48 -08:00

9.6 KiB

Quine OS: build & CI guidelines

These guidelines describe how build tooling and CI should be structured across Quine OS's apps and subsystems. They're drawn from, and should stay consistent with, patterns already established in swap-watchdog, quine-linux, quintodrome, and hyper.

1. Every app gets a Taskfile

Every standalone app (buildable, runnable, testable on its own) ships a Taskfile.yml as its entrypoint — task build, task install, task test, etc. Taskfile is the human-friendly front door; it is not where build logic lives (see §3). Dev-only apps with no real build step just wrap the run command directly (e.g. a Flutter app's Taskfile can be just flutter run -d linux — nothing more needed).

2. Build tooling preference order

When an app needs a real build step, prefer in this order:

  1. Nix, as a hermetic dependency-provisioning layer inside the Dagger container — not necessarily nix build/flakes, but nix-shell -p <pkgs> to pull build-time headers/toolchains into an ephemeral container, while the actual compile still runs the language's native builder (go build, cargo build, etc.). For example: Nix supplies gtk3-devel/dbus-devel-equivalent packages inside the container, the host's own Go toolchain is mounted in to avoid version drift against go.mod, and patchelf retargets the ELF interpreter so the binary still links against the host's real runtime libs. Nothing gets installed on the host or layered into the image for a one-off build dependency.
  2. If Nix genuinely can't cover it, the next-most-hermetic sandbox — e.g. a flatpak-builder sandbox in place of Nix, when a dependency's header/binding generation isn't well supported in nixpkgs. That's a legitimate, documented exception, not drift.
  3. Never: reaching for rpm-ostree install, dnf, or any host/image mutation just to satisfy a build-time dependency. If a dependency genuinely belongs in the OS image (not just at build time), that's a deliberate rpm-ostree layering decision made separately, not a build script shortcut.

3. CI: Dagger owns the logic, Gitea Actions stays thin

One Forgejo instance, Gitea Actions as the trigger, Dagger as the engine. The workflow YAML should do as little as possible:

- uses: actions/checkout@v6
- run: curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR="$HOME/.local/bin" sh
- run: dagger call ci

All real logic — build steps, test invocation, lint — lives in .dagger/ (Go SDK), never inlined as shell in the workflow file. If a workflow step is doing anything beyond checkout, installing the Dagger CLI, and one dagger call, that's a signal the logic should move into the Dagger module instead.

4. Apps vs. OS subsystems: different rigor, same tools

  • Apps: full Taskfile, Dagger-driven CI as above, real test coverage expected — they're independently buildable and testable, so there's no excuse not to.
  • Larger OS subsystems (not independently buildable/testable the way an app is — e.g. quine-linux itself): still require a Taskfile, and CI through either Dagger or a Taskfile-driven Gitea Actions job — not mandatory Dagger. Some kind of test/smoke-check should exist, but it's scoped to what's actually checkable (bootc status, image boots, a precondition script), not full unit/integration coverage.
  • Privileged/hardware-bound operations are a legitimate reason to skip Dagger entirely. An ISO build needing root podman/buildah, bootc-image-builder --privileged, a specific runner label, and tens of GB of disk doesn't containerize cleanly inside Dagger's own container model. Driving that build directly via Taskfile + CLI tools, with the reasoning commented inline, is the right call — document the "why no Dagger here" rather than treating it as a gap to close later.

5. The cross-platform desktop exception

Dagger's execution model is Linux/OCI containers. There is no Windows-container execution mode in Dagger, and no native-macOS-app-building container mode either. Running the Dagger client from a Windows or Mac host (which does work) just dispatches into a Linux VM underneath (WSL2, Docker Desktop, etc.) — that doesn't change the target you're building for.

But check how much of this is actually a container-engine limitation before reaching for a native runner matrix — less than it looks like:

  • Windows: cross-compile from inside the Dagger Linux container first. Go cross-compiles to Windows trivially (GOOS=windows). Rust/Tauri cross-compiles via cargo-xwin (MSVC target) or mingw-w64 (GNU target) — Tauri has first-class support for this (tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc). Authenticode signing doesn't need signtool or a Windows host either: osslsigncode signs PE/MSI/CAB files from Linux, including against a real HSM-backed cert via PKCS11. Put together, this likely covers the entire Windows leg (build + sign) inside Dagger, with no native Windows runner at all — try this before assuming the matrix approach quintodrome's desktop.yml currently uses is necessary. The one soft spot is native Node module rebuilds (e.g. node-pty, for Hyper): cross-compiling node-gyp addons to Windows from Linux is less reliable per-dependency, so that leg specifically may still need a real Windows runner — check it, don't assume either way.
  • macOS: don't cross-compile, and this isn't a Dagger limitation to wait out. osxcross exists, but it works by extracting Apple's SDK, and Apple's EULA restricts that SDK to execution on Apple-branded hardware — not something to adopt as org policy. More fundamentally, codesigning and notarization need Apple's own tools (codesign, notarytool) talking to Apple's signing infrastructure with real credentials — that's a credentials/API constraint, not a sandboxing one, so no amount of container tech (present or future) removes the need for a real Mac for that step. Apple's container project (WWDC 2025, the subject of dagger/dagger#11205 on adding it as a Dagger runtime) doesn't change this either: it runs Linux containers natively on Apple Silicon (one lightweight VM per container, via Virtualization.framework), which would make running Dagger's existing Linux pipelines on a Mac host faster/more native — it is not a way to build or sign macOS apps inside a container. Keep the native macOS runner for this leg.
  • There's no "Dagger for Windows containers" to switch to, either. BuildKit (the engine Dagger is built on) added experimental Windows container support in v0.13, but Dagger hasn't built Windows-container execution on top of it, and general Windows-native CI platforms (Azure DevOps, TeamCity) aren't containerized-pipeline-as-code tools the way Dagger is — they're a different trade-off, not a drop-in replacement.

So: cross-platform desktop apps (Quintodrome, Hyper) remain the exception to "everything through Dagger" — but narrower than it first appears. Try to collapse the Windows leg into Dagger via cross-compilation before matrixing it onto a native windows-latest runner; keep the macOS leg (and whichever Node-native-module rebuilds don't cross-compile cleanly) on real runners, since that part isn't going away. Re-verify this against Dagger's current docs before trusting it indefinitely — this is a fast-moving project — but the macOS signing constraint specifically is structural, not a version gap.

  • Keep Dagger for whatever portion is containerizable — a Go/Node server-side build, lint, or test job stays thin-Gitea-Actions-calls-Dagger as normal, and per the above, try to pull the Windows build+sign leg in here too rather than defaulting it onto a native runner.
  • Whatever's left over (macOS always; Windows only for legs that genuinely don't cross-compile) gets a real OS matrix in Gitea Actions (macos-latest, macos-13 for Intel, and windows-latest only if needed) with native toolchain setup steps. This is allowed to be as "fat" as it needs to be, since there's no Dagger path to thin it against.
  • Mark this divergence explicitly in the workflow (a comment explaining why Dagger isn't used there) so it reads as a deliberate, bounded exception rather than unexplained drift from the Dagger-first rule.
  • Electron apps follow the same shape: macOS signing/notarization and whichever native per-arch module rebuilds don't cross-compile cleanly mean that part of packaging CI stays on real runners, even once migrated off GitHub Actions onto Forgejo.

6. Mark Taskfiles and Dagger files as part of Quine OS

Since apps and subsystems get forked/vendored/copied around (and some started life as upstream forks on their own hosting), every Taskfile.yml and every .dagger/main.go should carry a one-line header identifying it as a Quine OS file. This is purely an identifying tag — not a license or copyright notice — so it stays short and makes no legal claim.

Taskfile.yml, as the first line:

# Part of Quine Linux. See GUIDELINES.md in the quine-linux repo for conventions.
version: "3"

.dagger/main.go, as the leading comment above package main (ahead of any file-specific build-rationale comment already there):

// Part of Quine Linux. See GUIDELINES.md in the quine-linux repo for conventions.
package main

If a file already has a longer header comment explaining its own build rationale (e.g. swap-watchdog's .dagger/main.go), add this as the first line of that comment block rather than a second, separate block.