Skip to content

process: forward HOME to build commands, and pin the forwarded set out of identity - #481

Merged
typeless merged 2 commits into
mainfrom
fix/480
Sep 20, 2026
Merged

typeless merged 2 commits into
mainfrom
fix/480

Conversation

@typeless

Copy link
Copy Markdown
Owner

Closes #480.

putup's default child environment diverged from tup's default_env[] in two ways that no requirement stated, so a reader diffing the two found them as defects rather than decisions. A rule reading $HOME saw an empty string, and a PATH change re-ran nothing.

HOME is forwarded

HOME joins the forwarded-when-set cohort #478 established: the value when putup's own environment holds a non-empty one, no entry at all when it is unset or empty, and no invented default, since none is correct for a per-user directory. The Windows keep list gains it too, so the requirement needs no platform scope.

What its absence breaks was measured rather than assumed:

  • go build refuses to run at all — build cache is required, but could not be located: GOCACHE is not defined and neither $XDG_CACHE_HOME nor $HOME are defined
  • dash, the /bin/sh a putup command runs under on Debian-derived systems, leaves ~ unexpanded, so a rule containing it failed under putup and succeeded in the shell that invoked putup

The ccache motive #480 inherited turned out to be an inference: the upstream commit adding HOME records no reason, and ccache falls back to getpwuid when HOME is unset.

Neither HOME nor PATH joins identity

Recording them was argued and rejected. Their values say where the invoking shell put things rather than what a command produces — the axis REQ-ENV-IMPORTED and REQ-ENV-TEMPDIR-IDENTITY already settled — and a recorded PATH would re-run every command in a project whenever direnv, nvm, an IDE terminal or a CI job composed PATH differently, putup's own test runner first.

The case a sticky PATH exists to catch — the compiler you will actually run having changed — is already reached by CONFIG_TRACKED_TOOLS: a bare tracked name resolves through putup's PATH and its path:size:mtime enters identity, so a PATH change that shadows the tool re-runs its commands and one that does not re-runs nothing, a distinction a recorded PATH string cannot draw. That route was opt-in and unspecified; it is now a requirement, and its PATH branch has a scenario for the first time.

It stays opt-in, so in a default configuration a PATH change that swaps the compiler still produces a stale build with no diagnostic. That gap is stated in the requirement rather than hidden, and deserves its own issue.

Spec

Five requirements in command-record.ears.md's env-values group now carry this: REQ-ENV-PATH and REQ-ENV-HOME for what reaches the subprocess, REQ-ENV-TEMPDIR unchanged but no longer noting HOME as an open divergence, REQ-ENV-FORWARDED-IDENTITY replacing REQ-ENV-TEMPDIR-IDENTITY over the whole forwarded set, and REQ-ENV-TRACKED-TOOLS for the compensating route.

Verification

  • The two new unit tests and Scenario: HOME set for putup reaches every build command were observed red before the production edit (HOME=[] vs the set value).
  • The four pins pass at HEAD, so each was shown able to fail: export HOME / export PATH in the fixture re-runs the command, dropping CONFIG_TRACKED_TOOLS makes the shadowed-tool build a no-op, and a changed literal breaks the PATH default.
  • make check rc=0 — 946 test cases, 182,428 assertions, 32 e2e shards; spec-check 21 areas, 200 requirements, 0 gaps.
  • The changed test TU cross-compiles clean with clang-cl /W4 /WX against the xwin CRT, the check that would have caught Build commands get PATH only, so TMPDIR is dropped and GCC writes temporaries into the source tree when /tmp is read-only #478's CI failure.

A different-model review found the /usr/bin:/bin substitution stated without platform scope (only the POSIX backend performs it) and the PATH unit test asserting nothing on Windows; both are corrected. It rejected two further charges after measuring them.

No record layout or content changes, so INDEX_VERSION is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01StgwMENEyfBnEoe4pvAdtQ

…t of identity

putup's default child environment diverged from tup's default_env[] in two
ways that no requirement stated, so a reader diffing the two found them as
defects rather than decisions. A rule reading $HOME saw an empty string, and
a PATH change re-ran nothing.

HOME is now forwarded, under the set-non-empty rule #478 established for the
temporary-directory variables: the value when putup's own environment holds a
non-empty one, no entry at all when it is unset or empty, and no invented
default, since none is correct for a per-user directory. The Windows keep list
gains it too, so the requirement needs no platform scope; cmd and PowerShell
leave HOME unset and contribute nothing. What its absence breaks was measured
rather than assumed: `go build` refuses to run at all ("GOCACHE is not defined
and neither $XDG_CACHE_HOME nor $HOME are defined"), and dash - the /bin/sh a
putup command runs under on Debian-derived systems - leaves ~ unexpanded, so a
rule containing it failed under putup and succeeded in the shell that invoked
putup. The ccache motive the issue inherited turned out to be an inference:
the upstream commit adding HOME records no reason and ccache falls back to
getpwuid.

Neither HOME nor PATH joins command identity. Recording them was argued and
rejected: their values say where the invoking shell put things rather than
what a command produces, which is the axis REQ-ENV-IMPORTED and
REQ-ENV-TEMPDIR-IDENTITY already settled, and a recorded PATH would re-run
every command in a project whenever direnv, nvm, an IDE terminal or a CI job
composed PATH differently - putup's own 33-rule test runner first. The case a
sticky PATH exists to catch, the compiler you will actually run having
changed, is already reached by CONFIG_TRACKED_TOOLS: a bare tracked name is
resolved through putup's PATH and its path:size:mtime enters identity, so a
PATH change that shadows the tool re-runs its commands and one that does not
re-runs nothing - a distinction a recorded PATH string cannot draw. That
route was opt-in and unspecified; it is now a requirement, and its PATH branch
has a scenario for the first time. It stays opt-in, so in a default
configuration a PATH change that swaps the compiler still produces a stale
build with no diagnostic; that gap is stated in the requirement rather than
hidden, and is worth its own issue.

Five requirements in command-record.ears.md's env-values group now carry this:
REQ-ENV-PATH and REQ-ENV-HOME for what reaches the subprocess, REQ-ENV-TEMPDIR
unchanged but no longer noting HOME as an open divergence,
REQ-ENV-FORWARDED-IDENTITY replacing REQ-ENV-TEMPDIR-IDENTITY over the whole
forwarded set, and REQ-ENV-TRACKED-TOOLS for the compensating route. Every
discharge names a test that asserts its sentence. The two new unit tests and
the HOME-forwarding scenario were observed red before the change; the three
no-op pins and the PATH-default pin pass at HEAD, so each was shown able to
fail - `export HOME` and `export PATH` in the fixture re-run the command,
dropping CONFIG_TRACKED_TOOLS makes the shadowed-tool build a no-op, and a
changed literal breaks the PATH default.

A different-model review of the branch found the `/usr/bin:/bin` substitution
stated without platform scope, though only the POSIX backend performs it - the
Windows keep list copies a PATH that exists and synthesises none - and found
the PATH unit test asserting nothing on Windows where its two siblings each
carry an arm; both are corrected here. It also rejected two charges after
measuring them: REQ-ENV-PATH stays tup-conformant, since the file already
labels a requirement that way and records the narrow divergence in its
reference (REQ-SIG-RECORD does exactly this), and a PATH change that preserves
a tracked tool's resolution is already entailed by REQ-SIG-RECORD together
with REQ-ENV-TRACKED-TOOLS rather than left unspecified.

No record layout or content changes, so INDEX_VERSION is unchanged.

Ref: #480

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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.61 s 15.1 k 0.7% 0.1% 0.632 s 34.8 MB
dry-run 2340 M 0.73 s 16.6 k 0.8% 0% 0.712 s 40.4 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) 537.3 (-0.3%)
Total time (ms) 699.2 (-0.8%)
Runner CPU AMD EPYC 7763 64-Core Processor

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 2.3 KB 98.8 KB 711.4 KB

Code churn (whole codebase, last 30d)

Files Lines written Still present Churned Churn rate
31 2346 2017 329 14%

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. 1592 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 · 17822/20066 lines covered

Deltas vs main@75c93eec2.

Updated for c4cbe05

…refix

The Windows arm added for the PATH requirement asserted
entry.starts_with("PATH="), but base_child_env copies each kept entry
out of GetEnvironmentStringsW verbatim and Windows spells the variable
Path, so the section failed on the windows-latest runner while every
other job passed. Its two sibling sections match a name case-insensitively
for exactly this reason; this one now does too.

Wine is not a witness here: it spells the entry PATH=, so the original
assertion passes under it and the defect is only observable on the CI
runner. The pre-existing TMPDIR section, which asserts Temp and Tmp
through the same case-insensitive comparison and passes there, is the
evidence that the shape is right.

Ref: #480

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@typeless
typeless merged commit 8dd8a5a into main Sep 20, 2026
13 checks passed
@typeless
typeless deleted the fix/480 branch September 20, 2026 13:27
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.

Default child environment: HOME is not forwarded and a PATH change re-runs nothing, neither pinned by a requirement

1 participant