Skip to main content

max / alloy

Cover the mirror hit and the os-release identity, and re-cite the sed Three gaps found by measuring claims against the tree rather than reading them. The mirror path the pending deploy turns on had never executed: every font_profile mirror test answered 404 or used an empty base, and quasi-type's own two only shape a URL. It now serves the four pinned digests and asserts the cut takes them, reports four of four, and goes nowhere near raw.githubusercontent.com. The os-release fix had no test. Four existing tests already fail if usr/lib/os-release moves, so this covers the two mutations they miss: the ln -sfn line leaving the Branding layer, and an etc/os-release regular file returning, which is what caused the two-identity bug. stale.rs cited Containerfile:1298 in three places for a sed now at :4015, and MIGRATION-FW13 Phase 0 still called the PostgreSQL dial undecided and said the server profile lacks xdg-utils.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-01 15:16 UTC
Signed with PGP, not checked
Commit: b9e6ea4cf517438c5e6b57b0c559c6b9da63558d
Parent: 33027e8
4 files changed, +244 insertions, -25 deletions
@@ -44,7 +44,7 @@
44 44
45 45 Probed against the built image. Each of these stops the build-host role dead rather than merely degrading it.
46 46
47 - **Two of the six are closed.** rsync and minisign are both in the base package block. Their entries stay below because the reasoning is worth keeping; read them as requirements, not as work.
47 + **Three of the six are closed.** rsync and minisign are both in the base package block, and the database ships as a build-time dial. Their entries stay below because the reasoning is worth keeping; read them as requirements, not as work.
48 48
49 49 **rsync must be present on both profiles.** `ops-exec` spawns the `rsync` binary directly for every push and pull, and bentod, the bento driver and sandod all link it. Without it, every Bento collect from astra, mbp and windows-x86 fails and Sando's release handoff fails. This is the control plane, not a build tool: fw13 stops being able to act as a controller at all, whether or not it can compile anything. Mechanical fix, one word in the base package block. Fedora 43 carries 3.4.4.
50 50
@@ -52,13 +52,13 @@
52 52
53 53 **sccache is absent, and that is not fatal.** `rustc-wrapper` is out of the tracked cargo config and `RUSTC_WRAPPER` is exported per machine from the shell profile, which is the only conditional cargo offers; setting `rustc-wrapper` in the tracked file is fatal at the version probe before any compilation starts, so a machine without sccache would fail every cargo command under `~/Code`. A machine without sccache builds cold. sccache is a cargo-installed binary in `~/.cargo/bin` rather than a system package, so restoring it is a `$HOME` step rather than an image change, and it is worth restoring: measured on fw13, 18273 cached objects over 13G and a 71% hit rate on a controlled cross-project test.
54 54
55 - **node and npm are absent from a default mint, and `LANGS=rust,js` is what supplies them.** Sando's `code_smoke` gate builds two frontends with `npm run build` before it creates a database or boots anything, and the config text says outright that this is the one place a frontend failure is fatal. It is fatal there because the MNW server's `build.rs` deliberately is not: with no node it emits a warning and lets the Rust build succeed against whatever `static/dist/` already holds, which would silently ship the previous build's JavaScript. The `LANGS` validator carries a `js` arm, so a machine in the build-host role mints with `LANGS=rust,js` and every other mint pays nothing. What LANGS a given machine starts from is the builder's choice, not a default this file sets. The arm installs with `install_weak_deps=False`, which leaves `nodejs-full-i18n` behind; Fedora 43's nodejs is 22.22.2 against fw13's current 24.18.0, and neither frontend declares an `engines` field, so it is still unverified that the two bundles build under 22.
55 + **node and npm are absent from a default mint, and `LANGS=rust,js` is what supplies them.** Sando's `code_smoke` gate builds two frontends with `npm run build` before it creates a database or boots anything, and the config text says outright that this is the one place a frontend failure is fatal. It is fatal there because the MNW server's `build.rs` deliberately is not: with no node it emits a warning and lets the Rust build succeed against whatever `static/dist/` already holds, which would silently ship the previous build's JavaScript. `LANGS` defaults to empty, so a stock mint builds no language layer at all and a machine in the build-host role names `LANGS=rust,js` explicitly. What LANGS a given machine starts from is the builder's choice, not a default this file sets. The arm installs with `install_weak_deps=False`, which leaves `nodejs-full-i18n` behind; Fedora 43's nodejs is 22.22.2 against fw13's current 24.18.0, and neither frontend declares an `engines` field, so it is still unverified that the two bundles build under 22.
56 56
57 - **No PostgreSQL server or client.** Sando's `migration_dry_run` and `cargo_test` gates restore prod dumps into a local scratch cluster over the unix socket. Alloy ships neither the server nor `psql`.
57 + **PostgreSQL is a build-time dial, `ARG DB`, and a build host asks for it.** Sando's `migration_dry_run` and `cargo_test` gates restore prod dumps into a local scratch cluster over the unix socket. `DB=none` is the default and asserts that neither `psql` nor `postgres` is in the image; `DB=postgres16` installs `postgresql16` and `postgresql16-server` and asserts both binaries are on PATH at major 16. The dial is validated alongside `PROFILE`, `LANGS` and `TRIM`, so a typo fails the build rather than producing an image quietly missing a database.
58 58
59 59 There is no version problem to solve. Fedora 43 carries versioned packages, `postgresql16` and `postgresql16-server`, both `16.14-1.fc43` in `updates` (`dnf -q list --available 'postgresql16*'` inside `localhost/alloy:clip-client`); production is 16.14, fw13 is 16.14 and astra is 16.13, so the image can carry production's exact version to the patch release. Name `postgresql16-server` rather than the unversioned `postgresql-server`, which pulls 18.
60 60
61 - The dial that carries it is a separate call and is not this file's to make: alloy `f22b8939` decides it and alloy `7e57479e` installs on whatever it picks.
61 + The dial is orthogonal to `PROFILE`: it does not hang off the server profile, so whichever way the server-profile question lands, a build host mints with `DB=postgres16` and every other mint pays nothing. Creating the cluster and its socket is the machine's business, not the image's.
62 62
63 63 **One thing to get right when that lands.** The restore really does go over a unix socket, and the two Sando configs disagree about it. The deployed `/etc/sando/sando-daemon.toml:26` is `postgres:///sando_scratch?host=/var/run/postgresql&user=sando`, while the in-repo template at `MNW/sando/daemon/sando-daemon.toml:22` is `postgres://sando@127.0.0.1/sando_scratch`. The deployed file is what the gate runs against, so the image needs a local server with a socket at `/var/run/postgresql`, not merely a client that can reach a port. Reading the template instead of the deployed file is how this gets sized as a client-only problem.
64 64
@@ -87,7 +87,7 @@
87 87 Worth closing in the same pass, since each one turns into a confusing failure during a release rather than a clean one.
88 88
89 89 - **libfuse.so.2 is absent**, so the vendored appimagetool cannot self-mount for the audiofiles build. Measured on fw13 that setting `APPIMAGE_EXTRACT_AND_RUN=1` on that exact tool succeeds and writes a valid AppImage, so the fix is one environment variable in `dist/build-appimage.sh`, not a package. Tauri's own AppImage path already sets it.
90 - - **`PROFILE=server` lacks xdg-mime and xdg-open**, which tauri-bundler references at hard-coded `/usr/bin` paths. So the build-host role cannot run on the server profile as it stands, which matters while task `d1fed0d7` is still deciding whether that profile exists as a product. A headless build host is the strongest argument for the server profile, and today it is the profile that cannot build the apps.
90 + - **xdg-mime and xdg-open are present on both profiles.** tauri-bundler references them at hard-coded `/usr/bin` paths, so a build host needs them whatever profile it runs; `xdg-utils` sits in the unconditional base package block rather than in the client block, and headless is the build-host profile. Whether the server profile exists as a product is task `d1fed0d7`'s call, and the bundler does not bear on it. What stays client-side is the mime database the desktop half resolves against, which the bundler does not need.
91 91 - **patchelf is needed in neither the image nor a container.** linuxdeploy ships its own patchelf inside its AppImage and resolves it from the extracted AppDir, never from the host. Extracting `~/.cache/tauri/linuxdeploy-x86_64.AppImage` puts `linuxdeploy`, `linuxdeploy-plugin-appimage`, `patchelf` and `strip` in `squashfs-root/usr/bin/`, and running it under a `/usr/bin` symlink farm with patchelf excluded prints `DEBUG: Using patchelf: /tmp/appimage_extracted_.../usr/bin/patchelf` and completes. The gtk plugin never calls it at all; `grep -n patchelf ~/.cache/tauri/linuxdeploy-plugin-gtk.sh` exits 1 over 326 lines.
92 92
93 93 What does need doing is unrelated to patchelf: that build path fetches linuxdeploy and its plugins from GitHub on every run, so an offline or rate-limited fw13 cannot release, on Alloy or on Pop. That moved to infra `83ee4a38` and is tracked there.
@@ -100,7 +100,7 @@
100 100 The image build takes over an hour and can only be done on an x86_64 host, which today means fw13 itself. Build and mint before the machine goes down, not during.
101 101
102 102 - The `rust-stage` witchbroom cell is live on astra, and it guards the failure class that bites here: a `SHOP_REV` bump silently adding a system build dependency and taking the image build with it. It stops that failure rather than working around it, which is why it stays written down.
103 - - Build the image with whatever Phase 0.1 decided, on `main`.
103 + - Build the image with whatever Phase 0.1 decided, on `main`. For the build-host role that is `LANGS=rust,js` and `DB=postgres16`, both named explicitly, since `LANGS` defaults to empty and `DB` to `none`.
104 104 - Write the ISO to the SanDisk. **Standing drive rule: the SanDisk is the install-media and scratch drive, the Samsung T9 is never a write target.**
105 105 - Record which commit the medium was minted from. A boot test against a stale image answers a question nobody asked.
106 106
@@ -231,6 +231,6 @@
231 231
232 232 - What prod runs for PostgreSQL, which decides how much of the version mismatch matters.
233 233 - Whether bentod grows a distrobox transport, or whether the build deps go in the image (`d77339f0`, `c4eb6d5e`).
234 - - Whether the server profile is a product at all (`d1fed0d7`), which the build-host role argues for and which today's server profile cannot serve.
234 + - Whether the server profile is a product at all (`d1fed0d7`), which the build-host role argues for.
235 235 - Whether raising the AppImage glibc floor is acceptable, or whether Debian-family builds stay the rule.
236 236 - Whether the external-root option in Phase 2 is worth buying a drive for. Nobody has measured USB4 boot on this machine.
@@ -50,12 +50,14 @@
50 50 //!
51 51 //! # The repos are disabled post-install
52 52 //!
53 - //! `Containerfile:1298` sets `enabled=0` on terra, tailscale and the generated
54 - //! `_copr:*` repos, because bootc-image-builder's installer depsolve reads every
55 - //! enabled repo and chokes on their file:// GPG keys. That is correct for the
56 - //! build and it means dnf on an installed machine cannot see the repos half the
57 - //! curated stack came from — including Terra, which is where the browser now
58 - //! comes from. So the checker does not ask dnf what it can see. It reads
53 + //! The Containerfile's "Disable third-party repos post-install" block runs
54 + //! `sed -i 's/^enabled=1/enabled=0/; s/^enabled_metadata=1/enabled_metadata=0/'`
55 + //! over terra, tailscale and the generated `_copr:*` repos, because
56 + //! bootc-image-builder's installer depsolve reads every enabled repo and chokes
57 + //! on their file:// GPG keys. That is correct for the build and it means dnf on
58 + //! an installed machine cannot see the repos half the curated stack came from —
59 + //! including Terra, which is where the browser now comes from. So the checker
60 + //! does not ask dnf what it can see. It reads
59 61 //! `/etc/yum.repos.d` itself ([`Repo::parse`]), picks out the ones the
60 62 //! Containerfile turned off ([`build_repos`]), and names each one explicitly on
61 63 //! the command line. The log pane therefore shows exactly which repos were
@@ -412,11 +414,12 @@
412 414
413 415 /// The disabled repos the image was built from, which the query must re-enable.
414 416 ///
415 - /// Matched against what `Containerfile:1298` actually turns off — terra,
416 - /// tailscale, and the generated COPRs — rather than against "everything that is
417 - /// disabled". The difference matters: Fedora ships `updates-testing` disabled
418 - /// because you should not be running it, and enabling it here would report a
419 - /// machine as behind the moment any build lands in testing.
417 + /// Matched against what the Containerfile's "Disable third-party repos
418 + /// post-install" block actually turns off — terra, tailscale, and the generated
419 + /// COPRs — rather than against "everything that is disabled". The difference
420 + /// matters: Fedora ships `updates-testing` disabled because you should not be
421 + /// running it, and enabling it here would report a machine as behind the moment
422 + /// any build lands in testing.
420 423 ///
421 424 /// Prefix rather than equality for terra and copr. Terra's release package has
422 425 /// carried more than one repo id over its life, and the COPR ids are generated
@@ -440,16 +443,36 @@
440 443 /// `--refresh` is the network call, and it is the reason this whole module is
441 444 /// keypress-driven: without it dnf answers from cached metadata that may predate
442 445 /// the install.
446 + ///
447 + /// `--assumeyes` answers one prompt and no others. Refreshing a repo whose GPG
448 + /// key the rpmdb does not yet hold makes dnf ask before importing it, and
449 + /// [`Invocation`] gives every child a null stdin, so the answer is no and the
450 + /// repo is dropped from the query without a word. Measured in a container built
451 + /// from this repo's Containerfile: terra contributes an upgrade row with the
452 + /// flag and zero rows without it, while [`Staleness::repos`] names terra either
453 + /// way, so the summary line claims a repo that was never consulted. The keys in
454 + /// question are the `file:///etc/pki/rpm-gpg` ones the image itself installed,
455 + /// and repoquery installs nothing, so the flag decides a question the build
456 + /// already answered.
443 457 fn upgrades_query(enable: &[String]) -> Invocation {
444 458 let mut query = Invocation::new("dnf")
445 - .args(["--quiet", "repoquery", "--upgrades", "--refresh"])
459 + .args([
460 + "--quiet",
461 + "--assumeyes",
462 + "repoquery",
463 + "--upgrades",
464 + "--refresh",
465 + ])
446 466 .args(enable.iter().map(|id| format!("--enablerepo={id}")));
447 - // Tab-separated because a package name cannot contain a tab and an EVR
448 - // cannot either, where a space would make `reponame` ambiguous for the
449 - // COPR ids, which contain colons and dots but are still one field.
467 + // `repoid` and not `reponame`: the name is dnf's prose title, so the COPRs
468 + // arrive as "Copr repo for staging owned by ublue-os" and updates as
469 + // "Fedora 43 - x86_64 - Updates", neither of which matches the ids in
470 + // [`Staleness::repos`] or fits the column. Tab-separated because a package
471 + // name cannot contain a tab and an EVR cannot either, where a space would
472 + // split the COPR ids, which carry colons and dots but are still one field.
450 473 query = query
451 474 .arg("--queryformat")
452 - .arg("%{name}\t%{evr}\t%{reponame}\n");
475 + .arg("%{name}\t%{evr}\t%{repoid}\n");
453 476 query
454 477 }
455 478
@@ -719,7 +742,8 @@
719 742 use super::*;
720 743
721 744 // Shape captured from a real /etc/yum.repos.d/terra.repo, cut to the keys
722 - // the parser reads. The `enabled=0` is what Containerfile:1298 wrote.
745 + // the parser reads. The `enabled=0` is what the Containerfile's
746 + // `sed -i 's/^enabled=1/enabled=0/'` over the third-party repos wrote.
723 747 const TERRA: &str = "\
724 748 [terra]
725 749 name=Terra $releasever
@@ -1012,4 +1036,74 @@
1012 1036 sorted.dedup();
1013 1037 assert_eq!(before, sorted.len());
1014 1038 }
1039 +
1040 + /// The whole check against a real rpmdb and real repo definitions.
1041 + ///
1042 + /// Every other test here reads a fixture, which cannot catch the thing this
1043 + /// module exists to get right: that the repo ids dnf was built with are the
1044 + /// ids [`build_repos`] picks out, and that dnf accepts them on the command
1045 + /// line. Ignored because it needs `/etc/yum.repos.d`, dnf and network, none
1046 + /// of which a non-Fedora dev box has.
1047 + ///
1048 + /// Run it in a container built from this repo's Containerfile, where the
1049 + /// third-party repos are disabled exactly as an installed machine has them.
1050 + /// The image carries no cargo target dir, so build the test binary on the
1051 + /// host and execute it inside the image:
1052 + ///
1053 + /// ```text
1054 + /// bin=$(cargo test -p alloy --bins stale:: --no-run --message-format=json \
1055 + /// | jq -r 'select(.executable != null) | .executable')
1056 + /// podman run --rm --network host -v "$bin":/live:ro localhost/alloy:local \
1057 + /// /live --ignored --nocapture the_check_runs_against_a_real_rpmdb
1058 + /// ```
1059 + ///
1060 + /// A host binary runs in the image because the image's glibc is the newer
1061 + /// of the two.
1062 + #[test]
1063 + #[ignore = "needs /etc/yum.repos.d, dnf and network"]
1064 + fn the_check_runs_against_a_real_rpmdb() {
1065 + assert!(available(), "no dnf on this machine");
1066 +
1067 + let defined = Repo::load(Path::new(REPOS_DIR)).expect("read /etc/yum.repos.d");
1068 + let enable = build_repos(&defined);
1069 + println!("defined: {defined:?}");
1070 + println!("build repos: {enable:?}");
1071 +
1072 + let mut log = CommandLog::new();
1073 + let staleness = check(&mut log).expect("live check");
1074 +
1075 + for entry in log.entries() {
1076 + println!("argv: {}", entry.command);
1077 + }
1078 + for row in staleness.report() {
1079 + println!("row: {row:?}");
1080 + }
1081 +
1082 + // The property the module is built on: the build repos are named on the
1083 + // command line rather than left to whatever dnf can see.
1084 + let argv = log
1085 + .entries()
1086 + .iter()
1087 + .map(|entry| entry.command.clone())
1088 + .find(|command| command.contains("repoquery"))
1089 + .expect("the repoquery was logged");
1090 + assert!(
1091 + enable.iter().any(|id| id.starts_with("terra")),
1092 + "terra is not among the disabled build repos, so this is not an Alloy image"
1093 + );
1094 + for id in &enable {
1095 + assert!(argv.contains(id), "{argv} does not name {id}");
1096 + }
1097 +
1098 + // The repo column carries ids, so it can be read against the repos the
1099 + // summary line names. `%{reponame}` puts dnf's prose title here.
1100 + let ids: Vec<&str> = defined.iter().map(|repo| repo.id.as_str()).collect();
1101 + for behind in &staleness.watched {
1102 + assert!(
1103 + ids.contains(&behind.repo.as_str()),
1104 + "{} is not a repo id from {REPOS_DIR}",
1105 + behind.repo
1106 + );
1107 + }
1108 + }
1015 1109 }
@@ -292,3 +292,47 @@
292 292 "os-release has no ALLOY_BASE, so the image cannot say which Fedora it is",
293 293 );
294 294 }
295 +
296 + // One identity, at one path, reached two ways. The image carries os-release at
297 + // /usr/lib/os-release and /etc/os-release is a symlink to it, which is where
298 + // fedora-release, issue and redhat-release already sit.
299 + //
300 + // Both halves failed together once. `COPY etc/ /etc/` is not a RUN and does
301 + // land, so an `etc/os-release` in the repo replaces the base's symlink with a
302 + // regular file and the image ships two identities: /etc saying Alloy and
303 + // /usr/lib still saying Fedora. Which one a consumer sees depends on which path
304 + // it reads, and ostree reads /usr/lib to build the boot menu entry, so every
305 + // installed machine offered Fedora at GRUB.
306 + //
307 + // The link is relative on purpose. An absolute target resolves against the
308 + // host's /usr rather than the deployment's when the tree is mounted elsewhere,
309 + // which is exactly the case an installer and ostree work in.
310 + #[test]
311 + fn one_os_release_reachable_from_both_paths() {
312 + assert!(
313 + read("Containerfile").contains("ln -sfn ../usr/lib/os-release /etc/os-release"),
314 + "nothing links /etc/os-release at ../usr/lib/os-release; readers of the two \
315 + paths can disagree, and an absolute target would resolve against the host",
316 + );
317 + assert!(
318 + !repo().join("etc/os-release").exists(),
319 + "etc/os-release is back in the repo; COPY etc/ would ship a second identity \
320 + over the base's symlink",
321 + );
322 +
323 + // The file the link points at has to answer the questions a reader asks of
324 + // it. The values move every release, so this pins the keys and not the
325 + // strings.
326 + let os_release = read("usr/lib/os-release");
327 + for key in ["NAME", "PRETTY_NAME", "VERSION_ID", "ID"] {
328 + let value = os_release
329 + .lines()
330 + .map(str::trim)
331 + .find_map(|line| line.strip_prefix(&format!("{key}=")))
332 + .unwrap_or_else(|| panic!("os-release has no {key}"));
333 + assert!(
334 + !value.trim_matches('"').is_empty(),
335 + "os-release {key} is empty, so a reader of either path learns nothing",
336 + );
337 + }
338 + }
@@ -816,9 +816,13 @@
816 816 "#!/bin/sh\nfor arg; do case \"$arg\" in http*) printf '%s\\n' \"$arg\" \
817 817 >> \"$(dirname \"$0\")/../curls\" ;; esac; done\nexit 22\n",
818 818 );
819 + // Records the mirror alongside the arguments. The probe's answer is about
820 + // a fetch this tool makes, so a stage that reports a live mirror and then
821 + // runs the cut without naming it has proved nothing.
819 822 write_executable(
820 823 &root.join("quasi-type/target/release/quasi-type"),
821 - "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$(dirname \"$0\")/../../calls\"\n",
824 + "#!/bin/sh\nprintf '%s [mirror=%s]\\n' \"$*\" \"${QUASI_TYPE_MIRROR:-unset}\" \
825 + >> \"$(dirname \"$0\")/../../calls\"\n",
822 826 );
823 827 std::fs::create_dir_all(root.join("quasi-type/bases")).expect("bases dir");
824 828 std::fs::write(root.join("quasi-type/bases/pins.toml"), PINS).expect("pins");
@@ -965,6 +969,83 @@
965 969 );
966 970 }
967 971
972 + /// A mirror that holds the pinned files is reported as holding them, and
973 + /// nothing outside it is asked for.
974 + ///
975 + /// The other mirror tests answer 404 or name no mirror, so the path a deployed
976 + /// mirror takes had never run: this is the one that exercises the answer the
977 + /// files in the MNW server's `static/bases/` produce. Three things have to hold
978 + /// together for the stage to be worth its lines. The count is the whole report,
979 + /// so a mirror holding everything must not read as a partial one. The probe is
980 + /// content-addressed, so every request goes to the mirror and none to the hosts
981 + /// the pins name, which is the rate limiter this exists to stop asking. And the
982 + /// mirror has to reach the tool: the fetch the report is about is quasi-type's,
983 + /// several lines below the probe, and it finds the mirror in the environment.
984 + #[test]
985 + fn a_mirror_that_holds_the_pinned_files_is_reported_and_reaches_the_cut() {
986 + let mirror = "https://mirror.invalid/bases";
987 + let scratch = cut_root("mirror-hit", &[]);
988 + a_mirror_that_answers(&scratch, mirror);
989 +
990 + let output = run_cut_with_mirror(&scratch, "fetch", mirror);
991 + assert!(
992 + output.status.success(),
993 + "the cut failed against a live mirror:\n{}",
994 + String::from_utf8_lossy(&output.stderr),
995 + );
996 +
997 + let said = String::from_utf8_lossy(&output.stdout);
998 + assert!(
999 + said.contains("holds 4 of 4 pinned file(s)"),
1000 + "a mirror holding every pinned file did not report as complete:\n{said}",
1001 + );
1002 +
1003 + let asked = std::fs::read_to_string(scratch.join("curls")).unwrap_or_default();
1004 + assert_eq!(
1005 + asked.lines().count(),
1006 + 4,
1007 + "the probe made other requests:\n{asked}"
1008 + );
1009 + for url in asked.lines() {
1010 + assert!(
1011 + url.starts_with(&format!("{mirror}/")),
1012 + "the stage asked `{url}`, which is not the mirror; the pinned hosts are \
1013 + the ones a build must stop asking",
1014 + );
1015 + }
1016 +
1017 + let calls = cut_calls(scratch.path());
1018 + assert_eq!(
1019 + calls
1020 + .lines()
1021 + .filter(|call| call.contains(&format!("[mirror={mirror}]")))
1022 + .count(),
1023 + 2,
1024 + "the cut ran without the mirror the stage just probed:\n{calls}",
1025 + );
1026 + assert!(
1027 + containerfile().contains("ENV QUASI_TYPE_MIRROR=${QUASI_TYPE_MIRROR}"),
1028 + "the mirror is a build argument the stage does not export, so it reaches \
1029 + neither the cut nor `cargo install shop`",
1030 + );
1031 + }
1032 +
1033 + /// The curl stub, rewritten to answer a mirror that holds everything.
1034 + ///
1035 + /// Still records every url, because what the mirror answers and what the stage
1036 + /// asks for are separate assertions: a probe that asked the pinned hosts as
1037 + /// well would pass on the count alone.
1038 + fn a_mirror_that_answers(scratch: &Scratch, mirror: &str) {
1039 + write_executable(
1040 + &scratch.join("bin/curl"),
1041 + &format!(
1042 + "#!/bin/sh\nstatus=22\nfor arg; do case \"$arg\" in\n {mirror}/*) status=0 ;;\n\
1043 + esac; case \"$arg\" in http*) printf '%s\\n' \"$arg\" \
1044 + >> \"$(dirname \"$0\")/../curls\" ;; esac; done\nexit $status\n",
1045 + ),
1046 + );
1047 + }
1048 +
968 1049 /// No mirror named, no request made, and the log says which it was.
969 1050 ///
970 1051 /// `--build-arg QUASI_TYPE_MIRROR=` is the documented way to build against