Skip to content
Merged
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
8 changes: 6 additions & 2 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<os>-"
# 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
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 16 additions & 7 deletions src/toolchain/probe.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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: <root>/include/ + <root>/lib64/ (or lib/).
Expand All @@ -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 <linux/limits.h>, 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 <linux/limits.h>");
}

mcpp::log::verbose("probe", std::format(
Expand Down
113 changes: 65 additions & 48 deletions src/xlings.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::filesystem::path>
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
Expand All @@ -89,7 +94,11 @@ namespace paths {
std::optional<std::filesystem::path> active_home_xpkgs();

// Like find_sibling_tool, but anchored at the active home's xpkgs.
std::optional<std::filesystem::path> 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<std::filesystem::path>
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);
Expand Down Expand Up @@ -543,20 +552,60 @@ std::optional<std::filesystem::path> active_home_xpkgs() {
return xpkgs;
}

std::optional<std::filesystem::path> 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<std::filesystem::path>
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<std::filesystem::path>
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<std::filesystem::path>
find_sibling_binary(const std::filesystem::path& compilerBin,
std::string_view tool,
Expand All @@ -578,54 +627,22 @@ find_sibling_binary(const std::filesystem::path& compilerBin,

std::optional<std::filesystem::path>
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;
Expand Down
103 changes: 103 additions & 0 deletions tests/unit/test_xlings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import std;
import mcpp.xlings;
import mcpp.platform.env;

namespace {

Expand Down Expand Up @@ -143,3 +144,105 @@ 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");
mcpp::platform::env::set("HOME", tmp.string());
auto found = mcpp::xlings::paths::find_sibling_package(gccBin, "linux-headers");
mcpp::platform::env::set("HOME", oldHome ? oldHome : "");

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");

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");
mcpp::platform::env::set("MCPP_HOME", oldMcppHome ? oldMcppHome : "");

ASSERT_TRUE(found.has_value());
EXPECT_EQ(*found, real);

std::filesystem::remove_all(tmp);
}
Loading