Build swap-watchdog binaries via a Nix-provisioned Dagger pipeline

Replaces the generated dagger scaffold with a real BuildLinux function
that compiles swap-watchdog, force-quit-gui, and kdotool (not packaged
for Fedora, so built from source via cargo) inside an ephemeral
nixos/nix container -- no gtk3-devel/dbus-devel/rust toolchain needs
installing on the host. The host's own Go toolchain is mounted in so
the build matches go.mod's required version exactly.

Nix's glibc uses a non-FHS dynamic linker path that doesn't exist
outside the container, so the built binaries are patched with
patchelf to point at the host's real ld.so; the stale nix-store
RUNPATH entries are harmless since ld.so just skips nonexistent
directories and falls through to the host's normal library search
path, where the real libgtk-3/libdbus-1/etc already live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-10-06 06:54:49 -08:00
parent e0daa6b1e5
commit 141d4f7690

View file

@ -1,37 +1,81 @@
// A generated module for SwapWatchdog functions
// Builds swap-watchdog's binaries (including kdotool, which isn't packaged
// for Fedora) without installing any build-time dev headers on the host.
//
// This module has been generated via dagger init and serves as a reference to
// basic module structure as you get started with Dagger.
// gtk3-devel and dbus-devel (needed to compile force-quit-gui and kdotool,
// respectively) are fetched inside an ephemeral Nix-provisioned container,
// never on the host. This only works because the corresponding *runtime*
// shared libraries (libgtk-3, libdbus-1, ...) are already present on any
// normal Linux desktop -- so the binaries built here link against the
// host's own runtime libs the moment they're copied out. Nothing is
// bundled or vendored.
//
// Two functions have been pre-created. You can modify, delete, or add to them,
// as needed. They demonstrate usage of arguments and return types using simple
// echo and grep commands. The functions can be called from the dagger CLI or
// from one of the SDKs.
//
// The first line in this comment block is a short description line and the
// rest is a long description with more detail on the module's purpose or usage,
// if appropriate. All modules should have a short description.
// The Go toolchain itself is passed in from the host (see install.sh) rather
// than pulled from nixpkgs, so the build always uses the exact same `go`
// version the host's go.mod requires, with no risk of nixpkgs happening to
// carry an older/newer one.
package main
import (
"context"
"dagger/swap-watchdog/internal/dagger"
)
type SwapWatchdog struct{}
// Returns a container that echoes whatever string argument is provided
func (m *SwapWatchdog) ContainerEcho(stringArg string) *dagger.Container {
return dag.Container().From("alpine:latest").WithExec([]string{"echo", stringArg})
}
// BuildLinux compiles swap-watchdog, force-quit-gui, and kdotool and returns
// a Directory containing all three, ready to export into bin/.
func (m *SwapWatchdog) BuildLinux(ctx context.Context,
// +defaultPath="."
src *dagger.Directory,
// Host Go toolchain root (i.e. `go env GOROOT`), mounted in so the
// container builds with the exact same `go` version go.mod requires.
goToolchain *dagger.Directory,
) *dagger.Directory {
nixPkgs := "gtk3 dbus pkg-config gcc cargo rustc git patchelf"
// Returns lines that match a pattern in the files of the provided Directory
func (m *SwapWatchdog) GrepDir(ctx context.Context, directoryArg *dagger.Directory, pattern string) (string, error) {
return dag.Container().
From("alpine:latest").
WithMountedDirectory("/mnt", directoryArg).
WithWorkdir("/mnt").
WithExec([]string{"grep", "-R", pattern, "."}).
Stdout(ctx)
// nixpkgs' glibc uses a non-FHS dynamic linker path
// (/nix/store/.../ld-linux-x86-64.so.2), which doesn't exist outside the
// build container. The runtime libs themselves resolve fine via the
// host's normal SONAME search path (ldd confirms this against the
// host's own libgtk-3/libdbus-1/...) -- only the ELF interpreter is
// wrong, so patchelf is enough; nothing needs bundling.
const hostInterp = "/lib64/ld-linux-x86-64.so.2"
base := dag.Container().
From("nixos/nix:latest").
WithDirectory("/usr/local/go", goToolchain).
WithEnvVariable("PATH", "/usr/local/go/bin:/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/usr/bin:/bin").
WithEnvVariable("GOROOT", "/usr/local/go").
WithMountedCache("/root/.cache/go-build", dag.CacheVolume("swap-watchdog-go-build")).
WithMountedCache("/root/.cargo/registry", dag.CacheVolume("swap-watchdog-cargo-registry"))
goBuilt := base.
WithDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"sh", "-c", `set -e
nix-shell -p ` + nixPkgs + ` --run '
set -e
mkdir -p /out
CGO_ENABLED=1 go build -o /out/swap-watchdog ./cmd/swap-watchdog
CGO_ENABLED=1 go build -o /out/force-quit-gui ./cmd/force-quit-gui
patchelf --set-interpreter ` + hostInterp + ` /out/force-quit-gui
'`})
kdotoolBuilt := base.
WithExec([]string{"sh", "-c", `set -e
nix-shell -p ` + nixPkgs + ` --run '
set -e
git clone --depth 1 https://github.com/jinliu/kdotool /tmp/kdotool
cd /tmp/kdotool
cargo build --release
mkdir -p /out
cp target/release/kdotool /out/kdotool
patchelf --set-interpreter ` + hostInterp + ` /out/kdotool
'`})
return dag.Directory().
WithFile("swap-watchdog", goBuilt.File("/out/swap-watchdog")).
WithFile("force-quit-gui", goBuilt.File("/out/force-quit-gui")).
WithFile("kdotool", kdotoolBuilt.File("/out/kdotool"))
}