Build commands got PATH alone on POSIX, so a compiler running under a
sandbox whose /tmp and /var/tmp are read-only walked libiberty's search
(TMPDIR, TMP, TEMP, then fixed
directories, then cwd) to its last entry,
and cwd is the Tupfile's source directory. GCC deletes those files on a
normal exit and not on SIGKILL, so an interrupted -j8 build left one
ccXXXXXX.s per in-flight job in the source tree; link steps leave
collect2's .res and .cdtor.* the same way, which -pipe cannot prevent.
Reproduced here under a read-only /tmp: strace on the old binary shows
g++ opening ./cc8Z8hOV.s beside x.cc, and on the new one
$TMPDIR/ccepYMwl.s.
base_child_env now appends TMPDIR, TMP and TEMP when putup's own
environment sets them to a non-empty value and nothing otherwise; an
empty value is dropped because tools that test only the pointer (sort,
tar) would use "" as the directory where before they saw no variable. The three are the
names gcc 14's compile and collect2's link both read, measured by
strace with each set alone; TEMPDIR is not forwarded because gcc
ignores it and clang, which reads it, falls back to a fixed /tmp rather
than cwd. Windows is unchanged: its keep list already carries TEMP and
TMP, which the platform always sets ahead of any cwd fallback, so a
Windows user with only TMPDIR degrades to TMP rather than to the source
tree. Letting putup choose a directory under the build dir was
rejected: it overrides a value the user set for a reason (LTO
temporaries are large enough that the disk is a choice), adds a mkdir
and its failure path, and leaves nobody to delete the directory.
The forwarded value stays out of command identity. Only exported
variables get a sticky edge, and `export TMPDIR` would fold the path
into every command, so a sandbox that hands each session a fresh
TMPDIR would rebuild the world every session; that is why "export it"
was not the fix. A rule whose text reads $TMPDIR therefore keeps stale
output across a TMPDIR change, and the new no-op scenario asserts
exactly that as the fence against a later "fix" that adds the edge.
That requirement (REQ-ENV-TEMPDIR-IDENTITY) is conditioned on the
Tupfile not exporting or importing the variable, since export folds it
in by REQ-ENV-SUBPROCESS; it is vacuously true before this change; its scenario goes red on the forwarding assertion, not on
identity. `export TMPDIR` in a Tupfile emits the name twice with the
same value, as `export PATH` already does.
docs/reference.md claimed the minimal environment keeps builds hermetic
against ambient shell state. It did not before this change: a rule
reading $PATH keeps its output across a PATH change and putup reports
nothing to do. The paragraph now states which variables are forwarded,
that only exported ones are recorded, and that `export` is the lever
when a value must decide staleness. PATH's exclusion from identity has
no requirement of its own; tup, whose default list also carries HOME,
re-runs every command when a default variable changes, and putup's
divergence there is noted in the new requirement's reference rather
than settled here.
Ref: #478
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fixes #478.
Build commands got
PATHalone on POSIX, so under a sandbox with a read-only/tmpand/var/tmpGCC's temp-dir search ended at cwd, which is the Tupfile's source directory; a killed parallel build left oneccXXXXXX.sper in-flight job there, and link steps leave collect2's.res/.cdtor.*the same way.base_child_envnow forwardsTMPDIR,TMPandTEMPfrom putup's own environment when set to a non-empty value, and nothing when unset or empty (an empty value would make pointer-only tools such assortuse""as the directory). Windows is unchanged (its keep list already carriesTEMP/TMP). The forwarded values stay out of command identity, so a per-sessionTMPDIRdoes not rebuild the world; a new scenario pins that.Verified
/tmp,strace -f -e openaton ag++ -crule: pre-fix binary opens./cc8Z8hOV.sbeside the source; fixed binary opens$TMPDIR/ccepYMwl.s.TMPDIR=empty in the command's environment); GREEN after.make test: 940 test cases, 32 e2e shards, all passed.make format,make tidy,make iwyu, spec-check all exit 0.clang-cl /W4 /WX, xwin splat). Not exercised here: running the Windows pin section of the unit test (assertsTEMP/TMPpresent and noTMPDIR); the Windows CI job is its first run.Design
A design-attorney pass (Opus) and an adversarial judge (Opus) settled the shape before implementation. Rejected: putup choosing a directory under the build dir (overrides a user's disk choice, adds a mkdir and failure path, nobody deletes it); telling users to
export TMPDIR(folds the path into every command's identity, rebuild per session); forwardingTEMPDIR(gcc ignores it; clang reads it but falls back to a fixed/tmp, never cwd); addingTMPDIRto the Windows list (Windows always setsTEMP/TMPahead of any cwd fallback).Docs and spec
docs/reference.mdclaimed the minimal environment keeps builds hermetic against ambient shell state. That was already false forPATH(a rule reading$PATHkeeps stale output across a PATH change, "Nothing to do"). The paragraph now states which variables are forwarded, that only exported ones are recorded, and thatexportis the lever when a value must decide staleness.command-record.ears.mdenv-values:REQ-ENV-TEMPDIR(deliberate deviation from tup'sdefault_env[], which forwardsPATH+HOMEon POSIX andTEMP/TMPonly on Windows) andREQ-ENV-TEMPDIR-IDENTITY. The identity requirement is vacuously true pre-fix; its scenario goes red on the forwarding assertion.Review round (Opus break-it + adversarial verify, 22 agents): 19 findings, 16 confirmed after dedup to 10 distinct, all applied: unused lambda broke the
/W4 /WXWindows cross-build (the first push'sbuild-windowsfailure); set-but-empty values were forwarded asVAR=; both new requirements were platform-unqualified and the identity one was unconditional againstexport; the docs paragraph droppedPATHfrom the Windows list and overclaimed "re-runs nothing" againstCONFIG_TRACKED_TOOLS; the tupdefault_env[]citation understated its Windows block; the Windows test section matched names case-sensitively against a case-insensitive keep list; the commit body's libiberty search order was wrong.Found, not fixed
PATHis forwarded and excluded from identity with no requirement of its own; tup re-runs every command when a default variable changes.HOME; putup does not (noted in the new requirement's reference, not settled here).///doc-header lines under the existing header convention, 0 other comments.🤖 Generated with Claude Code
https://claude.ai/code/session_01StgwMENEyfBnEoe4pvAdtQ