A rule whose output path was also a checked-in source file was accepted, and in an
in-tree build putup overwrote the tracked file on disk:
a/Tupfile: : gen.src |> cp %f %o |> x.dat (a/x.dat is checked in)
before: FROMSRC
after: FROMRULE <- the tracked file is gone
ensure_file_node silently upgraded an existing File node to Generated
(src/graph/dag.cpp:140-145), so the generated node simply won. Upstream tup rejects
the project instead, in-tree and in variant builds alike, and the diagnostic here
follows its wording including both remedies.
The bisect on the issue found no first-bad commit: this reproduces at the earliest
commit where a subdirectory rule can run at all, so there is nothing to revert and
the check has simply never existed.
What putup can decide, and what it cannot. The previous index is what tells a
source file apart from our own output sitting in the source tree after an in-tree
build -- the distinction tup's persistent node types carry. So the check fires on
exactly two answerable questions: with no previous index nothing we produced can be
on disk yet, so a file at an output's path is a source; with one, a path recorded
as a source File is a source whatever is on disk now. A generated file the previous
index does not mention is not decidable from here and is left alone -- that case is
real, not hypothetical: a scoped build drops out-of-scope commands from the saved
index, and the first draft of this check rejected the subsequent full build because
of it.
Deleting .pup while in-tree artifacts remain therefore errors until they are
cleaned. That is not a regression against tup: deleting .tup in the same situation
produces the same error and the same remedy, verified against the tup binary.
Config outputs are exempt. The two-stage configure design has a rule produce the
tup.config that the same build then reads as configuration, so that path is
generated and read on purpose rather than shadowing anything.
The [deviation]-tagged scenario added in #193 asserted the old shadow-wins
behaviour and said it should be rewritten if this issue were fixed by rejecting the
project. It now expects the rejection. The dedup #191 added for this shape is
consequently unreachable through a legal project and stays only as a defence, which
the test records.
Verified: rejection in-tree and out-of-tree with the source file intact; an in-tree
rebuild still succeeds with its own output on disk; scoped-then-full in-tree still
succeeds; GCC BSP parses (24 Tupfiles, 1853 commands) and busybox parses (31
Tupfiles, 293 commands).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLob4Ef3qyMd3Dnp9q3XmA
A rule whose output path was also a checked-in source file was accepted, and in an in-tree build putup overwrote the tracked file on disk:
ensure_file_nodesilently upgraded an existingFilenode toGenerated(src/graph/dag.cpp:140-145), so the generated node simply won. Upstream tup rejects the project instead — in-tree and in variant builds alike — and the diagnostic here follows its wording, including both remedies it offers.The bisect found no first-bad commit: this reproduces at the earliest commit where a subdirectory rule can run at all. Nothing to revert; the check has never existed.
What putup can decide, and what it can't
The previous index is what distinguishes a source file from our own output sitting in the source tree after an in-tree build — the distinction tup's persistent node types carry for free. So the check fires on exactly two answerable questions:
Fileis a source, whatever is on disk now.A generated file the previous index does not mention is not decidable from here and is left alone. That case is real rather than hypothetical: a scoped build drops out-of-scope commands from the saved index, and the first draft of this check rejected the subsequent full build because of it. That is the gap, stated deliberately — a shadowing rule added to an established project, whose target no rule reads, will still slip through.
Consequence worth knowing
Deleting
.pupwhile in-tree artifacts remain now errors until they are cleaned. That is not a regression against tup — deleting.tupin the same situation produces the same error and the same remedy, which I verified by running the tup binary:Config outputs are exempt
The two-stage configure design has a rule produce the
tup.configthat the same build then reads as configuration. That path is generated and read on purpose rather than shadowing anything, so it is skipped.The [deviation] test from #193 now expects the rejection
That scenario asserted the old shadow-wins behaviour and said outright that it should be rewritten if #194 were fixed by rejecting the project. It now expects the error. A consequence the test records: the dedup #191 added for this shape is unreachable through a legal project, so it remains only as a defence.
Verification
make test: 142412 assertions, 639 cases, 32 e2e shards.make tidy,make iwyuclean.By execution: rejection in-tree and out-of-tree with the source file intact; an in-tree rebuild still succeeds with its own output on disk; scoped-then-full in-tree still succeeds (the false positive that the first draft produced); GCC BSP parses (24 Tupfiles, 1853 commands); busybox parses (31 Tupfiles, 293 commands).
🤖 Generated with Claude Code
https://claude.ai/code/session_01TLob4Ef3qyMd3Dnp9q3XmA