Skip to content

Commit 9840ca9

Browse files
committed
fix(toolchain): one payload is an answer; two are a question
CI still had no runtime binding, and therefore no loader on the link line: -o bin/mcpp --sysroot=<...>/subos/default -B<...>/binutils/bin -static-libstdc++ Every compatibility source is something an earlier mechanism wrote -- gcc's specs, clang's cfg, and now the compiler's PT_INTERP -- and a machine can legitimately have none of them. Recorded loader paths can also name a subos VIEW rather than the payload; those carry no version at all, so they are canonicalised first (R6: artifacts bind the payload, never the view). The last resort is the installed payload set, and only when it is a singleton. This is not the rule the design removed: that one asked a directory for "the glibc" and took whatever readdir yielded first -- a choice, made by something unrelated to what the artifact loads, silently wrong as soon as a dependency's `>=` floor installed a second payload. Where exactly one glibc exists there is no choice to make; it is the only runtime any artifact from this toolchain could bind. Two or more and this stays silent, which is precisely the case the incident was and the case the subos has to settle. Verified under conditions this machine had to be forced into: gcc's specs file moved aside AND gcc's own PT_INTERP repointed at the host loader. The binding still resolves and the artifact still takes the payload loader. tests/unit/test_runtime_binding_fallback.cpp +5
1 parent 7e3fd66 commit 9840ca9

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/toolchain/post_install.cppm

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,14 @@ std::string baked_runtime_binding(const std::filesystem::path& compilerBin) {
597597
auto from_text = [](const std::string& text) -> std::string {
598598
auto loader = detect_baked_loader(text);
599599
if (loader.empty()) return {};
600+
// The recorded path may name a subos VIEW (`<home>/subos/default/lib/
601+
// ld-linux-...`), which is a symlink into the payload. R6: artifacts
602+
// bind the payload, never the mutable view -- so resolve to the
603+
// payload before reading a version off it. A view path has no version
604+
// component at all, and parsing one yields nothing.
605+
std::error_code cec;
606+
if (auto real = std::filesystem::canonical(loader, cec); !cec)
607+
loader = real.string();
600608
// .../xim-x-glibc/<ver>/lib64/ld-linux-... — the version is the
601609
// grandparent of the lib dir.
602610
auto dir = std::filesystem::path(loader).parent_path(); // lib64
@@ -643,6 +651,39 @@ std::string baked_runtime_binding(const std::filesystem::path& compilerBin) {
643651
// the same fact the specs held, from a mechanism that has not gone away.
644652
if (auto r = from_text(read_elf_interp(compilerBin)); !r.empty())
645653
return r;
654+
655+
// Last: the installed payload set, but ONLY when it is a singleton.
656+
//
657+
// This is not the rule this design removed. That rule asked a directory
658+
// for "the glibc" and took whatever `readdir` yielded first -- a CHOICE,
659+
// made by something with no bearing on what the artifact would load, and
660+
// silently wrong the moment a second payload appeared. Where exactly one
661+
// payload is installed there is no choice to make: it is the only glibc
662+
// any artifact from this toolchain could bind, and declining would refuse
663+
// a question that has a single answer.
664+
//
665+
// Two or more and this stays silent. That is the case the incident was,
666+
// and it is the case the subos must answer.
667+
if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin)) {
668+
std::vector<std::string> versions;
669+
for (auto it = std::filesystem::directory_iterator(*xpkgs / "xim-x-glibc", ec);
670+
!ec && it != std::filesystem::directory_iterator{}; it.increment(ec)) {
671+
if (!it->is_directory(ec)) continue;
672+
auto v = it->path().filename().string();
673+
if (!v.empty() && v.front() != '.') versions.push_back(v);
674+
}
675+
if (versions.size() == 1) {
676+
mcpp::log::verbose("probe", std::format(
677+
"runtime binding glibc@{} — the only glibc payload installed, "
678+
"so there is nothing to choose between", versions[0]));
679+
return "glibc@" + versions[0];
680+
}
681+
if (versions.size() > 1)
682+
mcpp::log::verbose("probe", std::format(
683+
"{} glibc payloads installed and nothing declares which one "
684+
"this build binds; declining rather than picking",
685+
versions.size()));
686+
}
646687
return {};
647688
}
648689

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// When nothing declares the runtime, how far may mcpp go on its own?
2+
//
3+
// The rule this replaced asked a directory for "the glibc" and took the first
4+
// entry readdir yielded. That is a CHOICE made by something with no bearing on
5+
// what the artifact loads, and it went wrong the moment a dependency's
6+
// `xim:glibc@>=2.38` floor installed a second payload: the compile side took
7+
// 2.44 while the interpreter still named 2.39.
8+
//
9+
// So the axis is not "may mcpp look at the payload directory" -- it is
10+
// "is there anything to choose between". One payload is an answer; two are a
11+
// question only the subos can settle.
12+
//
13+
// This mattered because the compatibility sources (gcc's specs, clang's cfg,
14+
// the compiler's PT_INTERP) are all things some earlier mechanism wrote, and a
15+
// machine can legitimately have none of them.
16+
17+
#include <gtest/gtest.h>
18+
19+
import std;
20+
import mcpp.toolchain.post_install;
21+
22+
namespace tc = mcpp::toolchain;
23+
24+
namespace {
25+
26+
struct Home {
27+
std::filesystem::path root, compiler;
28+
explicit Home(std::initializer_list<const char*> glibcVersions) {
29+
root = std::filesystem::temp_directory_path()
30+
/ std::format("mcpp_binding_{}", std::random_device{}());
31+
auto xpkgs = root / "data" / "xpkgs";
32+
compiler = xpkgs / "xim-x-gcc" / "16.1.0" / "bin" / "g++";
33+
std::filesystem::create_directories(compiler.parent_path());
34+
std::ofstream(compiler) << "not an elf";
35+
for (auto v : glibcVersions)
36+
std::filesystem::create_directories(
37+
xpkgs / "xim-x-glibc" / v / "lib64");
38+
}
39+
~Home() { std::error_code ec; std::filesystem::remove_all(root, ec); }
40+
};
41+
42+
TEST(RuntimeBindingFallback, OnePayloadIsAnAnswer) {
43+
Home h{"2.39"};
44+
EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "glibc@2.39");
45+
}
46+
47+
// The incident, in one assertion. Two payloads and nothing declaring which:
48+
// silence, not a coin flip.
49+
TEST(RuntimeBindingFallback, TwoPayloadsIsSilence) {
50+
Home h{"2.39", "2.44"};
51+
EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "");
52+
}
53+
54+
TEST(RuntimeBindingFallback, NoPayloadIsSilence) {
55+
Home h{};
56+
EXPECT_EQ(tc::baked_runtime_binding(h.compiler), "");
57+
}
58+
59+
// A compiler outside any xpkgs tree (a system gcc) has no payload set to read,
60+
// and must not acquire one from somewhere else.
61+
TEST(RuntimeBindingFallback, NonPayloadCompilerGetsNothing) {
62+
EXPECT_EQ(tc::baked_runtime_binding("/usr/bin/g++"), "");
63+
}
64+
65+
TEST(RuntimeBindingFallback, EmptyPathIsSilence) {
66+
EXPECT_EQ(tc::baked_runtime_binding({}), "");
67+
}
68+
69+
} // namespace

0 commit comments

Comments
 (0)