max / alloy
1 file changed,
+98 insertions,
-5 deletions
| @@ -28,7 +28,12 @@ | |||
| 28 | 28 | # left is the toolchain layer, shop, and the console. | |
| 29 | 29 | # | |
| 30 | 30 | # Usage: | |
| 31 | - | # build/check-rust-stage.sh # build it; say what happened | |
| 31 | + | # build/check-rust-stage.sh # build it; say what happened | |
| 32 | + | # build/check-rust-stage.sh --self-test # check the classifiers; builds nothing | |
| 33 | + | # | |
| 34 | + | # The self-test exists because the classifiers are the part of this script that | |
| 35 | + | # fails invisibly: a wrong verdict still looks like a verdict. Run it after | |
| 36 | + | # touching any of the three patterns. | |
| 32 | 37 | # | |
| 33 | 38 | # Run it after moving SHOP_REV, before pushing. The astra sweep runs it nightly | |
| 34 | 39 | # (`rust-stage` in sweep.toml), which is the backstop for when nobody does. | |
| @@ -59,6 +64,17 @@ | |||
| 59 | 64 | # that fills a disk quietly. | |
| 60 | 65 | IMAGE="localhost/alloy-rust-stage:check" | |
| 61 | 66 | ||
| 67 | + | # The three patterns this script classifies on, named so `--self-test` can | |
| 68 | + | # assert on them. Editing one without running the self-test is how the defect | |
| 69 | + | # below came back. | |
| 70 | + | # | |
| 71 | + | # CONTAINERFILE_ECHO matches lines podman wrote out of the Containerfile rather | |
| 72 | + | # than out of the build. Everything else is matched only against what survives | |
| 73 | + | # stripping these. | |
| 74 | + | CONTAINERFILE_ECHO='^(STEP [0-9]+/[0-9]+:|Error: building at STEP |\+ )' | |
| 75 | + | NETWORK='cannot reach .* to check SHOP_REV|serves zero refs|no such host|temporary failure in name resolution|connection refused|i/o timeout|TLS handshake timeout|error pinging container registry' | |
| 76 | + | MISSING_LIB='could not find .* pkg-config|pkg-config .* not found|No package .* found|PKG_CONFIG_PATH|cannot find -l[a-z]' | |
| 77 | + | ||
| 62 | 78 | # Exit 3, never 1: 1 is reserved for "the image does not build", and a caller | |
| 63 | 79 | # that cannot tell a broken build from a broken network learns the wrong thing | |
| 64 | 80 | # from both. | |
| @@ -66,8 +82,55 @@ | |||
| 66 | 82 | ||
| 67 | 83 | trap 'rc=$?; [ "$rc" -eq 3 ] && exit 3; die "unexpected failure at line ${LINENO}"' ERR | |
| 68 | 84 | ||
| 85 | + | # Fixtures are real: every line below was copied from | |
| 86 | + | # ~/sweep-runs/2026-08-11T224229Z on astra, the run that misclassified. | |
| 87 | + | self_test() { | |
| 88 | + | local fails=0 tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN | |
| 89 | + | ||
| 90 | + | # The regression. STEP 4's header carries the SHOP_REV guard's own source, | |
| 91 | + | # with `$url` unexpanded; STEP 17 is the real failure. Classifying on the raw | |
| 92 | + | # log calls this a network problem and throws the finding away. | |
| 93 | + | cat > "$tmp/misclassified" <<'FIXTURE' | |
| 94 | + | STEP 4/21: RUN set -eu; url=https://makenot.work/git/max/shop.git; refs=$(git ls-remote "$url" 2>/dev/null) || { echo "cannot reach $url to check SHOP_REV" >&2; exit 1; } | |
| 95 | + | expected one makeover themes dir, got: /root/.cargo/registry/src/i/makeover-2.4.1/themes /root/.cargo/registry/src/i/makeover-2.5.0/themes | |
| 96 | + | Error: building at STEP "RUN set -eux; set -- /root/.cargo/registry/src/*/makeover-*/themes": while running runtime: exit status 1 | |
| 97 | + | FIXTURE | |
| 98 | + | ||
| 99 | + | # The guard actually firing. The URL is expanded, which is the whole | |
| 100 | + | # difference between the message and the code that would print it. | |
| 101 | + | cat > "$tmp/really_unreachable" <<'FIXTURE' | |
| 102 | + | STEP 4/21: RUN set -eu; url=https://makenot.work/git/max/shop.git; || { echo "cannot reach $url to check SHOP_REV" >&2; exit 1; } | |
| 103 | + | cannot reach https://makenot.work/git/max/shop.git to check SHOP_REV | |
| 104 | + | FIXTURE | |
| 105 | + | ||
| 106 | + | # The same trap one classifier down: a dnf or ENV line naming PKG_CONFIG_PATH | |
| 107 | + | # is source, not a probe that failed. | |
| 108 | + | cat > "$tmp/env_mentions_pkgconfig" <<'FIXTURE' | |
| 109 | + | STEP 3/21: RUN dnf install -y pkgconf && export PKG_CONFIG_PATH=/usr/lib64/pkgconfig | |
| 110 | + | error[E0433]: failed to resolve: use of undeclared crate or module `foo` | |
| 111 | + | FIXTURE | |
| 112 | + | ||
| 113 | + | check() { # name file pattern expect(yes|no) | |
| 114 | + | local got=no | |
| 115 | + | grep -vE "$CONTAINERFILE_ECHO" "$2" | grep -qEi "$3" && got=yes | |
| 116 | + | if [ "$got" != "$4" ]; then | |
| 117 | + | printf 'self-test FAIL: %s expected %s, got %s\n' "$1" "$4" "$got" >&2 | |
| 118 | + | fails=$((fails + 1)) | |
| 119 | + | fi | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | check "a real build failure is not a network failure" "$tmp/misclassified" "$NETWORK" no | |
| 123 | + | check "an unreachable remote still reads as one" "$tmp/really_unreachable" "$NETWORK" yes | |
| 124 | + | check "a real build failure is not a missing lib" "$tmp/misclassified" "$MISSING_LIB" no | |
| 125 | + | check "PKG_CONFIG_PATH in a RUN line is not a probe" "$tmp/env_mentions_pkgconfig" "$MISSING_LIB" no | |
| 126 | + | ||
| 127 | + | [ "$fails" -eq 0 ] || { printf '%d self-test failure(s)\n' "$fails" >&2; exit 1; } | |
| 128 | + | echo "self-test: 4 passed" | |
| 129 | + | } | |
| 130 | + | ||
| 69 | 131 | case "${1:-}" in | |
| 70 | - | -h|--help) sed -n '2,48p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; | |
| 132 | + | --self-test) self_test; exit 0 ;; | |
| 133 | + | -h|--help) sed -n '2,55p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; | |
| 71 | 134 | "") ;; | |
| 72 | 135 | *) die "unknown argument: $1 (see --help)" ;; | |
| 73 | 136 | esac | |
| @@ -82,7 +145,33 @@ | |||
| 82 | 145 | printf 'building the rust-build stage; SHOP_REV=%s\n' "$SHOP_REV" | |
| 83 | 146 | ||
| 84 | 147 | log="$(mktemp -t alloy-rust-stage.XXXXXX.log)" | |
| 85 | - | trap 'rm -f "$log"' EXIT | |
| 148 | + | # What the build said, with what the Containerfile says stripped out. See | |
| 149 | + | # `classify_on_output_only` below for why the two have to be separated. | |
| 150 | + | out="$(mktemp -t alloy-rust-stage.XXXXXX.out)" | |
| 151 | + | trap 'rm -f "$log" "$out"' EXIT | |
| 152 | + | ||
| 153 | + | # Podman writes the Containerfile's own source into the build log alongside the | |
| 154 | + | # build's output: once per `STEP n/N:` header, again inside | |
| 155 | + | # `Error: building at STEP "..."` when a RUN fails, and once per traced command | |
| 156 | + | # as `+ ...` under `set -x`. So the log holds both the failures and the code | |
| 157 | + | # that would report them, and a grep over the whole thing matches whichever | |
| 158 | + | # comes first. | |
| 159 | + | # | |
| 160 | + | # That is not hypothetical. On 2026-08-11 this cell hit a real build failure -- | |
| 161 | + | # two makeover versions in the shared registry, STEP 17 -- and reported it as a | |
| 162 | + | # network failure, because STEP 4's header carries the literal | |
| 163 | + | # `cannot reach $url to check SHOP_REV` from the SHOP_REV guard. The `$url` sits | |
| 164 | + | # there unexpanded, which is the tell. The cell scored `error` instead of | |
| 165 | + | # `fail`, and `error` means the check learned nothing about the code, so | |
| 166 | + | # witchbroom excluded a genuine defect from grading entirely. | |
| 167 | + | # | |
| 168 | + | # Every classifier below therefore reads `$out`, never `$log`. A line podman | |
| 169 | + | # echoed out of the Containerfile is not evidence about the build, and no | |
| 170 | + | # wording chosen for these patterns can be safe while the source they describe | |
| 171 | + | # is in the same file being searched. | |
| 172 | + | classify_on_output_only() { | |
| 173 | + | grep -vE "$CONTAINERFILE_ECHO" "$log" > "$out" || true | |
| 174 | + | } | |
| 86 | 175 | ||
| 87 | 176 | status=0 | |
| 88 | 177 | podman build \ | |
| @@ -100,13 +189,17 @@ | |||
| 100 | 189 | ||
| 101 | 190 | # Failed. Which kind of failed is the whole value of this script: a red cell | |
| 102 | 191 | # saying "podman exited 1" costs a person the build they are trying to avoid. | |
| 192 | + | classify_on_output_only | |
| 103 | 193 | ||
| 104 | 194 | # The evidence-hole cases first. Each of these is something outside the image: | |
| 105 | 195 | # the registry serving the pinned base, or the git remote serving shop. The | |
| 106 | 196 | # ls-remote wording is the Containerfile's own guard talking (it prints | |
| 107 | 197 | # `cannot reach ... to check SHOP_REV`), and the zero-refs case is the one that | |
| 108 | 198 | # already happened once, recorded there against GoingsOn problem 49732815. | |
| 109 | - | if grep -qEi 'cannot reach .* to check SHOP_REV|serves zero refs|no such host|temporary failure in name resolution|connection refused|i/o timeout|TLS handshake timeout|error pinging container registry' "$log"; then | |
| 199 | + | # | |
| 200 | + | # Reading `$out` rather than `$log` is what keeps that first pattern from | |
| 201 | + | # matching the guard's own source instead of the guard firing. | |
| 202 | + | if grep -qEi "$NETWORK" "$out"; then | |
| 110 | 203 | die "the build could not reach something it needs (registry or git remote), so this says nothing about the image" | |
| 111 | 204 | fi | |
| 112 | 205 | ||
| @@ -117,7 +210,7 @@ | |||
| 117 | 210 | # reaching a linker or a pkg-config probe has got past the network and past | |
| 118 | 211 | # cargo's resolver, so it is a missing header on the dnf line until proven | |
| 119 | 212 | # otherwise. | |
| 120 | - | if grep -qEi 'could not find .* pkg-config|pkg-config .* not found|No package .* found|PKG_CONFIG_PATH|cannot find -l[a-z]' "$log"; then | |
| 213 | + | if grep -qEi "$MISSING_LIB" "$out"; then | |
| 121 | 214 | cat <<'EOF' | |
| 122 | 215 | ||
| 123 | 216 | It looks like a missing system library rather than a code error: a build script |