Skip to main content

max / alloy

image: build the console into the image The Alloy console was not in the image at all. A distro whose headline surface is its console shipped without one, and the gap was invisible because the Containerfile's own header asserted the policy that caused it ("single-stage, no cargo-built binaries") rather than recording a decision. Adds a `rust-build` stage on fedora:43 — the same base as the runtime stage, so the toolchain and the glibc the console links against are the ones it runs on. Not built on fw13 and copied in: fw13 is Pop!_OS, a different libc, and builds are native per architecture. The dependency graph builds against a stub main before the real source lands, so a console edit rebuilds one crate rather than the whole graph. Measured: 9.5s instead of a full compile. The COPY sits last before the lint, so a console-only rebuild reuses every package and config layer. Adds .containerignore, which did not exist. Every build was tarring output/ — a 3.5GB ISO — into its own build context before reading the first instruction. Two doc corrections found while doing this. IMAGE.md claimed the rust-build stage already existed and held wl-screenrec; it did not, since wl-screenrec was deferred, so there was no stage at all. STACK.md defers wl-screenrec until "a second cargo-built binary shares the stage's cost" — now half true, since the toolchain is paid for but clang and the ffmpeg headers are not. Re-cost it rather than assume it, next time it comes up. Verified: image builds, bootc container lint passes, /usr/bin/alloy is 1.9M and links only libc/libm/libgcc. ISO built and written to USB, readback sha256 matches.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-19 23:43 UTC
Signed with PGP, not checked
Commit: 9bee8a599875985ccd5db17af417e9607e20c172
Parent: e16740f
4 files changed, +85 insertions, -8 deletions
M Containerfile +57 -6
@@ -8,9 +8,43 @@
8 8 # with STACK.md), then config tree (changes most often, so lives at
9 9 # the tail).
10 10 #
11 - # Single-stage. The image carries no cargo-built binaries: everything
12 - # ships from a repo. Screen recording is the one gap (see the deferral
13 - # note in docs/STACK.md#screen-recorder).
11 + # Two stages. Everything the image runs ships from a repo except the
12 + # Alloy console, which is this project's own binary and has no repo to
13 + # ship from — the one case docs/IMAGE.md's stage policy allows. Screen
14 + # recording is still a gap (docs/STACK.md#screen-recorder).
15 +
16 + # =====================================================================
17 + # Build stage — the Alloy console.
18 + # =====================================================================
19 + # Fedora 43 rather than a rust: image or the host, so the toolchain and
20 + # the glibc the console links against are the ones it will run on. This
21 + # is also why the console is not simply built on fw13 and copied in:
22 + # fw13 is Pop!_OS 24.04, a different libc, and per CLAUDE.md builds are
23 + # native per architecture rather than cross-compiled.
24 + #
25 + # Cargo.toml pins edition 2024 and rust-version 1.86; Fedora 43 clears
26 + # both. `--locked` so the image builds the dependency graph the repo
27 + # committed rather than whatever resolves that day.
28 + FROM quay.io/fedora/fedora:43 AS rust-build
29 +
30 + RUN dnf install -y cargo rust && dnf clean all
31 +
32 + WORKDIR /src
33 +
34 + # The dependency graph first, against a stub main. Without this split
35 + # every console edit re-downloads and rebuilds every crate underneath it,
36 + # and the console is the part of this image that changes most often.
37 + COPY Cargo.toml Cargo.lock ./
38 + COPY crates/alloy/Cargo.toml crates/alloy/Cargo.toml
39 + RUN mkdir -p crates/alloy/src \
40 + && echo 'fn main() {}' > crates/alloy/src/main.rs \
41 + && cargo build --release --locked \
42 + && rm -f target/release/alloy target/release/deps/alloy-*
43 +
44 + # Then the real source. The removals above are what make cargo rebuild
45 + # the binary rather than find the stub's artifact already in place.
46 + COPY crates/ crates/
47 + RUN cargo build --release --locked
14 48
15 49 # =====================================================================
16 50 # Runtime image — the bootable container itself.
@@ -41,9 +75,13 @@
41 75 # We're not using their base image, but their COPR is a legitimate
42 76 # adoption (same relationship the rest of Alloy has to Fedora repos).
43 77 #
44 - # Verified 2026-07-19: it does NOT carry wl-screenrec, which is why
45 - # that one needed a cargo stage and is now deferred. Check before
46 - # assuming a Rust Wayland tool is here.
78 + # Verified 2026-07-19: it does NOT carry wl-screenrec. That was the
79 + # reason it needed its own cargo stage and got deferred; a cargo stage
80 + # now exists for the console, so the toolchain half of that cost is
81 + # already paid and only clang plus the ffmpeg headers remain. See the
82 + # deferral note in docs/STACK.md#screen-recorder — the pick still
83 + # stands, the arithmetic behind refusing it has changed. Check here
84 + # before assuming a Rust Wayland tool is packaged.
47 85 RUN dnf copr enable -y ublue-os/staging
48 86
49 87 # nushell binary (Terra has crates only)
@@ -253,6 +291,19 @@
253 291 # actual base version, independent of Alloy's product version.
254 292 RUN echo 43 > /etc/dnf/vars/releasever
255 293
294 + # =====================================================================
295 + # The Alloy console.
296 + # =====================================================================
297 + # Last before the lint on purpose, and later than the config tree the
298 + # header calls the most-changed layer — the console changes with every
299 + # commit to crates/, which is more often still. Nothing below it means a
300 + # console-only rebuild reuses every package and config layer above.
301 + #
302 + # /usr/bin rather than /usr/local/bin: /usr/local is not part of a bootc
303 + # image's immutable tree, and the console is shipped software, not
304 + # something the operator dropped in.
305 + COPY --from=rust-build /src/target/release/alloy /usr/bin/alloy
306 +
256 307 # =====================================================================
257 308 # bootc validation — fails the build if the image isn't a valid
258 309 # bootable container.
M docs/IMAGE.md +1 -1
@@ -94,6 +94,6 @@
94 94
95 95 - [ ] Verify which Alloy packages are in Fedora main vs. need COPRs. Candidates that may need COPRs: `satty`, `wl-screenrec` (depending on Fedora version). Audit at v0 packaging time.
96 96 - [ ] `bootc-image-builder` for ISO generation. First-time-user path is `bootc install` from a live environment; the ISO is what makes that a smooth experience. Verify the ISO builder handles Alloy's specific package set.
97 - - [x] **Containerfile stage policy: multi-stage only where a non-Fedora Rust binary needs to ship.** Currently that's one stage (`rust-build` for wl-screenrec); everything else comes from RPM in the runtime stage. Do not add stages preemptively.
97 + - [x] **Containerfile stage policy: multi-stage only where a non-Fedora Rust binary needs to ship.** Currently that is one stage, `rust-build`, and it builds the Alloy console. Everything else comes from RPM in the runtime stage. Do not add stages preemptively. Corrected 2026-07-19: this line named wl-screenrec as the stage's occupant, which was stale — wl-screenrec was deferred rather than shipped, so for a while there was no stage at all. The console is the case the policy was written for and did not anticipate: it is the one binary Alloy authors, so no repo can ever carry it, and a distro whose headline surface is its console cannot ship without one. It is built on `fedora:43` rather than on the dev box so the toolchain and glibc match the runtime stage.
98 98 - [x] sr.ht account: **`~maxmj`** for v0. LLC-owned `~makecreative` remains the long-term target; migration if/when the project grows.
99 99 - [x] **Publish `:latest`, `:<fedora-version>`, and `:<fedora-version>-YYYYMMDD` tags.** Currently `:latest`, `:43`, `:43-YYYYMMDD`. `:latest` for casual users, the bare version for version-pinned users, the dated tag for reproducibility. Wired up when distribution starts.
M docs/STACK.md +1 -1
@@ -261,7 +261,7 @@
261 261
262 262 Rejected: wf-recorder (C++, older, no hardware accel by default), OBS (way overscope for base image; power users install).
263 263
264 - **Not in the v0 image (deferred 2026-07-19).** wl-screenrec is packaged in neither Fedora main nor Terra, so shipping it meant a dedicated `cargo install` build stage that pulled a Rust toolchain, clang, and the ffmpeg headers (912 packages) into every cold build for one binary. The pick stands; only the packaging cost is being refused. It returns when a repo carries it, or when there is a second cargo-built binary to share the stage's cost. Until then the image has no screen recorder and the `screenrec` Nu function is removed from `etc/skel/.config/nushell/aliases.nu`. Users who want it now: `cargo install wl-screenrec`.
264 + **Not in the v0 image (deferred 2026-07-19).** wl-screenrec is packaged in neither Fedora main nor Terra, so shipping it meant a dedicated `cargo install` build stage that pulled a Rust toolchain, clang, and the ffmpeg headers (912 packages) into every cold build for one binary. The pick stands; only the packaging cost is being refused. It returns when a repo carries it, or when there is a second cargo-built binary to share the stage's cost. Half of that second condition arrived on 2026-07-19: the Containerfile now has a `rust-build` stage for the Alloy console, so the Rust toolchain is already paid for. What is not paid for is the rest — clang and the ffmpeg headers, which is where the bulk of those 912 packages came from, and which the console's stage does not need. The deferral therefore stands on a narrower argument than it did, and should be re-costed rather than assumed when the screen recorder next comes up. Until then the image has no screen recorder and the `screenrec` Nu function is removed from `etc/skel/.config/nushell/aliases.nu`. Users who want it now: `cargo install wl-screenrec`.
265 265
266 266 ### Volume/brightness OSD: **swayosd**
267 267
@@ -1,0 +1,26 @@
1 + # Build context excludes.
2 + #
3 + # podman tars this directory into every build before reading the first
4 + # instruction, so anything large that no COPY reads is pure wait. output/
5 + # alone is a multi-gigabyte ISO, which the build was shipping to itself on
6 + # every run.
7 + #
8 + # Keep this in step with the COPY lines in the Containerfile: the build
9 + # reads Cargo.toml, Cargo.lock, crates/, etc/, and usr/, and nothing else.
10 +
11 + # Cargo artifacts. The rust-build stage compiles from scratch inside the
12 + # image on purpose — host artifacts are built against a different libc and
13 + # must not reach it.
14 + /target
15 +
16 + # bootc-image-builder output: the ISO, raw and qcow2 images, manifests.
17 + /output
18 + /dist
19 +
20 + # Not read by any COPY.
21 + /.git
22 + /build
23 + /builds.disabled
24 + /docs
25 + /schemas
26 + /tools