Skip to content

A config variable aliases a same-named source file in the config directory, and edits to that file become invisible #486

Description

@typeless

Symptom

With an in-source tup.config, a CONFIG_<KEY> whose key matches a source file's name in the config directory makes that file invisible: edits to it are never noticed, and the build reports Nothing to do (up to date) for ever.

Reproduction

On c02eeeb (PR #485 branch; also on 2987923bf and on a fix-reverted binary - this predates both). A/B against an identical project whose only difference is the config key's name.

mkdir p && cd p && touch Tupfile.ini
printf 'CONFIG_LICENSE=y\n' > tup.config
printf 'MIT\n' > LICENSE
printf ': LICENSE |> cp %%f %%o |> out.txt\n' > Tupfile

putup -B .              # Build completed: 1 commands      out.txt: MIT
printf 'BSD\n' > LICENSE
putup -B .              # Nothing to do (up to date).      out.txt: MIT    # stale

Control, CONFIG_OTHER=y in place of CONFIG_LICENSE=y and nothing else changed: the second build re-runs and out.txt is BSD.

Mechanism

add_tupfile creates a NodeType::Variable node per non-CONFIG_ config key, named with the bare key and parented to the config directory (src/graph/builder.cpp:2490-2512). add_file_node interns <parent path>/<name> into a PathId and registers it in path_to_node (src/graph/dag.cpp:96-103), so the config variable occupies the path <config_dir>/LICENSE.

The rule's input then resolves through ensure_file_node (src/graph/dag.cpp:127-129), which returns the existing path_to_node entry and only special-cases NodeType::Generated. It hands back the Variable node.

show graph for the failing project confirms there is no File node for LICENSE at all - the rule's input edge points at the Variable node:

f3 [label="LICENSE"];        # the config variable, doubling as the input
f3 -> c1;

Nothing ever stats the file, so no content change can be detected.

config_dir_id falls back to NodeId { 0 } (the source root) when options.config_path is empty (src/graph/builder.cpp:2445), which is why the in-source tup.config shape is the one that triggers it - an out-of-tree -B build puts the config directory at build/, where source files are not.

Relationship to #484

Same class, one map over. #484 was a NodeType::Variable keyed by a user-spellable name sharing a namespace with nodes putup owns; this is a NodeType::Variable sharing a path namespace with source files. PR #485 fixed the first by moving putup's keys outside the Tupfile-spellable name space; it does not touch config_var_nodes, and this reproduces unchanged on that branch.

See also the sibling filed alongside this one: add_file_node discards SortedPairVec::insert's duplicate-key return, so a path collision of this kind is silent by construction.

Decisions to make

  • Whether a config variable belongs in the path namespace at all. It is not a file; giving it a PathId under a real directory is what puts it in reach of ensure_file_node.
  • Failing that, whether ensure_file_node should refuse to return a node whose type is not a file kind, rather than only special-casing Generated.
  • Whether the config_dir_id = NodeId { 0 } fallback is right - parenting config variables to the source root maximises the collision surface.

Activity

  1. typeless commented on Sep 21, 2026

    @typeless
    OwnerAuthor

    Correction to the issue body, found while picking this up. The out-of-tree build shape is affected too, and its symptom is worse.

    The body says the in-source tup.config shape is the realistic trigger, on the reasoning that an out-of-tree -B build puts the config directory at build/ "where source files are not". That is wrong. options.config_path is assigned at src/cli/context.cpp:993, so config_dir_id is the parent of tup.config — build/ for an out-of-tree build — and a variant build resolves a rule's input against the variant directory, which lands on the config variable's node.

    Reproduction on 04ed389f0, A/B differing only in the config key's name:

    mkdir p && cd p && touch Tupfile.ini
    printf 'MIT\n' > LICENSE
    printf ': LICENSE |> cp %%f %%o |> out.txt\n' > Tupfile
    mkdir build && printf 'CONFIG_LICENSE=y\n' > build/tup.config
    
    putup -B build
    # [build] FAILED: cp build/LICENSE build/out.txt
    # cp: cannot stat 'build/LICENSE': No such file or directory
    # [build] Build completed: 0 commands (1 failed)

    Control, CONFIG_OTHER=y in place of CONFIG_LICENSE=y: Build completed: 1 commands, build/out.txt is MIT.

    show graph shows the config variable occupying the variant path and the command built against it:

    f3 [label="build/LICENSE"];
    c1 [label="cp build/LICENSE build/out.txt"];
    

    So the one cause has two manifestations:

    • in-source tup.config (-B .): the input silently resolves to the config variable at the source root, nothing stats the file, and edits are invisible — a stale build.
    • out-of-tree -B build: the input resolves to the config variable at the variant path, and the command runs against a path that does not exist — a hard failure, loud but with a misleading message.

    The second is the common build shape, which raises the practical severity: any project with a root-level source file whose name matches a config key fails to build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions