Skip to content

Commit 91066be

Browse files
yinlianghuiclaude
andauthored
fix(scripts): make the R7a bash-3.2 control hold on a host that IS bash 3.2 (#12298)
R7a probed `mapfile` itself to prove the BASH_ENV harness really removes a builtin. On stock macOS `/bin/bash` is 3.2, so `mapfile` is absent from the PLAIN run too — `plain=` came back empty exactly like `sim.out=`, the assertion could not be satisfied by any correct harness, and `check:objectui-changeset` sat red on every macOS seat while CI (ubuntu-latest, bash 5) stayed green. Option B: the probe builtin is now chosen at RUNTIME from bash 2.x-era builtins the host itself reports as builtins and that have no external `/usr/bin` twin, so `enable -n` on one yields the same 127 "command not found" a missing bash-4 builtin yields on 3.2. No skip leg: a host offering no qualifying builtin reddens loudly. A second leg, R7a2, asserts the state R7b actually depends on — mapfile unavailable in the R7b shell — which holds on both hosts for different reasons, so neither host can pass R7b vacuously. `scripts/bump-objectui.sh` is untouched; R7/R7b/R7c still redden when a bash-4 builtin is reintroduced into it. Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ce5f8c commit 91066be

1 file changed

Lines changed: 75 additions & 7 deletions

File tree

scripts/objectui-changeset-digest.mjs

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2770,22 +2770,90 @@ function selfTest() {
27702770
// script under test inherits it through `spawnSync`.
27712771
const noBash4 = join(tmp, 'no-bash4-builtins.sh');
27722772
writeFileSync(noBash4, 'enable -n mapfile readarray 2>/dev/null\n');
2773+
27732774
// The instrument must not be vacuous. If BASH_ENV were ever ignored (a
27742775
// posix-mode bash, a future harness change), R7b would pass by proving
27752776
// nothing, so the disabling is measured on a probe FIRST, both ways.
2777+
//
2778+
// ⭐ #12254 — that probe used to be `mapfile` ITSELF, which made this leg a
2779+
// GUARANTEED FAIL on the one platform it exists to model. On stock macOS
2780+
// `/bin/bash` IS 3.2, so `mapfile` is missing from the plain run too: the
2781+
// leg reported `plain=` empty exactly like `sim.out=`, "the simulation
2782+
// removed something" could not be satisfied by any correct harness, and
2783+
// `check:objectui-changeset` sat red on every macOS seat while CI
2784+
// (ubuntu-latest, bash 5) stayed green — so nothing would ever catch it.
2785+
// A control written to stop a vacuous pass had become a platform-
2786+
// conditional failure, which is the SAME error family one level up: a
2787+
// check whose own precondition was never measured across the population of
2788+
// hosts it runs on.
2789+
//
2790+
// The fix is to stop hardcoding a belief about another platform's builtin
2791+
// table and ASK THE HOST. `probeBuiltin` is chosen at runtime from bash
2792+
// 2.x-era builtins that (a) this shell reports as a builtin and (b) have no
2793+
// external `/usr/bin` twin to fall through to — so `enable -n` on one
2794+
// produces the very same 127 "command not found" that a missing bash-4
2795+
// builtin produces on 3.2, and the leg measures the HARNESS instead of the
2796+
// platform. Deliberately not a skip: a host that offers no qualifying
2797+
// builtin fails this leg loudly, because a control that quietly does
2798+
// nothing is the exact shape R7a exists to prevent.
2799+
const probeCandidates = ['shopt', 'caller', 'compgen', 'hash', 'umask'];
2800+
const pickProbe = spawnSync(
2801+
'bash',
2802+
[
2803+
'-c',
2804+
'for b in "$@"; do ' +
2805+
'[ "$(type -t "$b" 2>/dev/null)" = builtin ] || continue; ' +
2806+
'[ -n "$(type -P "$b" 2>/dev/null)" ] && continue; ' +
2807+
'printf %s "$b"; exit 0; done; exit 1',
2808+
'pick-probe-builtin',
2809+
...probeCandidates,
2810+
],
2811+
{ encoding: 'utf8' },
2812+
);
2813+
const probeBuiltin = pickProbe.stdout.trim();
2814+
const builtinProbe = join(tmp, 'builtin-probe.sh');
2815+
writeFileSync(builtinProbe, `${probeBuiltin} >/dev/null\nprintf 'PROBE-STATUS=%s\\n' "$?"\n`);
2816+
const noProbeBuiltin = join(tmp, 'no-probe-builtin.sh');
2817+
writeFileSync(noProbeBuiltin, `enable -n ${probeBuiltin} 2>/dev/null\n`);
2818+
const probePlain = spawnSync('bash', [builtinProbe], { encoding: 'utf8' });
2819+
const probeSim = spawnSync('bash', [builtinProbe], {
2820+
encoding: 'utf8',
2821+
env: { ...process.env, BASH_ENV: noProbeBuiltin },
2822+
});
2823+
const probeStatus = (r) => (r.stdout.match(/PROBE-STATUS=(\d+)/) ?? [])[1];
2824+
check(
2825+
'#12071/#12254 R7a the BASH_ENV harness really removes a builtin THIS host has (else R7b proves nothing)',
2826+
probeBuiltin !== '' &&
2827+
probeStatus(probePlain) !== undefined &&
2828+
probeStatus(probePlain) !== '127' &&
2829+
probeStatus(probeSim) === '127' &&
2830+
probeSim.stderr.includes(probeBuiltin),
2831+
`builtin=${probeBuiltin || `<none of ${probeCandidates.join(',')} qualified on this host>`} ` +
2832+
`plain=${probePlain.stdout.trim()} sim=${probeSim.stdout.trim()} sim.err=${probeSim.stderr.trim()}`,
2833+
);
2834+
2835+
// …and separately, the state R7b actually depends on: in the shell R7b
2836+
// runs, `mapfile` is unavailable. This holds on BOTH hosts and for
2837+
// DIFFERENT reasons — `enable -n` removed it on bash 5, it was never there
2838+
// on bash 3.2 — which is why it is its own leg rather than folded into the
2839+
// harness leg above: neither leg alone is unconditional, and together they
2840+
// leave no host on which R7b can pass vacuously. The detail line names
2841+
// which way this host got there, so a macOS operator reading a GREEN run
2842+
// still learns that their `/bin/bash` is the genuine 3.2 article.
27762843
const mapfileProbe = join(tmp, 'mapfile-probe.sh');
27772844
writeFileSync(mapfileProbe, 'mapfile -t x < <(printf "a\\n") && echo MAPFILE-WORKS\n');
2778-
const probePlain = spawnSync('bash', [mapfileProbe], { encoding: 'utf8' });
2779-
const probeSim = spawnSync('bash', [mapfileProbe], {
2845+
const mapfilePlain = spawnSync('bash', [mapfileProbe], { encoding: 'utf8' });
2846+
const mapfileSim = spawnSync('bash', [mapfileProbe], {
27802847
encoding: 'utf8',
27812848
env: { ...process.env, BASH_ENV: noBash4 },
27822849
});
2850+
const hostHasMapfile = mapfilePlain.stdout.includes('MAPFILE-WORKS');
27832851
check(
2784-
'#12071 R7a the simulated-3.2 harness really removes the builtin (else R7b proves nothing)',
2785-
probePlain.stdout.includes('MAPFILE-WORKS') &&
2786-
!probeSim.stdout.includes('MAPFILE-WORKS') &&
2787-
probeSim.stderr.includes('mapfile'),
2788-
`plain=${probePlain.stdout.trim()} sim.out=${probeSim.stdout.trim()} sim.err=${probeSim.stderr.trim()}`,
2852+
'#12071/#12254 R7a2 … and in the R7b shell mapfile is gone, however this host got there',
2853+
!mapfileSim.stdout.includes('MAPFILE-WORKS') && mapfileSim.stderr.includes('mapfile'),
2854+
`host-has-mapfile=${hostHasMapfile} (${
2855+
hostHasMapfile ? 'bash 4+, so enable -n has to remove it' : 'host bash is already 3.2-era, nothing to remove'
2856+
}) sim.out=${mapfileSim.stdout.trim()} sim.err=${mapfileSim.stderr.trim()}`,
27892857
);
27902858

27912859
// The R2 shape again — pushed, never merged — through a shell that has no

0 commit comments

Comments
 (0)