Skip to content

process: forward TMPDIR, TMP and TEMP to build commands - #479

Merged
typeless merged 1 commit into
mainfrom
fix/478
Sep 20, 2026
Merged

typeless merged 1 commit into
mainfrom
fix/478

Conversation

@typeless

@typeless typeless commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Fixes #478.

Build commands got PATH alone on POSIX, so under a sandbox with a read-only /tmp and /var/tmp GCC's temp-dir search ended at cwd, which is the Tupfile's source directory; a killed parallel build left one ccXXXXXX.s per in-flight job there, and link steps leave collect2's .res/.cdtor.* the same way.

base_child_env now forwards TMPDIR, TMP and TEMP from 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 as sort use "" as the directory). Windows is unchanged (its keep list already carries TEMP/TMP). The forwarded values stay out of command identity, so a per-session TMPDIR does not rebuild the world; a new scenario pins that.

Verified

  • Reproduction under this sandbox's read-only /tmp, strace -f -e openat on a g++ -c rule: pre-fix binary opens ./cc8Z8hOV.s beside the source; fixed binary opens $TMPDIR/ccepYMwl.s.
  • RED observed for all three new tests before the fix (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.
  • The changed test TU cross-compiles clean with the Windows CI flags (clang-cl /W4 /WX, xwin splat). Not exercised here: running the Windows pin section of the unit test (asserts TEMP/TMP present and no TMPDIR); 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); forwarding TEMPDIR (gcc ignores it; clang reads it but falls back to a fixed /tmp, never cwd); adding TMPDIR to the Windows list (Windows always sets TEMP/TMP ahead of any cwd fallback).

Docs and spec

  • docs/reference.md claimed the minimal environment keeps builds hermetic against ambient shell state. That was already false for PATH (a rule reading $PATH keeps stale output across a PATH change, "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.
  • Two requirements added to command-record.ears.md env-values: REQ-ENV-TEMPDIR (deliberate deviation from tup's default_env[], which forwards PATH+HOME on POSIX and TEMP/TMP only on Windows) and REQ-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 /WX Windows cross-build (the first push's build-windows failure); set-but-empty values were forwarded as VAR=; both new requirements were platform-unqualified and the identity one was unconditional against export; the docs paragraph dropped PATH from the Windows list and overclaimed "re-runs nothing" against CONFIG_TRACKED_TOOLS; the tup default_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

  • PATH is forwarded and excluded from identity with no requirement of its own; tup re-runs every command when a default variable changes.
  • tup also forwards HOME; putup does not (noted in the new requirement's reference, not settled here).
  • Comment sweep of added lines: 6 /// doc-header lines under the existing header convention, 0 other comments.

🤖 Generated with Claude Code

https://claude.ai/code/session_01StgwMENEyfBnEoe4pvAdtQ

@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

PR metrics

Performance (gcc example, Linux)

Workload Instructions CPU time Page faults D1 miss LL miss Wall Peak RSS
parse 1685 M 0.36 s 15.1 k 0.5% 0% 0.355 s 34.7 MB (-0.1MB)
dry-run 2340 M 0.42 s 16.6 k 0.6% 0% 0.43 s 40.5 MB

Deterministic signals: instructions (cachegrind-simulated instruction reads — exact across runs, no PMU needed), page faults, peak RSS, and the cachegrind D1/LL miss rates. CPU time is user+sys from time(1).

Internal statistics (gcc example, up-to-date dry run)

Metric Value
Tupfiles parsed 24
Commands 3545
Commands scheduled 0
Files checked 5834
Files changed 0
Files in index 6173
Graph edges 381415
Index size (bytes) 7811994
Implicit deps 344126
Hash computations 197
Hashes skipped (stat cache) 5636
Stat calls 5885
Parse time (ms) 286.3 (+8.6%)
Total time (ms) 414.5 (+9.9%)
Runner CPU INTEL(R) XEON(R) PLATINUM 8573C

Counters from putup -n --stat on the fully-built gcc example (up-to-date dry run): deterministic work measures — a jump in commands scheduled, hash computations, or stat calls is a real behavior change, not noise. Timings are the minimum over repeated runs, compared only against a baseline from the same CPU model; the counters are the regression signal.

Binary size (Linux)

Binary .text .data .bss File
putup 599.6 KB (+0.1%) 2.3 KB 98.8 KB 711.4 KB

Code churn (whole codebase, last 30d)

Files Lines written Still present Churned Churn rate
30 2338 2015 323 13.8%

Of the lines written across the codebase in the last 30 days, how many are already gone — work that was written and then discarded or rewritten inside the same window. This is the state of the tree including this PR, not a measure of the PR itself. Only code we write is counted: tests, examples, vendored and generated files, CI plumbing and prose are excluded. 1585 lines were deleted in the window in total, most of them older than it.

Where the churn is
File Lines written then discarded
src/parser/eval.cpp 77
src/graph/builder.cpp 69
src/graph/dag.cpp 53
src/index/entry.cpp 44
include/pup/core/token_list.hpp 23
include/pup/parser/eval.hpp 16
src/cli/cmd_build.cpp 8
src/index/reader.cpp 8
include/pup/core/instruction.hpp 7
src/core/instruction.cpp 7

Test coverage (lines)

Overall Median file Min file Max file
88.8% 96.9% 14.7% include/pup/parser/token.hpp 100.0% include/pup/core/arena.hpp

105 files · 17811/20066 lines covered

Deltas vs main@dca0140d9.

Updated for 62747aa

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>
@typeless
typeless merged commit 75c93ee into main Sep 20, 2026
13 checks passed
@typeless
typeless deleted the fix/478 branch September 20, 2026 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build commands get PATH only, so TMPDIR is dropped and GCC writes temporaries into the source tree when /tmp is read-only

1 participant