diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index de5c0198..b411d9b0 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -557,6 +557,11 @@ the package/feature boundary, not on an individual target. > mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for > "nothing" and any file left under `src/` was swept in. +> **`**/` stands for whole directories.** In `sources` and every glob that +> shares its grammar, `**/` matches zero or more directories and what follows +> it starts a path segment: `**/main.cpp` names every `main.cpp` and not +> `src/domain.cpp`. A `*` matches within one segment. + > **A `sources` entry may carry the accelerator it is for** (2026.9.5.2+): > `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`. The glob > joins the list like any other; the constraint decides whether it applies to a diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 856c9e95..58750f04 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -530,6 +530,11 @@ mcpp 刻意不在一次构建里用两种方式编译同一份共享源码:一 > 分发包需要表达的意思。在 mcpp 2026.8.18.1 之前两者逐字节相同,所以 > 没有拼法能表达「什么都没有」,`src/` 下留下的任何文件都会被卷进来。 +> **`**/` 代表整段目录。** 在 `sources` 以及沿用其文法的每一处 glob 中, +> `**/` 匹配零个或多个目录,其后的部分从一个路径段的开头算起: +> `**/main.cpp` 选中每个 `main.cpp`,而不选中 `src/domain.cpp`。`*` 只在 +> 一个路径段内匹配。 + > **一条 `sources` 条目可以携带它所面向的加速器**(2026.9.5.2+): > `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`。glob > 像其它条目一样加入这份列表;约束决定它是否适用于某次给定的构建。它 diff --git a/modules/manifest/src/glob.cppm b/modules/manifest/src/glob.cppm index 793fedc2..70638b09 100644 --- a/modules/manifest/src/glob.cppm +++ b/modules/manifest/src/glob.cppm @@ -198,13 +198,19 @@ bool relative_path_matches_glob(std::string_view relative, std::string_view glob [&](std::size_t si, std::size_t pi) -> bool { while (pi < p.size()) { if (p[pi] == '*' && pi + 1 < p.size() && p[pi + 1] == '*') { - // ** : skip zero or more chars/segments + // `**/` : zero or more WHOLE directories, so the rest of + // the pattern resumes where it stands or just after a `/`. + // Resuming inside a name made `**/main.cpp` match + // `src/domain.cpp`. A `**` not followed by `/` skips any + // characters, segments included. pi += 2; - if (pi < p.size() && p[pi] == '/') ++pi; + const bool wholeDirs = pi < p.size() && p[pi] == '/'; + if (wholeDirs) ++pi; if (pi >= p.size()) return true; - while (si <= s.size()) { + const std::size_t from = si; + for (; si <= s.size(); ++si) { + if (wholeDirs && si != from && s[si - 1] != '/') continue; if (rec(si, pi)) return true; - ++si; } return false; } else if (p[pi] == '*') { diff --git a/tests/unit/test_modgraph.cpp b/tests/unit/test_modgraph.cpp index 8656af0b..5469e99b 100644 --- a/tests/unit/test_modgraph.cpp +++ b/tests/unit/test_modgraph.cpp @@ -1444,12 +1444,14 @@ TEST(ModuleResolution, OneFileReachedTwiceIsNamedAsSuchWhateverItsSpelling) { // A kept walk matched by the literal tail of a glob: `**/` also matches no // directory, so `**/main.cpp` matches a `main.cpp` at the root as well as one -// below it, as the per-pattern walk did. +// below it, as the per-pattern walk did. `domain.cpp` ends with the same tail +// and is still not a `main.cpp`. TEST(Scanner, ADoubleStarSlashTailMatchesAtTheRoot) { auto dir = make_tempdir("mcpp-glob-tail"); write(dir / "main.cpp", "int main() {}\n"); write(dir / "sub" / "main.cpp", "int main() {}\n"); write(dir / "sub" / "other.cpp", "int x;\n"); + write(dir / "sub" / "domain.cpp", "int y;\n"); auto files = expand_glob(dir, "**/main.cpp"); ASSERT_EQ(files.size(), 2u); EXPECT_EQ(files[0], dir / "main.cpp"); @@ -1457,3 +1459,31 @@ TEST(Scanner, ADoubleStarSlashTailMatchesAtTheRoot) { std::filesystem::remove_all(dir); } +// `**/` stands for zero or more whole directories: what follows it starts a +// path segment. It used to resume at any character, so `**/main.cpp` named +// `src/domain.cpp` and `tests/**/test_*.cpp` named `tests/mytest_a.cpp`. +// A `**` not followed by `/` still crosses segments and names. +TEST(Glob, ADoubleStarSlashStandsForWholeDirectories) { + EXPECT_TRUE(relative_path_matches_glob("main.cpp", "**/main.cpp")); + EXPECT_TRUE(relative_path_matches_glob("src/main.cpp", "**/main.cpp")); + EXPECT_TRUE(relative_path_matches_glob("src/a/b/main.cpp", "**/main.cpp")); + EXPECT_FALSE(relative_path_matches_glob("domain.cpp", "**/main.cpp")); + EXPECT_FALSE(relative_path_matches_glob("src/domain.cpp", "**/main.cpp")); + EXPECT_FALSE(relative_path_matches_glob("src/a/domain.cpp", "**/main.cpp")); + + EXPECT_TRUE(relative_path_matches_glob("tests/test_a.cpp", "tests/**/test_*.cpp")); + EXPECT_TRUE(relative_path_matches_glob("tests/x/test_a.cpp", "tests/**/test_*.cpp")); + EXPECT_FALSE(relative_path_matches_glob("tests/mytest_a.cpp", "tests/**/test_*.cpp")); + EXPECT_FALSE(relative_path_matches_glob("tests/x/mytest_a.cpp", "tests/**/test_*.cpp")); + + const std::string_view dnn = "**/modules/dnn/**/*.avx2.cpp"; + EXPECT_TRUE(relative_path_matches_glob("modules/dnn/l.avx2.cpp", dnn)); + EXPECT_TRUE(relative_path_matches_glob("a/modules/dnn/b/l.avx2.cpp", dnn)); + EXPECT_FALSE(relative_path_matches_glob("a/submodules/dnn/l.avx2.cpp", dnn)); + EXPECT_FALSE(relative_path_matches_glob("modules/xdnn/l.avx2.cpp", dnn)); + + EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**")); + EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "src/**.cpp")); + EXPECT_TRUE(relative_path_matches_glob("src/a/b.cpp", "**")); + EXPECT_FALSE(relative_path_matches_glob("srcx/a.cpp", "src/**/*.cpp")); +}