diff --git a/scripts/_builder.sh b/scripts/_builder.sh index aae8ea1..944ce5d 100644 --- a/scripts/_builder.sh +++ b/scripts/_builder.sh @@ -438,15 +438,21 @@ builder_dispatch() { # value we actually resolved, which is not necessarily the one in the environment. local want_native; want_native="$(builder_clean_value "${DUPLO_BUILD_NATIVE:-$(_envv DUPLO_BUILD_NATIVE)}")" - # Resolve the CLI before probing. Failure is only fatal when a runtime was NAMED: the caller said - # which one to use, and quietly building with a different one — or dropping to a native build they - # did not ask for — is not what they asked for. Auto-detect finding nothing stays silent, because - # builder_mode's native fallback below still builds on a machine with the full toolchain, and that is - # what keeps a runtime-less CI runner working. The re-run is to print the specific reason, which was - # suppressed on the first call. - runtime_resolve 2>/dev/null || { - if [ -n "$(runtime_requested)" ]; then runtime_resolve; exit 1; fi - } + # Resolve the CLI before probing — but only when native wasn't already forced. want_native=1 outranks + # everything in builder_mode below, so a runtime that fails to resolve at this point (e.g. RUNTIME=podman + # read back out of the bind-mounted .env while running *inside* the builder container, which has no + # reason to carry a podman binary) must not be fatal: nothing downstream is going to use it. + # + # Failure is only fatal when a runtime was NAMED: the caller said which one to use, and quietly building + # with a different one — or dropping to a native build they did not ask for — is not what they asked + # for. Auto-detect finding nothing stays silent, because builder_mode's native fallback below still + # builds on a machine with the full toolchain, and that is what keeps a runtime-less CI runner working. + # The re-run is to print the specific reason, which was suppressed on the first call. + if [ "$want_native" != 1 ]; then + runtime_resolve 2>/dev/null || { + if [ -n "$(runtime_requested)" ]; then runtime_resolve; exit 1; fi + } + fi local mode; mode="$(builder_mode \ "$want_native" \ diff --git a/tests/test-builder-dispatch.sh b/tests/test-builder-dispatch.sh new file mode 100755 index 0000000..85626ca --- /dev/null +++ b/tests/test-builder-dispatch.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Checks builder_dispatch's runtime-resolution guard: a forced-native build (DUPLO_BUILD_NATIVE=1, which +# is exactly what the builder container's own compose environment sets — see docker-compose.yml's +# `builder` service) must never fail because of a $RUNTIME value it has no intention of using. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 +PASS=0; FAIL=0 +t() { printf ' %s … ' "$1"; } +ok() { echo "ok"; PASS=$((PASS+1)); } +bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } + +echo "builder_dispatch native-skip guard:" + +# dispatch_rc +# +# Returns (as its own exit status — never captured via $(), since the code under test calls `exit` +# directly on its failure path, which would terminate a command-substitution subshell before any +# echo-the-exit-code line inside it ever ran) whatever builder_dispatch itself exits with. +# +# `command -v podman` is shadowed to simulate the builder container, which has no reason to carry a +# podman binary even when the bind-mounted .env the re-entrant build reads says RUNTIME=podman. +dispatch_rc() { + ( RV="$1"; NATIVE="$2"; HASPODMAN="$3" + export DUPLO_ENV_FILE=/dev/null + [ -n "$RV" ] && export RUNTIME="$RV" || unset RUNTIME + [ -n "$NATIVE" ] && export DUPLO_BUILD_NATIVE="$NATIVE" || unset DUPLO_BUILD_NATIVE + command() { + if [ "${1-}" = -v ] && [ "${2-}" = podman ] && [ "$HASPODMAN" != 1 ]; then return 1; fi + builtin command "$@" + } + . ./scripts/_runtime.sh + . ./scripts/_builder.sh + builder_probe_in_container() { echo "$NATIVE"; } + builder_probe_toolchain() { echo 1; } + builder_probe_runtime() { echo 0; } + builder_dispatch some-script.sh some-dir + ) >/dev/null 2>&1 +} + +t "a forced-native build is unaffected by a \$RUNTIME it cannot resolve" +# This is the exact failure reported from a real build: the builder container's own compose environment +# sets DUPLO_BUILD_NATIVE=1, but the whole repo — .env included — is bind-mounted into it, so the +# re-entrant build.sh invocation reads RUNTIME=podman straight off the host's real .env even though the +# builder image has no podman binary and never needed one. +dispatch_rc podman 1 0; RC=$? +[ "$RC" = 0 ] && ok || bad "forced-native build still fataled on an unresolvable \$RUNTIME (rc=$RC)" + +t "a forced-native build is unaffected when nothing is on PATH at all" +dispatch_rc "" 1 0; RC=$? +[ "$RC" = 0 ] && ok || bad "rc=$RC" + +t "a HOST build (not forced-native) still fatals when the named runtime cannot be resolved" +# The guard must be scoped to the forced-native case only — a developer on their own machine who pinned +# RUNTIME=podman without installing it still needs the real error, not a silent native fallback they +# never asked for. +dispatch_rc podman "" 0; RC=$? +[ "$RC" != 0 ] && ok || bad "a host build with a genuinely broken \$RUNTIME exited 0 — the guard is too broad" + +t "a HOST build with no \$RUNTIME pinned still resolves normally (auto-detect, not skipped)" +dispatch_rc "" "" 1; RC=$? +[ "$RC" = 0 ] && ok || bad "rc=$RC (auto-detect should have found podman and proceeded)" + +t "bash -n on the touched files" +if bash -n scripts/_builder.sh 2>/dev/null; then ok; else bad "syntax error"; fi + +echo +echo "$PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ]