# Quine OS: build & CI guidelinesThese guidelines describe how build tooling and CI should be structuredacross Quine OS's apps and subsystems. They're drawn from, and should stayconsistent with, patterns already established in swap-watchdog, quine-linux,quintodrome, and hyper.## 1. Every app gets a TaskfileEvery 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 buildlogic lives (see ยง3). Dev-only apps with no real build step just wrap the runcommand directly (e.g. a Flutter app's Taskfile can be just`flutter run -d linux` โ nothing more needed).## 2. Build tooling preference orderWhen an app needs a real build step, prefer in this order:1. **Nix, as a hermetic dependency-provisioning layer inside the Daggercontainer** โ not necessarily `nix build`/flakes, but `nix-shell -p<pkgs>` to pull build-time headers/toolchains into an ephemeralcontainer, while the actual compile still runs the language's nativebuilder (`go build`, `cargo build`, etc.). For example: Nix supplies`gtk3-devel`/`dbus-devel`-equivalent packages inside the container, thehost's own Go toolchain is mounted in to avoid version drift againstgo.mod, and `patchelf` retargets the ELF interpreter so the binary stilllinks against the host's real runtime libs. Nothing gets installed onthe 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'sheader/binding generation isn't well supported in nixpkgs. That's alegitimate, documented exception, not drift.3. **Never**: reaching for `rpm-ostree install`, `dnf`, or any host/imagemutation just to satisfy a build-time dependency. If a dependencygenuinely belongs in the OS image (not just at build time), that's adeliberate rpm-ostree layering decision made separately, not a buildscript shortcut.## 3. CI: Dagger owns the logic, Gitea Actions stays thinOne Forgejo instance, Gitea Actions as the trigger, Dagger as the engine.The workflow YAML should do as little as possible:```yaml- 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 isdoing anything beyond checkout, installing the Dagger CLI, and one`dagger call`, that's a signal the logic should move into the Dagger moduleinstead.## 4. Apps vs. OS subsystems: different rigor, same tools- **Apps**: full Taskfile, Dagger-driven CI as above, real test coverageexpected โ they're independently buildable and testable, so there's noexcuse not to.- **Larger OS subsystems** (not independently buildable/testable the way anapp is โ e.g. quine-linux itself): still require a Taskfile, and CIthrough *either* Dagger *or* a Taskfile-driven Gitea Actions job โ notmandatory Dagger. Some kind of test/smoke-check should exist, but it'sscoped to what's actually checkable (`bootc status`, image boots, aprecondition script), not full unit/integration coverage.- **Privileged/hardware-bound operations are a legitimate reason to skipDagger entirely.** An ISO build needing root podman/buildah,`bootc-image-builder --privileged`, a specific runner label, and tens ofGB of disk doesn't containerize cleanly inside Dagger's own containermodel. Driving that build directly via Taskfile + CLI tools, with thereasoning commented inline, is the right call โ document the "why noDagger here" rather than treating it as a gap to close later.## 5. The cross-platform desktop exceptionDagger's execution model is Linux/OCI containers. There is no Windows-containerexecution mode in Dagger, and no native-macOS-app-building container modeeither. Running the Dagger *client* from a Windows or Mac host (which doeswork) 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 limitationbefore 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/Tauricross-compiles via [`cargo-xwin`](https://github.com/Jake-Shadle/xwin)(MSVC target) or mingw-w64 (GNU target) โ Tauri has first-class supportfor this (`tauri build --runner cargo-xwin --targetx86_64-pc-windows-msvc`). Authenticode signing doesn't need `signtool` ora Windows host either: `osslsigncode` signs PE/MSI/CAB files from Linux,including against a real HSM-backed cert via PKCS11. Put together, thislikely covers the *entire* Windows leg (build + sign) inside Dagger, withno native Windows runner at all โ try this before assuming the matrixapproach quintodrome's `desktop.yml` currently uses is necessary. The onesoft spot is native Node module rebuilds (e.g. node-pty, for Hyper):cross-compiling node-gyp addons to Windows from Linux is less reliableper-dependency, so that leg specifically may still need a real Windowsrunner โ check it, don't assume either way.- **macOS: don't cross-compile, and this isn't a Dagger limitation to waitout.** `osxcross` exists, but it works by extracting Apple's SDK, andApple's EULA restricts that SDK to execution on Apple-branded hardware โnot something to adopt as org policy. More fundamentally, codesigning andnotarization need Apple's own tools (`codesign`, `notarytool`) talking toApple's signing infrastructure with real credentials โ that's acredentials/API constraint, not a sandboxing one, so no amount ofcontainer tech (present or future) removes the need for a real Mac forthat step. Apple's `container` project (WWDC 2025, the subject of[dagger/dagger#11205](https://github.com/dagger/dagger/issues/11205) onadding it as a Dagger runtime) doesn't change this either: it runs*Linux* containers natively on Apple Silicon (one lightweight VM percontainer, via `Virtualization.framework`), which would make runningDagger's existing Linux pipelines on a Mac host faster/more native โ itis not a way to build or sign macOS apps inside a container. Keep thenative 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* Windowscontainer support in v0.13, but Dagger hasn't built Windows-containerexecution on top of it, and general Windows-native CI platforms (AzureDevOps, TeamCity) aren't containerized-pipeline-as-code tools the wayDagger 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 tocollapse the Windows leg into Dagger via cross-compilation before matrixingit onto a native `windows-latest` runner; keep the macOS leg (and whicheverNode-native-module rebuilds don't cross-compile cleanly) on real runners,since that part isn't going away. Re-verify this against Dagger's currentdocs before trusting it indefinitely โ this is a fast-moving project โ butthe macOS signing constraint specifically is structural, not a version gap.- Keep Dagger for whatever portion *is* containerizable โ a Go/Nodeserver-side build, lint, or test job stays thin-Gitea-Actions-calls-Daggeras normal, and per the above, try to pull the Windows build+sign leg inhere too rather than defaulting it onto a native runner.- Whatever's left over (macOS always; Windows only for legs that genuinelydon't cross-compile) gets a real OS matrix in Gitea Actions (`macos-latest`,`macos-13` for Intel, and `windows-latest` only if needed) with nativetoolchain 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 whyDagger isn't used there) so it reads as a deliberate, bounded exceptionrather than unexplained drift from the Dagger-first rule.- Electron apps follow the same shape: macOS signing/notarization andwhichever native per-arch module rebuilds don't cross-compile cleanly meanthat part of packaging CI stays on real runners, even once migrated offGitHub Actions onto Forgejo.## 6. Mark Taskfiles and Dagger files as part of Quine OSSince apps and subsystems get forked/vendored/copied around (and somestarted life as upstream forks on their own hosting), every `Taskfile.yml`and every `.dagger/main.go` should carry a one-line header identifying it asa Quine OS file. This is purely an identifying tag โ not a license orcopyright notice โ so it stays short and makes no legal claim.`Taskfile.yml`, as the first line:```yaml# 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 ofany file-specific build-rationale comment already there):```go// 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 buildrationale (e.g. swap-watchdog's `.dagger/main.go`), add this as the firstline of that comment block rather than a second, separate block.