putup refused an output under the source file sub only when a rule had
already read sub. Named first, the output minted a Directory node for
sub, the later input bound to it, and the command failed in the shell
("Directory nonexistent"). And an output could claim a path an earlier
output had made a directory (gen/x, then gen), failing in the shell with
"Is a directory". Upstream refuses both at parse time: the first in
find_dir_tupid_dt_pg, which knows the file from its scan, the second in
validate_output.
In an in-tree build, before binding an output or creating a group,
putup now stats each component of the directory that no node holds yet,
outermost first, and binds a regular file as a File node, so
add_file_node's parent check (#494) refuses it. The graph stays free of
disk access; the stat sits in the builder next to resolve_input_node,
which already types source paths from the disk. Variant builds skip it:
real tup accepts both an output and a group under a source file there,
because both land in the variant tree. Outputs into directories that do
not exist yet are unaffected.
After binding, an output whose node is not a file, a generated file or
a ghost is refused with upstream's message, naming the type. It fires
under an unsatisfied guard too, as REQ-OUTPUT-INSIDE-HIERARCHY does.
The output-group site discarded get_or_create_group_node's error, so a
group under a file was refused only when some consumer happened to
resolve the same group. It now returns the error.
Not covered, recorded in the requirements: an output onto a source
directory or a group's directory is not refused at parse time (tup
refuses it; in-tree putup refuses the source directory later as a file
the build does not own), and a group under an earlier rule's generated
output is accepted. In a variant build, an input sub named after an
output sub/x.o still binds to the output's directory node.
Verified red-then-green: the new cases ran the command and failed in
the shell, or built, before the change; removing the group-site stat
turns its case red again. Real tup was run on the variant and group
cases to settle the in-tree-only scope. The new stats run at parse
time, once per directory component with no node yet (a Tupfile's own
directory already has one); the stat-call metric counts only change
detection (cmd_build.cpp), so it does not see them.
Ref: #495
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closes #495.
Problem
Whether putup refused an output under a non-directory depended on rule order, and an output could claim a path another output had made a directory. Both ran the command and failed in the shell, where tup refuses at parse time.
Change
add_file_nodethen refuses it, whichever rule comes first. Variant builds skip this: real tup accepts both an output and a group under a source file there.validate_outputmessage. It fires under an unsatisfied guard too, like REQ-OUTPUT-INSIDE-HIERARCHY.get_or_create_group_node's error, so a group under a file was refused only when some consumer resolved the same group.ensure_file_node(separate commit). The three alias branches read the id through afindpointer after inserting into the sameSortedPairVec, which shifts entries. The new parse-time stat reached it:d/f/x.oover a source filed/fwas built atd/x.o. A sweep of find-then-mutate pointer sites insrc/found no other.Checked against real tup
sub/x.o, groupsub/<g>under source filesubgen/xthen outputgenNot covered (recorded in
spec/requirements/output-paths.ears.md)subnamed after an outputsub/x.obinds to the output's directory node, so the command fails in the shell. tup accepts it.Verification
make check: 182598 assertions in 961 test cases, 32 e2e shards.🤖 Generated with Claude Code