guix-builds/main.go
2026-09-16 03:35:43 -08:00

94 lines
3.2 KiB
Go

// A Dagger module that builds a reproducible Guix installation image
// inside a container (x86_64 ISO by default; pass --arch for arm64).
//
// Edit channels.scm to change which Guix commit is used, then run:
//
// dagger call build export --path=./out
// dagger call build --arch=aarch64 export --path=./out
package main
import (
"context"
"dagger/guix-iso/internal/dagger"
)
const guixVersion = "1.5.0"
// platformFor maps a Guix system string to the Dagger container platform.
var platformFor = map[string]dagger.Platform{
"x86_64": "linux/amd64",
"aarch64": "linux/arm64",
}
// setupScript runs after the tarball is extracted; it creates the build
// users and authorizes the substitute servers. It has no dependency on
// channels.scm, so Dagger caches it as a layer.
const setupScript = `
set -euo pipefail
groupadd --system guixbuild
for i in $(seq -w 1 10); do
useradd -g guixbuild -G guixbuild -d /var/empty -s /bin/false \
-c "Guix build user $i" --system "guixbuilder$i"
done
GUIX=/var/guix/profiles/per-user/root/current-guix/bin/guix
"$GUIX" archive --authorize < /var/guix/profiles/per-user/root/current-guix/share/guix/bordeaux.guix.gnu.org.pub
"$GUIX" archive --authorize < /var/guix/profiles/per-user/root/current-guix/share/guix/ci.guix.gnu.org.pub
`
type GuixIso struct{}
func normalizeArch(arch string) string {
if arch == "" {
return "x86_64"
}
return arch
}
// setup builds a container with Guix installed for the given architecture
// (daemon not started).
func setup(arch string) *dagger.Container {
tarball := "guix-binary-" + guixVersion + "." + arch + "-linux.tar.xz"
return dag.Container(dagger.ContainerOpts{Platform: platformFor[arch]}).
From("debian:bookworm-slim").
WithEnvVariable("DEBIAN_FRONTEND", "noninteractive").
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "--no-install-recommends",
"curl", "ca-certificates", "xz-utils", "gnupg", "passwd", "netbase", "git", "python3"}).
WithExec([]string{"curl", "-fsSL", "-o", "/tmp/" + tarball,
"https://ftp.gnu.org/gnu/guix/" + tarball}).
WithExec([]string{"tar", "-C", "/", "--warning=no-timestamp", "-xf", "/tmp/" + tarball}).
WithExec([]string{"bash", "-c", setupScript})
}
// buildContainer runs the full pull + image build.
func buildContainer(arch string) *dagger.Container {
src := dag.CurrentModule().Source()
return setup(arch).
WithMountedDirectory("/workspace", src).
WithExec([]string{"bash", "/workspace/pull.sh"},
dagger.ContainerWithExecOpts{InsecureRootCapabilities: true}).
WithExec([]string{"bash", "/workspace/image.sh", arch},
dagger.ContainerWithExecOpts{InsecureRootCapabilities: true})
}
// Build assembles the Guix installation image and returns it as a directory
// containing guix-install-<arch>-linux.<ext>.
func (m *GuixIso) Build(
ctx context.Context,
// Architecture of the image: "x86_64" or "aarch64".
// +optional
arch string,
) *dagger.Directory {
arch = normalizeArch(arch)
return buildContainer(arch).Directory("/out")
}
// Debug returns the setup container for inspection without running the build.
func (m *GuixIso) Debug(
// +optional
arch string,
) *dagger.Container {
return setup(normalizeArch(arch))
}