Skip to content

Commit cced0fd

Browse files
authored
feat(doctor): detect missing toolchain runtime deps / dangling subos symlinks (#141)
Add a new "toolchain runtime deps" section to `mcpp self doctor` that catches the failure class where a provider xim package gets removed, leaving an installed toolchain's RUNPATH (and subos/default/lib symlinks) pointing at files that no longer exist. Concretely: deleting `xim-x-zlib` left clang++'s baked RUNPATH pointing at the gone `xim-x-zlib/<v>/lib` dir, so the package compiled fine but the produced binary died at runtime with "libz.so.1: cannot open shared object" (exit 127). Nothing surfaced the broken state until a build mysteriously failed. The new section (Linux/ELF only, guarded by `#if !defined(__APPLE__) && !defined(_WIN32)`): 1. Enumerates installed xim toolchains the same way `mcpp toolchain list` does (iterate <xlingsHome>/data/xpkgs/xim-x-<compiler>/<ver>, resolve the frontend via toolchain_frontend()). 2. Reads each compiler's RUNPATH/RPATH via `readelf -d` (reusing mcpp::platform::process::capture — no new process code, no new deps) and warns for every absolute RUNPATH dir that is now missing, naming the toolchain and hinting the providing xim package may have been removed. 3. Scans <xlingsHome>/subos/default/lib for dangling symlinks (std::filesystem::exists follows links -> false when the target is gone) and warns with the symlink + its broken target. Exit-code semantics are unchanged (warnings only). RUNPATH parsing is factored into an exported, process-free `parse_readelf_runpath()` helper with self-contained gtest coverage (tests/unit/test_doctor_runpath.cpp), including the zlib-removal RUNPATH shape, legacy DT_RPATH, no-runpath, and empty-token cases.
1 parent 48cec33 commit cced0fd

2 files changed

Lines changed: 183 additions & 0 deletions

File tree

src/doctor.cppm

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,51 @@ import mcpp.build.plan;
1616
import mcpp.config;
1717
import mcpp.fallback.install_integrity;
1818
import mcpp.fetcher.progress;
19+
import mcpp.platform.process;
1920
import mcpp.toolchain.detect;
21+
import mcpp.toolchain.registry;
2022
import mcpp.toolchain.stdmod;
2123
import mcpp.ui;
2224
import mcpp.xlings;
2325

2426
namespace mcpp::doctor {
2527

28+
// Parse the RUNPATH/RPATH search dirs out of a `readelf -d <binary>` dump.
29+
// readelf prints (one per DT_RUNPATH / DT_RPATH dynamic entry):
30+
// 0x...001d (RUNPATH) Library runpath: [/a/lib:/b/lib:...]
31+
// 0x...000f (RPATH) Library rpath: [/a/lib:/b/lib:...]
32+
// We pull the text inside the [...] and split on ':'. Exported so it can be
33+
// unit-tested without spawning a process. Empty entries are dropped.
34+
export std::vector<std::string> parse_readelf_runpath(std::string_view dump) {
35+
std::vector<std::string> out;
36+
std::size_t pos = 0;
37+
while (pos < dump.size()) {
38+
auto nl = dump.find('\n', pos);
39+
std::string_view line = dump.substr(pos, nl == std::string_view::npos
40+
? std::string_view::npos : nl - pos);
41+
pos = (nl == std::string_view::npos) ? dump.size() : nl + 1;
42+
43+
if (line.find("(RUNPATH)") == std::string_view::npos
44+
&& line.find("(RPATH)") == std::string_view::npos)
45+
continue;
46+
auto lb = line.find('[');
47+
auto rb = line.find(']', lb == std::string_view::npos ? 0 : lb);
48+
if (lb == std::string_view::npos || rb == std::string_view::npos || rb <= lb + 1)
49+
continue;
50+
std::string_view body = line.substr(lb + 1, rb - lb - 1);
51+
std::size_t s = 0;
52+
while (s <= body.size()) {
53+
auto c = body.find(':', s);
54+
std::string_view tok = body.substr(s, c == std::string_view::npos
55+
? std::string_view::npos : c - s);
56+
if (!tok.empty()) out.emplace_back(tok);
57+
if (c == std::string_view::npos) break;
58+
s = c + 1;
59+
}
60+
}
61+
return out;
62+
}
63+
2664
// `mcpp self env`.
2765
export int env_report() {
2866
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
@@ -144,6 +182,98 @@ export int doctor_report() {
144182
}
145183
}
146184

185+
#if !defined(__APPLE__) && !defined(_WIN32)
186+
// ─── Toolchain runtime dependencies (Linux/ELF only) ────────────────
187+
//
188+
// Installed xim toolchains bake absolute RUNPATH entries into their
189+
// compiler binaries (e.g. clang++ points at xim-x-zlib/.../lib for
190+
// libz.so.1). If the providing xim package is later removed, the
191+
// RUNPATH dir vanishes and `<compiler>` dies at runtime with
192+
// "libz.so.1: cannot open shared object" (exit 127) — the package
193+
// builds fine but the produced binary can't run. We detect the broken
194+
// state here before a build mysteriously fails.
195+
//
196+
// Two symptoms, both stemming from a deleted provider package:
197+
// 1. a compiler RUNPATH entry pointing at a now-missing dir, and
198+
// 2. dangling symlinks under <xlingsHome>/subos/default/lib
199+
// (std::filesystem::exists follows symlinks → false for dangling).
200+
mcpp::ui::status("Checking", "toolchain runtime deps");
201+
if (cfg) {
202+
auto pkgsDir = (*cfg).xlingsHome() / "data" / "xpkgs";
203+
std::error_code ec;
204+
bool sawAny = false;
205+
bool anyMissing = false;
206+
207+
if (std::filesystem::exists(pkgsDir, ec)) {
208+
// Mirror `mcpp toolchain list`: each xim-x-<compiler>/<version>/bin
209+
// holds one installed toolchain frontend (clang++/g++/musl-gcc-…).
210+
for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) {
211+
auto name = entry.path().filename().string();
212+
if (name.rfind("xim-x-", 0) != 0) continue; // toolchains only
213+
std::string compiler = name.substr(std::string("xim-x-").size());
214+
215+
for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) {
216+
auto bin = mcpp::toolchain::toolchain_frontend(
217+
vEntry.path() / "bin", compiler);
218+
if (bin.empty()) continue; // not a compiler pkg
219+
sawAny = true;
220+
221+
auto label = mcpp::toolchain::display_label(
222+
compiler, vEntry.path().filename().string());
223+
224+
// readelf is part of binutils, always present in our sandbox.
225+
auto cmd = std::format("readelf -d \"{}\"", bin.string());
226+
auto r = mcpp::platform::process::capture(cmd);
227+
if (r.exit_code != 0) {
228+
warn(std::format(
229+
"{}: could not read RUNPATH from '{}' (readelf exit {})",
230+
label, bin.string(), r.exit_code));
231+
continue;
232+
}
233+
for (auto& dir : parse_readelf_runpath(r.output)) {
234+
// Only absolute paths name on-disk dirs we can verify;
235+
// $ORIGIN-relative entries are resolved by the loader.
236+
if (dir.empty() || dir.front() != '/') continue;
237+
if (!std::filesystem::exists(dir, ec)) {
238+
anyMissing = true;
239+
warn(std::format(
240+
"{}: RUNPATH dir missing: {} "
241+
"(its providing xim package may have been removed — "
242+
"reinstall the toolchain to repair)",
243+
label, dir));
244+
}
245+
}
246+
}
247+
}
248+
}
249+
if (sawAny && !anyMissing)
250+
ok("all installed toolchain RUNPATH dirs present");
251+
else if (!sawAny)
252+
ok("no installed toolchains to check");
253+
254+
// Dangling symlinks under registry/subos/default/lib — these point
255+
// into xim payload lib dirs; a removed package leaves them broken.
256+
auto subosLib = (*cfg).xlingsHome() / "subos" / "default" / "lib";
257+
if (std::filesystem::exists(subosLib, ec)) {
258+
bool anyDangling = false;
259+
for (auto& e : std::filesystem::directory_iterator(subosLib, ec)) {
260+
if (!e.is_symlink(ec)) continue;
261+
// exists() follows the link → false when the target is gone.
262+
if (!std::filesystem::exists(e.path(), ec)) {
263+
anyDangling = true;
264+
auto target = std::filesystem::read_symlink(e.path(), ec);
265+
warn(std::format(
266+
"dangling subos symlink: {} -> {} "
267+
"(target's xim package may have been removed)",
268+
e.path().filename().string(), target.string()));
269+
}
270+
}
271+
if (!anyDangling)
272+
ok(std::format("subos lib symlinks all resolve ({})", subosLib.string()));
273+
}
274+
}
275+
#endif
276+
147277
std::println("");
148278
if (errors) std::println("Doctor result: {} errors, {} warnings", errors, warns);
149279
else if (warns) std::println("Doctor result: {} warnings", warns);

tests/unit/test_doctor_runpath.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include <gtest/gtest.h>
2+
3+
import std;
4+
import mcpp.doctor;
5+
6+
using mcpp::doctor::parse_readelf_runpath;
7+
8+
// `readelf -d` line shape (the case that motivated the check): clang++ with a
9+
// RUNPATH that includes a now-removed xim-x-zlib lib dir.
10+
TEST(DoctorRunpath, ParsesRunpathColonSeparatedDirs) {
11+
std::string dump =
12+
" 0x0000000000000001 (NEEDED) Shared library: [libz.so.1]\n"
13+
" 0x000000000000001d (RUNPATH) Library runpath: "
14+
"[/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib:"
15+
"/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib:"
16+
"/home/u/.mcpp/registry/subos/default/lib]\n"
17+
" 0x000000000000000c (INIT) 0x1000\n";
18+
19+
auto dirs = parse_readelf_runpath(dump);
20+
ASSERT_EQ(dirs.size(), 3u);
21+
EXPECT_EQ(dirs[0], "/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib");
22+
EXPECT_EQ(dirs[1], "/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib");
23+
EXPECT_EQ(dirs[2], "/home/u/.mcpp/registry/subos/default/lib");
24+
}
25+
26+
// DT_RPATH (legacy) is parsed the same way as DT_RUNPATH.
27+
TEST(DoctorRunpath, ParsesLegacyRpath) {
28+
std::string dump =
29+
" 0x000000000000000f (RPATH) Library rpath: [/opt/a/lib:/opt/b/lib]\n";
30+
auto dirs = parse_readelf_runpath(dump);
31+
ASSERT_EQ(dirs.size(), 2u);
32+
EXPECT_EQ(dirs[0], "/opt/a/lib");
33+
EXPECT_EQ(dirs[1], "/opt/b/lib");
34+
}
35+
36+
// A binary with no RUNPATH/RPATH entry yields no dirs.
37+
TEST(DoctorRunpath, NoRunpathYieldsEmpty) {
38+
std::string dump =
39+
" 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]\n"
40+
" 0x000000000000000c (INIT) 0x1000\n";
41+
EXPECT_TRUE(parse_readelf_runpath(dump).empty());
42+
}
43+
44+
// Empty path tokens (e.g. a trailing ':') are dropped, not reported as a
45+
// missing dir.
46+
TEST(DoctorRunpath, DropsEmptyTokens) {
47+
std::string dump =
48+
" 0x000000000000001d (RUNPATH) Library runpath: [/a/lib::/b/lib:]\n";
49+
auto dirs = parse_readelf_runpath(dump);
50+
ASSERT_EQ(dirs.size(), 2u);
51+
EXPECT_EQ(dirs[0], "/a/lib");
52+
EXPECT_EQ(dirs[1], "/b/lib");
53+
}

0 commit comments

Comments
 (0)