From b493d29fb0ca4f6570e2404f6c23596718b81f5d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 12 Sep 2026 18:35:51 +0800 Subject: [PATCH 1/4] rules-slang: normalise a per_file key without std::filesystem::path MSVC 14.52 (both 36629 and 36725, measured on xrgui's CI) refuses to instantiate _Path_iterator's hidden-friend operator== inside a module interface unit that imports std: include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' must be a non-static member lexically_normal() was the one call 0.7.0 added that reaches it. The key needs two normalisations, separators and a leading ./, and both are string operations. --- rules/slang.cppm | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rules/slang.cppm b/rules/slang.cppm index 3621da5..f097106 100644 --- a/rules/slang.cppm +++ b/rules/slang.cppm @@ -325,9 +325,25 @@ inline bool write_header(const std::string& header, const std::string& inc) { // The path as `per_file` keys it and as `mcpp::device_sources()` lists it are // both package-relative, but one may have been typed on Windows and the other -// derived there. Components are compared, not characters. +// derived there: separators are unified and a leading `./` dropped before the +// two are compared. +// +// NOT `std::filesystem::path::lexically_normal()`, AND THE REASON IS A COMPILER. +// This unit is compiled as a module interface that imports `std`, and MSVC +// 14.52 (measured on both 14.52.36629 and 14.52.36725, xrgui's CI) refuses to +// instantiate `_Path_iterator`'s hidden-friend `operator==` there: +// +// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' +// must be a non-static member +// +// `lexically_normal` walks the path's components and is the one call 0.7.0 +// added that reaches that operator. The two normalisations this key needs are +// string operations, so nothing is lost by not going through `path` at all. inline std::string key_of(std::string_view path) { - return std::filesystem::path(path).lexically_normal().generic_string(); + std::string s(path); + for (auto& c : s) if (c == '\\') c = '/'; + while (s.starts_with("./")) s.erase(0, 2); + return s; } inline bool compile(std::span shaders, options opt = {}) { From fdb8b1a99e31f8f93711a9d87e72851128015b77 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 12 Sep 2026 18:47:35 +0800 Subject: [PATCH 2/4] a member does not instantiate the path iterator; relative_to() is the string form, in the lib root The first fix removed lexically_normal() and the error stayed. The lib root compiles lexically_relative and passes; a member that imports the lib root and reaches _Path_iterator's hidden-friend operator== again fails under MSVC 14.52 (36629 and 36725, xrgui CI). So members do not: the Slang rule's sidecar name and the AppImage member's launcher path go through one string helper, mcpp::plugins::names::relative_to. --- dist/appimage.cppm | 4 ++-- rules/slang.cppm | 17 ++++------------- src/plugins.cppm | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/dist/appimage.cppm b/dist/appimage.cppm index bb3fd5f..7b918bf 100644 --- a/dist/appimage.cppm +++ b/dist/appimage.cppm @@ -361,8 +361,8 @@ inline plan plan_for(options opt = {}) { p.reason = "no launcher in the staged tree"; return p; } - const auto launcher_rel = - std::filesystem::path(launcher).lexically_relative(stage).generic_string(); + // Strings, not `lexically_relative`: see `mcpp::plugins::names::relative_to`. + const auto launcher_rel = mcpp::plugins::names::relative_to(launcher, stage); // ── The three files AppImage requires, written into the staged tree ──── const std::string name = app_name_for(opt); diff --git a/rules/slang.cppm b/rules/slang.cppm index f097106..8cd659b 100644 --- a/rules/slang.cppm +++ b/rules/slang.cppm @@ -328,17 +328,9 @@ inline bool write_header(const std::string& header, const std::string& inc) { // derived there: separators are unified and a leading `./` dropped before the // two are compared. // -// NOT `std::filesystem::path::lexically_normal()`, AND THE REASON IS A COMPILER. -// This unit is compiled as a module interface that imports `std`, and MSVC -// 14.52 (measured on both 14.52.36629 and 14.52.36725, xrgui's CI) refuses to -// instantiate `_Path_iterator`'s hidden-friend `operator==` there: -// -// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' -// must be a non-static member -// -// `lexically_normal` walks the path's components and is the one call 0.7.0 -// added that reaches that operator. The two normalisations this key needs are -// string operations, so nothing is lost by not going through `path` at all. +// Strings, not `std::filesystem::path`: this member must not instantiate the +// path iterator -- see `mcpp::plugins::names::relative_to` for the compiler +// that refuses it. The two normalisations this key needs are string operations. inline std::string key_of(std::string_view path) { std::string s(path); for (auto& c : s) if (c == '\\') c = '/'; @@ -474,8 +466,7 @@ inline bool compile(std::span shaders, options opt = {}) { // Where a sidecar is found at run time: relative to the package root, // which is where `mcpp run` starts the program. The cost of that is // stated on `mcpp::plugins::surface::storage::sidecar`. - const auto sidecarName = - std::filesystem::path(spv).lexically_relative(root).generic_string(); + const auto sidecarName = mcpp::plugins::names::relative_to(spv, root); items.push_back({ .identifier = p.stem().string(), .name_space = ns, .data_header = headerRel, diff --git a/src/plugins.cppm b/src/plugins.cppm index 1c18bb1..be157d9 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -63,6 +63,28 @@ inline constexpr std::string_view version = "0.7.0"; // is guaranteed rather than two files that happen to say the same thing. export namespace mcpp::plugins::names { +// `a` made relative to the directory `b`, as strings: separators unified, and +// the prefix stripped when `a` lies under `b`; `a` unchanged otherwise. +// +// A MEMBER MUST NOT INSTANTIATE `std::filesystem::path`'s ITERATOR. This lib +// root may -- `common_base_dir` below calls `lexically_relative` and compiles +// -- but a member that imports this module and reaches the same iterator again +// fails under MSVC 14.52 (36629 and 36725, measured on xrgui's CI): +// +// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' +// must be a non-static member +// +// the STL's hidden-friend comparison, refused when it is instantiated a second +// time behind `import std` plus this module's BMI. So the members' relative-path +// arithmetic is this string function, defined once here. +inline std::string relative_to(std::string a, std::string b) { + for (auto& c : a) if (c == '\\') c = '/'; + for (auto& c : b) if (c == '\\') c = '/'; + while (!b.empty() && b.back() == '/') b.pop_back(); + if (!b.empty() && a.starts_with(b + "/")) return a.substr(b.size() + 1); + return a; +} + // A GENERATED NAME THE C++ COMPILER WILL ACCEPT. // // Three transformations, and the third is the one every hand-rolled copy of From 8050b59829fca1313ae1cdcc684f1dbe1bebbfaf Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 12 Sep 2026 18:58:08 +0800 Subject: [PATCH 3/4] the lib root reads paths apart as strings; its BMI no longer carries _Path_iterator Removing the iterator-instantiating calls from the members changed nothing: the rule failed with the same C2801 with nothing but create_directories and path construction left, while declare.cppm -- same import set, no std::filesystem use at all -- compiled. What every failure shared was importing both std and mcpp.plugins, whose BMI carried _Path_iterator instantiated by common_base_dir / namespace_of, and then touching path. components() splits on both separators, which is the component comparison 0.5.2 introduced lexically_relative for; nothing in the lib root instantiates the iterator now. create_directories stays: the STL walks wchar_t pointers there, not the iterator. --- src/plugins.cppm | 77 +++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/plugins.cppm b/src/plugins.cppm index be157d9..a31e783 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -66,17 +66,9 @@ export namespace mcpp::plugins::names { // `a` made relative to the directory `b`, as strings: separators unified, and // the prefix stripped when `a` lies under `b`; `a` unchanged otherwise. // -// A MEMBER MUST NOT INSTANTIATE `std::filesystem::path`'s ITERATOR. This lib -// root may -- `common_base_dir` below calls `lexically_relative` and compiles -// -- but a member that imports this module and reaches the same iterator again -// fails under MSVC 14.52 (36629 and 36725, measured on xrgui's CI): -// -// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' -// must be a non-static member -// -// the STL's hidden-friend comparison, refused when it is instantiated a second -// time behind `import std` plus this module's BMI. So the members' relative-path -// arithmetic is this string function, defined once here. +// NOTHING IN THIS PACKAGE INSTANTIATES `std::filesystem::path`'s ITERATOR -- +// see `components()` below for the compiler that refuses it. The members' +// relative-path arithmetic is this string function, defined once here. inline std::string relative_to(std::string a, std::string b) { for (auto& c : a) if (c == '\\') c = '/'; for (auto& c : b) if (c == '\\') c = '/'; @@ -176,13 +168,44 @@ inline std::vector split_module_name(std::string_view name) { // A single path has no common prefix with anything, so its own directory is the // base and its namespace is empty -- which is the same answer the general case // gives once a second file appears beside it. +// A path's components as strings: both separators split, empty and `.` +// components dropped. This is the one place the lib root reads a path apart, +// and it does so WITHOUT `std::filesystem::path`'s iterator on purpose: +// +// A MODULE THAT INSTANTIATES `_Path_iterator` POISONS ITS IMPORTERS UNDER MSVC +// 14.52. Measured on xrgui's CI (14.52.36629 and .36725): this unit compiled +// while it iterated paths, and every member importing it that then touched +// `std::filesystem` at all failed inside the STL -- +// +// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator ==' +// must be a non-static member +// +// -- the iterator's hidden-friend comparison, refused when the importer meets +// it both through `import std` and through this module's BMI. Removing the +// calls from the members changed nothing; the instantiation had to leave the +// lib root. Component comparison is what `lexically_relative` bought in 0.5.2 +// (a Windows separator bug), and splitting on both separators keeps that. +inline std::vector components(std::string_view path) { + std::vector out; + std::string cur; + auto flush = [&] { if (!cur.empty() && cur != ".") out.push_back(cur); cur.clear(); }; + for (char c : path) { if (c == '/' || c == '\\') flush(); else cur += c; } + flush(); + return out; +} + +// The directory part of a path, as written: everything before the last +// separator, or empty when there is none. +inline std::string_view parent_of(std::string_view path) { + const auto slash = path.find_last_of("/\\"); + return slash == std::string_view::npos ? std::string_view{} : path.substr(0, slash); +} + inline std::string common_base_dir(std::span paths) { std::vector prefix; bool first = true; for (auto const& src : paths) { - std::vector segs; - for (auto const& part : std::filesystem::path(src).parent_path()) - if (auto s = part.string(); !s.empty() && s != ".") segs.push_back(s); + auto segs = components(parent_of(src)); if (first) { prefix = std::move(segs); first = false; continue; } std::size_t keep = 0; while (keep < prefix.size() && keep < segs.size() && prefix[keep] == segs[keep]) ++keep; @@ -208,29 +231,29 @@ inline std::string common_base_dir(std::span paths) { // 'image' in namespace 'island_interface::kernels'`, while the same fixture // passed on Linux and macOS. // -// `lexically_relative` compares COMPONENTS, so the separator a caller happened -// to write is not part of the question. A base that is not a prefix yields a -// path starting `..`, which is a caller error rather than a namespace; it -// answers with no segments rather than with the whole absolute path, which is -// what the string form produced. +// COMPONENTS are compared, so the separator a caller happened to write is not +// part of the question -- `components()` splits on both. A base that is not a +// prefix is a caller error rather than a namespace; it answers with no +// segments rather than with the whole absolute path, which is what the first +// string form produced. (Through `components()` rather than +// `lexically_relative` for the reason stated on it.) inline std::vector namespace_of(std::string_view src, std::string_view base) { std::vector out; - const auto dir = std::filesystem::path(src).parent_path(); - auto rel = dir; + auto dir = components(parent_of(src)); if (!base.empty()) { - rel = dir.lexically_relative(std::filesystem::path(base)); - if (rel.empty() || rel.begin()->string() == "..") return out; + const auto b = components(base); + if (b.size() > dir.size()) return out; + for (std::size_t i = 0; i < b.size(); ++i) if (b[i] != dir[i]) return out; + dir.erase(dir.begin(), dir.begin() + static_cast(b.size())); } - for (auto const& part : rel) { - auto s = part.string(); - if (s.empty() || s == "." || s == ".." || s == "/" || s == "\\") continue; + for (auto const& s : dir) { + if (s == "..") continue; // `shaders/default/` is an ordinary directory name and // `namespace default {` is not a namespace. out.push_back(identifier(s, "dir")); } return out; } - } // namespace mcpp::plugins::names From 394f33bd81acad227810f9c1b17098b4e4a448b8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 12 Sep 2026 19:19:15 +0800 Subject: [PATCH 4/4] 0.7.1: nothing in the package instantiates std::filesystem::path's iterator MSVC 14.52 (36629 and 36725, xrgui CI) refuses the STL's own hidden-friend _Path_iterator::operator== in any module that imports both std and a module whose BMI already carries that instantiation. The three commits before this one found the shape by elimination; this one is the version that carries it. --- README.md | 13 +++++++++++-- mcpp.toml | 2 +- src/plugins.cppm | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fb59cec..f997292 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ imports each one from `build.mcpp` under the module name the member declares. ```toml [build-dependencies.mcpp] -plugins = { version = "0.7.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.7.1", features = ["rules-spirv"], host-module = true } ``` `[build-dependencies]`, not `[dependencies]`. The two keys answer separate @@ -81,7 +81,7 @@ A project names the rule and nothing else: ```toml [build-dependencies.mcpp] -plugins = { version = "0.7.0", features = ["rules-cuda"], host-module = true } +plugins = { version = "0.7.1", features = ["rules-cuda"], host-module = true } ``` The payloads each rule drives are declared **here**, under the feature that @@ -165,6 +165,15 @@ looks harmless. 0.5.0, 0.5.1 and 0.5.2 do not move it. Naming an island's entry points is a change to what this package generates, not to what it asks the engine for. +0.7.1 does not move it either, and records a compiler rather than an engine: +under MSVC 14.52 (36629 and 36725, measured on xrgui's CI) a module that has +instantiated `std::filesystem::path`'s iterator poisons every importer that +touches `path` again -- `filesystem(1572): error C2801: '_Path_iterator<...>::operator ==' +must be a non-static member`. Nothing in this package instantiates that +iterator now: the lib root reads paths apart as strings +(`mcpp::plugins::names::components`), and the members' relative-path +arithmetic is `mcpp::plugins::names::relative_to`. + The previous shared floor was 2026.9.7.1, the release that reads `device_extensions` and `rule_module`, reports `[language] modules` and the package's own name to a build program, writes the build program a declared rule diff --git a/mcpp.toml b/mcpp.toml index a615622..9606f89 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.7.0" +version = "0.7.1" description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/plugins.cppm b/src/plugins.cppm index a31e783..75a2086 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -49,7 +49,7 @@ export namespace mcpp::plugins { // // One package, one version: the number lives in mcpp.toml, and the CI step // `the collection states its own version` compares the two. -inline constexpr std::string_view version = "0.7.0"; +inline constexpr std::string_view version = "0.7.1"; } // namespace mcpp::plugins