From be86dc34253e038ebbaa320b6d0857d9fb45822a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 18:41:18 +0800 Subject: [PATCH 1/2] fix: payload discovery must skip delegating-package husks (#120) A delegating index package (xim:linux-headers -> scode:linux-headers) leaves a metadata-only husk (.xim-installed + .xpkg.lua) under its own prefix; find_sibling_package counted .xpkg.lua as content, returned the husk, and probe_payload_paths silently dropped the kernel-header include path -- every glibc toolchain build then died at (e2e 29/31 on a cold sandbox). - payload qualification: dot-prefixed entries are metadata, not content; optional requiredRelPath must exist in the version dir - find_home_tool: search across all index prefixes (was hardcoded xim-x-) with the same qualification rules - probe: require include/linux/limits.h (linux-headers) and include/features.h (glibc fallback); log when no payload qualifies - CI cache hygiene: give ci-linux/ci-windows sandbox caches a '-ci-' lineage disjoint from release's '-release-' caches; a bare restore prefix used to swap in the release-flavored sandbox, which is what exposed this cold-path bug on main --- .github/workflows/ci-linux.yml | 8 ++- .github/workflows/ci-windows.yml | 5 +- .github/workflows/release.yml | 2 - src/toolchain/probe.cppm | 23 +++++-- src/xlings.cppm | 113 ++++++++++++++++++------------- tests/unit/test_xlings.cpp | 102 ++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index fbd75d87..cabe5334 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -40,9 +40,13 @@ jobs: uses: actions/cache@v4 with: path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} + # NOTE: the "-ci-" segment keeps this lineage disjoint from + # release.yml's "-release-" caches. A bare "mcpp-sandbox--" + # restore prefix used to match the release sandbox too, silently + # swapping in a differently-populated registry (issue #120). + key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} restore-keys: | - mcpp-sandbox-${{ runner.os }}- + mcpp-sandbox-${{ runner.os }}-ci- # Cache xlings + its locally installed packages (xim:mcpp etc.). # Saves the xlings bootstrap roundtrip + the mcpp xpkg download diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 4796bb13..fbb05680 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -28,9 +28,10 @@ jobs: uses: actions/cache@v4 with: path: ~\.mcpp - key: mcpp-sandbox-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} + # Disjoint from release.yml's "-release-" cache lineage (issue #120). + key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} restore-keys: | - mcpp-sandbox-${{ runner.os }}- + mcpp-sandbox-${{ runner.os }}-ci- - name: Cache xlings uses: actions/cache@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ad35b46..0a9aa392 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,6 @@ jobs: key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} restore-keys: | mcpp-sandbox-${{ runner.os }}-release- - mcpp-sandbox-${{ runner.os }}- # Cache xlings + xim:mcpp install. - name: Cache xlings @@ -466,7 +465,6 @@ jobs: key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} restore-keys: | mcpp-sandbox-${{ runner.os }}-release- - mcpp-sandbox-${{ runner.os }}- - name: Cache xlings uses: actions/cache@v4 diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index ad2f17d0..2df1b967 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -325,7 +325,7 @@ probe_payload_paths(const std::filesystem::path& compilerBin) { // its owner home, while the active home may own (or have just installed) // the sysroot payloads. auto glibc = paths::find_sibling_tool(compilerBin, "glibc"); - if (!glibc) glibc = paths::find_home_tool("glibc"); + if (!glibc) glibc = paths::find_home_tool("glibc", "include/features.h"); if (!glibc) return std::nullopt; // Glibc layout: /include/ + /lib64/ (or lib/). @@ -344,13 +344,22 @@ probe_payload_paths(const std::filesystem::path& compilerBin) { pp.glibcLib = glibcLib; // Find linux kernel headers (optional — search across index prefixes, - // then the active home registry). - auto linuxHeaders = paths::find_sibling_package(compilerBin, "linux-headers"); - if (!linuxHeaders) linuxHeaders = paths::find_home_tool("linux-headers"); + // then the active home registry). Require the actual payload: a + // delegating index package (xim:linux-headers → scode:linux-headers) + // leaves a metadata-only husk under its own prefix, and the discovery + // must skip it instead of giving up (issue #120: glibc's local_lim.h + // needs , so a silent miss breaks every glibc build). + constexpr std::string_view kLinuxLimits = "include/linux/limits.h"; + auto linuxHeaders = + paths::find_sibling_package(compilerBin, "linux-headers", kLinuxLimits); + if (!linuxHeaders) + linuxHeaders = paths::find_home_tool("linux-headers", kLinuxLimits); if (linuxHeaders) { - auto linuxInclude = *linuxHeaders / "include"; - if (std::filesystem::exists(linuxInclude / "linux" / "limits.h")) - pp.linuxInclude = linuxInclude; + pp.linuxInclude = *linuxHeaders / "include"; + } else { + mcpp::log::verbose("probe", + "linux-headers payload not found under any index prefix — " + "glibc builds will fail at "); } mcpp::log::verbose("probe", std::format( diff --git a/src/xlings.cppm b/src/xlings.cppm index e48db8f9..8620422f 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -78,9 +78,14 @@ namespace paths { // Find a sibling package across all index prefixes. // e.g. find_sibling_package(gcc_bin, "linux-headers") searches for // xim-x-linux-headers, scode-x-linux-headers, etc. + // Metadata-only dirs (.xim-installed/.xpkg.lua husks left by delegating + // index packages) never qualify; when requiredRelPath is given, only a + // version dir containing it qualifies (the payload may live under a + // different prefix than the husk — issue #120). std::optional find_sibling_package(const std::filesystem::path& compilerBin, - std::string_view packageName); + std::string_view packageName, + std::string_view requiredRelPath = {}); // xpkgs root of the ACTIVE mcpp home ($MCPP_HOME or ~/.mcpp). Payload // discovery consults this in addition to compiler siblings: an @@ -89,7 +94,11 @@ namespace paths { std::optional active_home_xpkgs(); // Like find_sibling_tool, but anchored at the active home's xpkgs. - std::optional find_home_tool(std::string_view tool); + // Searches across index prefixes (xim-x-, scode-x-, …) with the same + // husk/requiredRelPath rules as find_sibling_package. + std::optional + find_home_tool(std::string_view tool, + std::string_view requiredRelPath = {}); // index data root: env.home / "data" std::filesystem::path index_data(const Env& env); @@ -543,20 +552,60 @@ std::optional active_home_xpkgs() { return xpkgs; } -std::optional find_home_tool(std::string_view tool) { - auto xpkgs = active_home_xpkgs(); - if (!xpkgs) return std::nullopt; +namespace { - auto root = *xpkgs / std::format("xim-x-{}", tool); +// A version dir qualifies as a payload only if it has real content — +// dot-prefixed entries (.xim-installed, .xpkg.lua) are install metadata, +// and a dir holding nothing else is the husk a delegating index package +// leaves behind (the payload lives under another prefix; issue #120). +// When requiredRelPath is given, the dir must also contain that path. +bool payload_dir_qualifies(const std::filesystem::path& versionDir, + std::string_view requiredRelPath) { std::error_code ec; - if (!std::filesystem::exists(root, ec)) return std::nullopt; + bool hasContent = false; + for (auto& f : std::filesystem::directory_iterator(versionDir, ec)) { + if (!f.path().filename().string().starts_with(".")) { + hasContent = true; + break; + } + } + if (!hasContent) return false; + if (!requiredRelPath.empty() + && !std::filesystem::exists(versionDir / requiredRelPath, ec)) + return false; + return true; +} - for (auto& v : std::filesystem::directory_iterator(root, ec)) { - if (v.is_directory(ec)) return v.path(); +// Scan an xpkgs root across index prefixes (xim-x-, scode-x-, compat-x-, …) +// for the first qualifying version dir of `packageName`. +std::optional +find_package_in_xpkgs(const std::filesystem::path& xpkgs, + std::string_view packageName, + std::string_view requiredRelPath) { + std::error_code ec; + std::string suffix = std::format("-x-{}", packageName); + for (auto& entry : std::filesystem::directory_iterator(xpkgs, ec)) { + if (!entry.is_directory(ec)) continue; + auto name = entry.path().filename().string(); + if (!name.ends_with(suffix)) continue; + for (auto& v : std::filesystem::directory_iterator(entry.path(), ec)) { + if (!v.is_directory(ec)) continue; + if (payload_dir_qualifies(v.path(), requiredRelPath)) + return v.path(); + } } return std::nullopt; } +} // namespace + +std::optional +find_home_tool(std::string_view tool, std::string_view requiredRelPath) { + auto xpkgs = active_home_xpkgs(); + if (!xpkgs) return std::nullopt; + return find_package_in_xpkgs(*xpkgs, tool, requiredRelPath); +} + std::optional find_sibling_binary(const std::filesystem::path& compilerBin, std::string_view tool, @@ -578,54 +627,22 @@ find_sibling_binary(const std::filesystem::path& compilerBin, std::optional find_sibling_package(const std::filesystem::path& compilerBin, - std::string_view packageName) { + std::string_view packageName, + std::string_view requiredRelPath) { auto xpkgs = xpkgs_from_compiler(compilerBin); if (!xpkgs) return std::nullopt; // Search across index prefixes: xim-x-, scode-x-, compat-x-, etc. - std::error_code ec; - std::string suffix = std::format("-x-{}", packageName); - for (auto& entry : std::filesystem::directory_iterator(*xpkgs, ec)) { - if (!entry.is_directory(ec)) continue; - auto name = entry.path().filename().string(); - if (!name.ends_with(suffix)) continue; - // Return the first (highest) version dir that has actual content. - for (auto& v : std::filesystem::directory_iterator(entry.path(), ec)) { - if (!v.is_directory(ec)) continue; - // Skip empty packages (only .xim-installed marker) - bool hasContent = false; - for (auto& f : std::filesystem::directory_iterator(v.path(), ec)) { - if (f.path().filename() != ".xim-installed") { - hasContent = true; - break; - } - } - if (hasContent) return v.path(); - } - } + if (auto found = find_package_in_xpkgs(*xpkgs, packageName, requiredRelPath)) + return found; // Also check ~/.xlings/data/xpkgs/ (xlings global home) as fallback. + std::error_code ec; const char* home = std::getenv("HOME"); if (home) { auto xlingsXpkgs = std::filesystem::path(home) / ".xlings" / "data" / "xpkgs"; - if (xlingsXpkgs != *xpkgs && std::filesystem::exists(xlingsXpkgs, ec)) { - for (auto& entry : std::filesystem::directory_iterator(xlingsXpkgs, ec)) { - if (!entry.is_directory(ec)) continue; - auto name = entry.path().filename().string(); - if (!name.ends_with(suffix)) continue; - for (auto& v : std::filesystem::directory_iterator(entry.path(), ec)) { - if (!v.is_directory(ec)) continue; - bool hasContent = false; - for (auto& f : std::filesystem::directory_iterator(v.path(), ec)) { - if (f.path().filename() != ".xim-installed") { - hasContent = true; - break; - } - } - if (hasContent) return v.path(); - } - } - } + if (xlingsXpkgs != *xpkgs && std::filesystem::exists(xlingsXpkgs, ec)) + return find_package_in_xpkgs(xlingsXpkgs, packageName, requiredRelPath); } return std::nullopt; diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index e44b2087..faf70227 100644 --- a/tests/unit/test_xlings.cpp +++ b/tests/unit/test_xlings.cpp @@ -1,4 +1,5 @@ #include +#include import std; import mcpp.xlings; @@ -143,3 +144,104 @@ TEST(XlingsIndexFreshness, AcceptsOfficialPackageCacheWithCurrentPath) { std::filesystem::remove_all(home); } + +// ─── Sibling/home payload discovery (issue #120) ───────────────────── +// +// A delegating index package (e.g. xim:linux-headers forwarding to +// scode:linux-headers) leaves a metadata-only husk dir under its own +// prefix (.xim-installed + .xpkg.lua, no payload). Discovery must not +// stop at the husk: the real payload lives under another prefix. + +namespace { + +void touch(const std::filesystem::path& p, std::string_view content = "x") { + std::filesystem::create_directories(p.parent_path()); + std::ofstream(p) << content; +} + +} // namespace + +TEST(XlingsSiblingPackage, MetadataOnlyHuskIsNotContent) { + auto tmp = make_tempdir("mcpp-husk"); + auto xpkgs = tmp / "xpkgs"; + auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; + touch(gccBin); + + // Only a husk exists: .xim-installed + .xpkg.lua, no payload. + auto husk = xpkgs / "xim-x-linux-headers" / "5.11.1"; + touch(husk / ".xim-installed"); + touch(husk / ".xpkg.lua", "package = {}"); + + // Isolate from the host's ~/.xlings fallback. + const char* oldHome = std::getenv("HOME"); + ::setenv("HOME", tmp.c_str(), 1); + auto found = mcpp::xlings::paths::find_sibling_package(gccBin, "linux-headers"); + if (oldHome) ::setenv("HOME", oldHome, 1); else ::unsetenv("HOME"); + + EXPECT_FALSE(found.has_value()); + + std::filesystem::remove_all(tmp); +} + +TEST(XlingsSiblingPackage, SkipsHuskAndFindsPayloadUnderOtherPrefix) { + auto tmp = make_tempdir("mcpp-husk"); + auto xpkgs = tmp / "xpkgs"; + auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; + touch(gccBin); + + auto husk = xpkgs / "xim-x-linux-headers" / "5.11.1"; + touch(husk / ".xim-installed"); + touch(husk / ".xpkg.lua", "package = {}"); + + auto real = xpkgs / "scode-x-linux-headers" / "5.11.1"; + touch(real / "include" / "linux" / "limits.h"); + + auto found = mcpp::xlings::paths::find_sibling_package(gccBin, "linux-headers"); + ASSERT_TRUE(found.has_value()); + EXPECT_EQ(*found, real); + + std::filesystem::remove_all(tmp); +} + +TEST(XlingsSiblingPackage, RequiredRelPathRejectsContentfulButWrongCandidate) { + auto tmp = make_tempdir("mcpp-husk"); + auto xpkgs = tmp / "xpkgs"; + auto gccBin = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++"; + touch(gccBin); + + // Contentful but missing the payload that matters. + auto stray = xpkgs / "xim-x-linux-headers" / "5.11.1"; + touch(stray / "README.md"); + + auto real = xpkgs / "scode-x-linux-headers" / "5.11.1"; + touch(real / "include" / "linux" / "limits.h"); + + auto found = mcpp::xlings::paths::find_sibling_package( + gccBin, "linux-headers", "include/linux/limits.h"); + ASSERT_TRUE(found.has_value()); + EXPECT_EQ(*found, real); + + std::filesystem::remove_all(tmp); +} + +TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) { + auto tmp = make_tempdir("mcpp-husk-home"); + auto xpkgs = tmp / "registry" / "data" / "xpkgs"; + + auto husk = xpkgs / "xim-x-linux-headers" / "5.11.1"; + touch(husk / ".xim-installed"); + touch(husk / ".xpkg.lua", "package = {}"); + + auto real = xpkgs / "scode-x-linux-headers" / "5.11.1"; + touch(real / "include" / "linux" / "limits.h"); + + ::setenv("MCPP_HOME", tmp.c_str(), 1); + auto found = mcpp::xlings::paths::find_home_tool( + "linux-headers", "include/linux/limits.h"); + ::unsetenv("MCPP_HOME"); + + ASSERT_TRUE(found.has_value()); + EXPECT_EQ(*found, real); + + std::filesystem::remove_all(tmp); +} From c85b15ed289e6c374b3b439681dc63b8163976f6 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 5 Jun 2026 19:08:00 +0800 Subject: [PATCH 2/2] test: use portable env setter (Windows has no setenv/unsetenv) --- tests/unit/test_xlings.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_xlings.cpp b/tests/unit/test_xlings.cpp index faf70227..9187f461 100644 --- a/tests/unit/test_xlings.cpp +++ b/tests/unit/test_xlings.cpp @@ -1,8 +1,8 @@ #include -#include import std; import mcpp.xlings; +import mcpp.platform.env; namespace { @@ -174,9 +174,9 @@ TEST(XlingsSiblingPackage, MetadataOnlyHuskIsNotContent) { // Isolate from the host's ~/.xlings fallback. const char* oldHome = std::getenv("HOME"); - ::setenv("HOME", tmp.c_str(), 1); + mcpp::platform::env::set("HOME", tmp.string()); auto found = mcpp::xlings::paths::find_sibling_package(gccBin, "linux-headers"); - if (oldHome) ::setenv("HOME", oldHome, 1); else ::unsetenv("HOME"); + mcpp::platform::env::set("HOME", oldHome ? oldHome : ""); EXPECT_FALSE(found.has_value()); @@ -235,10 +235,11 @@ TEST(XlingsHomeTool, FindsPayloadUnderNonXimPrefix) { auto real = xpkgs / "scode-x-linux-headers" / "5.11.1"; touch(real / "include" / "linux" / "limits.h"); - ::setenv("MCPP_HOME", tmp.c_str(), 1); + const char* oldMcppHome = std::getenv("MCPP_HOME"); + mcpp::platform::env::set("MCPP_HOME", tmp.string()); auto found = mcpp::xlings::paths::find_home_tool( "linux-headers", "include/linux/limits.h"); - ::unsetenv("MCPP_HOME"); + mcpp::platform::env::set("MCPP_HOME", oldMcppHome ? oldMcppHome : ""); ASSERT_TRUE(found.has_value()); EXPECT_EQ(*found, real);