Summary
remove_stale_outputs deletes generated files whose producing command no longer exists, but it runs after every detection pass and its deletions never enter changed_files. A consumer that reaches the deleted file by a route not visible in its own command text is therefore not notified during that build.
Pass order in build_single_variant (src/cli/cmd_build.cpp):
find_changed_files_with_implicit -> reconcile_input_set -> expand_implicit_deps
-> detect_new_commands -> [--rerun] -> remove_stale_outputs -> "Nothing to do" check
remove_stale_outputs returns void, and nothing re-runs after it.
Repro
mkdir -p v/{a,b} && cd v && touch Tupfile.ini
printf ': |> echo hi > %%o |> gen.txt\n' > a/Tupfile
printf ': | ../a/gen.txt |> ls ../a > %%o |> listing.txt\n' > b/Tupfile # order-only
putup configure && putup
cat b/listing.txt # Tupfile gen.txt
printf '' > a/Tupfile # drop a's rule; gen.txt becomes stale
putup # reports success
ls a/gen.txt # gone, deleted as stale
cat b/listing.txt # STILL "Tupfile gen.txt" <- names a file that no longer exists
gen.txt is order-only for b, so it never appears in b's rendered text and never affects its identity. At detection time it still hashes clean, so it is not "changed"; it is still in the graph, so reconcile_input_set does not treat it as removed; then it is deleted.
Severity: one build, not permanent
The next invocation does heal it:
putup # Build completed: 1 commands
cat b/listing.txt # Tupfile
So the failure is "one build reports success with a stale output", not indefinite staleness. Worth stating explicitly because it bounds the impact — but a build that exits 0 having left an output inconsistent with the tree is still wrong, and in CI the healing run may never happen.
Fix
Have remove_stale_outputs return the paths it deleted and feed them into changed_files before the up-to-date check. The deleted path is still a graph node at that point, so collect_affected_commands routes it in one hop through the edges that already exist.
More durably: this is the #166 asymmetry again — detection walks the old index, routing walks the current graph, and putup's own deletions belong inside that reconciliation rather than after it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HDzr9JV1ofTZKCL1H2qDPg
Summary
remove_stale_outputsdeletes generated files whose producing command no longer exists, but it runs after every detection pass and its deletions never enterchanged_files. A consumer that reaches the deleted file by a route not visible in its own command text is therefore not notified during that build.Pass order in
build_single_variant(src/cli/cmd_build.cpp):remove_stale_outputsreturnsvoid, and nothing re-runs after it.Repro
mkdir -p v/{a,b} && cd v && touch Tupfile.ini printf ': |> echo hi > %%o |> gen.txt\n' > a/Tupfile printf ': | ../a/gen.txt |> ls ../a > %%o |> listing.txt\n' > b/Tupfile # order-only putup configure && putup cat b/listing.txt # Tupfile gen.txt printf '' > a/Tupfile # drop a's rule; gen.txt becomes stale putup # reports success ls a/gen.txt # gone, deleted as stale cat b/listing.txt # STILL "Tupfile gen.txt" <- names a file that no longer existsgen.txtis order-only forb, so it never appears inb's rendered text and never affects its identity. At detection time it still hashes clean, so it is not "changed"; it is still in the graph, soreconcile_input_setdoes not treat it as removed; then it is deleted.Severity: one build, not permanent
The next invocation does heal it:
So the failure is "one build reports success with a stale output", not indefinite staleness. Worth stating explicitly because it bounds the impact — but a build that exits 0 having left an output inconsistent with the tree is still wrong, and in CI the healing run may never happen.
Fix
Have
remove_stale_outputsreturn the paths it deleted and feed them intochanged_filesbefore the up-to-date check. The deleted path is still a graph node at that point, socollect_affected_commandsroutes it in one hop through the edges that already exist.More durably: this is the #166 asymmetry again — detection walks the old index, routing walks the current graph, and putup's own deletions belong inside that reconciliation rather than after it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HDzr9JV1ofTZKCL1H2qDPg