Skip to content

remove_stale_outputs runs after all detection, so its own deletions are never routed to consumers #172

Description

@typeless

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions