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
130 changes: 130 additions & 0 deletions src/doctor.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,51 @@ import mcpp.build.plan;
import mcpp.config;
import mcpp.fallback.install_integrity;
import mcpp.fetcher.progress;
import mcpp.platform.process;
import mcpp.toolchain.detect;
import mcpp.toolchain.registry;
import mcpp.toolchain.stdmod;
import mcpp.ui;
import mcpp.xlings;

namespace mcpp::doctor {

// Parse the RUNPATH/RPATH search dirs out of a `readelf -d <binary>` dump.
// readelf prints (one per DT_RUNPATH / DT_RPATH dynamic entry):
// 0x...001d (RUNPATH) Library runpath: [/a/lib:/b/lib:...]
// 0x...000f (RPATH) Library rpath: [/a/lib:/b/lib:...]
// We pull the text inside the [...] and split on ':'. Exported so it can be
// unit-tested without spawning a process. Empty entries are dropped.
export std::vector<std::string> parse_readelf_runpath(std::string_view dump) {
std::vector<std::string> out;
std::size_t pos = 0;
while (pos < dump.size()) {
auto nl = dump.find('\n', pos);
std::string_view line = dump.substr(pos, nl == std::string_view::npos
? std::string_view::npos : nl - pos);
pos = (nl == std::string_view::npos) ? dump.size() : nl + 1;

if (line.find("(RUNPATH)") == std::string_view::npos
&& line.find("(RPATH)") == std::string_view::npos)
continue;
auto lb = line.find('[');
auto rb = line.find(']', lb == std::string_view::npos ? 0 : lb);
if (lb == std::string_view::npos || rb == std::string_view::npos || rb <= lb + 1)
continue;
std::string_view body = line.substr(lb + 1, rb - lb - 1);
std::size_t s = 0;
while (s <= body.size()) {
auto c = body.find(':', s);
std::string_view tok = body.substr(s, c == std::string_view::npos
? std::string_view::npos : c - s);
if (!tok.empty()) out.emplace_back(tok);
if (c == std::string_view::npos) break;
s = c + 1;
}
}
return out;
}

// `mcpp self env`.
export int env_report() {
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
Expand Down Expand Up @@ -144,6 +182,98 @@ export int doctor_report() {
}
}

#if !defined(__APPLE__) && !defined(_WIN32)
// ─── Toolchain runtime dependencies (Linux/ELF only) ────────────────
//
// Installed xim toolchains bake absolute RUNPATH entries into their
// compiler binaries (e.g. clang++ points at xim-x-zlib/.../lib for
// libz.so.1). If the providing xim package is later removed, the
// RUNPATH dir vanishes and `<compiler>` dies at runtime with
// "libz.so.1: cannot open shared object" (exit 127) — the package
// builds fine but the produced binary can't run. We detect the broken
// state here before a build mysteriously fails.
//
// Two symptoms, both stemming from a deleted provider package:
// 1. a compiler RUNPATH entry pointing at a now-missing dir, and
// 2. dangling symlinks under <xlingsHome>/subos/default/lib
// (std::filesystem::exists follows symlinks → false for dangling).
mcpp::ui::status("Checking", "toolchain runtime deps");
if (cfg) {
auto pkgsDir = (*cfg).xlingsHome() / "data" / "xpkgs";
std::error_code ec;
bool sawAny = false;
bool anyMissing = false;

if (std::filesystem::exists(pkgsDir, ec)) {
// Mirror `mcpp toolchain list`: each xim-x-<compiler>/<version>/bin
// holds one installed toolchain frontend (clang++/g++/musl-gcc-…).
for (auto& entry : std::filesystem::directory_iterator(pkgsDir, ec)) {
auto name = entry.path().filename().string();
if (name.rfind("xim-x-", 0) != 0) continue; // toolchains only
std::string compiler = name.substr(std::string("xim-x-").size());

for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) {
auto bin = mcpp::toolchain::toolchain_frontend(
vEntry.path() / "bin", compiler);
if (bin.empty()) continue; // not a compiler pkg
sawAny = true;

auto label = mcpp::toolchain::display_label(
compiler, vEntry.path().filename().string());

// readelf is part of binutils, always present in our sandbox.
auto cmd = std::format("readelf -d \"{}\"", bin.string());
auto r = mcpp::platform::process::capture(cmd);
if (r.exit_code != 0) {
warn(std::format(
"{}: could not read RUNPATH from '{}' (readelf exit {})",
label, bin.string(), r.exit_code));
continue;
}
for (auto& dir : parse_readelf_runpath(r.output)) {
// Only absolute paths name on-disk dirs we can verify;
// $ORIGIN-relative entries are resolved by the loader.
if (dir.empty() || dir.front() != '/') continue;
if (!std::filesystem::exists(dir, ec)) {
anyMissing = true;
warn(std::format(
"{}: RUNPATH dir missing: {} "
"(its providing xim package may have been removed — "
"reinstall the toolchain to repair)",
label, dir));
}
}
}
}
}
if (sawAny && !anyMissing)
ok("all installed toolchain RUNPATH dirs present");
else if (!sawAny)
ok("no installed toolchains to check");

// Dangling symlinks under registry/subos/default/lib — these point
// into xim payload lib dirs; a removed package leaves them broken.
auto subosLib = (*cfg).xlingsHome() / "subos" / "default" / "lib";
if (std::filesystem::exists(subosLib, ec)) {
bool anyDangling = false;
for (auto& e : std::filesystem::directory_iterator(subosLib, ec)) {
if (!e.is_symlink(ec)) continue;
// exists() follows the link → false when the target is gone.
if (!std::filesystem::exists(e.path(), ec)) {
anyDangling = true;
auto target = std::filesystem::read_symlink(e.path(), ec);
warn(std::format(
"dangling subos symlink: {} -> {} "
"(target's xim package may have been removed)",
e.path().filename().string(), target.string()));
}
}
if (!anyDangling)
ok(std::format("subos lib symlinks all resolve ({})", subosLib.string()));
}
}
#endif

std::println("");
if (errors) std::println("Doctor result: {} errors, {} warnings", errors, warns);
else if (warns) std::println("Doctor result: {} warnings", warns);
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/test_doctor_runpath.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include <gtest/gtest.h>

import std;
import mcpp.doctor;

using mcpp::doctor::parse_readelf_runpath;

// `readelf -d` line shape (the case that motivated the check): clang++ with a
// RUNPATH that includes a now-removed xim-x-zlib lib dir.
TEST(DoctorRunpath, ParsesRunpathColonSeparatedDirs) {
std::string dump =
" 0x0000000000000001 (NEEDED) Shared library: [libz.so.1]\n"
" 0x000000000000001d (RUNPATH) Library runpath: "
"[/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib:"
"/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib:"
"/home/u/.mcpp/registry/subos/default/lib]\n"
" 0x000000000000000c (INIT) 0x1000\n";

auto dirs = parse_readelf_runpath(dump);
ASSERT_EQ(dirs.size(), 3u);
EXPECT_EQ(dirs[0], "/home/u/.mcpp/data/xpkgs/xim-x-llvm/20.1.7/lib");
EXPECT_EQ(dirs[1], "/home/u/.mcpp/data/xpkgs/xim-x-zlib/1.3.1/lib");
EXPECT_EQ(dirs[2], "/home/u/.mcpp/registry/subos/default/lib");
}

// DT_RPATH (legacy) is parsed the same way as DT_RUNPATH.
TEST(DoctorRunpath, ParsesLegacyRpath) {
std::string dump =
" 0x000000000000000f (RPATH) Library rpath: [/opt/a/lib:/opt/b/lib]\n";
auto dirs = parse_readelf_runpath(dump);
ASSERT_EQ(dirs.size(), 2u);
EXPECT_EQ(dirs[0], "/opt/a/lib");
EXPECT_EQ(dirs[1], "/opt/b/lib");
}

// A binary with no RUNPATH/RPATH entry yields no dirs.
TEST(DoctorRunpath, NoRunpathYieldsEmpty) {
std::string dump =
" 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]\n"
" 0x000000000000000c (INIT) 0x1000\n";
EXPECT_TRUE(parse_readelf_runpath(dump).empty());
}

// Empty path tokens (e.g. a trailing ':') are dropped, not reported as a
// missing dir.
TEST(DoctorRunpath, DropsEmptyTokens) {
std::string dump =
" 0x000000000000001d (RUNPATH) Library runpath: [/a/lib::/b/lib:]\n";
auto dirs = parse_readelf_runpath(dump);
ASSERT_EQ(dirs.size(), 2u);
EXPECT_EQ(dirs[0], "/a/lib");
EXPECT_EQ(dirs[1], "/b/lib");
}
Loading