Skip to content

Commit bc36d6c

Browse files
committed
fix(fallback): a vendored xlings behind the pin, and a sysroot from elsewhere
D4. acquire_xlings_binary returned on mere existence, so a home kept the first xlings it ever acquired. Measured here: 2026.8.2.1 against a pin of 2026.8.6.3, with `mcpp self env` printing both numbers beside each other and saying nothing about the gap. Not cosmetic -- the subos_info block arrived in xlings 2026.8.5.1, so on that machine every subos declaration was discarded by a client too old to have the API, and the mcpp#352 fix could not take effect however current mcpp itself was. Missing features are silent by construction: an absent block is also a legitimate state. The first cut of this deleted the vendored binary and re-acquired. That replaced 2026.8.2.1 with the system's 0.4.51 -- older still, and missing the very feature the replacement existed to restore. Being behind the pin justifies looking for a replacement; it does not justify taking whatever turns up. candidate_source_version() now prices the replacement before the old one is removed, and the epoch-crossing comparison (0.4.x vs YYYY.M.D.N) is pinned by a test, since both schemes live on the same disk. D5. remap_xlings_baked_sysroot asked whether gcc's baked sysroot exists. Wrong axis: gcc records `--sysroot=<...>/.xlings/subos/default` as a string when it is built and keeps reporting it in every project it later serves, and a machine with several checkouts has many directories by that name. The baked path therefore routinely exists and belongs to someone else -- a build inside mcpp resolved a sysroot under an unrelated repo. The predicate is now ownership, not existence. doctor reports both. The sysroot finding is anchored on the mcpp home and the project rather than on the compiler's own tree: written the obvious way it consulted xpkgs_from_compiler(binaryPath), which returns nothing when the compiler is reached through an xvm shim -- the default project-local case, and this machine's. It would have shipped as a check that never checked. tests/unit/test_xlings_version_pin.cpp +1 (epoch crossing) tests/unit/test_sysroot_ownership.cpp +7 (new; no filesystem, on purpose)
1 parent f33d8ff commit bc36d6c

6 files changed

Lines changed: 377 additions & 8 deletions

File tree

src/config.cppm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,8 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
600600

601601
// 6. Acquire xlings binary if needed
602602
if (cfg.xlingsBinaryMode == "bundled") {
603-
auto xbin = mcpp::fallback::acquire_xlings_binary(cfg.xlingsBinary, quiet);
603+
auto xbin = mcpp::fallback::acquire_xlings_binary(
604+
cfg.xlingsBinary, quiet, kXlingsPinnedVersion);
604605
if (!xbin) return std::unexpected(ConfigError{xbin.error()});
605606
} else if (cfg.xlingsBinaryMode == "system") {
606607
auto sysPath = mcpp::platform::fs::which(

src/doctor.cppm

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import mcpp.bmi_cache.maintenance;
1414
import mcpp.build.prepare;
1515
import mcpp.build.plan;
1616
import mcpp.config;
17+
import mcpp.fallback.probe_sysroot;
18+
import mcpp.fallback.xlings_binary;
1719
import mcpp.fallback.install_integrity;
1820
import mcpp.fetcher.progress;
1921
import mcpp.home;
@@ -69,6 +71,7 @@ export std::vector<std::string> parse_readelf_runpath(std::string_view dump) {
6971
// `mcpp self env`.
7072
export int env_report() {
7173
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
74+
7275
if (!cfg) { mcpp::ui::error(cfg.error().message); return 4; }
7376

7477
mcpp::config::print_env(*cfg);
@@ -98,6 +101,7 @@ export int doctor_report() {
98101
err(std::format("toolchain detection failed: {}", tc.error().message));
99102
} else {
100103
ok(std::format("{} at {}", tc->label(), tc->binaryPath.string()));
104+
101105
}
102106

103107
// Windows: report the system MSVC (msvc@system). Absence is a warning,
@@ -204,6 +208,28 @@ export int doctor_report() {
204208

205209
mcpp::ui::status("Checking", "registry");
206210
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
211+
212+
// Whose sysroot is this? gcc bakes `--sysroot=<...>/subos/default`
213+
// at build time, and that path is a string, not a reference -- it
214+
// keeps naming wherever the compiler was built no matter which
215+
// project it now serves. A developer machine has many directories by
216+
// that name, so the baked one frequently EXISTS while belonging to an
217+
// unrelated checkout, and headers then come from a tree this build
218+
// never declared. Existence is not ownership.
219+
if (tc && cfg) {
220+
std::error_code cwdEc;
221+
auto project = std::filesystem::current_path(cwdEc);
222+
if (mcpp::fallback::sysroot_is_foreign(
223+
tc->sysroot, (*cfg).registryDir,
224+
cwdEc ? std::filesystem::path{} : project))
225+
warn(std::format(
226+
"sysroot {} belongs to neither this mcpp home ({}) nor "
227+
"this project — headers would come from a tree nothing "
228+
"here declared. mcpp remaps a baked sysroot when it can "
229+
"find the equivalent under the registry; seeing it here "
230+
"means it could not",
231+
tc->sysroot.string(), (*cfg).registryDir.string()));
232+
}
207233
if (!cfg) {
208234
err(cfg.error().message);
209235
} else {
@@ -366,6 +392,33 @@ export int doctor_report() {
366392
else if (!sawAny)
367393
ok("no installed toolchains to check");
368394

395+
// The vendored xlings, against the version this mcpp expects.
396+
//
397+
// Nothing else surfaces this. `mcpp self env` prints both numbers and
398+
// says nothing about the gap, and a home that acquired its xlings once
399+
// never revisited it -- so a machine could sit years behind while every
400+
// command looked healthy. What goes missing is silent by nature:
401+
// features mcpp reads FROM xlings (the subos_info block, for one)
402+
// simply never appear, and the code that consumes them degrades
403+
// quietly because a missing block is also a legitimate state.
404+
{
405+
auto have = mcpp::fallback::vendored_xlings_version((*cfg).xlingsBinary);
406+
const auto want = std::string(mcpp::config::kXlingsPinnedVersion);
407+
if (have.empty()) {
408+
warn(std::format("cannot read the vendored xlings version at {}",
409+
(*cfg).xlingsBinary.string()));
410+
} else if (mcpp::fallback::version_is_older(have, want)) {
411+
warn(std::format(
412+
"vendored xlings is {} but this mcpp expects {} — features "
413+
"mcpp reads from xlings may be silently absent (the subos "
414+
"self-description arrived in 2026.8.5.1). It is replaced "
415+
"automatically on the next `mcpp self init`",
416+
have, want));
417+
} else {
418+
ok(std::format("vendored xlings {} (pinned {})", have, want));
419+
}
420+
}
421+
369422
// Dangling symlinks under registry/subos/default/lib — these point
370423
// into xim payload lib dirs; a removed package leaves them broken.
371424
auto subosLib = (*cfg).xlingsHome() / "subos" / "default" / "lib";

src/fallback/probe_sysroot.cppm

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,40 @@ import mcpp.log;
1717

1818
export namespace mcpp::fallback {
1919

20-
// When GCC reports a sysroot ending in "subos/default" that doesn't exist
21-
// on the current machine (baked build-time path), remap it to the
22-
// equivalent sysroot relative to the compiler's own xpkgs directory.
20+
// When GCC reports a baked "subos/default" sysroot that does not belong to
21+
// THIS toolchain's home, remap it to the equivalent sysroot under the
22+
// compiler's own xpkgs tree.
23+
//
24+
// The predicate used to be "does not exist", which is the wrong axis. A
25+
// developer machine has more than one project-local `.xlings/subos/default`,
26+
// and gcc's baked path can name one of them: it exists, so the remap was
27+
// skipped and every build in an unrelated project inherited another
28+
// project's sysroot. Measured -- a build in mcpp resolved
29+
// `--sysroot=<other-repo>/.xlings/subos/default`.
30+
//
31+
// Existence says nothing about ownership. What matters is whether the path is
32+
// under the same registry as the compiler, and a path that is not gets
33+
// remapped whether or not something happens to be there.
2334
std::optional<std::filesystem::path>
2435
remap_xlings_baked_sysroot(std::string_view reportedPath,
2536
const std::filesystem::path& compilerBin) {
2637
if (reportedPath.empty()) return std::nullopt;
2738
if (!reportedPath.ends_with("subos/default")) return std::nullopt;
28-
if (std::filesystem::exists(std::string(reportedPath))) return std::nullopt;
2939

30-
if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin)) {
40+
auto xpkgsOpt = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin);
41+
if (xpkgsOpt) {
42+
// Owned by this toolchain's registry? Then it is the right answer.
43+
auto registry = xpkgsOpt->parent_path().parent_path();
44+
std::error_code ec;
45+
auto rel = std::filesystem::path(std::string(reportedPath))
46+
.lexically_relative(registry);
47+
const bool inside = !rel.empty()
48+
&& rel.native().rfind("..", 0) != 0;
49+
if (inside && std::filesystem::exists(std::string(reportedPath), ec))
50+
return std::nullopt;
51+
}
52+
53+
if (auto xpkgs = std::move(xpkgsOpt)) {
3154
// xpkgs is <registry>/data/xpkgs -> registry = xpkgs/../..
3255
auto registrySysroot = xpkgs->parent_path().parent_path()
3356
/ "subos" / "default";
@@ -37,6 +60,30 @@ remap_xlings_baked_sysroot(std::string_view reportedPath,
3760
return std::nullopt;
3861
}
3962

63+
// Is this sysroot foreign -- neither this mcpp home's registry nor a tree
64+
// belonging to the project being built?
65+
//
66+
// The hazard is specific and was measured: gcc bakes `--sysroot=<...>/.xlings/
67+
// subos/default` as a STRING at build time, and a developer machine has many
68+
// directories by that name. The baked one therefore frequently EXISTS while
69+
// belonging to an unrelated checkout, and headers silently come from a tree
70+
// this build never declared. remap_xlings_baked_sysroot repairs the case it
71+
// can see; this predicate is what reports the case it cannot.
72+
//
73+
// Both anchors are required. Registry alone would flag every legitimate
74+
// project-local tree; project alone would flag every payload sysroot.
75+
bool sysroot_is_foreign(const std::filesystem::path& sysroot,
76+
const std::filesystem::path& registryRoot,
77+
const std::filesystem::path& projectRoot) {
78+
if (sysroot.empty()) return false;
79+
auto under = [&](const std::filesystem::path& anchor) {
80+
if (anchor.empty()) return false;
81+
auto rel = sysroot.lexically_relative(anchor);
82+
return !rel.empty() && rel.native().rfind("..", 0) != 0;
83+
};
84+
return !under(registryRoot) && !under(projectRoot);
85+
}
86+
4087
// Parse a Clang .cfg file alongside the compiler binary for --sysroot=.
4188
std::optional<std::filesystem::path>
4289
parse_clang_cfg_sysroot(const std::filesystem::path& compilerBin) {

src/fallback/xlings_binary.cppm

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
module;
99
#include <cstdlib>
10+
#include <cctype>
1011

1112
export module mcpp.fallback.xlings_binary;
1213

@@ -17,9 +18,70 @@ export namespace mcpp::fallback {
1718

1819
// Try to acquire (copy) the xlings binary to destBin.
1920
// Returns destBin on success or an error string.
21+
// The version already vendored at destBin, or empty when it cannot be read.
22+
std::string vendored_xlings_version(const std::filesystem::path& bin);
23+
24+
// The version the acquisition chain WOULD install, without installing it.
25+
// Empty when nothing is available. Replacing a vendored binary is only an
26+
// improvement when this is newer than what is already there.
27+
std::string candidate_source_version();
28+
29+
// True when `have` is strictly older than `want`, comparing dot-separated
30+
// numeric components. Anything unparseable answers false -- a version this
31+
// code does not understand is not evidence of being behind.
32+
bool version_is_older(std::string_view have, std::string_view want);
33+
34+
// Try to acquire (copy) the xlings binary to destBin.
35+
//
36+
// `pinnedVersion` is the version this mcpp expects. A vendored binary OLDER
37+
// than the pin is replaced; one that is newer or equal is left alone.
38+
//
39+
// This used to return early on mere existence, with no version comparison at
40+
// all, and nothing else ever revisited the file. A home created once kept
41+
// whatever xlings it first acquired forever -- measured at 2026.8.2.1 against
42+
// a pin of 2026.8.6.3, with `mcpp self env` printing both numbers side by side
43+
// and saying nothing about the gap. The cost is not cosmetic: features mcpp
44+
// reads from xlings simply do not appear. The subos_info block arrived in
45+
// 2026.8.5.1, so on that machine the graphics packages' declarations were
46+
// discarded by a client too old to have the API, and mcpp#352's fix could
47+
// never take effect.
48+
//
49+
// Strictly-older, not not-equal: a user who put a newer xlings there on
50+
// purpose must not be downgraded by an mcpp that happens to pin an older one.
2051
std::expected<std::filesystem::path, std::string>
21-
acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false) {
22-
if (std::filesystem::exists(destBin)) return destBin;
52+
acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false,
53+
std::string_view pinnedVersion = {}) {
54+
if (std::filesystem::exists(destBin)) {
55+
auto have = vendored_xlings_version(destBin);
56+
if (pinnedVersion.empty() || have.empty()
57+
|| !version_is_older(have, pinnedVersion))
58+
return destBin;
59+
60+
// Behind the pin -- but replacing is only an improvement if what we
61+
// would put there is actually newer. The acquisition chain below ends
62+
// at whatever `which xlings` finds, and on a machine whose system
63+
// xlings is ANCIENT that is a downgrade dressed up as an update.
64+
//
65+
// Learned by doing it: this code first deleted the vendored binary and
66+
// re-acquired, which replaced 2026.8.2.1 with the system's 0.4.51 --
67+
// older still, and equally missing the feature the check exists to
68+
// restore. Look before leaping.
69+
auto candidate = candidate_source_version();
70+
if (candidate.empty() || !version_is_older(have, candidate)) {
71+
if (!quiet)
72+
std::println("{:>12} vendored xlings {} is older than the "
73+
"pinned {}, but no newer source is available "
74+
"(keeping it; run `xlings self update`)",
75+
"Note", have, pinnedVersion);
76+
return destBin;
77+
}
78+
if (!quiet)
79+
std::println("{:>12} vendored xlings {} -> {} (pinned {})",
80+
"Updating", have, candidate, pinnedVersion);
81+
std::error_code rec;
82+
std::filesystem::remove(destBin, rec);
83+
// fall through and re-acquire
84+
}
2385

2486
std::error_code ec;
2587
std::filesystem::create_directories(destBin.parent_path(), ec);
@@ -82,4 +144,75 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet = false)
82144
(destBin.parent_path().parent_path() / "config.toml").string()));
83145
}
84146

147+
148+
std::string vendored_xlings_version(const std::filesystem::path& bin) {
149+
std::error_code ec;
150+
if (!std::filesystem::exists(bin, ec)) return {};
151+
auto r = mcpp::platform::process::capture(std::format(
152+
"{} --version 2>/dev/null", mcpp::platform::shell::quote(bin.string())));
153+
if (r.exit_code != 0) return {};
154+
// Output carries ANSI colour; take the first dotted-numeric run.
155+
std::string out;
156+
for (std::size_t i = 0; i < r.output.size(); ++i) {
157+
if (r.output[i] == '\x1b') { // skip CSI
158+
while (i < r.output.size() && r.output[i] != 'm') ++i;
159+
continue;
160+
}
161+
out += r.output[i];
162+
}
163+
std::size_t i = 0;
164+
while (i < out.size()) {
165+
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
166+
auto j = i;
167+
while (j < out.size()
168+
&& (std::isdigit(static_cast<unsigned char>(out[j]))
169+
|| out[j] == '.')) ++j;
170+
auto cand = out.substr(i, j - i);
171+
if (cand.find('.') != std::string::npos) return cand;
172+
i = j;
173+
} else ++i;
174+
}
175+
return {};
176+
}
177+
178+
bool version_is_older(std::string_view have, std::string_view want) {
179+
auto parts = [](std::string_view v) {
180+
std::vector<long long> out;
181+
std::size_t i = 0;
182+
while (i <= v.size()) {
183+
auto dot = v.find('.', i);
184+
auto seg = v.substr(i, dot == std::string_view::npos
185+
? std::string_view::npos : dot - i);
186+
long long n = 0;
187+
auto [p, e] = std::from_chars(seg.data(), seg.data() + seg.size(), n);
188+
if (e != std::errc{}) return std::vector<long long>{};
189+
out.push_back(n);
190+
if (dot == std::string_view::npos) break;
191+
i = dot + 1;
192+
}
193+
return out;
194+
};
195+
auto a = parts(have), b = parts(want);
196+
if (a.empty() || b.empty()) return false; // unparseable is not "behind"
197+
for (std::size_t i = 0; i < std::max(a.size(), b.size()); ++i) {
198+
long long x = i < a.size() ? a[i] : 0;
199+
long long y = i < b.size() ? b[i] : 0;
200+
if (x != y) return x < y;
201+
}
202+
return false;
203+
}
204+
205+
206+
std::string candidate_source_version() {
207+
if (const char* e = std::getenv("MCPP_VENDORED_XLINGS"); e && *e) {
208+
std::error_code ec;
209+
if (std::filesystem::exists(std::filesystem::path(e), ec))
210+
return vendored_xlings_version(std::filesystem::path(e));
211+
}
212+
if (auto sys = mcpp::platform::fs::which(
213+
std::string("xlings") + std::string(mcpp::platform::exe_suffix)))
214+
return vendored_xlings_version(*sys);
215+
return {};
216+
}
217+
85218
} // namespace mcpp::fallback

0 commit comments

Comments
 (0)