-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathdoctor.cppm
More file actions
682 lines (635 loc) · 31.1 KB
/
Copy pathdoctor.cppm
File metadata and controls
682 lines (635 loc) · 31.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
// mcpp.doctor — diagnostics + self-maintenance: environment report,
// health checks, resolution explanation (why), error-code explanations,
// sandbox init/reset, and xlings mirror configuration.
// Bodies moved verbatim from the CLI layer. Zero behavior change.
module;
#include <cstdio>
#include <cstdlib>
export module mcpp.doctor;
import std;
import mcpp.bmi_cache.maintenance;
import mcpp.build.prepare;
import mcpp.build.plan;
import mcpp.config;
import mcpp.fallback.probe_sysroot;
import mcpp.fallback.xlings_binary;
import mcpp.fallback.install_integrity;
import mcpp.fetcher.progress;
import mcpp.home;
import mcpp.platform;
import mcpp.platform.process;
import mcpp.pm.index_refresh; // staleness_note for `mcpp why deps`
import mcpp.toolchain.detect;
import mcpp.toolchain.msvc;
import mcpp.toolchain.registry;
import mcpp.toolchain.stdmod;
import mcpp.toolchain.abi;
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());
if (!cfg) { mcpp::ui::error(cfg.error().message); return 4; }
mcpp::config::print_env(*cfg);
auto tc = mcpp::toolchain::detect();
if (tc) {
std::println("");
std::println("Toolchain = {}", tc->label());
std::println("std module src = {}", tc->stdModuleSource.string());
} else {
std::println("");
std::println("Toolchain = (not detected: {})", tc.error().message);
}
return 0;
}
// `mcpp self doctor`.
export int doctor_report() {
int warns = 0, errors = 0;
auto ok = [](std::string_view m) { mcpp::ui::status("ok", m); };
auto warn = [&](std::string_view m) { mcpp::ui::warning(m); ++warns; };
auto err = [&](std::string_view m) { mcpp::ui::error(m); ++errors; };
mcpp::ui::status("Checking", "toolchain");
auto tc = mcpp::toolchain::detect();
if (!tc) {
err(std::format("toolchain detection failed: {}", tc.error().message));
} else {
ok(std::format("{} at {}", tc->label(), tc->binaryPath.string()));
}
// Windows: report the system MSVC (msvc@system). Absence is a warning,
// not an error — mcpp works with LLVM/Clang without it, and mcpp never
// installs MSVC itself.
if (mcpp::platform::is_windows) {
mcpp::ui::status("Checking", "msvc (system)");
if (auto inst = mcpp::toolchain::msvc::detect_installation()) {
ok(std::format("msvc {}{} (VC tools {})",
inst->display_version(),
inst->vsProduct.empty()
? std::string{}
: std::format(" (VS {})", inst->vsProduct),
inst->toolsVersion));
ok(std::format("cl at {}", inst->clPath.string()));
if (inst->hasStdModules) {
ok("import std: std.ixx available");
} else {
warn("MSVC STL std.ixx missing (VC tools too old for import std?)");
}
} else {
warn("msvc not detected — run `mcpp toolchain default msvc` for "
"setup guidance (mcpp does not install MSVC)");
}
// Windows SDK (native cl.exe builds need its UCRT/um headers).
if (auto sdk = mcpp::toolchain::msvc::find_windows_sdk()) {
ok(std::format("Windows SDK {} at {}", sdk->version,
sdk->root.string()));
} else {
warn("no Windows SDK found — native msvc builds will fail "
"(install the 'Windows 11 SDK' VS component)");
}
mcpp::ui::status("Checking", "mingw (xim:mingw-gcc)");
{
auto pkgs = mcpp::home::root()
/ "registry" / "data" / "xpkgs" / "xim-x-mingw-gcc";
std::error_code ec;
bool any = false;
if (std::filesystem::exists(pkgs, ec)) {
for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) {
if (!v.is_directory(ec)) continue;
ok(std::format("mingw {} installed", v.path().filename().string()));
any = true;
}
}
if (!any)
ok("mingw not installed (optional — `mcpp toolchain install mingw 16.1.0`)");
}
// The other direction: a windows-hosted cross toolchain that produces
// Linux ELF. Same shape as the mingw probe above; the package is named
// by triple, matching to_xim_package()'s `<triple>-gcc`.
{
auto triple = std::string(mcpp::platform::host_arch) + "-linux-musl";
auto label = std::format("linux cross (xim:{}-gcc)", triple);
mcpp::ui::status("Checking", label);
auto pkgs = mcpp::home::root() / "registry" / "data" / "xpkgs"
/ std::format("xim-x-{}-gcc", triple);
std::error_code ec;
bool any = false;
if (std::filesystem::exists(pkgs, ec)) {
for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) {
if (!v.is_directory(ec)) continue;
ok(std::format("{} {} installed",
triple, v.path().filename().string()));
any = true;
}
}
if (!any)
ok(std::format("{} not installed (optional — "
"`mcpp toolchain install gcc 16.1.0 --target {}`)",
triple, triple));
}
}
mcpp::ui::status("Checking", "std module");
if (tc) {
// Entries live at <cache>/std/<identity>/{gcm,pcm}.cache/std.*; the
// object sits at the entry root. Look for the object rather than one
// compiler's BMI extension so this reports on clang and MSVC too.
auto stdRoot = mcpp::toolchain::default_cache_root() / "std";
std::error_code ec;
if (std::filesystem::exists(stdRoot, ec)) {
bool found = false;
for (auto& e : std::filesystem::directory_iterator(stdRoot, ec)) {
for (auto name : {"std.o", "std.obj"}) {
auto obj = e.path() / name;
if (!std::filesystem::exists(obj, ec)) continue;
ok(std::format("{} (entry {})", e.path().string(),
mcpp::bmi_cache::human_bytes(
mcpp::bmi_cache::dir_size(e.path()))));
found = true;
break;
}
if (found) break;
}
if (!found) warn("no std module cached yet (built on first `mcpp build`)");
} else {
warn(std::format("no std module cache at '{}' yet "
"(built on first `mcpp build`)", stdRoot.string()));
}
}
mcpp::ui::status("Checking", "registry");
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
// Whose sysroot is this? gcc bakes `--sysroot=<...>/subos/default`
// at build time, and that path is a string, not a reference -- it
// keeps naming wherever the compiler was built no matter which
// project it now serves. A developer machine has many directories by
// that name, so the baked one frequently EXISTS while belonging to an
// unrelated checkout, and headers then come from a tree this build
// never declared. Existence is not ownership.
if (tc && cfg) {
std::error_code cwdEc;
auto project = std::filesystem::current_path(cwdEc);
if (mcpp::fallback::sysroot_is_foreign(
tc->sysroot, (*cfg).registryDir,
cwdEc ? std::filesystem::path{} : project))
warn(std::format(
"sysroot {} belongs to neither this mcpp home ({}) nor "
"this project — headers would come from a tree nothing "
"here declared. mcpp remaps a baked sysroot when it can "
"find the equivalent under the registry; seeing it here "
"means it could not",
tc->sysroot.string(), (*cfg).registryDir.string()));
}
if (!cfg) {
err(cfg.error().message);
} else {
if (std::filesystem::exists((*cfg).xlingsBinary)) {
ok(std::format("xlings at {}", (*cfg).xlingsBinary.string()));
} else {
warn(std::format("xlings binary missing at '{}'",
(*cfg).xlingsBinary.string()));
}
ok(std::format("default index = '{}'", (*cfg).defaultIndex));
}
mcpp::ui::status("Checking", "cache health");
auto bmiRoot = mcpp::toolchain::default_cache_root();
auto sz = mcpp::bmi_cache::dir_size(bmiRoot);
if (sz > std::uintmax_t(4) * 1024 * 1024 * 1024) {
warn(std::format("build cache occupies {} "
"(`mcpp cache gc --max-size 4GiB` to collect)",
mcpp::bmi_cache::human_bytes(sz)));
} else {
ok(std::format("build cache size = {}", mcpp::bmi_cache::human_bytes(sz)));
}
// The pre-v1 cache was keyed by whole-project fingerprint, which folded in
// the consumer's own name and version — so it accumulated one copy of every
// dependency per project configuration and never produced a cross-project
// hit. Nothing reads it now. Report the size, never delete it.
{
auto legacyRoot = mcpp::home::legacy_bmi_root();
std::error_code lec;
if (std::filesystem::is_directory(legacyRoot, lec)) {
auto lsz = mcpp::bmi_cache::dir_size(legacyRoot);
warn(std::format("pre-v1 cache at '{}' occupies {} and is no longer "
"used — `mcpp cache clean --legacy` reclaims it",
legacyRoot.string(),
mcpp::bmi_cache::human_bytes(lsz)));
}
}
// Pre-#311 builds could park the std BMI cache in the current working
// directory (`.mcpp-bmi/`) whenever neither MCPP_HOME nor HOME resolved —
// the common case on Windows PowerShell. Point at leftovers; never delete.
{
std::error_code lec;
auto legacy = std::filesystem::current_path(lec) / ".mcpp-bmi";
if (!lec && std::filesystem::is_directory(legacy, lec)) {
warn(std::format("legacy BMI cache at '{}' — no longer used, safe to delete",
legacy.string()));
}
}
mcpp::ui::status("Checking", "runtime capabilities");
{
// Capability/provider-driven — no platform special-casing in mcpp.
// Required capabilities and the sonames to probe come entirely from the
// dependency graph's provider packages (e.g. compat.glx-runtime); the
// search dirs are the resolved runtime library_dirs. The same code path
// works on every platform — providers carry the platform knowledge.
auto pctx = mcpp::build::prepare_build(/*print_fingerprint=*/false);
if (!pctx) {
ok("(run inside a package to check its runtime capabilities)");
} else if (pctx->plan.runtimeCapabilities.empty()) {
ok("no host runtime capabilities required");
} else {
auto& plan = pctx->plan;
for (auto& cap : plan.runtimeCapabilities) {
std::string provider;
for (auto& [c, p] : plan.runtimeProviders)
if (c == cap) { provider = p; break; }
ok(std::format("{}: required (provider {})",
cap, provider.empty() ? "?" : provider));
}
auto resolves = [&](std::string_view soname) {
for (auto& dir : plan.runtimeLibraryDirs) {
std::error_code ec;
if (!std::filesystem::exists(dir, ec)) continue;
for (auto& e : std::filesystem::directory_iterator(dir, ec)) {
auto fn = e.path().filename().string();
if (fn == soname || fn.rfind(soname, 0) == 0) return true;
}
}
return false;
};
for (auto& lib : plan.runtimeDlopenLibs) {
if (resolves(lib)) ok(std::format("dlopen {}: resolvable on RUNPATH", lib));
else warn(std::format("dlopen {}: not found on resolved runtime dirs", lib));
}
}
}
#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-<name>/<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
auto id = mcpp::toolchain::identify_xim_payload(
name.substr(std::string("xim-x-").size()));
if (!id) continue; // not a compiler pkg
for (auto& vEntry : std::filesystem::directory_iterator(entry.path(), ec)) {
mcpp::toolchain::ToolchainSpec s;
s.family = id->family;
s.version = vEntry.path().filename().string();
s.target = id->target;
auto bin = mcpp::toolchain::toolchain_frontend(
vEntry.path() / "bin", mcpp::toolchain::to_xim_package(s));
if (bin.empty()) continue;
sawAny = true;
auto label = s.display();
// 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");
// The vendored xlings, against the version this mcpp expects.
//
// Nothing else surfaces this. `mcpp self env` prints both numbers and
// says nothing about the gap, and a home that acquired its xlings once
// never revisited it -- so a machine could sit years behind while every
// command looked healthy. What goes missing is silent by nature:
// features mcpp reads FROM xlings (the subos_info block, for one)
// simply never appear, and the code that consumes them degrades
// quietly because a missing block is also a legitimate state.
{
auto have = mcpp::fallback::vendored_xlings_version((*cfg).xlingsBinary);
const auto want = std::string(mcpp::config::kXlingsPinnedVersion);
if (have.empty()) {
warn(std::format("cannot read the vendored xlings version at {}",
(*cfg).xlingsBinary.string()));
} else if (mcpp::fallback::version_is_older(have, want)) {
warn(std::format(
"vendored xlings is {} but this mcpp expects {} — features "
"mcpp reads from xlings may be silently absent (the subos "
"self-description arrived in 2026.8.5.1). It is replaced "
"automatically on the next `mcpp self init`",
have, want));
} else {
ok(std::format("vendored xlings {} (pinned {})", have, want));
}
}
// 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);
else std::println("Doctor result: all checks passed");
return errors ? 2 : (warns ? 1 : 0);
}
// `mcpp why [topic]` / `mcpp resolve --explain`.
export int why_report(const std::string& topic) {
const bool all = topic.empty() || topic == "all";
auto ctx = mcpp::build::prepare_build(/*print_fingerprint=*/false);
if (!ctx) { std::println(stderr, "error: {}", ctx.error()); return 2; }
auto& tc = ctx->tc;
auto& plan = ctx->plan;
if (all || topic == "toolchain") {
const auto prof = mcpp::toolchain::abi_profile(tc);
std::println("toolchain: {}", tc.label());
std::println(" abi(libc)={} cxxstdlib={} arch={} os={} triple={}",
prof.libc, prof.cxxStdlib, prof.arch, prof.os, tc.targetTriple);
std::println(" reason: [toolchain] in mcpp.toml if set, else platform-native default");
if (!ctx->manifest.package.platforms.empty()) {
std::string ps;
for (auto& p : ctx->manifest.package.platforms) {
if (!ps.empty()) ps += ", ";
ps += p;
}
std::println(" declared platforms: {} (CI matrix hint)", ps);
}
}
if (all || topic == "runtime") {
std::println("runtime library dirs (baked into binary RUNPATH):");
if (plan.runtimeLibraryDirs.empty()) std::println(" (none)");
for (auto& d : plan.runtimeLibraryDirs) {
auto s = d.string();
std::string note;
if (s.find("glx_runtime") != std::string::npos)
note = " <- host GL/GLX runtime (compat.glx-runtime)";
else if (s.find("glibc") != std::string::npos) note = " <- glibc";
else if (s.find("xim-x-gcc") != std::string::npos
|| s.find("xim-x-llvm") != std::string::npos) note = " <- toolchain";
std::println(" - {}{}", s, note);
}
if (!plan.runtimeCapabilities.empty()) {
std::println("runtime capabilities (provider):");
for (auto& cap : plan.runtimeCapabilities) {
std::string prov;
for (auto& [c, p] : plan.runtimeProviders) if (c == cap) { prov = p; break; }
std::println(" - {} -> {}", cap, prov.empty() ? "?" : prov);
}
}
}
if (all || topic == "deps") {
// Which index answered, and how stale it is. Since #315 a build only
// refreshes on a resolution miss, so "why did I get this version"
// frequently has "because that is the newest one your local index
// knows" as its answer — which is unguessable without this line.
if (auto cfgW = mcpp::config::load_or_init(/*quiet=*/true)) {
std::println("package index: {}",
mcpp::pm::staleness_note(mcpp::config::make_xlings_env(*cfgW)));
}
std::println("dependencies (mcpp.lock):");
std::ifstream in(ctx->projectRoot / "mcpp.lock");
if (!in) {
std::println(" (no mcpp.lock — run `mcpp build` or `mcpp update`)");
} else {
std::string line, cur;
auto quoted = [](const std::string& l) -> std::string {
auto a = l.find('"'); if (a == std::string::npos) return {};
auto b = l.find('"', a + 1); if (b == std::string::npos) return {};
return l.substr(a + 1, b - a - 1);
};
while (std::getline(in, line)) {
if (line.find("[package.\"") != std::string::npos) cur = quoted(line);
else if (!cur.empty() && line.find("version") != std::string::npos) {
std::println(" - {} {}", cur, quoted(line));
cur.clear();
}
}
}
}
return 0;
}
// ─── M4 #8.2: mcpp --explain CODE ───────────────────────────────────────
export int explain_code(std::string_view code) {
struct Entry { std::string_view code, title, body; };
static constexpr Entry table[] = {
{"E0001", "dependency name mismatch",
"The package located at the [dependencies.<key>] path declares a different\n"
"name in its own [package].name. Either rename the [dependencies.<key>] in\n"
"the consumer's mcpp.toml to match the producer, or fix the producer's\n"
"[package].name."},
{"E0002", "module imported but not provided",
"A source file does `import X;` but no source file in the build graph\n"
"exports `X`. Either add a dependency that provides X (mcpp add or\n"
"[dependencies.X] path = \"...\") or fix the import."},
{"E0003", "version constraint unsatisfiable",
"No published version of the package matches the requested constraint.\n"
"Run `mcpp search <pkg>` to list available versions, then loosen the\n"
"constraint in mcpp.toml (e.g. ^1.2 instead of =1.2.3)."},
{"E0004", "toolchain pin mismatch",
"The [toolchain] pin in mcpp.toml does not match the detected toolchain.\n"
"Either install the pinned toolchain (xlings install ...) or relax the\n"
"pin (e.g. \"gcc@>=15\" instead of \"gcc@15.1.0\")."},
{"E0005", "build cache corruption",
"A file listed in a cache entry's entry.json is missing on disk. Such an\n"
"entry is treated as a miss and rebuilt, so this is never wrong output —\n"
"only wasted space. `mcpp cache verify` lists every affected entry and\n"
"`mcpp cache gc --older-than 0s` reclaims them."},
{"E0006", "index requires a newer mcpp",
"The package index declares (index.toml [index].min_mcpp) that its\n"
"descriptors need a newer mcpp than this binary — parsing them would\n"
"silently misbehave, so resolution stops instead. Upgrade mcpp:\n"
" curl -fsSL https://github.com/mcpp-community/mcpp/releases/latest/download/install.sh | bash\n"
"To bypass for debugging only: MCPP_INDEX_FLOOR=ignore mcpp build"},
};
for (auto& e : table) {
if (e.code == code) {
std::println("{}: {}", e.code, e.title);
std::println("");
std::println("{}", e.body);
return 0;
}
}
std::println(stderr, "error: unknown error code '{}'", code);
std::println(stderr, " known codes: E0001..E0006");
return 2;
}
// ─── M6.1: `mcpp self ...` — about mcpp itself ──────────────────────────
//
// `self` is declared as a parent subcommand on the top-level App with
// nested `doctor / env / version / explain` subcommands. Each nested
// subcommand has its own action; these helpers wrap the bodies so we
// can share `cmd_doctor` / `cmd_env` between top-level and `mcpp self`.
// `mcpp self init [--force]`.
export int self_init(bool force) {
if (force) {
// --force: delete registry (sandbox) + caches and re-bootstrap.
// Preserves: bin/mcpp (self-contained mode), config.toml, log/.
mcpp::ui::info("Resetting", "mcpp sandbox (registry, caches)");
// Resolve MCPP_HOME without running bootstrap (which may fail). The
// shared resolver also covers self-contained installs — the local copy
// this replaced would have wiped ~/.mcpp for a `<root>/bin/mcpp` tree.
std::filesystem::path home = mcpp::home::root();
if (!home.empty()) {
std::error_code ec;
std::filesystem::remove_all(home / "registry", ec);
std::filesystem::remove_all(home / "cache", ec); // index metadata
std::filesystem::remove_all(home / "build-cache", ec); // compiled artifacts
std::filesystem::remove_all(home / "bmi", ec); // pre-v1 build cache
}
}
// (Re-)run the full load_or_init, which does bootstrap.
mcpp::ui::info("Initializing", "mcpp sandbox");
auto cfg = mcpp::config::load_or_init();
if (!cfg) {
mcpp::ui::error(cfg.error().message);
return 1;
}
// Clean any incomplete xpkg installations (interrupted downloads, etc.).
auto xpkgsBase = cfg->xlingsHome() / "data" / "xpkgs";
int cleaned = mcpp::fallback::clean_all_incomplete(xpkgsBase);
if (cleaned > 0) {
mcpp::ui::info("Cleaned", std::format(
"{} incomplete installation(s)", cleaned));
}
// Verify result.
auto problem = mcpp::config::check_base_init(*cfg);
if (!problem.empty()) {
mcpp::ui::error(std::format("init incomplete: {}", problem));
return 1;
}
mcpp::ui::status("Ready", "sandbox initialized");
return 0;
}
std::string upper_ascii(std::string s) {
for (char& ch : s) {
if (ch >= 'a' && ch <= 'z') ch = static_cast<char>(ch - 'a' + 'A');
}
return s;
}
// `mcpp self config [--mirror CN|GLOBAL]` (mirror = raw option value).
export int self_config(std::string mirror) {
if (!mirror.empty()) {
mirror = upper_ascii(std::move(mirror));
if (mirror != "CN" && mirror != "GLOBAL") {
mcpp::ui::error(std::format(
"invalid mirror '{}'; expected CN or GLOBAL", mirror));
return 2;
}
}
// When --mirror is given AND this is a fresh MCPP_HOME, seed .xlings.json
// with the user's choice on the very first write so the immediately-
// following xlings sandbox bootstrap (patchelf / ninja download) uses
// their mirror — not the historical CN default that an overseas user
// is trying to redirect away from. For an already-initialized MCPP_HOME
// the seed is skipped and config_set_mirror below updates the existing
// file via xlings.
//
// TODO(mirror-default): the default "CN" lives in
// mcpp::xlings::seed_xlings_json — see the matching note there for the
// long-term plan (flip default to GLOBAL, or auto-detect on first init).
auto cfg = mcpp::config::load_or_init(
/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback(), mirror);
if (!cfg) {
mcpp::ui::error(cfg.error().message);
return 4;
}
auto env = mcpp::config::make_xlings_env(*cfg);
if (mirror.empty()) {
auto rc = mcpp::xlings::config_show(env);
return rc == 0 ? 0 : 1;
}
auto rc = mcpp::xlings::config_set_mirror(env, mirror, /*quiet=*/true);
if (rc != 0) {
mcpp::ui::error(std::format("failed to set xlings mirror to {}", mirror));
return 1;
}
mcpp::ui::status("Configured", std::format("xlings mirror = {}", mirror));
return 0;
}
} // namespace mcpp::doctor