Skip to main content

max / alloy

Read the builder's default where it is now declared Two profile_split tests were red against main. Choices derives Default, so the langs default is the derive's empty set rather than a literal, and the test's search for `langs: BTreeSet::` found the first such line in the file, which is a fixture in a test below it. The db test demanded a `db: Db::default()` line the derive replaced. Both now read whichever default is in force rather than one spelling of it, so they check the agreement they exist for: an ARG that drifts from the builder still fails, either way round. The langs doc comment also still carried the superseded 2026-08-17 rule. The field's own doc has the narrowing; this one now points at it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_0153yKAYUmoC9bZz2eV6cA88
Author: Max Johnson <me@maxj.phd> · 2026-08-29 17:47 UTC
Signed with PGP, not checked
Commit: 93d550a844202f69f8aaea9a1892971635cb34af
Parent: 91890a0
1 file changed, +53 insertions, -18 deletions
@@ -241,19 +241,23 @@
241 241 /// The builder's default language set and the Containerfile's `ARG LANGS`
242 242 /// default are one decision written in two files, so they are asserted equal.
243 243 ///
244 - /// The rule behind the value (Max, 2026-08-17) is that the default is whatever
245 - /// compiles the stack Alloy ships: a user rebuilding a program that came with
246 - /// the image should not have to install a toolchain first. That makes the
247 - /// value a measurement of the shipped binaries rather than a preference, and
248 - /// it makes drift between the two files a real defect rather than an
249 - /// inconsistency — a build that bypasses the TUI has to get the stack the TUI
250 - /// would have asked for, and `alloy image` printing a command whose `LANGS`
251 - /// matches the ARG it is overriding is how anyone would ever notice.
244 + /// The rule behind the value (Max, 2026-08-17) was that the default is whatever
245 + /// compiles the stack Alloy ships. That ruling has since been narrowed on the
246 + /// `langs` field's own doc comment: compiling the stack is an argument about a
247 + /// machine being a build host, a build host is a thing a mint asks for, and
248 + /// fw13 and astra both pass `LANGS=rust` explicitly. So the default is now no
249 + /// toolchain at all, in both files. What the test asserts is unchanged: they are
250 + /// one decision, and drift between them is a real defect, because a build that
251 + /// bypasses the TUI has to get the stack the TUI would have asked for.
252 252 ///
253 - /// Asserted against the literal in `image.rs` rather than by importing it: the
254 - /// crate is a binary, the test is a text check over the Containerfile in the
255 - /// spirit of the rest of this file, and the point is that the two spellings
256 - /// agree.
253 + /// Asserted against `image.rs` as text rather than by importing it: the crate is
254 + /// a binary, the test is a text check over the Containerfile in the spirit of
255 + /// the rest of this file, and the point is that the two spellings agree.
256 + ///
257 + /// `Choices` derives `Default`, so the empty set is the derive rather than a
258 + /// literal. That is read here rather than demanded either way: a hand-written
259 + /// `impl Default` naming languages is a legitimate future state, and this has to
260 + /// compare against whichever one is in force, not fail because the other is.
257 261 #[test]
258 262 fn the_langs_default_matches_the_builder() {
259 263 let text = containerfile();
@@ -265,16 +269,27 @@
265 269 let source =
266 270 std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/image.rs"))
267 271 .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.
272 + // The derive is the default while there is no hand-written one, and a
273 + // derived `BTreeSet` is empty. Scoped to `impl Default for Choices` rather
274 + // than any `langs: BTreeSet::` line, because the tests below `Choices`
275 + // build fixtures with that exact shape and the first match in the file used
276 + // to be one of them.
270 277 let builder = source
271 - .lines()
272 - .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::"))
273 - .expect("no langs default in image.rs");
278 + .split_once("impl Default for Choices {")
279 + .and_then(|(_, rest)| {
280 + rest.lines()
281 + .take_while(|line| !line.starts_with('}'))
282 + .find_map(|line| line.trim().strip_prefix("langs: BTreeSet::"))
283 + })
284 + .unwrap_or("");
274 285 let builder = builder
275 286 .strip_prefix("from([")
276 287 .and_then(|rest| rest.split(']').next())
277 288 .unwrap_or("");
289 + assert!(
290 + derives_default(&source),
291 + "Choices neither derives Default nor writes one, so it has no default to agree with",
292 + );
278 293
279 294 let arg = arg.trim();
280 295 let from_arg: Vec<&str> = if arg.is_empty() {
@@ -328,7 +343,7 @@
328 343 std::fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/image.rs"))
329 344 .expect("cannot read image.rs");
330 345 assert!(
331 - source.contains("db: Db::default()"),
346 + source.contains("db: Db::default()") || derives_default(&source),
332 347 "the builder does not default its db field"
333 348 );
334 349
@@ -356,3 +371,23 @@
356 371 "ARG DB={arg} and the default variant of Db disagree; they are one decision",
357 372 );
358 373 }
374 +
375 + /// Whether `Choices` takes its default from the derive.
376 + ///
377 + /// The two tests above compare a Containerfile `ARG` against the builder's
378 + /// default, and the builder has had that default written both ways. Reading
379 + /// which one is in force keeps them checking the agreement rather than the
380 + /// spelling: `#[derive(Default)]` on the struct means every field's default is
381 + /// declared on its own type, which is where the `#[default]` variant checks
382 + /// point anyway.
383 + fn derives_default(source: &str) -> bool {
384 + source
385 + .split_once("pub(crate) struct Choices {")
386 + .and_then(|(before, _)| {
387 + before
388 + .lines()
389 + .rev()
390 + .find(|line| line.starts_with("#[derive("))
391 + })
392 + .is_some_and(|derive| derive.contains("Default"))
393 + }