Skip to main content

max / alloy

alloy-nvidia: a derived aarch64 image carrying the open NVIDIA module Mechanism decided in alloy 2a382a75 (Max chose option c). The shipped Containerfile is not touched. Derived rather than a build ARG, and the reason is structural. The kernel and the module come from the same digest by construction, so the kernel-lock failure the ARG option had is impossible here: there is no build-time assertion to write, and no way for refresh-base-digests.sh to ship a machine that boots with no GPU and no signal, because moving the base digest cannot reach an image built from the old one. It also keeps the shipped image out of it -- an aarch64 NVIDIA card is not a role almost all Alloy users have, which under wiki alloy-selection-discipline means the shipped image should not serve one by default. The digest is the input, so build-nvidia.sh resolves it and prints it rather than deriving from a tag: two runs a week apart from a tag would produce modules for different kernels while claiming the same provenance, which is the drift the Containerfile's base pins exist to stop. The kernel is read out of /usr/lib/modules rather than written down. There is exactly one and it came with the digest, so a number spelled by hand is the same drift from the other end. NOT kmod-nvidia, which exists prebuilt for aarch64 and is built against RPM Fusion's kernel rather than the image's. Installing it would put a module in the image whose vermagic does not match the kernel beside it -- the exact failure this shape exists to make impossible -- and it would look installed. Three things the build asserts rather than assumes, each because its absence is invisible until a machine boots with no display: that a module was produced, that its vermagic is this image's kernel, and that its licence is Dual MIT/GPL. The last is what tells the open module from the proprietary one, and Blackwell GB203 does not run on the proprietary one. akmods at build time, not on the machine: a bootc filesystem is immutable and its kernel only changes by taking a new image, so there is no later moment for the module to be built in. dracut and depmod follow, or the module ships present and unloadable early enough to drive the console. nouveau is blacklisted in the image rather than by a kernel argument, so the fact travels with the image that needs it.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 23:42 UTC
Signed with PGP, not checked
Commit: fd7cb4ef4a52d48193995cc0fd2a3dec0d73dbea
Parent: 93d550a
3 files changed, +234 insertions, -42 deletions
M Cargo.lock +42 -42
@@ -2643,21 +2643,57 @@
2643 2643 "winnow",
2644 2644 ]
2645 2645
2646 + [[patch.unused]]
2647 + name = "docengine"
2648 + version = "0.7.0"
2649 +
2650 + [[patch.unused]]
2651 + name = "quasi-axum"
2652 + version = "0.79.0"
2653 +
2654 + [[patch.unused]]
2655 + name = "quasi-basics"
2656 + version = "0.79.0"
2657 +
2658 + [[patch.unused]]
2659 + name = "quasi-http"
2660 + version = "0.79.0"
2661 +
2662 + [[patch.unused]]
2663 + name = "quasi-immediate"
2664 + version = "0.79.0"
2665 +
2666 + [[patch.unused]]
2667 + name = "quasi-notifs"
2668 + version = "0.79.0"
2669 +
2670 + [[patch.unused]]
2671 + name = "quasi-router"
2672 + version = "0.79.0"
2673 +
2674 + [[patch.unused]]
2675 + name = "quasi-store"
2676 + version = "0.1.0"
2677 +
2678 + [[patch.unused]]
2679 + name = "quasi-tauri"
2680 + version = "0.79.0"
2681 +
2682 + [[patch.unused]]
2683 + name = "quasi-webview"
2684 + version = "0.79.0"
2685 +
2646 2686 [[patch.unused]]
2647 2687 name = "synckit-client"
2648 - version = "0.9.1"
2688 + version = "0.10.0"
2649 2689
2650 2690 [[patch.unused]]
2651 2691 name = "synckit-config"
2652 2692 version = "0.2.0"
2653 2693
2654 - [[patch.unused]]
2655 - name = "docengine"
2656 - version = "0.7.0"
2657 -
2658 2694 [[patch.unused]]
2659 2695 name = "quasi-type"
2660 - version = "0.1.0"
2696 + version = "0.1.3"
2661 2697
2662 2698 [[patch.unused]]
2663 2699 name = "kberg"
@@ -2674,39 +2710,3 @@
2674 2710 [[patch.unused]]
2675 2711 name = "tagtree"
2676 2712 version = "0.4.1"
2677 -
2678 - [[patch.unused]]
2679 - name = "quasi-axum"
2680 - version = "0.61.1"
2681 -
2682 - [[patch.unused]]
2683 - name = "quasi-basics"
2684 - version = "0.61.1"
2685 -
2686 - [[patch.unused]]
2687 - name = "quasi-http"
2688 - version = "0.61.1"
2689 -
2690 - [[patch.unused]]
2691 - name = "quasi-immediate"
2692 - version = "0.61.1"
2693 -
2694 - [[patch.unused]]
2695 - name = "quasi-notifs"
2696 - version = "0.61.1"
2697 -
2698 - [[patch.unused]]
2699 - name = "quasi-router"
2700 - version = "0.61.1"
2701 -
2702 - [[patch.unused]]
2703 - name = "quasi-store"
2704 - version = "0.1.0"
2705 -
2706 - [[patch.unused]]
2707 - name = "quasi-tauri"
2708 - version = "0.61.1"
2709 -
2710 - [[patch.unused]]
2711 - name = "quasi-webview"
2712 - version = "0.61.1"
@@ -1,0 +1,101 @@
1 + # alloy-nvidia — Alloy plus the open NVIDIA kernel module, aarch64.
2 + #
3 + # Derived, not a build ARG on the shipped Containerfile. Max chose option (c)
4 + # on alloy `2a382a75` (2026-08-30) and the reason is structural rather than
5 + # stylistic: the kernel and the module come from the same digest by
6 + # construction, so the kernel-lock failure the ARG option had is impossible
7 + # here. There is no build-time assertion to write and no way for
8 + # build/refresh-base-digests.sh to ship a machine that boots with no GPU and no
9 + # signal, because moving the base digest cannot reach an image built from the
10 + # old one.
11 + #
12 + # It also keeps the shipped image out of it. An aarch64 NVIDIA card is not a
13 + # role almost all Alloy users have, and wiki `alloy-selection-discipline` says
14 + # the shipped image should not serve one by default.
15 + #
16 + # WHAT ASTRA NEEDS THIS FOR: an RTX 5070 Ti, Blackwell GB203, which the
17 + # proprietary module does not drive. `akmod-nvidia` on aarch64 IS the open
18 + # build -- `modinfo` reports `license: Dual MIT/GPL`, where the proprietary one
19 + # reports `NVIDIA`. There is no separate `akmod-nvidia-open` for this arch, so
20 + # do not go looking for one.
21 + #
22 + # Usage: build/build-nvidia.sh. It resolves the digest, so this file names none.
23 +
24 + ARG ALLOY_DIGEST
25 + FROM localhost/alloy@${ALLOY_DIGEST}
26 +
27 + # RPM Fusion nonfree, which is where the akmod lives. `--nogpgcheck` on the
28 + # release package alone and nothing else: it is the package that installs the
29 + # key every later transaction is then checked against, which is the one
30 + # bootstrap that cannot itself be checked.
31 + #
32 + # `$releasever` resolves to 43 without help, because the base image already
33 + # pins it in /etc/dnf/vars/releasever -- Alloy re-brands os-release, so dnf
34 + # would otherwise expand it to Alloy's own VERSION_ID and ask for a nonexistent
35 + # Fedora.
36 + RUN dnf install -y --nogpgcheck \
37 + "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm" \
38 + "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm" \
39 + && dnf clean all
40 +
41 + # The akmod source and the userspace it pairs with.
42 + #
43 + # NOT `kmod-nvidia`, which exists prebuilt in the repo and is built against RPM
44 + # Fusion's own kernel rather than this image's. Installing it would put a
45 + # module in the image whose vermagic does not match the kernel beside it, which
46 + # is the failure this whole derived shape exists to make impossible -- and it
47 + # would look installed.
48 + #
49 + # Weak deps off, as everywhere else in this project.
50 + RUN dnf install -y --setopt=install_weak_deps=False \
51 + akmod-nvidia \
52 + xorg-x11-drv-nvidia-cuda \
53 + && dnf clean all
54 +
55 + # Build the module against THIS image's kernel, now, at build time.
56 + #
57 + # akmods normally runs on the machine after a kernel update, which a bootc
58 + # image does not do: the filesystem is immutable and the kernel only changes by
59 + # taking a new image. So the build happens here, against the one kernel this
60 + # image has, and the result ships inside it.
61 + #
62 + # The kernel is read out of /usr/lib/modules rather than passed in. There is
63 + # exactly one, it came with the digest above, and a number written here by hand
64 + # is the drift this file exists to avoid.
65 + RUN set -eux; \
66 + kernel="$(ls /usr/lib/modules | head -1)"; \
67 + akmods --force --kernels "$kernel"; \
68 + # The check, and it is the done condition. A build that produced nothing,
69 + # or produced a module for a different kernel, fails here rather than at
70 + # boot on a machine with no display.
71 + module="/usr/lib/modules/$kernel/extra/nvidia/nvidia.ko.xz"; \
72 + [ -f "$module" ] || module="/usr/lib/modules/$kernel/extra/nvidia/nvidia.ko"; \
73 + test -f "$module"; \
74 + vermagic="$(modinfo -F vermagic "$module" | awk '{print $1}')"; \
75 + test "$vermagic" = "$kernel" \
76 + || { echo "vermagic $vermagic is not this image's kernel $kernel" >&2; exit 1; }; \
77 + # Open, not proprietary. Blackwell GB203 needs the open module, and the two
78 + # are told apart by their licence: the open one is Dual MIT/GPL.
79 + licence="$(modinfo -F license "$module")"; \
80 + case "$licence" in \
81 + *MIT*) : ;; \
82 + *) echo "nvidia.ko reports licence '$licence'; expected the open Dual MIT/GPL build" >&2; exit 1 ;; \
83 + esac; \
84 + echo "built $module: vermagic $vermagic, licence $licence"
85 +
86 + # The initramfs has to carry it, or the module is present and not loaded early
87 + # enough to drive the console.
88 + RUN set -eux; \
89 + kernel="$(ls /usr/lib/modules | head -1)"; \
90 + depmod -a "$kernel"; \
91 + dracut --force --no-hostonly \
92 + --kver "$kernel" \
93 + "/usr/lib/modules/$kernel/initramfs.img"
94 +
95 + # `nouveau` and the nvidia module cannot both drive the card, and nouveau wins
96 + # by loading first. Blacklisted here rather than as a kernel argument so the
97 + # fact travels with the image that needs it.
98 + RUN printf 'blacklist nouveau\noptions nouveau modeset=0\n' \
99 + > /etc/modprobe.d/alloy-nvidia-blacklist.conf
100 +
101 + RUN bootc container lint
@@ -1,0 +1,91 @@
1 + #!/usr/bin/env bash
2 + #
3 + # build-nvidia.sh — build alloy-nvidia, Alloy plus the open NVIDIA module.
4 + #
5 + # A thin derived image over an Alloy that already exists. It does not build
6 + # Alloy: the whole point of the derived shape (alloy `2a382a75`, option c) is
7 + # that the module is compiled against the kernel of one specific Alloy image,
8 + # so the input is a digest and this script's job is to resolve one and hold it.
9 + #
10 + # aarch64 only, and that is not a limitation to lift. The card this exists for
11 + # is in astra; CLAUDE.md forbids cross-compiling, and an x86_64 machine with an
12 + # NVIDIA card would want the proprietary module rather than the open one
13 + # Blackwell requires.
14 + #
15 + # Rootful, for build-image.sh's reason: it shares one container store with the
16 + # Alloy build, so the digest below resolves without a rootless->rootful copy.
17 + #
18 + # Usage:
19 + # build/build-nvidia.sh # derive from localhost/alloy:local
20 + # build/build-nvidia.sh --digest sha256:... # derive from a named digest
21 + # build/build-nvidia.sh --tag alloy-nvidia:aarch64
22 +
23 + set -euo pipefail
24 +
25 + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
26 +
27 + # priv / privc. See build/privilege.sh.
28 + # shellcheck source=build/privilege.sh
29 + . "$REPO_ROOT/build/privilege.sh"
30 +
31 + BASE="localhost/alloy:local"
32 + DIGEST=""
33 + TAG="localhost/alloy-nvidia:aarch64"
34 +
35 + while [ $# -gt 0 ]; do
36 + case "$1" in
37 + --digest) DIGEST="${2:?--digest needs a sha256:... value}"; shift 2 ;;
38 + --base) BASE="${2:?--base needs an image}"; shift 2 ;;
39 + --tag) TAG="${2:?--tag needs a name}"; shift 2 ;;
40 + -h|--help) sed -n '2,25p' "$0"; exit 0 ;;
41 + *) echo "error: unknown argument $1" >&2; exit 2 ;;
42 + esac
43 + done
44 +
45 + arch="$(uname -m)"
46 + if [ "$arch" != "aarch64" ]; then
47 + echo "error: alloy-nvidia is aarch64 only; this is $arch." >&2
48 + echo " The card it exists for is astra's. Builds are native per" >&2
49 + echo " architecture (CLAUDE.md), so build it there." >&2
50 + exit 1
51 + fi
52 +
53 + # The digest is the input, so it is resolved and printed rather than assumed.
54 + # Deriving from a tag would mean two runs a week apart produced modules for
55 + # different kernels while claiming the same provenance, which is the drift the
56 + # base pins in the Containerfile exist to stop.
57 + if [ -z "$DIGEST" ]; then
58 + DIGEST="$(priv podman image inspect "$BASE" --format '{{.Digest}}' 2>/dev/null || true)"
59 + [ -n "$DIGEST" ] || {
60 + echo "error: no image $BASE to derive from." >&2
61 + echo " Build Alloy first: build/build-image.sh --skip-bib" >&2
62 + exit 1
63 + }
64 + fi
65 +
66 + kernel="$(priv podman run --rm "$BASE" ls /usr/lib/modules | head -1)"
67 + echo "deriving from $BASE"
68 + echo " digest: $DIGEST"
69 + echo " kernel: $kernel"
70 + echo
71 +
72 + priv podman build \
73 + --build-arg "ALLOY_DIGEST=$DIGEST" \
74 + -f "$REPO_ROOT/build/Containerfile.nvidia" \
75 + -t "$TAG" \
76 + "$REPO_ROOT"
77 +
78 + echo
79 + echo "built $TAG"
80 + # Read back out of the finished image rather than trusting the build log: the
81 + # Containerfile asserts this too, and a second reading from the outside is what
82 + # a person checking the machine would do.
83 + priv podman run --rm "$TAG" sh -c '
84 + kernel="$(ls /usr/lib/modules | head -1)"
85 + module="/usr/lib/modules/$kernel/extra/nvidia/nvidia.ko.xz"
86 + [ -f "$module" ] || module="/usr/lib/modules/$kernel/extra/nvidia/nvidia.ko"
87 + echo " kernel: $kernel"
88 + echo " vermagic: $(modinfo -F vermagic "$module")"
89 + echo " licence: $(modinfo -F license "$module")"
90 + echo " version: $(modinfo -F version "$module")"
91 + '