From dbd2ab938d9b337d646d99e3458f27515da2f892 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 22 Oct 2026 10:04:52 +0900 Subject: [PATCH 1/2] fix: a Luban system's root is declared and its /xlings upstream is its store (2026.10.22.1) In a Luban system the compiler's sysroot / is the system itself (os-release ID=luban, /usr/lib/xlings-abi), and [xlings] upstream = ["/xlings"] reads the home its root record (/etc/xlings/usr) names when /xlings is not there: a system made on a host keeps its home at the path of the home that made it. Verified in a Luban KVM machine: mcpp self doctor without the sysroot or upstream warning, a first mcpp run of a new project in 4 s. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 13 ++++++ mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/config.cppm | 5 +++ src/fallback/probe_sysroot.cppm | 8 +++- src/luban.cppm | 61 ++++++++++++++++++++++++++ tests/unit/test_luban_root.cpp | 66 +++++++++++++++++++++++++++++ 7 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/luban.cppm create mode 100644 tests/unit/test_luban_root.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 703aa1f3..f905a106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.10.22.1] - 2026-10-22 + +### Fixed + +- In a Luban system, the compiler's sysroot `/` is the system's own declared + root (`ID=luban` in `/etc/os-release` and `/usr/lib/xlings-abi`): `mcpp self + doctor` no longer reports it as belonging to neither the mcpp home nor the + project. +- In a Luban system, `[xlings] upstream = ["/xlings"]` reads the store the + system's root record names (`/etc/xlings/usr`) when `/xlings` is not there -- + a system made on a host keeps its home at the path of the home that made it. + Its gcc and glibc are reused rather than reported unreachable. + ## [2026.10.21.1] - 2026-10-21 ### Fixed diff --git a/mcpp.toml b/mcpp.toml index 122b836b..44f0e4d7 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.21.1" +version = "2026.10.22.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index b92c656d..74ebc512 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.10.21.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.22.1"; } // namespace mcpp diff --git a/src/config.cppm b/src/config.cppm index a7abf566..b3c32d6e 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -31,6 +31,7 @@ import mcpp.provider.select; import mcpp.platform; import mcpp.log; import mcpp.diag; +import mcpp.luban; import mcpp.fallback.xlings_binary; import mcpp.fallback.config_migration; @@ -838,6 +839,10 @@ std::expected load_or_init( dir = std::filesystem::path(h) / (entry.size() > 2 ? entry.substr(2) : std::string{}); dir = dir.lexically_normal(); std::error_code ec; + // A Luban system's /xlings is its store, wherever its xlings put it + // (the home that made it, for a system made on a host). + if (dir == "/xlings" && !std::filesystem::is_directory(dir, ec)) + if (auto store = mcpp::luban::system_store()) dir = *store; if (!dir.is_absolute() || !std::filesystem::is_directory(dir, ec)) { // Fewer packages reused than asked for: said, not hidden. mcpp::diag::degraded("xlings/upstream", std::format("[xlings] upstream {} is not a directory", entry), diff --git a/src/fallback/probe_sysroot.cppm b/src/fallback/probe_sysroot.cppm index 3436ac9b..c33c0cef 100644 --- a/src/fallback/probe_sysroot.cppm +++ b/src/fallback/probe_sysroot.cppm @@ -15,6 +15,7 @@ import mcpp.provider; import mcpp.provider.select; import mcpp.platform; import mcpp.log; +import mcpp.luban; export namespace mcpp::fallback { @@ -99,10 +100,15 @@ bool sysroot_is_owned(std::string_view reportedPath, // // Both anchors are required. Registry alone would flag every legitimate // project-local tree; project alone would flag every payload sysroot. +// In a Luban system (`systemRoot`, "/" but for a test) the sysroot `/` is the +// system's own declared root (xlings design 2026-10-21 §3.7). bool sysroot_is_foreign(const std::filesystem::path& sysroot, const std::filesystem::path& registryRoot, - const std::filesystem::path& projectRoot) { + const std::filesystem::path& projectRoot, + const std::filesystem::path& systemRoot = "/") { if (sysroot.empty()) return false; + // A Luban system's own root: its compiler's `/` is the system it is. + if (sysroot.lexically_normal() == "/" && mcpp::luban::is_luban_root(systemRoot)) return false; return !path_is_under(sysroot, registryRoot) && !path_is_under(sysroot, projectRoot); } diff --git a/src/luban.cppm b/src/luban.cppm new file mode 100644 index 00000000..3afa7afc --- /dev/null +++ b/src/luban.cppm @@ -0,0 +1,61 @@ +// mcpp.luban — what a Luban system says about itself (xlings design +// 2026-10-21 §3.7, J5): its root is the system's declared root, and its store +// is where its xlings put it. +// +// A Luban system's /etc/os-release says ID=luban and its /usr/lib/xlings-abi +// is the runtime xlings bound there; its compiler's sysroot is `/`, which is +// then the system itself, not a tree nothing declared. Its packages live in +// the home its root record names (/etc/xlings/usr: /subos//root/usr), +// which is /xlings for a system installed on a machine and the path of the +// home that made it for one made on a host -- the edition's [xlings] upstream +// says /xlings for both. +// +// `root` is "/" but for a test. + +export module mcpp.luban; + +import std; + +export namespace mcpp::luban { + +bool is_luban_root(const std::filesystem::path& root = "/"); + +// The home whose store the system reads, from its root record; nullopt when +// `root` is not a Luban system or says nothing readable. +std::optional system_store(const std::filesystem::path& root = "/"); + +} // namespace mcpp::luban + +namespace mcpp::luban { + +bool is_luban_root(const std::filesystem::path& root) { + std::error_code ec; + if (!std::filesystem::exists(root / "usr/lib/xlings-abi", ec)) return false; + std::ifstream in(root / "etc/os-release"); + for (std::string line; std::getline(in, line);) { + if (!line.starts_with("ID=")) continue; + auto id = line.substr(3); + if (id.size() >= 2 && (id.front() == '"' || id.front() == '\'')) id = id.substr(1, id.size() - 2); + return id == "luban"; + } + return false; +} + +std::optional system_store(const std::filesystem::path& root) { + if (!is_luban_root(root)) return std::nullopt; + std::ifstream in(root / "etc/xlings/usr"); + std::string line; + if (!std::getline(in, line)) return std::nullopt; + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); + const std::filesystem::path usr = std::filesystem::path(line).lexically_normal(); + // /subos//root/usr + const auto rootDir = usr.parent_path(), instance = rootDir.parent_path(), subos = instance.parent_path(); + if (!usr.is_absolute() || usr.filename() != "usr" || rootDir.filename() != "root" || subos.filename() != "subos") + return std::nullopt; + const auto home = subos.parent_path(); + std::error_code ec; + if (!std::filesystem::is_directory(home, ec)) return std::nullopt; + return home; +} + +} // namespace mcpp::luban diff --git a/tests/unit/test_luban_root.cpp b/tests/unit/test_luban_root.cpp new file mode 100644 index 00000000..5f13ce0a --- /dev/null +++ b/tests/unit/test_luban_root.cpp @@ -0,0 +1,66 @@ +// A Luban system says what it is (xlings design 2026-10-21 §3.7, J5): its +// root is declared (os-release ID=luban and /usr/lib/xlings-abi), so its +// compiler's sysroot `/` is not foreign there; and its store is the home its +// root record names, which `[xlings] upstream = ["/xlings"]` means for a +// system made on a host (whose home is not at /xlings). + +#include + +import std; +import mcpp.luban; +import mcpp.fallback.probe_sysroot; + +namespace fs = std::filesystem; + +namespace { + +struct Tree { + fs::path dir; + Tree() { + dir = fs::temp_directory_path() / std::format("mcpp-luban-root-{}-{}", ::testing::UnitTest::GetInstance()->random_seed(), + std::chrono::steady_clock::now().time_since_epoch().count()); + fs::create_directories(dir); + } + ~Tree() { std::error_code ec; fs::remove_all(dir, ec); } + void write(const fs::path& rel, std::string_view text) const { + fs::create_directories((dir / rel).parent_path()); + std::ofstream(dir / rel) << text; + } +}; + +} // namespace + +TEST(LubanRoot, ALubanSystemsRootIsDeclared) { + Tree t; + EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)); + t.write("etc/os-release", "NAME=\"Luban\"\nID=luban\nVERSION_ID=2026.10.22.1\n"); + EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)) << "its os-release alone is not enough"; + t.write("usr/lib/xlings-abi/README", ""); + EXPECT_TRUE(mcpp::luban::is_luban_root(t.dir)); + t.write("etc/os-release", "ID=\"debian\"\n"); + EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)); +} + +TEST(LubanRoot, ItsSysrootSlashIsNotForeign) { + Tree t; + const fs::path registry = "/home/u/.mcpp/registry", project = "/home/u/work/demo"; + EXPECT_TRUE(mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)) << "anywhere else, / is nothing declared"; + t.write("etc/os-release", "ID=luban\n"); + t.write("usr/lib/xlings-abi/README", ""); + EXPECT_FALSE(mcpp::fallback::sysroot_is_foreign("/", registry, project, t.dir)); + EXPECT_TRUE(mcpp::fallback::sysroot_is_foreign("/opt/other/sysroot", registry, project, t.dir)) << "only its own root"; +} + +TEST(LubanRoot, ItsStoreIsTheHomeItsRootRecordNames) { + Tree t; + t.write("etc/os-release", "ID=luban\n"); + t.write("usr/lib/xlings-abi/README", ""); + EXPECT_FALSE(mcpp::luban::system_store(t.dir).has_value()) << "no record"; + const auto home = t.dir / "host-home"; + fs::create_directories(home / "subos" / "box" / "root" / "usr"); + t.write("etc/xlings/usr", (home / "subos" / "box" / "root" / "usr").string() + "\n"); + ASSERT_TRUE(mcpp::luban::system_store(t.dir).has_value()); + EXPECT_EQ(*mcpp::luban::system_store(t.dir), home.lexically_normal()); + t.write("etc/xlings/usr", "/somewhere/else/usr\n"); + EXPECT_FALSE(mcpp::luban::system_store(t.dir).has_value()) << "a record of another shape"; +} From 2a5e79a38efa6d5f6876f2e262c5f2d67a45ad60 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Thu, 22 Oct 2026 12:11:59 +0900 Subject: [PATCH 2/2] refactor: what a Luban system says of itself is the provider's layout (declared_system_root, system_home) -- mcpp names none of xlings' paths Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/config.cppm | 3 +- src/fallback/probe_sysroot.cppm | 3 +- src/luban.cppm | 61 ------------------------------- src/provider/provider.cppm | 7 ++++ src/provider/xlings/provider.cppm | 30 +++++++++++++++ tests/unit/test_luban_root.cpp | 18 ++++----- 6 files changed, 48 insertions(+), 74 deletions(-) delete mode 100644 src/luban.cppm diff --git a/src/config.cppm b/src/config.cppm index b3c32d6e..21d663f9 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -31,7 +31,6 @@ import mcpp.provider.select; import mcpp.platform; import mcpp.log; import mcpp.diag; -import mcpp.luban; import mcpp.fallback.xlings_binary; import mcpp.fallback.config_migration; @@ -842,7 +841,7 @@ std::expected load_or_init( // A Luban system's /xlings is its store, wherever its xlings put it // (the home that made it, for a system made on a host). if (dir == "/xlings" && !std::filesystem::is_directory(dir, ec)) - if (auto store = mcpp::luban::system_store()) dir = *store; + if (auto store = mcpp::provider::layout().system_home("/"); !store.empty()) dir = store; if (!dir.is_absolute() || !std::filesystem::is_directory(dir, ec)) { // Fewer packages reused than asked for: said, not hidden. mcpp::diag::degraded("xlings/upstream", std::format("[xlings] upstream {} is not a directory", entry), diff --git a/src/fallback/probe_sysroot.cppm b/src/fallback/probe_sysroot.cppm index c33c0cef..f8b3e72a 100644 --- a/src/fallback/probe_sysroot.cppm +++ b/src/fallback/probe_sysroot.cppm @@ -15,7 +15,6 @@ import mcpp.provider; import mcpp.provider.select; import mcpp.platform; import mcpp.log; -import mcpp.luban; export namespace mcpp::fallback { @@ -108,7 +107,7 @@ bool sysroot_is_foreign(const std::filesystem::path& sysroot, const std::filesystem::path& systemRoot = "/") { if (sysroot.empty()) return false; // A Luban system's own root: its compiler's `/` is the system it is. - if (sysroot.lexically_normal() == "/" && mcpp::luban::is_luban_root(systemRoot)) return false; + if (sysroot.lexically_normal() == "/" && mcpp::provider::layout().declared_system_root(systemRoot)) return false; return !path_is_under(sysroot, registryRoot) && !path_is_under(sysroot, projectRoot); } diff --git a/src/luban.cppm b/src/luban.cppm deleted file mode 100644 index 3afa7afc..00000000 --- a/src/luban.cppm +++ /dev/null @@ -1,61 +0,0 @@ -// mcpp.luban — what a Luban system says about itself (xlings design -// 2026-10-21 §3.7, J5): its root is the system's declared root, and its store -// is where its xlings put it. -// -// A Luban system's /etc/os-release says ID=luban and its /usr/lib/xlings-abi -// is the runtime xlings bound there; its compiler's sysroot is `/`, which is -// then the system itself, not a tree nothing declared. Its packages live in -// the home its root record names (/etc/xlings/usr: /subos//root/usr), -// which is /xlings for a system installed on a machine and the path of the -// home that made it for one made on a host -- the edition's [xlings] upstream -// says /xlings for both. -// -// `root` is "/" but for a test. - -export module mcpp.luban; - -import std; - -export namespace mcpp::luban { - -bool is_luban_root(const std::filesystem::path& root = "/"); - -// The home whose store the system reads, from its root record; nullopt when -// `root` is not a Luban system or says nothing readable. -std::optional system_store(const std::filesystem::path& root = "/"); - -} // namespace mcpp::luban - -namespace mcpp::luban { - -bool is_luban_root(const std::filesystem::path& root) { - std::error_code ec; - if (!std::filesystem::exists(root / "usr/lib/xlings-abi", ec)) return false; - std::ifstream in(root / "etc/os-release"); - for (std::string line; std::getline(in, line);) { - if (!line.starts_with("ID=")) continue; - auto id = line.substr(3); - if (id.size() >= 2 && (id.front() == '"' || id.front() == '\'')) id = id.substr(1, id.size() - 2); - return id == "luban"; - } - return false; -} - -std::optional system_store(const std::filesystem::path& root) { - if (!is_luban_root(root)) return std::nullopt; - std::ifstream in(root / "etc/xlings/usr"); - std::string line; - if (!std::getline(in, line)) return std::nullopt; - while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); - const std::filesystem::path usr = std::filesystem::path(line).lexically_normal(); - // /subos//root/usr - const auto rootDir = usr.parent_path(), instance = rootDir.parent_path(), subos = instance.parent_path(); - if (!usr.is_absolute() || usr.filename() != "usr" || rootDir.filename() != "root" || subos.filename() != "subos") - return std::nullopt; - const auto home = subos.parent_path(); - std::error_code ec; - if (!std::filesystem::is_directory(home, ec)) return std::nullopt; - return home; -} - -} // namespace mcpp::luban diff --git a/src/provider/provider.cppm b/src/provider/provider.cppm index 8311ded8..db2a4f15 100644 --- a/src/provider/provider.cppm +++ b/src/provider/provider.cppm @@ -377,6 +377,13 @@ public: // Whether `name` is one of the provider's own indexes (its default and // the ones it adds itself), which stay where the provider keeps them. virtual bool is_provider_index(std::string_view name) const = 0; + // A system the provider made, as `root` (/, but for a test) says of + // itself (xlings design 2026-10-21 §3.7): its compiler's sysroot `/` is + // then the system's own declared root, not a tree nothing declared. + virtual bool declared_system_root(const std::filesystem::path& root) const = 0; + // The home such a system reads its packages from, by its root record; + // empty: not one, or its record says nothing usable. + virtual std::filesystem::path system_home(const std::filesystem::path& root) const = 0; }; // What a provider says of a package's payload without installing anything diff --git a/src/provider/xlings/provider.cppm b/src/provider/xlings/provider.cppm index d4e027a4..fce592e8 100644 --- a/src/provider/xlings/provider.cppm +++ b/src/provider/xlings/provider.cppm @@ -196,6 +196,36 @@ public: return {{dir, {}, {"xim"}}}; return {}; } + // A Luban system (xlings design 2026-10-21 §3.7): its os-release says + // ID=luban and /usr/lib/xlings-abi is the runtime xlings bound; its root + // record (/etc/xlings/usr) names its generation, /subos//root/usr + // -- /xlings for one installed on a machine, the path of the home that + // made it for one made on a host. + bool declared_system_root(const std::filesystem::path& root) const override { + std::error_code ec; + if (!std::filesystem::exists(root / "usr/lib/xlings-abi", ec)) return false; + std::ifstream in(root / "etc/os-release"); + for (std::string line; std::getline(in, line);) { + if (!line.starts_with("ID=")) continue; + auto id = line.substr(3); + if (id.size() >= 2 && (id.front() == '"' || id.front() == '\'')) id = id.substr(1, id.size() - 2); + return id == "luban"; + } + return false; + } + std::filesystem::path system_home(const std::filesystem::path& root) const override { + if (!declared_system_root(root)) return {}; + std::ifstream in(root / "etc/xlings/usr"); + std::string line; + if (!std::getline(in, line)) return {}; + while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.pop_back(); + const std::filesystem::path usr = std::filesystem::path(line).lexically_normal(); + const auto rootDir = usr.parent_path(), instance = rootDir.parent_path(), subos = instance.parent_path(); + if (!usr.is_absolute() || usr.filename() != "usr" || rootDir.filename() != "root" || subos.filename() != "subos") return {}; + std::error_code ec; + const auto home = subos.parent_path(); + return std::filesystem::is_directory(home, ec) ? home : std::filesystem::path{}; + } bool is_provider_index(std::string_view name) const override { // xlings' default index and the ones it adds beside it. return name == "xim" || name == "awesome" || name == "scode" || name == "d2x"; diff --git a/tests/unit/test_luban_root.cpp b/tests/unit/test_luban_root.cpp index 5f13ce0a..0a48b825 100644 --- a/tests/unit/test_luban_root.cpp +++ b/tests/unit/test_luban_root.cpp @@ -7,7 +7,8 @@ #include import std; -import mcpp.luban; +import mcpp.provider; +import mcpp.provider.select; import mcpp.fallback.probe_sysroot; namespace fs = std::filesystem; @@ -32,13 +33,13 @@ struct Tree { TEST(LubanRoot, ALubanSystemsRootIsDeclared) { Tree t; - EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)); + EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)); t.write("etc/os-release", "NAME=\"Luban\"\nID=luban\nVERSION_ID=2026.10.22.1\n"); - EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)) << "its os-release alone is not enough"; + EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)) << "its os-release alone is not enough"; t.write("usr/lib/xlings-abi/README", ""); - EXPECT_TRUE(mcpp::luban::is_luban_root(t.dir)); + EXPECT_TRUE(mcpp::provider::layout().declared_system_root(t.dir)); t.write("etc/os-release", "ID=\"debian\"\n"); - EXPECT_FALSE(mcpp::luban::is_luban_root(t.dir)); + EXPECT_FALSE(mcpp::provider::layout().declared_system_root(t.dir)); } TEST(LubanRoot, ItsSysrootSlashIsNotForeign) { @@ -55,12 +56,11 @@ TEST(LubanRoot, ItsStoreIsTheHomeItsRootRecordNames) { Tree t; t.write("etc/os-release", "ID=luban\n"); t.write("usr/lib/xlings-abi/README", ""); - EXPECT_FALSE(mcpp::luban::system_store(t.dir).has_value()) << "no record"; + EXPECT_TRUE(mcpp::provider::layout().system_home(t.dir).empty()) << "no record"; const auto home = t.dir / "host-home"; fs::create_directories(home / "subos" / "box" / "root" / "usr"); t.write("etc/xlings/usr", (home / "subos" / "box" / "root" / "usr").string() + "\n"); - ASSERT_TRUE(mcpp::luban::system_store(t.dir).has_value()); - EXPECT_EQ(*mcpp::luban::system_store(t.dir), home.lexically_normal()); + EXPECT_EQ(mcpp::provider::layout().system_home(t.dir), home.lexically_normal()); t.write("etc/xlings/usr", "/somewhere/else/usr\n"); - EXPECT_FALSE(mcpp::luban::system_store(t.dir).has_value()) << "a record of another shape"; + EXPECT_TRUE(mcpp::provider::layout().system_home(t.dir).empty()) << "a record of another shape"; }