max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+94 insertions,
-49 deletions
| @@ -763,11 +763,23 @@ | |||
| 763 | 763 | # the gate lives here so it holds even when the TUI is bypassed. | |
| 764 | 764 | # | |
| 765 | 765 | # THE DEFAULT IS WHAT THE IMAGE ITSELF REQUIRES, ruled by Max 2026-08-17. | |
| 766 | - | # So `rust`, alone. Rust is not a preference here: sandod shells out to | |
| 767 | - | # `cargo build --release` and refuses to compile anywhere but its | |
| 768 | - | # configured build host, so an image that quietly stopped carrying cargo | |
| 769 | - | # would break Sando rather than merely save disk. Nothing else in the | |
| 770 | - | # image makes that claim. | |
| 766 | + | # EMPTY, ruled by Max 2026-08-27: nothing in the image requires a | |
| 767 | + | # toolchain, so a stock image carries none. A toolchain that is merely | |
| 768 | + | # useful is picked at mint time. | |
| 769 | + | # | |
| 770 | + | # The 2026-08-17 ruling is unchanged; what changed is that it was never | |
| 771 | + | # applied to its last survivor. `rust` was kept on the claim that sandod | |
| 772 | + | # shells out to `cargo build --release` and would break without cargo, | |
| 773 | + | # which is an argument about the machine being a BUILD HOST rather than | |
| 774 | + | # about the image requiring rust to function. Measured 2026-08-27: the | |
| 775 | + | # `cargo install --git ...shop.git` at the top of this file runs in the | |
| 776 | + | # `rust-build` builder stage, and the shipped image starts from | |
| 777 | + | # `fedora-bootc` 600 lines later and compiles nothing at runtime. So the | |
| 778 | + | # toolchain that builds shop was never the toolchain that ships. | |
| 779 | + | # | |
| 780 | + | # The go/python/zig sentence below is therefore the whole rule now, | |
| 781 | + | # applied to every language including rust, rather than a note about | |
| 782 | + | # three of them. | |
| 771 | 783 | # | |
| 772 | 784 | # GO WAS IN THIS DEFAULT AND CAME BACK OUT THE SAME DAY. Recording both | |
| 773 | 785 | # rulings, because the second reverses the first and a bare `rust` reads | |
| @@ -801,8 +813,17 @@ | |||
| 801 | 813 | # the trade, and it is reversible at zero cost, which most things here are | |
| 802 | 814 | # not. | |
| 803 | 815 | # | |
| 804 | - | # python and zig stay off for the reason go now is: nothing in the image | |
| 805 | - | # requires them. | |
| 816 | + | # python and zig are off for the same reason, which is the rule: nothing | |
| 817 | + | # in the image requires them. | |
| 818 | + | # | |
| 819 | + | # THE BUILD-HOST COST, and it is the one that bites this fleet. A build | |
| 820 | + | # host is now a mint-time choice rather than a property of any profile. | |
| 821 | + | # fw13's cutover turns on the machine still being able to compile, and an | |
| 822 | + | # Alloy astra is a build host too; both need `LANGS=rust` passed when | |
| 823 | + | # their images are minted, which wiki `alloy-fw13-migration` and | |
| 824 | + | # `alloy-astra-migration` now say. Sando is the same fact from the other | |
| 825 | + | # end: sandod refuses to compile anywhere but its configured build host, | |
| 826 | + | # so that host's image is one that asked for rust. | |
| 806 | 827 | # | |
| 807 | 828 | # js is the newest arm and is off by the same rule. Nothing in the image | |
| 808 | 829 | # is JavaScript. What asks for it is the build-host role: Sando's | |
| @@ -826,7 +847,7 @@ | |||
| 826 | 847 | # estimate was measuring a transaction this arm does not run. Node costs | |
| 827 | 848 | # about 40% of what the task predicted. nodejs-full-i18n, 31.6 MiB of the | |
| 828 | 849 | # estimate, is confirmed absent from the built image. | |
| 829 | - | ARG LANGS=rust | |
| 850 | + | ARG LANGS= | |
| 830 | 851 | ||
| 831 | 852 | # DB is the database the image carries, and it exists because the version | |
| 832 | 853 | # matters more than the presence. `postgres16` installs postgresql16 and | |
| @@ -931,7 +952,7 @@ | |||
| 931 | 952 | client:*|server:none) ;; \ | |
| 932 | 953 | server:*) echo "PROFILE=server ships no graphical session and cannot carry BROWSER=$BROWSER" >&2; exit 1 ;; \ | |
| 933 | 954 | esac; \ | |
| 934 | - | echo "building profile=$PROFILE browser=$BROWSER langs=$LANGS trim=$TRIM db=$DB" | |
| 955 | + | echo "building profile=$PROFILE browser=$BROWSER langs=${LANGS:-none} trim=$TRIM db=$DB" | |
| 935 | 956 | ||
| 936 | 957 | # ===================================================================== | |
| 937 | 958 | # Install-time filtering: translations and documentation | |
| @@ -1177,12 +1198,13 @@ | |||
| 1177 | 1198 | # Measured on the built images rather than predicted, because the | |
| 1178 | 1199 | # prediction was half wrong: gcc-c++, libstdc++-devel, xsel, xdg-utils, | |
| 1179 | 1200 | # libX11 and man-pages are gone from `server`, and gcc, cpp, binutils and | |
| 1180 | - | # make are STILL THERE. `LANGS` defaults to rust and is unconditional, and | |
| 1181 | - | # rust requires them, which the LANGS block further down says outright. | |
| 1182 | - | # So the C compiler on a headless box is a language decision and not an | |
| 1183 | - | # accident, and it is the LANGS default that would have to change to shed | |
| 1184 | - | # it. The flag removes the C++ half and the X half; it does not remove the | |
| 1185 | - | # toolchain. | |
| 1201 | + | # make were STILL THERE, because `LANGS` defaulted to rust and rust | |
| 1202 | + | # requires them. So the C compiler on a headless box was a language | |
| 1203 | + | # decision and not an accident, and the LANGS default was what would have | |
| 1204 | + | # to change to shed it. It changed on 2026-08-27: `LANGS` is empty by | |
| 1205 | + | # default, so a stock image of either profile carries no compiler at all | |
| 1206 | + | # and one minted with `LANGS=rust` gets the C toolchain back along with | |
| 1207 | + | # it. The flag itself still only removes the C++ half and the X half. | |
| 1186 | 1208 | # | |
| 1187 | 1209 | # One recommend IS wanted and is now named below rather than inherited: | |
| 1188 | 1210 | # `helix-parsers`. It is 185 MiB of tree-sitter grammars and it is what | |
| @@ -1807,7 +1829,8 @@ | |||
| 1807 | 1829 | && dnf clean all \ | |
| 1808 | 1830 | # bibata-cursor-theme installs FOURTEEN directories and Alloy names one. | |
| 1809 | 1831 | # At 179 MB it was the third-largest package in the image, behind the | |
| 1810 | - | # browser and helix's parsers, and ahead of rustc. Fedora ships no | |
| 1832 | + | # browser and helix's parsers, and ahead of rustc on a mint that asked | |
| 1833 | + | # for rust (nothing does by default since 2026-08-27). Fedora ships no | |
| 1811 | 1834 | # per-variant subpackage, so a prune is the only lever. | |
| 1812 | 1835 | # | |
| 1813 | 1836 | # Measured in the built image: Bibata-Modern-{Amber,Classic,Ice} at 27 MB | |
| @@ -1914,21 +1937,24 @@ | |||
| 1914 | 1937 | # the way this stops being one number for everyone", and this is that, | |
| 1915 | 1938 | # arrived at from the builder rather than from mint time. | |
| 1916 | 1939 | # | |
| 1917 | - | # Rust is in the default set and that is not politeness: sandod shells | |
| 1918 | - | # out to `cargo build --release` and refuses to compile anywhere but its | |
| 1919 | - | # configured build host, so an image that quietly stopped carrying cargo | |
| 1920 | - | # would break Sando rather than merely save disk. It costs 610 MiB across | |
| 1921 | - | # 16 packages, most of it rust-std-static (164 MiB) and llvm-libs (139 | |
| 1922 | - | # MiB), which is an order of magnitude above the hardware-health group | |
| 1923 | - | # and by a distance the largest optional thing in the image. | |
| 1940 | + | # Rust is not in the default set any more (2026-08-27): nothing in the | |
| 1941 | + | # image requires it, and the Sando argument that kept it is about a | |
| 1942 | + | # machine being a build host rather than about the image working. It | |
| 1943 | + | # costs 610 MiB across 16 packages, most of it rust-std-static (164 MiB) | |
| 1944 | + | # and llvm-libs (139 MiB), which is an order of magnitude above the | |
| 1945 | + | # hardware-health group and by a distance the largest optional thing in | |
| 1946 | + | # the image — so it is exactly the kind of thing a mint should be asked | |
| 1947 | + | # about rather than handed. | |
| 1924 | 1948 | # | |
| 1925 | 1949 | # No separate linker line: `rust` pulls gcc, binutils and glibc-devel, so | |
| 1926 | 1950 | # cc arrives with it. rustup is deliberately not offered here — it | |
| 1927 | 1951 | # installs into $HOME, needs nothing from the image, and stays the answer | |
| 1928 | 1952 | # for a pinned or nightly toolchain. | |
| 1929 | 1953 | # | |
| 1930 | - | # Unconditional: languages are as useful on a build host as on a laptop, | |
| 1931 | - | # which is most of why the build-host role works at all. | |
| 1954 | + | # Profile-blind: languages are as useful on a build host as on a laptop, | |
| 1955 | + | # which is most of why the build-host role works at all. The empty | |
| 1956 | + | # default is profile-blind for the same reason — a client image has no | |
| 1957 | + | # more claim on a compiler than a server one. | |
| 1932 | 1958 | RUN set -eu; \ | |
| 1933 | 1959 | for lang in $(echo "$LANGS" | tr ',' ' '); do \ | |
| 1934 | 1960 | case "$lang" in \ |
| @@ -117,8 +117,10 @@ | |||
| 117 | 117 | deployed, and the 0.9 GB between those two numbers is the subject of half this section. | |
| 118 | 118 | ||
| 119 | 119 | Every figure below is from that build, so they describe an image built with Go in it. | |
| 120 | - | The default became `LANGS=rust` later the same day, which takes the language layer down | |
| 121 | - | with it; the numbers are left as measured rather than adjusted on paper. | |
| 120 | + | The default became `LANGS=rust` later the same day and then empty on 2026-08-27, which | |
| 121 | + | takes the whole language layer out of a stock image; the numbers are left as measured | |
| 122 | + | rather than adjusted on paper. A default mint today carries no toolchain at all, so the | |
| 123 | + | 778 MB language row below is a layer only a machine that asked for one builds. | |
| 122 | 124 | ||
| 123 | 125 | The same applies to the browser, and more sharply. `BROWSER=helium` was removed on | |
| 124 | 126 | 2026-08-18 and Firefox is the only answer now, so the browser layer below is a | |
| @@ -138,7 +140,7 @@ | |||
| 138 | 140 | | Size | Layer | Why it is that size | | |
| 139 | 141 | |------|-------|---------------------| | |
| 140 | 142 | | 1.44 GB | sway, `xdg-desktop-portal{,-gtk,-wlr}`, the session | The product. This is the desktop. | | |
| 141 | - | | 778 MB | `LANGS=rust,go` | As measured. Go is 230 MB of this layer, and 356 MB by exclusive closure once the packages only it pulls in are counted; that gap is the two questions the measurements answer, not a disagreement. Go left the default on 2026-08-17, so a stock image builds this layer at `LANGS=rust`. Argued at `ARG LANGS`. | | |
| 143 | + | | 778 MB | `LANGS=rust,go` | As measured. Go is 230 MB of this layer, and 356 MB by exclusive closure once the packages only it pulls in are counted; that gap is the two questions the measurements answer, not a disagreement. Go left the default on 2026-08-17 and rust on 2026-08-27, so a stock image does not build this layer at all. Argued at `ARG LANGS`. | | |
| 142 | 144 | | 674 MB | `helium-browser-bin` | The product. Superseded: this is Helium, removed 2026-08-18, and Firefox's layer is unmeasured. | | |
| 143 | 145 | | 663 MB | Base package list | The stack, per [STACK.md](STACK.md). | | |
| 144 | 146 | | 170 MB | initramfs + `rpmostree-unpackaged-content` | Bootable image. | | |
| @@ -154,7 +156,8 @@ | |||
| 154 | 156 | syntax highlighting. Settled. | |
| 155 | 157 | - **`rust-std-static`, 166 MB.** The name suggests static-musl targets nothing here | |
| 156 | 158 | builds. It is the standard library: `rust` carries a versioned hard requirement on it, | |
| 157 | - | and removing it takes `rust` and `cargo` with it. Not separable from `LANGS=rust`. | |
| 159 | + | and removing it takes `rust` and `cargo` with it. Not separable from `LANGS=rust`, and | |
| 160 | + | absent entirely from a mint that did not ask for rust. | |
| 158 | 161 | - **`llvm-libs`, 139 MB.** `rpm -q --whatrequires llvm-libs` returns nothing, which is | |
| 159 | 162 | what makes it look orphaned, and the query is the thing that is wrong: dependents | |
| 160 | 163 | require the soname. `libLLVM.so.21.1()(64bit)` is required by `mesa-dri-drivers`, |
| @@ -281,6 +281,8 @@ | |||
| 281 | 281 | /// Rust's figure is the Containerfile's own measurement, and it is the | |
| 282 | 282 | /// reason this row exists: 610 MiB is an order of magnitude above the | |
| 283 | 283 | /// hardware-health group and by a distance the largest thing in the image. | |
| 284 | + | /// Off by default since 2026-08-27, so like Go's it is now a number | |
| 285 | + | /// somebody is deciding to spend rather than one they are inheriting. | |
| 284 | 286 | /// | |
| 285 | 287 | /// Go's is its marginal cost from the 2026-08-17 exclusive-closure pass — | |
| 286 | 288 | /// what turning this row on actually adds, rather than what `golang` | |
| @@ -288,7 +290,7 @@ | |||
| 288 | 290 | /// number is what someone is deciding to spend. | |
| 289 | 291 | const fn label(self) -> &'static str { | |
| 290 | 292 | match self { | |
| 291 | - | Lang::Rust => "rust (610 MiB; needed for the build-host role)", | |
| 293 | + | Lang::Rust => "rust (610 MiB; the build-host role asks for this)", | |
| 292 | 294 | Lang::Go => "go (356 MB; rebuilds syncthing, restic, tailscale)", | |
| 293 | 295 | Lang::Python => "python", | |
| 294 | 296 | Lang::Zig => "zig", | |
| @@ -461,18 +463,22 @@ | |||
| 461 | 463 | profile: Profile::default(), | |
| 462 | 464 | browser: Browser::default(), | |
| 463 | 465 | // The default is what the image itself requires (Max, | |
| 464 | - | // 2026-08-17), which is Rust and nothing else: sandod refuses to | |
| 465 | - | // compile anywhere but its configured host, so an image that | |
| 466 | - | // silently stopped being able to build Rust would break Sando | |
| 467 | - | // rather than merely save disk. | |
| 466 | + | // 2026-08-17), and as of 2026-08-27 that is nothing: measured, the | |
| 467 | + | // shipped image compiles nothing at runtime, since the `cargo | |
| 468 | + | // install` that builds shop runs in a builder stage the final image | |
| 469 | + | // is not built from. A toolchain that is merely useful is picked | |
| 470 | + | // here, at mint time. | |
| 468 | 471 | // | |
| 469 | - | // Go was in this default earlier the same day, under the rule "the | |
| 470 | - | // default is what compiles the shipped stack" — 7 of the shipped | |
| 471 | - | // binaries are Go (syncthing, restic, tailscale, gopass, direnv, | |
| 472 | - | // fzf, cliphist). That rule already stopped short of the C tier, so | |
| 473 | - | // it was two-thirds true, and Go's marginal cost measured 356 MB. | |
| 474 | - | // Reversed. `ARG LANGS` in the Containerfile carries both rulings. | |
| 475 | - | langs: BTreeSet::from([Lang::Rust]), | |
| 472 | + | // Rust survived the first ruling on the claim that sandod refuses | |
| 473 | + | // to compile anywhere but its configured host. That is an argument | |
| 474 | + | // about a machine being a build host, not about the image working, | |
| 475 | + | // and a build host is now a thing a mint asks for: fw13 and astra | |
| 476 | + | // both take `LANGS=rust`. Go was in the default earlier still, | |
| 477 | + | // under "the default is what compiles the shipped stack", which | |
| 478 | + | // already stopped short of the C tier and so was two-thirds true. | |
| 479 | + | // `ARG LANGS` in the Containerfile carries every one of those | |
| 480 | + | // rulings. | |
| 481 | + | langs: BTreeSet::new(), | |
| 476 | 482 | hostname: String::new(), | |
| 477 | 483 | pubkey: String::new(), | |
| 478 | 484 | artifact: Artifact::default(), | |
| @@ -1471,9 +1477,9 @@ | |||
| 1471 | 1477 | let choices = Choices::default(); | |
| 1472 | 1478 | assert_eq!(choices.profile, Profile::Client); | |
| 1473 | 1479 | assert_eq!(choices.browser, Browser::Firefox); | |
| 1474 | - | // Rust, and only Rust: it is the one toolchain the image itself needs, | |
| 1475 | - | // for the build-host role. See the comment on `Choices::default`. | |
| 1476 | - | assert_eq!(choices.langs, BTreeSet::from([Lang::Rust])); | |
| 1480 | + | // No toolchain. The image requires none to function, and a build | |
| 1481 | + | // host asks for one at mint. See the comment on `Choices::default`. | |
| 1482 | + | assert!(choices.langs.is_empty()); | |
| 1477 | 1483 | assert_eq!(choices.artifact, Artifact::Iso); | |
| 1478 | 1484 | // Trimmed by default. What it costs is foreign-architecture emulation, | |
| 1479 | 1485 | // which the house rules forbid using in the first place. | |
| @@ -1497,7 +1503,7 @@ | |||
| 1497 | 1503 | // the two defaults are one decision written in two files, and a build | |
| 1498 | 1504 | // that bypasses the TUI must get the same stack the TUI would have | |
| 1499 | 1505 | // asked for. | |
| 1500 | - | assert_eq!(args[2].1, "rust"); | |
| 1506 | + | assert_eq!(args[2].1, ""); | |
| 1501 | 1507 | assert_eq!(args[3].1, "unused"); | |
| 1502 | 1508 | assert_eq!(args[4].1, "none"); | |
| 1503 | 1509 | } |
| @@ -265,13 +265,23 @@ | |||
| 265 | 265 | let source = | |
| 266 | 266 | std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/image.rs")) | |
| 267 | 267 | .expect("cannot read image.rs"); | |
| 268 | + | // Either spelling of the default, because an empty default is a real | |
| 269 | + | // answer here and `BTreeSet::from([])` is not how it is written. | |
| 268 | 270 | let builder = source | |
| 269 | 271 | .lines() | |
| 270 | - | .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::from([")) | |
| 271 | - | .and_then(|rest| rest.split(']').next()) | |
| 272 | + | .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::")) | |
| 272 | 273 | .expect("no langs default in image.rs"); | |
| 274 | + | let builder = builder | |
| 275 | + | .strip_prefix("from([") | |
| 276 | + | .and_then(|rest| rest.split(']').next()) | |
| 277 | + | .unwrap_or(""); | |
| 273 | 278 | ||
| 274 | - | let from_arg: Vec<&str> = arg.trim().split(',').collect(); | |
| 279 | + | let arg = arg.trim(); | |
| 280 | + | let from_arg: Vec<&str> = if arg.is_empty() { | |
| 281 | + | Vec::new() | |
| 282 | + | } else { | |
| 283 | + | arg.split(',').collect() | |
| 284 | + | }; | |
| 275 | 285 | let from_builder: Vec<String> = builder | |
| 276 | 286 | .split(',') | |
| 277 | 287 | .filter_map(|entry| entry.trim().strip_prefix("Lang::").map(str::to_lowercase)) | |
| @@ -279,7 +289,7 @@ | |||
| 279 | 289 | ||
| 280 | 290 | assert_eq!( | |
| 281 | 291 | from_arg, from_builder, | |
| 282 | - | "ARG LANGS={arg} and image.rs's default disagree; they are one decision", | |
| 292 | + | "ARG LANGS={arg:?} and image.rs's default disagree; they are one decision", | |
| 283 | 293 | ); | |
| 284 | 294 | } | |
| 285 | 295 |