diff --git a/src/graph/builder.cpp b/src/graph/builder.cpp index bbdfca8..dbae056 100644 --- a/src/graph/builder.cpp +++ b/src/graph/builder.cpp @@ -694,11 +694,13 @@ auto expand_glob_pattern( } auto match_path_sv = pool.get(pup::strip_path_prefix(node_path_sv, build_root_name)); if (glob.matches(match_path_sv)) { - matches.push_back(pool.intern(node_path_sv)); + // Stripped, not physical: one path space for both halves (resolve_input_node + // is BuildRoot-first, so this still names the generated node). + matches.push_back(pool.intern(match_path_sv)); } } - // In-tree builds see a generated file from both sources; it is one input. + // Sorting and deduping across two path spaces would order by where the build tree lives. std::ranges::sort(matches, {}, [&pool](StringId id) { return pool.get(id); }); matches.erase(std::unique(matches.begin(), matches.end()), matches.end()); for (auto id : matches) { diff --git a/test/e2e/fixtures/glob_mixed_space/Tupfile.fixture b/test/e2e/fixtures/glob_mixed_space/Tupfile.fixture new file mode 100644 index 0000000..21d376e --- /dev/null +++ b/test/e2e/fixtures/glob_mixed_space/Tupfile.fixture @@ -0,0 +1,2 @@ +# Placeholder - overwritten by test +: |> true |> diff --git a/test/e2e/fixtures/glob_mixed_space/Tupfile.ini b/test/e2e/fixtures/glob_mixed_space/Tupfile.ini new file mode 100644 index 0000000..05e6015 --- /dev/null +++ b/test/e2e/fixtures/glob_mixed_space/Tupfile.ini @@ -0,0 +1 @@ +# Project root marker diff --git a/test/e2e/fixtures/glob_mixed_space/tup.config b/test/e2e/fixtures/glob_mixed_space/tup.config new file mode 100644 index 0000000..f4bd2fa --- /dev/null +++ b/test/e2e/fixtures/glob_mixed_space/tup.config @@ -0,0 +1 @@ +# Test config diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 8f33a7f..02004e7 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -4522,6 +4522,100 @@ SCENARIO("Editing a rule's recipe does not make it a different rule", "[e2e][ide } } +SCENARIO("A glob's %f order does not depend on the build directory's name", "[e2e][glob][pathspace]") +{ + GIVEN("a glob matching one generated and one source file, built out-of-tree") + { + auto f = E2EFixture { "glob_mixed_space" }; + f.mkdir("a"); + f.write_file("a/Tupfile", ": src.in |> cp %f %o |> gen.dat\n: *.dat |> cat %f > %o |> all.txt\n"); + f.write_file("a/src.in", "GEN\n"); + f.write_file("a/keep.dat", "KEEP\n"); + + WHEN("built into two differently-named build directories") + { + f.mkdir("AAA"); + REQUIRE(f.pup({ "configure", "-B", "AAA" }).success()); + REQUIRE(f.build({ "-B", "AAA" }).success()); + auto from_aaa = f.read_file("AAA/a/all.txt"); + + f.mkdir("zz"); + REQUIRE(f.pup({ "configure", "-B", "zz" }).success()); + REQUIRE(f.build({ "-B", "zz" }).success()); + auto from_zz = f.read_file("zz/a/all.txt"); + + THEN("the artifact is byte-identical") + { + INFO("AAA: " << from_aaa); + INFO("zz: " << from_zz); + REQUIRE(from_aaa == from_zz); + // Pins both halves and the canonical order: equality alone would still + // hold if the filesystem half stopped contributing entirely. + REQUIRE(from_aaa == "GEN\nKEEP\n"); + } + } + } +} + +SCENARIO("A generated file shadowing a source file is one glob match", "[e2e][glob][pathspace][deviation]") +{ + // Upstream tup rejects this project outright ("Attempting to insert 'x.dat' as a generated + // node when it already exists as a different type"). putup accepts it and lets the generated + // node win; issue #194 tracks that deviation. This asserts only the dedup #191 is about -- + // one match rather than two -- and is not a claim that shadowing should be legal. + GIVEN("a generated file whose name also exists as a checked-in source") + { + auto f = E2EFixture { "glob_mixed_space" }; + f.mkdir("a"); + f.write_file("a/Tupfile", ": gen.src |> cp %f %o |> x.dat\n: *.dat |> cat %f > %o |> all.txt\n"); + f.write_file("a/gen.src", "FROMRULE\n"); + f.write_file("a/x.dat", "FROMSRC\n"); + f.mkdir("build"); + REQUIRE(f.pup({ "configure", "-B", "build" }).success()); + + WHEN("built out-of-tree") + { + REQUIRE(f.build({ "-B", "build" }).success()); + + THEN("it is consumed once, not twice") + { + auto content = f.read_file("build/a/all.txt"); + INFO("all.txt: " << content); + REQUIRE(content == "FROMRULE\n"); + } + } + } +} + +SCENARIO("An exclusion applies to generated glob matches out-of-tree", "[e2e][glob][pathspace]") +{ + GIVEN("a glob over generated files with one of them excluded") + { + auto f = E2EFixture { "glob_mixed_space" }; + f.write_file("Tupfile", + ": keep.in |> cp %f %o |> keep.gen\n" + ": skip.in |> cp %f %o |> skip.gen\n" + ": *.gen !skip.gen |> echo %f > %o |> out.txt\n"); + f.write_file("keep.in", "k\n"); + f.write_file("skip.in", "s\n"); + f.mkdir("build"); + REQUIRE(f.pup({ "configure", "-B", "build" }).success()); + + WHEN("built out-of-tree") + { + REQUIRE(f.build({ "-B", "build" }).success()); + + THEN("the excluded file is absent from %f") + { + auto content = f.read_file("build/out.txt"); + INFO("out.txt: " << content); + REQUIRE(content.find("keep.gen") != std::string::npos); + REQUIRE(content.find("skip.gen") == std::string::npos); + } + } + } +} + SCENARIO("Dropping one output of a rule removes that output", "[e2e][identity][join][stale]") { GIVEN("a built rule that declares two outputs")