max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_0153yKAYUmoC9bZz2eV6cA88
5 files changed,
+228 insertions,
-38 deletions
| @@ -119,6 +119,35 @@ | |||
| 119 | 119 | # that a `COPY` into /usr/bin is the worst of the shapes — an unowned file | |
| 120 | 120 | # cannot even be layered over, it dies in checkout. | |
| 121 | 121 | # | |
| 122 | + | # The mirror of quasi-type's pinned bases, named here rather than in the font | |
| 123 | + | # stage below because the fetch it exists for is `cargo install shop`, 180 lines | |
| 124 | + | # further down. shop's `shop-font/build.rs` cuts a third face from the same | |
| 125 | + | # Atkinson base with the cache directory and the offline flag both hardcoded, so | |
| 126 | + | # the seed cache the font stage carries has no path and no flag to aim at. A | |
| 127 | + | # mirror is the only one of the available fixes that covers that fetch without | |
| 128 | + | # shop changing, which is why it is an ENV over the whole stage rather than a | |
| 129 | + | # flag on the cut. | |
| 130 | + | # | |
| 131 | + | # A base URL. quasi-type addresses every pinned file under it by the sha256 the | |
| 132 | + | # pin already carries, tries it before upstream, and verifies the bytes against | |
| 133 | + | # that digest either way, so a mirror can serve the pinned file or nothing. It | |
| 134 | + | # cannot serve a different one, which is what makes this a second source rather | |
| 135 | + | # than a second thing to trust. Bytes that do not match fall through to the | |
| 136 | + | # pinned url rather than failing the build, so an out-of-date mirror is not | |
| 137 | + | # worse than no mirror. | |
| 138 | + | # | |
| 139 | + | # Set it empty to switch it off and fetch upstream only, which is what a build | |
| 140 | + | # with no access to our infrastructure wants. Nothing requires it to answer: the | |
| 141 | + | # font stage reports which pinned files it holds rather than asserting any, since | |
| 142 | + | # a mirror that is down is a slower build and not a broken one. | |
| 143 | + | # | |
| 144 | + | # Needs a QUASI_TYPE_REV at or after 43845e3, where the variable starts being | |
| 145 | + | # read, and a SHOP_REV whose own quasi-type pin is at or after it. All three move | |
| 146 | + | # together for that reason. The files are in the MNW server's `static/bases/`, | |
| 147 | + | # served by the `/static` ServeDir with no route of its own. | |
| 148 | + | ARG QUASI_TYPE_MIRROR=https://makenot.work/static/bases | |
| 149 | + | ENV QUASI_TYPE_MIRROR=${QUASI_TYPE_MIRROR} | |
| 150 | + | ||
| 122 | 151 | # Pinned by revision, not by branch. An unpinned build is a different image | |
| 123 | 152 | # every day for the same Containerfile, and the terminal is the one component | |
| 124 | 153 | # where "it worked yesterday" has no recovery path from inside the session. | |
| @@ -145,7 +174,7 @@ | |||
| 145 | 174 | # alone, so console edits — the thing that changes most often — reuse it | |
| 146 | 175 | # instead of rebuilding a terminal that did not change. Moving it below the | |
| 147 | 176 | # `COPY crates/` would defeat the dependency-cache split that follows. | |
| 148 | - | ARG SHOP_REV=af8c79c22e79f95874097bddb661ec46d8c60cb5 | |
| 177 | + | ARG SHOP_REV=144438245fa3b25b897f4c61ae88f49b3508f66a | |
| 149 | 178 | ||
| 150 | 179 | # Does the remote have anything to build from, before anything expensive runs. | |
| 151 | 180 | # | |
| @@ -244,29 +273,33 @@ | |||
| 244 | 273 | # cannot happen, and that makes this a property of the distribution rather than | |
| 245 | 274 | # a flaky step. | |
| 246 | 275 | # | |
| 247 | - | # WHAT THE SEED COVERS IS THIS BLOCK'S TWO CUTS, AND NOTHING ELSE. Say it here | |
| 248 | - | # rather than let a reader infer a whole-build guarantee from the word `sealed`, | |
| 249 | - | # because the fetch it does not cover is the one that failed. `cargo install | |
| 250 | - | # shop` is 85 lines above, and moving the COPY above it would change nothing: | |
| 251 | - | # shop's `shop-font/build.rs` calls `quasi_type::cut("quasi-mono", | |
| 252 | - | # &out.join("bases"), false)`, with the cache directory hardcoded to its own | |
| 253 | - | # OUT_DIR and `offline` hardcoded false, so there is no path and no flag for | |
| 254 | - | # this file to aim at a carried copy. Read from shop at rev ${SHOP_REV} on | |
| 255 | - | # 2026-08-19. | |
| 276 | + | # WHAT THE SEED COVERS IS STILL THIS BLOCK'S TWO CUTS, AND NOTHING ELSE. Say it | |
| 277 | + | # here rather than let a reader infer a whole-build guarantee from the word | |
| 278 | + | # `sealed`. `cargo install shop` is 140 lines above, and moving the COPY above it | |
| 279 | + | # would change nothing: shop's `shop-font/build.rs` calls | |
| 280 | + | # `quasi_type::cut_native(&out, &out.join("bases"), false, ...)`, with the cache | |
| 281 | + | # directory hardcoded to its own OUT_DIR and `offline` hardcoded false, so there | |
| 282 | + | # is no path and no flag for this file to aim at a carried copy. Read from shop | |
| 283 | + | # at rev ${SHOP_REV} on 2026-08-29. | |
| 256 | 284 | # | |
| 257 | - | # THE CONTRACT SHOP WOULD HAVE TO IMPLEMENT, so that this is a state to fix and | |
| 258 | - | # not a shrug: read the cache directory from an environment variable, falling | |
| 259 | - | # back to `OUT_DIR/bases` when it is unset so nothing about a plain `cargo | |
| 260 | - | # build` changes, and take `offline` from a second one. Two lines in that build | |
| 261 | - | # script. When they exist, the COPY moves above `cargo install shop`, this | |
| 262 | - | # stage exports both variables, and `sealed` becomes a claim about the build | |
| 263 | - | # rather than about the cut. It is shop's change to make, and it is not made | |
| 264 | - | # here. Until it is, a build host that wants the third fetch covered warms | |
| 265 | - | # cargo's target directory instead, which is what shop's own build script says | |
| 266 | - | # it is for. | |
| 285 | + | # WHAT COVERS THE FETCH THE SEED DOES NOT COVER IS THE MIRROR, and that is why | |
| 286 | + | # the mirror is the fix this stage took. `QUASI_TYPE_MIRROR` is read inside | |
| 287 | + | # quasi-type, which is the crate on both sides of the hardcoding, so it reaches | |
| 288 | + | # a fetch this file cannot otherwise touch. The seed still does not cover that | |
| 289 | + | # fetch: a mirror is a better source and not a carried copy, so `sealed` means | |
| 290 | + | # exactly what it meant. | |
| 267 | 291 | # | |
| 268 | - | # TWO THINGS STAND AGAINST THE OTHER TWO FETCHES, and neither is the durable | |
| 269 | - | # fix. | |
| 292 | + | # THE CONTRACT SHOP WOULD HAVE TO IMPLEMENT, if `sealed` is ever to be a claim | |
| 293 | + | # about the build and not only about the cut: read the cache directory from an | |
| 294 | + | # environment variable, falling back to `OUT_DIR/bases` when it is unset so | |
| 295 | + | # nothing about a plain `cargo build` changes, and take `offline` from a second | |
| 296 | + | # one. Two lines in that build script. When they exist, the COPY moves above | |
| 297 | + | # `cargo install shop` and this stage exports both variables. It is shop's change | |
| 298 | + | # to make and it is not made here; the mirror lowered its urgency rather than | |
| 299 | + | # removing it, since an offline build host still cannot seal that fetch. | |
| 300 | + | # | |
| 301 | + | # TWO MORE THINGS STAND AGAINST THE OTHER TWO FETCHES, and neither is the durable | |
| 302 | + | # fix the mirror is. | |
| 270 | 303 | # | |
| 271 | 304 | # 1. The retry is upstream, in quasi-type's own fetch: curl `--retry 5 | |
| 272 | 305 | # --retry-delay 2 --retry-all-errors`, which covers 429, 408, the 5xx | |
| @@ -311,11 +344,16 @@ | |||
| 311 | 344 | # pin, and it would go stale the first time QUASI_TYPE_REV moved to a base with | |
| 312 | 345 | # a new version in its file names. | |
| 313 | 346 | # | |
| 314 | - | # WHAT IS STILL MISSING is a copy of the bases on infrastructure we own, | |
| 315 | - | # addressed by the same sha256, so that an unseeded build has somewhere to reach | |
| 316 | - | # that is not somebody else's rate limiter. That is a hosting decision (astra or | |
| 317 | - | # makenot.work) and it is not made here. It is also the only one of these that | |
| 318 | - | # would cover shop's fetch without shop changing at all. | |
| 347 | + | # THE COPY ON INFRASTRUCTURE WE OWN EXISTS NOW, and it is the piece this comment | |
| 348 | + | # used to say was missing. The four pinned files live in the MNW server's | |
| 349 | + | # `static/bases/` named by their sha256, and `QUASI_TYPE_MIRROR` near the top of | |
| 350 | + | # this stage points quasi-type at them. It is an ENV over the whole stage rather | |
| 351 | + | # than a flag here because the fetch that took the build down was shop's, not | |
| 352 | + | # these two, and shop reads the same variable through the same crate. | |
| 353 | + | # | |
| 354 | + | # It adds a source and does not remove one: every base still carries its | |
| 355 | + | # upstream url and falls back to it, so a build with no access to makenot.work | |
| 356 | + | # is slower on a cold cache and nothing more. | |
| 319 | 357 | # | |
| 320 | 358 | # THE CUT IS UNCONDITIONAL, and that is a decision rather than an omission. Only | |
| 321 | 359 | # the client installs these faces (see the profile block near the end of this | |
| @@ -326,7 +364,7 @@ | |||
| 326 | 364 | # What it would cost is the property that makes this stage cheap. Nothing in it | |
| 327 | 365 | # reads `$PROFILE`, so both profiles share every layer of it, including the | |
| 328 | 366 | # console build below and the RPM packaging after that. | |
| 329 | - | ARG QUASI_TYPE_REV=e40330602e68fbdd2a9d2917bdec33a7ce573a1f | |
| 367 | + | ARG QUASI_TYPE_REV=52c5bc069fa0b0f3f74bad6957e2d5ca12c0b57d | |
| 330 | 368 | ARG QUASI_BASES=fetch | |
| 331 | 369 | COPY build/base-cache/ /base-cache/ | |
| 332 | 370 | RUN set -eu; \ | |
| @@ -381,6 +419,18 @@ | |||
| 381 | 419 | test -f /quasi-type/bases/cache/"$name" || missing="$missing $name"; \ | |
| 382 | 420 | done; \ | |
| 383 | 421 | echo "quasi-type bases: seeded $seeded file(s) from /base-cache, mode $QUASI_BASES, missing:${missing:- none}"; \ | |
| 422 | + | mirror="${QUASI_TYPE_MIRROR:-}"; \ | |
| 423 | + | if [ -n "$mirror" ] && [ "$QUASI_BASES" = fetch ]; then \ | |
| 424 | + | have=0; total=0; \ | |
| 425 | + | for digest in $(sed -n 's/^\(license_\)\{0,1\}sha256 = "\([0-9a-f]\{64\}\)"$/\2/p' /quasi-type/bases/pins.toml); do \ | |
| 426 | + | total=$((total + 1)); \ | |
| 427 | + | curl --fail --silent --head --location --max-time 20 "$mirror/$digest" >/dev/null 2>&1 \ | |
| 428 | + | && have=$((have + 1)); \ | |
| 429 | + | done; \ | |
| 430 | + | echo "quasi-type mirror: $mirror holds $have of $total pinned file(s); the rest fall through to their upstream urls"; \ | |
| 431 | + | else \ | |
| 432 | + | echo "quasi-type mirror: not consulted (mirror '$mirror', mode $QUASI_BASES)"; \ | |
| 433 | + | fi; \ | |
| 384 | 434 | if [ "$QUASI_BASES" = sealed ]; then \ | |
| 385 | 435 | [ -z "$missing" ] \ | |
| 386 | 436 | || { echo "QUASI_BASES=sealed and the seed is short of:$missing. The cut would fail on the first of them after building the tool. Copy them into build/base-cache/ (its README says where from) or build with QUASI_BASES=fetch" >&2; exit 1; }; \ |
| @@ -1,3 +1,3 @@ | |||
| 1 | 1 | # The SHOP_REV that build/check-rust-stage.sh last built clean. | |
| 2 | 2 | # Written by that script on success. Do not edit by hand. | |
| 3 | - | af8c79c22e79f95874097bddb661ec46d8c60cb5 | |
| 3 | + | 144438245fa3b25b897f4c61ae88f49b3508f66a |
| @@ -18,9 +18,17 @@ | |||
| 18 | 18 | terminal's bundled face while `cargo install shop` runs. That third one is the | |
| 19 | 19 | one that broke the build on 2026-08-17, and this directory does not cover it: | |
| 20 | 20 | shop's build script passes its own `OUT_DIR` as the cache directory, so there is | |
| 21 | - | no path for the image build to point at a carried copy. Covering it needs a | |
| 22 | - | change in shop (read the cache directory and the offline flag from the | |
| 23 | - | environment); until then a warm cargo target directory is what covers it. | |
| 21 | + | no path for the image build to point at a carried copy. Covering it with a seed | |
| 22 | + | needs a change in shop (read the cache directory and the offline flag from the | |
| 23 | + | environment). | |
| 24 | + | ||
| 25 | + | What covers all three without that change is the mirror. `QUASI_TYPE_MIRROR`, | |
| 26 | + | set in the Containerfile to `https://makenot.work/static/bases`, is read inside | |
| 27 | + | `quasi-type` itself, so it reaches the fetch this directory cannot. It is a | |
| 28 | + | second source and not a carried copy: files are addressed by the sha256 the pin | |
| 29 | + | already carries and verified against it, upstream stays the fallback, and | |
| 30 | + | `sealed` still means the two cuts. A warm cargo target directory is what covers | |
| 31 | + | the terminal's fetch on a host that has built once. | |
| 24 | 32 | ||
| 25 | 33 | The rest of the build is not offline either, in any mode: it clones two | |
| 26 | 34 | repositories, resolves crates.io, and installs packages. |
| @@ -141,8 +141,20 @@ | |||
| 141 | 141 | that the terminal's own build script cuts while `cargo install shop` runs. The | |
| 142 | 142 | seed below covers the first two. It does not cover the terminal's, which is the | |
| 143 | 143 | fetch that failed in August, because the terminal's build script names its own | |
| 144 | - | cache directory and nothing in the image build can point it elsewhere. Warming | |
| 145 | - | the cargo target directory is what covers that one today. | |
| 144 | + | cache directory and nothing in the image build can point it elsewhere. | |
| 145 | + | ||
| 146 | + | What covers all three is a mirror, and the build takes one by default: | |
| 147 | + | `QUASI_TYPE_MIRROR` is set to `https://makenot.work/static/bases`, where the | |
| 148 | + | pinned files are served under the sha256 each pin already carries. The tool | |
| 149 | + | tries the mirror first and falls back to the pinned upstream url, and it checks | |
| 150 | + | the bytes against that digest either way, so a mirror can serve the pinned file | |
| 151 | + | or nothing. A build with no access to makenot.work is a slower first build and | |
| 152 | + | nothing more; to switch it off, | |
| 153 | + | ||
| 154 | + | build/build-iso.sh --build-arg QUASI_TYPE_MIRROR= | |
| 155 | + | ||
| 156 | + | The build log says which way it went. Warming the cargo target directory still | |
| 157 | + | covers the terminal's fetch on a host that has built once. | |
| 146 | 158 | ||
| 147 | 159 | To seed the two the image cuts, put the four files in `build/base-cache/` (its | |
| 148 | 160 | README names them and says where to copy them from). They are carried into the |
| @@ -206,7 +206,7 @@ | |||
| 206 | 206 | let bin = scratch.join("bin"); | |
| 207 | 207 | std::fs::create_dir_all(&bin).expect("bin"); | |
| 208 | 208 | for tool in [ | |
| 209 | - | "rm", "cp", "mkdir", "ls", "wc", "grep", "dirname", "cat", "chmod", "awk", | |
| 209 | + | "rm", "cp", "mkdir", "ls", "wc", "grep", "dirname", "cat", "chmod", "awk", "sed", | |
| 210 | 210 | ] { | |
| 211 | 211 | let Some(real) = ["/usr/bin", "/bin", "/usr/local/bin"] | |
| 212 | 212 | .iter() | |
| @@ -758,13 +758,14 @@ | |||
| 758 | 758 | version = \"2.001\"\n\ | |
| 759 | 759 | license = \"OFL-1.1\"\n\ | |
| 760 | 760 | license_url = \"https://example.invalid/mono/OFL.txt\"\n\ | |
| 761 | + | license_sha256 = \"1111111111111111111111111111111111111111111111111111111111111111\"\n\ | |
| 761 | 762 | copyright = \"Braille Institute\"\n\ | |
| 762 | 763 | designer = \"Braille Institute\"\n\ | |
| 763 | 764 | \n\ | |
| 764 | 765 | [[base.face]]\n\ | |
| 765 | 766 | style = \"ExtraLight\"\n\ | |
| 766 | 767 | url = \"https://example.invalid/mono/AtkinsonHyperlegibleMono%5Bwght%5D.ttf\"\n\ | |
| 767 | - | sha256 = \"00\"\n\ | |
| 768 | + | sha256 = \"2222222222222222222222222222222222222222222222222222222222222222\"\n\ | |
| 768 | 769 | variable = true\n\ | |
| 769 | 770 | \n\ | |
| 770 | 771 | [[base]]\n\ | |
| @@ -773,13 +774,14 @@ | |||
| 773 | 774 | version = \"2.001\"\n\ | |
| 774 | 775 | license = \"OFL-1.1\"\n\ | |
| 775 | 776 | license_url = \"https://example.invalid/next/OFL.txt\"\n\ | |
| 777 | + | license_sha256 = \"3333333333333333333333333333333333333333333333333333333333333333\"\n\ | |
| 776 | 778 | copyright = \"Braille Institute\"\n\ | |
| 777 | 779 | designer = \"Braille Institute\"\n\ | |
| 778 | 780 | \n\ | |
| 779 | 781 | [[base.face]]\n\ | |
| 780 | 782 | style = \"ExtraLight\"\n\ | |
| 781 | 783 | url = \"https://example.invalid/next/AtkinsonHyperlegibleNext%5Bwght%5D.ttf\"\n\ | |
| 782 | - | sha256 = \"00\"\n\ | |
| 784 | + | sha256 = \"4444444444444444444444444444444444444444444444444444444444444444\"\n\ | |
| 783 | 785 | variable = true\n\ | |
| 784 | 786 | \n\ | |
| 785 | 787 | [[slot]]\n\ | |
| @@ -810,6 +812,14 @@ | |||
| 810 | 812 | // them, and a checkout with no pins is a build that cannot check a seed. | |
| 811 | 813 | write_executable(&bin.join("git"), "#!/bin/sh\nexit 0\n"); | |
| 812 | 814 | write_executable(&bin.join("cargo"), "#!/bin/sh\nexit 0\n"); | |
| 815 | + | // Records every url the mirror probe asks for and answers 404 to all of | |
| 816 | + | // them, which is the state of a mirror that has not been deployed yet. A | |
| 817 | + | // test that wants the other answer overwrites this stub. | |
| 818 | + | write_executable( | |
| 819 | + | &bin.join("curl"), | |
| 820 | + | "#!/bin/sh\nfor arg; do case \"$arg\" in http*) printf '%s\\n' \"$arg\" \ | |
| 821 | + | >> \"$(dirname \"$0\")/../curls\" ;; esac; done\nexit 22\n", | |
| 822 | + | ); | |
| 813 | 823 | write_executable( | |
| 814 | 824 | &root.join("quasi-type/target/release/quasi-type"), | |
| 815 | 825 | "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$(dirname \"$0\")/../../calls\"\n", | |
| @@ -825,13 +835,24 @@ | |||
| 825 | 835 | scratch | |
| 826 | 836 | } | |
| 827 | 837 | ||
| 828 | - | /// Run the cut block against a fake root, in one of the two modes. | |
| 838 | + | /// Run the cut block against a fake root, in one of the two modes, with no | |
| 839 | + | /// mirror named. | |
| 829 | 840 | fn run_cut(scratch: &Scratch, mode: &str) -> std::process::Output { | |
| 841 | + | run_cut_with_mirror(scratch, mode, "") | |
| 842 | + | } | |
| 843 | + | ||
| 844 | + | /// [`run_cut`] against a named mirror. | |
| 845 | + | /// | |
| 846 | + | /// Separate rather than a parameter on every call site, because a mirror is not | |
| 847 | + | /// what any of the seed tests are about and an empty one is what a build with | |
| 848 | + | /// `--build-arg QUASI_TYPE_MIRROR=` gets. | |
| 849 | + | fn run_cut_with_mirror(scratch: &Scratch, mode: &str, mirror: &str) -> std::process::Output { | |
| 830 | 850 | Command::new(shell()) | |
| 831 | 851 | .arg("-c") | |
| 832 | 852 | .arg(cut_script(scratch.path())) | |
| 833 | 853 | .env("QUASI_BASES", mode) | |
| 834 | 854 | .env("QUASI_TYPE_REV", "deadbeef") | |
| 855 | + | .env("QUASI_TYPE_MIRROR", mirror) | |
| 835 | 856 | .env("PATH", scratch.join("bin").to_string_lossy().to_string()) | |
| 836 | 857 | .output() | |
| 837 | 858 | .expect("running the cut block") | |
| @@ -890,6 +911,105 @@ | |||
| 890 | 911 | ); | |
| 891 | 912 | } | |
| 892 | 913 | ||
| 914 | + | /// The mirror is probed for every digest the pins carry, and for nothing else. | |
| 915 | + | /// | |
| 916 | + | /// The probe derives the digests from the pinned checkout's own `pins.toml` | |
| 917 | + | /// rather than naming them here, which is the same rule the seed's file names | |
| 918 | + | /// follow: a list in this repo would be a second copy of the pin and would go | |
| 919 | + | /// stale on the first base version bump, reporting a live mirror as a dead one. | |
| 920 | + | /// The fixture's four digests are the two faces and the two licence texts. | |
| 921 | + | #[test] | |
| 922 | + | fn the_mirror_is_asked_for_every_pinned_digest() { | |
| 923 | + | let scratch = cut_root("mirror-probe", &[]); | |
| 924 | + | let output = run_cut_with_mirror(&scratch, "fetch", "https://mirror.invalid/bases"); | |
| 925 | + | assert!( | |
| 926 | + | output.status.success(), | |
| 927 | + | "the mirror probe stopped the cut:\n{}", | |
| 928 | + | String::from_utf8_lossy(&output.stderr), | |
| 929 | + | ); | |
| 930 | + | ||
| 931 | + | let asked = std::fs::read_to_string(scratch.join("curls")).unwrap_or_default(); | |
| 932 | + | let mut urls: Vec<&str> = asked.lines().collect(); | |
| 933 | + | urls.sort_unstable(); | |
| 934 | + | assert_eq!( | |
| 935 | + | urls, | |
| 936 | + | [ | |
| 937 | + | "https://mirror.invalid/bases/1111111111111111111111111111111111111111111111111111111111111111", | |
| 938 | + | "https://mirror.invalid/bases/2222222222222222222222222222222222222222222222222222222222222222", | |
| 939 | + | "https://mirror.invalid/bases/3333333333333333333333333333333333333333333333333333333333333333", | |
| 940 | + | "https://mirror.invalid/bases/4444444444444444444444444444444444444444444444444444444444444444", | |
| 941 | + | ], | |
| 942 | + | "the probe asked for something other than the four digests the pins name", | |
| 943 | + | ); | |
| 944 | + | } | |
| 945 | + | ||
| 946 | + | /// A mirror that answers nothing is reported, not fatal. | |
| 947 | + | /// | |
| 948 | + | /// The whole argument for defaulting `QUASI_TYPE_MIRROR` to makenot.work is | |
| 949 | + | /// that it adds a source: every base still carries its upstream url and falls | |
| 950 | + | /// back to it, so a mirror that is down or not yet deployed is a slower first | |
| 951 | + | /// build. A build that failed here would turn a second source into a second | |
| 952 | + | /// dependency, which is the opposite of the point. | |
| 953 | + | #[test] | |
| 954 | + | fn a_mirror_that_holds_nothing_is_reported_rather_than_fatal() { | |
| 955 | + | let scratch = cut_root("mirror-empty", &PINNED); | |
| 956 | + | let output = run_cut_with_mirror(&scratch, "fetch", "https://mirror.invalid/bases"); | |
| 957 | + | assert!(output.status.success(), "an empty mirror failed the build"); | |
| 958 | + | ||
| 959 | + | let said = String::from_utf8_lossy(&output.stdout); | |
| 960 | + | assert!( | |
| 961 | + | said.contains("holds 0 of 4 pinned file(s)"), | |
| 962 | + | "the build did not say what the mirror holds, so a mirror nobody deployed \ | |
| 963 | + | reads the same as a live one:\n{said}", | |
| 964 | + | ); | |
| 965 | + | assert_eq!( | |
| 966 | + | cut_calls(scratch.path()).lines().count(), | |
| 967 | + | 2, | |
| 968 | + | "the cut did not run after the probe", | |
| 969 | + | ); | |
| 970 | + | } | |
| 971 | + | ||
| 972 | + | /// No mirror named, no request made, and the log says which it was. | |
| 973 | + | /// | |
| 974 | + | /// `--build-arg QUASI_TYPE_MIRROR=` is the documented way to build against | |
| 975 | + | /// upstream alone, and a probe that ran anyway would be a request a person | |
| 976 | + | /// asked not to make. | |
| 977 | + | #[test] | |
| 978 | + | fn an_empty_mirror_is_not_probed() { | |
| 979 | + | let scratch = cut_root("mirror-off", &PINNED); | |
| 980 | + | let output = run_cut(&scratch, "fetch"); | |
| 981 | + | assert!(output.status.success(), "the unmirrored cut failed"); | |
| 982 | + | ||
| 983 | + | assert!( | |
| 984 | + | !scratch.join("curls").exists(), | |
| 985 | + | "a build with no mirror still made a request", | |
| 986 | + | ); | |
| 987 | + | assert!( | |
| 988 | + | String::from_utf8_lossy(&output.stdout).contains("mirror: not consulted"), | |
| 989 | + | "the build did not say the mirror was skipped", | |
| 990 | + | ); | |
| 991 | + | } | |
| 992 | + | ||
| 993 | + | /// `sealed` names no mirror either, because a sealed cut fetches nothing. | |
| 994 | + | /// | |
| 995 | + | /// The probe would be a request in the one mode whose point is that there are | |
| 996 | + | /// none, and its answer could not change anything: `--offline` means a file the | |
| 997 | + | /// seed is short of fails rather than being fetched from anywhere. | |
| 998 | + | #[test] | |
| 999 | + | fn a_sealed_cut_does_not_probe_the_mirror() { | |
| 1000 | + | let scratch = cut_root("mirror-sealed", &PINNED); | |
| 1001 | + | let output = run_cut_with_mirror(&scratch, "sealed", "https://mirror.invalid/bases"); | |
| 1002 | + | assert!( | |
| 1003 | + | output.status.success(), | |
| 1004 | + | "the sealed cut failed:\n{}", | |
| 1005 | + | String::from_utf8_lossy(&output.stderr), | |
| 1006 | + | ); | |
| 1007 | + | assert!( | |
| 1008 | + | !scratch.join("curls").exists(), | |
| 1009 | + | "a sealed build probed a mirror it may not fetch from", | |
| 1010 | + | ); | |
| 1011 | + | } | |
| 1012 | + | ||
| 893 | 1013 | /// `sealed` is the mode with a guarantee: the cut may not reach the network. | |
| 894 | 1014 | /// With every pinned file seeded it runs, and both slots are cut offline. | |
| 895 | 1015 | #[test] |