max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
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 |