Skip to content

Commit a86d09b

Browse files
authored
fix: a glob's **/ stands for whole directories, not part of a name (#878)
relative_path_matches_glob consumed `**/` and then retried the rest of the pattern at every character of the path, so what followed `**/` could begin in the middle of a file or directory name. `**/main.cpp` selected `src/domain.cpp`, `tests/**/test_*.cpp` selected `tests/mytest_a.cpp`, and `**/modules/dnn/**/*.avx2.cpp` selected `a/submodules/dnn/l.avx2.cpp`. The matcher serves every glob with the `sources` grammar: `sources`, `[build] flags`, `[scan_overrides]`, test `discover`, `[pack] include`/`exclude`, include-dir globs and `rerun_if_changed_glob`. A `[build] flags` entry for `**/main.cpp` therefore also compiled `src/app/domain.cpp` with its defines. After `**/` the rest of the pattern now resumes only where it stands (zero directories) or just after a `/`. A `**` not followed by `/` (`src/**`, `src/**.cpp`) keeps crossing segments and names as before. The literal-prefix and tail pre-filters of expand_glob are supersets of the new match and are unchanged. Tests: Glob.ADoubleStarSlashStandsForWholeDirectories checks the matcher directly, and Scanner.ADoubleStarSlashTailMatchesAtTheRoot gains a `sub/domain.cpp` that the `**/main.cpp` walk must not return. docs/04 states the rule in both languages. Closes #877
1 parent 6028cce commit a86d09b

4 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎docs/04-mcpp-toml.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,11 @@ the package/feature boundary, not on an individual target.
557557
> mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for
558558
> "nothing" and any file left under `src/` was swept in.
559559
560+
> **`**/` stands for whole directories.** In `sources` and every glob that
561+
> shares its grammar, `**/` matches zero or more directories and what follows
562+
> it starts a path segment: `**/main.cpp` names every `main.cpp` and not
563+
> `src/domain.cpp`. A `*` matches within one segment.
564+
560565
> **A `sources` entry may carry the accelerator it is for** (2026.9.5.2+):
561566
> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`. The glob
562567
> joins the list like any other; the constraint decides whether it applies to a

‎docs/zh/04-mcpp-toml.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,11 @@ mcpp 刻意不在一次构建里用两种方式编译同一份共享源码:一
530530
> 分发包需要表达的意思。在 mcpp 2026.8.18.1 之前两者逐字节相同,所以
531531
> 没有拼法能表达「什么都没有」,`src/` 下留下的任何文件都会被卷进来。
532532
533+
> **`**/` 代表整段目录。** 在 `sources` 以及沿用其文法的每一处 glob 中,
534+
> `**/` 匹配零个或多个目录,其后的部分从一个路径段的开头算起:
535+
> `**/main.cpp` 选中每个 `main.cpp`,而不选中 `src/domain.cpp`。`*` 只在
536+
> 一个路径段内匹配。
537+
533538
> **一条 `sources` 条目可以携带它所面向的加速器**(2026.9.5.2+):
534539
> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`。glob
535540
> 像其它条目一样加入这份列表;约束决定它是否适用于某次给定的构建。它

‎modules/manifest/src/glob.cppm‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,19 @@ bool relative_path_matches_glob(std::string_view relative, std::string_view glob
210210
[&](std::size_t si, std::size_t pi) -> bool {
211211
while (pi < p.size()) {
212212
if (p[pi] == '*' && pi + 1 < p.size() && p[pi + 1] == '*') {
213-
// ** : skip zero or more chars/segments
213+
// `**/` : zero or more WHOLE directories, so the rest of
214+
// the pattern resumes where it stands or just after a `/`.
215+
// Resuming inside a name made `**/main.cpp` match
216+
// `src/domain.cpp`. A `**` not followed by `/` skips any
217+
// characters, segments included.
214218
pi += 2;
215-
if (pi < p.size() && p[pi] == '/') ++pi;
219+
const bool wholeDirs = pi < p.size() && p[pi] == '/';
220+
if (wholeDirs) ++pi;
216221
if (pi >= p.size()) return true;
217-
while (si <= s.size()) {
222+
const std::size_t from = si;
223+
for (; si <= s.size(); ++si) {
224+
if (wholeDirs && si != from && s[si - 1] != '/') continue;
218225
if (rec(si, pi)) return true;
219-
++si;
220226
}
221227
return false;
222228
} else if (p[pi] == '*') {

‎tests/unit/test_modgraph.cpp‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1444,16 +1444,46 @@ TEST(ModuleResolution, OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling) {
14441444

14451445
// A kept walk matched by the literal tail of a glob: `**/` also matches no
14461446
// directory, so `**/main.cpp` matches a `main.cpp` at the root as well as one
1447-
// below it, as the per-pattern walk did.
1447+
// below it, as the per-pattern walk did. `domain.cpp` ends with the same tail
1448+
// and is still not a `main.cpp`.
14481449
TEST(Scanner, ADoubleStarSlashTailMatchesAtTheRoot) {
14491450
auto dir = make_tempdir("mcpp-glob-tail");
14501451
write(dir / "main.cpp", "int main() {}\n");
14511452
write(dir / "sub" / "main.cpp", "int main() {}\n");
14521453
write(dir / "sub" / "other.cpp", "int x;\n");
1454+
write(dir / "sub" / "domain.cpp", "int y;\n");
14531455
auto files = expand_glob(dir, "**/main.cpp");
14541456
ASSERT_EQ(files.size(), 2u);
14551457
EXPECT_EQ(files[0], dir / "main.cpp");
14561458
EXPECT_EQ(files[1], dir / "sub" / "main.cpp");
14571459
std::filesystem::remove_all(dir);
14581460
}
14591461

1462+
// `**/` stands for zero or more whole directories: what follows it starts a
1463+
// path segment. It used to resume at any character, so `**/main.cpp` named
1464+
// `src/domain.cpp` and `tests/**/test_*.cpp` named `tests/mytest_a.cpp`.
1465+
// A `**` not followed by `/` still crosses segments and names.
1466+
TEST(Glob, ADoubleStarSlashStandsForWholeDirectories) {
1467+
EXPECT_TRUE(relative_path_matches_glob("main.cpp", "**/main.cpp"));
1468+
EXPECT_TRUE(relative_path_matches_glob("src/main.cpp", "**/main.cpp"));
1469+
EXPECT_TRUE(relative_path_matches_glob("src/a/b/main.cpp", "**/main.cpp"));
1470+
EXPECT_FALSE(relative_path_matches_glob("domain.cpp", "**/main.cpp"));
1471+
EXPECT_FALSE(relative_path_matches_glob("src/domain.cpp", "**/main.cpp"));
1472+
EXPECT_FALSE(relative_path_matches_glob("src/a/domain.cpp", "**/main.cpp"));
1473+
1474+
EXPECT_TRUE(relative_path_matches_glob("tests/test_a.cpp", "tests/**/test_*.cpp"));
1475+
EXPECT_TRUE(relative_path_matches_glob("tests/x/test_a.cpp", "tests/**/test_*.cpp"));
1476+
EXPECT_FALSE(relative_path_matches_glob("tests/mytest_a.cpp", "tests/**/test_*.cpp"));
1477+
EXPECT_FALSE(relative_path_matches_glob("tests/x/mytest_a.cpp", "tests/**/test_*.cpp"));
1478+
1479+
const std::string_view dnn = "**/modules/dnn/**/*.avx2.cpp";
1480+
EXPECT_TRUE(relative_path_matches_glob("modules/dnn/l.avx2.cpp", dnn));
1481+
EXPECT_TRUE(relative_path_matches_glob("a/modules/dnn/b/l.avx2.cpp", dnn));
1482+
EXPECT_FALSE(relative_path_matches_glob("a/submodules/dnn/l.avx2.cpp", dnn));
1483+
EXPECT_FALSE(relative_path_matches_glob("modules/xdnn/l.avx2.cpp", dnn));
1484+
1485+
EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**"));
1486+
EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**.cpp"));
1487+
EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "**"));
1488+
EXPECT_FALSE(relative_path_matches_glob("srcx/a.cpp", "src/**/*.cpp"));
1489+
}

0 commit comments

Comments
 (0)