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..21d663f9 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -838,6 +838,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::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 3436ac9b..f8b3e72a 100644 --- a/src/fallback/probe_sysroot.cppm +++ b/src/fallback/probe_sysroot.cppm @@ -99,10 +99,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::provider::layout().declared_system_root(systemRoot)) return false; return !path_is_under(sysroot, registryRoot) && !path_is_under(sysroot, projectRoot); } 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 new file mode 100644 index 00000000..0a48b825 --- /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.provider; +import mcpp.provider.select; +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::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::provider::layout().declared_system_root(t.dir)) << "its os-release alone is not enough"; + t.write("usr/lib/xlings-abi/README", ""); + EXPECT_TRUE(mcpp::provider::layout().declared_system_root(t.dir)); + t.write("etc/os-release", "ID=\"debian\"\n"); + EXPECT_FALSE(mcpp::provider::layout().declared_system_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_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"); + EXPECT_EQ(mcpp::provider::layout().system_home(t.dir), home.lexically_normal()); + t.write("etc/xlings/usr", "/somewhere/else/usr\n"); + EXPECT_TRUE(mcpp::provider::layout().system_home(t.dir).empty()) << "a record of another shape"; +}