Moved from the now-abandoned quine-os repo. Documents the Taskfile/ Dagger/Nix conventions already in use across swap-watchdog, quine-linux, quintodrome, and hyper, plus a simple identifying header for Taskfiles and Dagger files, and a researched note on how much of the cross-platform desktop exception can actually be collapsed into Dagger via cross- compilation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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:
- Nix, as a hermetic dependency-provisioning layer inside the Dagger
container — not necessarily
nix build/flakes, butnix-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 suppliesgtk3-devel/dbus-devel-equivalent packages inside the container, the host's own Go toolchain is mounted in to avoid version drift against go.mod, andpatchelfretargets 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. - If Nix genuinely can't cover it, the next-most-hermetic sandbox —
e.g. a
flatpak-buildersandbox 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. - 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 viacargo-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 needsigntoolor a Windows host either:osslsigncodesigns 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'sdesktop.ymlcurrently 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.
osxcrossexists, 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'scontainerproject (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, viaVirtualization.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-13for Intel, andwindows-latestonly 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 OS. 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 OS. 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.