🏡 index : quine-global/guix-builds.git

// 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))
}