Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/zh/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
> 像其它条目一样加入这份列表;约束决定它是否适用于某次给定的构建。它
Expand Down
14 changes: 10 additions & 4 deletions modules/manifest/src/glob.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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] == '*') {
Expand Down
32 changes: 31 additions & 1 deletion tests/unit/test_modgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1444,16 +1444,46 @@ 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");
EXPECT_EQ(files[1], dir / "sub" / "main.cpp");
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"));
}
Loading
⌂ Proxy