Cross-build images natively; drop store cache; multithreaded xz

This commit is contained in:
Philip Peterson 2026-09-13 14:15:29 -08:00
parent 10a722c6d6
commit a58911092f
3 changed files with 27 additions and 72 deletions

View file

@ -28,29 +28,8 @@ jobs:
curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=v0.20.6 BIN_DIR="$HOME/.local/bin" sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Restore Guix store cache
id: cache
uses: actions/cache/restore@v4
with:
path: cache
key: guix-store-${{ matrix.arch }}
- name: Prepare cache placeholder
run: |
mkdir -p cache
[ -s cache/guix-cache.tar.gz ] || : > cache/guix-cache.tar.gz
- name: Build image and refresh store cache
run: dagger call build --arch=${{ matrix.arch }} --cache=./cache/guix-cache.tar.gz export --path=./out
- name: Move refreshed cache into place
run: mv -f ./out/guix-cache.tar.gz ./cache/guix-cache.tar.gz
- name: Save Guix store cache
uses: actions/cache/save@v4
with:
path: cache
key: guix-store-${{ matrix.arch }}
- name: Build and export the image
run: dagger call build --arch=${{ matrix.arch }} export --path=./out
- name: Publish image as a release asset
uses: softprops/action-gh-release@v2

View file

@ -21,24 +21,27 @@ done
# trips over module resolution inside the container.
# x86_64 boots via BIOS (iso9660, grub-bootloader); aarch64 boots via UEFI
# (efi-raw, grub-efi). The installer hardcodes a BIOS bootloader unless built
# with #:efi-only? #t, so set that for aarch64.
# with #:efi-only? #t, so set that for aarch64. aarch64 is cross-built with
# --target from this native amd64 container.
if [ "${arch}" = "aarch64" ]; then
image_type="efi-raw"
ext="raw"
image_expr='((@ (gnu system install) make-installation-os) #:efi-only? #t)'
args=(--verbosity=3 --fallback --target=aarch64-linux-gnu -t "${image_type}" -e "${image_expr}")
else
image_type="iso9660"
ext="iso"
image_expr='(@ (gnu system install) installation-os)'
args=(--verbosity=3 --fallback -t "${image_type}" -e "${image_expr}")
fi
image="$(guix system image --verbosity=3 --fallback -t "${image_type}" -e "${image_expr}")"
image="$(guix system image "${args[@]}")"
echo "built image: ${image}"
mkdir -p /out
cp "${image}" "/out/guix-install-${arch}-linux.${ext}"
if [ "${arch}" = "aarch64" ]; then
# Raw EFI images exceed GitHub's 2 GiB release-asset limit, so compress them.
xz -6 "/out/guix-install-${arch}-linux.${ext}"
xz -T0 -6 "/out/guix-install-${arch}-linux.${ext}"
fi
ls -lh /out

65
main.go
View file

@ -1,17 +1,11 @@
// A Dagger module that builds a reproducible Guix installation image
// inside a container (x86_64 ISO by default; pass --arch for arm64).
// inside a native amd64 container, cross-building the target architecture
// with --target (no QEMU emulation).
//
// 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
//
// The exported directory contains the installation image plus a
// guix-cache.tar.gz store snapshot. Feed that snapshot back via --cache on
// the next run to skip recompiling derivations:
//
// dagger call build --arch=aarch64 --cache=./cache/guix-cache.tar.gz \
// export --path=./out
package main
@ -23,11 +17,11 @@ import (
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",
}
// We always build in a native amd64 container (GitHub runners are x86_64) and
// cross-build the image for the requested architecture, so the heavy
// compilation runs at native speed instead of under QEMU emulation.
const buildPlatform = dagger.Platform("linux/amd64")
const tarball = "guix-binary-" + guixVersion + ".x86_64-linux.tar.xz"
// setupScript runs after the tarball is extracted; it creates the build
// users and authorizes the substitute servers. It has no dependency on
@ -53,11 +47,9 @@ func normalizeArch(arch string) string {
return arch
}
// setup builds a container with Guix installed for the given architecture,
// restoring a previous store cache on top if provided (daemon not started).
func setup(arch string, cache *dagger.File) *dagger.Container {
tarball := "guix-binary-" + guixVersion + "." + arch + "-linux.tar.xz"
c := dag.Container(dagger.ContainerOpts{Platform: platformFor[arch]}).
// setup builds an amd64 container with Guix installed (daemon not started).
func setup() *dagger.Container {
return dag.Container(dagger.ContainerOpts{Platform: buildPlatform}).
From("debian:stable-slim").
WithEnvVariable("DEBIAN_FRONTEND", "noninteractive").
WithExec([]string{"apt-get", "update"}).
@ -67,51 +59,32 @@ func setup(arch string, cache *dagger.File) *dagger.Container {
"https://ftp.gnu.org/gnu/guix/" + tarball}).
WithExec([]string{"tar", "-C", "/", "--warning=no-timestamp", "-xf", "/tmp/" + tarball}).
WithExec([]string{"bash", "-c", setupScript})
if cache != nil {
c = c.WithMountedFile("/tmp/guix-cache.tar.gz", cache).
WithExec([]string{"bash", "-c",
"[ -s /tmp/guix-cache.tar.gz ] && tar -xzf /tmp/guix-cache.tar.gz -C / || true"})
}
return c
}
// buildContainer runs the full pull + image build, then snapshots the Guix
// store (plus daemon database) into /out/guix-cache.tar.gz so it can be
// reused on the next run. It returns the container with both artifacts in /out.
func buildContainer(arch string, cache *dagger.File) *dagger.Container {
// buildContainer runs the full pull + image build.
func buildContainer(arch string) *dagger.Container {
src := dag.CurrentModule().Source()
return setup(arch, cache).
return setup().
WithMountedDirectory("/workspace", src).
WithExec([]string{"bash", "/workspace/pull.sh"},
dagger.ContainerWithExecOpts{InsecureRootCapabilities: true}).
WithExec([]string{"bash", "/workspace/image.sh", arch},
dagger.ContainerWithExecOpts{InsecureRootCapabilities: true}).
WithExec([]string{"bash", "-c",
"tar -czf /out/guix-cache.tar.gz -C / gnu/store var/guix/db var/guix/profiles root/.config/guix"})
dagger.ContainerWithExecOpts{InsecureRootCapabilities: true})
}
// Build assembles the Guix installation image and a snapshot of the Guix
// store cache, and returns them as a directory containing:
//
// guix-install-<arch>-linux.<ext> the installation image
// guix-cache.tar.gz store snapshot for reuse via --cache
// 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,
// Optional store cache from a previous build to reuse derivations.
// +optional
cache *dagger.File,
) *dagger.Directory {
arch = normalizeArch(arch)
return buildContainer(arch, cache).Directory("/out")
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), nil)
func (m *GuixIso) Debug() *dagger.Container {
return setup()
}