Repository navigation
Expand file tree
/
Copy pathdoctor.cppm
More file actions
1892 lines (1794 loc) · 91.8 KB
/
Copy pathdoctor.cppm
File metadata and controls
1892 lines (1794 loc) · 91.8 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
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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.build.program_protocol;
import mcpp.source_kind;
import mcpp.manifest;
import mcpp.bmi_cache.maintenance;
import mcpp.build.prepare;
import mcpp.build.refusal;
import mcpp.targetside;
import mcpp.wire;
import mcpp.build.plan;
import mcpp.build.runtime_validation;
import mcpp.config;
import mcpp.fallback.probe_sysroot;
import mcpp.fallback.xlings_binary;
import mcpp.fetcher.progress;
import mcpp.home;
import mcpp.libs.json;
import mcpp.platform;
import mcpp.platform.process;
import mcpp.platform.env;
import mcpp.runtime.elf;
import mcpp.pm.index_refresh; // staleness_note for `mcpp why deps`
import mcpp.pm.index_contract; // the floor each index declares
import mcpp.version; // MCPP_VERSION, compared with each floor
import mcpp.project;
import mcpp.toolchain.detect;
import mcpp.toolchain.msvc;
import mcpp.toolchain.registry;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.stdmod;
import mcpp.toolchain.abi;
import mcpp.ui;
import mcpp.provider;
import mcpp.provider.select;
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;
}
// A frozen copy of the BUILD MACHINE's own libc header, found in a GCC
// payload's `include-fixed`.
//
// GCC's `fixincludes` step runs once, at BUILD time, against whatever glibc
// happened to be on the machine that produced the payload, and writes the
// edited copy into `lib/gcc/<triple>/<version>/include-fixed` — which
// `-idirafter` (mcpp's own C-library search order) puts AHEAD of the
// realised glibc every project actually builds against. A header the
// payload should never have needed to fix (glibc moved on since) is a
// stale, frozen snapshot sitting in front of the one mcpp resolved, and
// mcpp#687 was `<mutex>`/`<memory>` breaking against it.
export struct FixincludesFinding {
std::filesystem::path file; // relative to `payloadRoot`
std::string source; // the banner's own path, or empty if unparsed
};
// Scan one installed toolchain payload for fixincludes-frozen headers.
//
// PURE AND PAYLOAD-ROOT SCOPED, so a unit test can point it at a synthetic
// directory tree instead of the real registry (`tests/unit/
// test_doctor_fixincludes.cpp`), and `doctor_report` below is its one real
// caller. `payloadRoot` is a version directory (e.g. `<registry>/data/xpkgs/
// xim-x-gcc/13.3.0`) — not the whole payload store — so the same function
// scans a native, a musl-gcc, or a cross-gcc payload alike; the directory
// SHAPE (`lib/gcc/<triple>/<version>/include-fixed`) is a GCC-family
// constant this does not need to be told.
//
// THE FIRST ~2 KiB IS ENOUGH. fixincludes writes its banner as the file's
// very first comment, before the copyright notice it also carries — reading
// a bounded prefix instead of the whole file keeps this cheap to run over
// an entire registry, and a header that legitimately mentions the phrase
// past that point (none does; it is fixincludes' own wording) is not this
// function's concern.
export std::vector<FixincludesFinding>
find_fixincludes_banners(const std::filesystem::path& payloadRoot) {
constexpr std::string_view kBanner = "auto-edited by fixincludes";
constexpr std::size_t kHeadBytes = 2048;
std::vector<FixincludesFinding> out;
std::error_code ec;
for (auto it = std::filesystem::recursive_directory_iterator(
payloadRoot, std::filesystem::directory_options::skip_permission_denied, ec);
!ec && it != std::filesystem::recursive_directory_iterator{};
it.increment(ec)) {
std::error_code isDirEc;
if (!it->is_directory(isDirEc) || isDirEc) continue;
if (it->path().filename() != "include-fixed") continue;
std::error_code innerEc;
for (auto& fe : std::filesystem::directory_iterator(it->path(), innerEc)) {
std::error_code isFileEc;
if (innerEc || !fe.is_regular_file(isFileEc) || isFileEc) continue;
std::ifstream in(fe.path(), std::ios::binary);
if (!in) continue;
std::string head(kHeadBytes, '\0');
in.read(head.data(), static_cast<std::streamsize>(head.size()));
head.resize(static_cast<std::size_t>(std::max<std::streamoff>(in.gcount(), 0)));
auto pos = head.find(kBanner);
if (pos == std::string::npos) continue;
FixincludesFinding f;
std::error_code relEc;
f.file = std::filesystem::relative(fe.path(), payloadRoot, relEc);
if (relEc || f.file.empty()) f.file = fe.path();
// The banner's shape (see the fixture in the unit test and the
// real payloads under xim-x-gcc):
// It has been auto-edited by fixincludes from:
// "/quoted/source/path"
// The source is the first double-quoted span after the banner.
if (auto q1 = head.find('"', pos); q1 != std::string::npos)
if (auto q2 = head.find('"', q1 + 1); q2 != std::string::npos)
f.source = head.substr(q1 + 1, q2 - q1 - 1);
out.push_back(std::move(f));
}
}
std::ranges::sort(out, {}, [](auto const& f) { return f.file.string(); });
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; };
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
// ONE PREPARE, READ-ONLY, AND THE SECTIONS BELOW REPORT ON IT (design
// 2026-10-17 D7).
//
// doctor ran a full `prepare_build` for its runtime-capabilities line and
// reported the PATH compiler as "the toolchain". On a user's home whose
// llvm replacement failed on every build, doctor started the same
// replacement (22-45 s), called its failure "run inside a package" while
// the user was in one, and printed `ok` for `~/.xlings/.../g++`, a
// compiler no build there used. A diagnosis that changes what it
// diagnoses, and reports something else, is two defects.
//
// Offline for the call: nothing is downloaded, installed or replaced, so
// a build that would install first is reported as exactly that. Planned
// to describe, not to build (`plan_only`, as `mcpp emit build-database`
// plans): the std module is described rather than compiled, generated
// files are compared rather than written, and the lock this planning
// produces goes to a scratch directory, not the project.
const bool inPackage = mcpp::project::find_manifest_root(std::filesystem::current_path()).has_value();
bool prepareWouldInstall = false;
auto pctx = [&]() -> decltype(mcpp::build::prepare_build(false)) {
if (!inPackage) return std::unexpected(std::string("not inside a package"));
mcpp::platform::env::ScopedEnv offline("MCPP_OFFLINE", "1");
const bool wasQuiet = mcpp::ui::is_quiet();
mcpp::ui::set_quiet(true);
(void)mcpp::build::refusal::take();
mcpp::build::BuildOverrides ov;
ov.plan_only = true;
std::error_code tec;
ov.work_dir = std::filesystem::temp_directory_path(tec)
/ std::format("mcpp-doctor-{}", std::chrono::steady_clock::now().time_since_epoch().count());
auto r = mcpp::build::prepare_build(/*print_fingerprint=*/false, /*includeDevDeps=*/false, {}, ov);
std::filesystem::remove_all(ov.work_dir, tec);
prepareWouldInstall = !r && mcpp::build::refusal::take()
== mcpp::build::refusal::Code::OfflineDownloadRequired;
mcpp::ui::set_quiet(wasQuiet);
return r;
}();
const auto first_line = [](std::string_view text) {
return std::string(text.substr(0, text.find('\n')));
};
// What the provider says of the payload a compiler is in: its packaging
// against the index (D1/D6). Nothing for a compiler outside mcpp's store.
const auto report_payload = [&](const std::filesystem::path& compiler) {
if (!cfg) return;
const auto entry = mcpp::provider::layout().entry_of(compiler);
if (entry.dir.empty()) return;
const std::array<mcpp::provider::PackageRef, 1> refs{{{entry.ns, entry.name, entry.version}}};
const auto st = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->status(refs);
if (st.size() != 1 || !st[0].error.empty()) return;
const auto& s0 = st[0];
if (!s0.complete)
err(std::format("payload {}@{} is incomplete; the next build reinstalls it", s0.name, s0.version));
else if (s0.current)
ok(std::format("payload {}@{}, packaging revision {} (current)", s0.name, s0.version,
s0.installedRevision < 0 ? 0 : s0.installedRevision));
else if (s0.usable)
ok(std::format("payload {} (builds keep it; `mcpp toolchain install` takes the new one)",
mcpp::provider::describe_currency(s0)));
else
warn(std::format("payload {}", mcpp::provider::describe_currency(s0)));
};
mcpp::ui::status("Checking", "toolchain");
std::optional<mcpp::toolchain::Toolchain> tc;
if (pctx) {
tc = pctx->tc;
ok(std::format("{} at {} (what a build here uses)", tc->label(), tc->binaryPath.string()));
report_payload(tc->binaryPath);
} else if (inPackage) {
if (prepareWouldInstall)
warn(std::format("a build here installs or replaces packages first: {}",
first_line(pctx.error())));
else
err(std::format("a build here fails before it compiles: {}", first_line(pctx.error())));
} else if (cfg) {
// Outside a package: the configured default, as `mcpp new && mcpp
// build` would resolve it.
const auto spec = cfg->defaultToolchain;
auto parsed = mcpp::toolchain::parse_toolchain_spec(spec);
if (spec.empty()) {
ok("no default toolchain configured (the first build installs one)");
} else if (!parsed) {
err(std::format("the configured default toolchain '{}' does not parse", spec));
} else {
auto pkg = mcpp::toolchain::to_xim_package(*parsed);
auto dir = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->pinned(pkg.ximName, pkg.ximVersion);
auto frontend = mcpp::toolchain::payload_frontend(dir, pkg);
std::error_code fec;
if (frontend && !frontend->empty() && std::filesystem::exists(*frontend, fec)) {
if (auto detected = mcpp::toolchain::detect(*frontend)) {
tc = *detected;
ok(std::format("{} at {} (the configured default)", tc->label(), tc->binaryPath.string()));
} else {
err(std::format("the configured default {} does not run: {}", spec,
detected.error().message));
}
report_payload(*frontend);
} else {
warn(std::format("the configured default {} is not installed (the first build installs it)", spec));
}
}
}
// mcpp's package home, as the provider reads it (X3): records that point
// at nothing are dropped by the next install; the rest needs the
// provider's own repair, run in that home.
if (cfg) {
mcpp::ui::status("Checking", "package home");
auto provider = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg));
std::size_t broken = 0, notices = 0;
std::string example;
bool answered = false;
if (auto r = provider->call("system_status", "{}"); r && r->exitCode == 0) {
for (const auto& d : r->dataEvents) {
if (d.dataKind != "binding_state") continue;
auto j = nlohmann::json::parse(d.payloadJson, nullptr, false);
if (!j.is_object() || !j.contains("findings") || !j["findings"].is_array()) continue;
answered = true;
for (const auto& f : j["findings"]) {
const bool isBroken = f.value("severity", std::string{}) == "broken";
(isBroken ? broken : notices) += 1;
if (isBroken && example.empty())
example = std::format("{}@{}: {}", f.value("target", std::string{}),
f.value("version", std::string{}), f.value("summary", std::string{}));
}
}
}
if (!answered)
ok("(the provider reports no binding state)");
else if (broken == 0)
ok(notices == 0 ? std::string("consistent")
: std::format("consistent ({} notice(s))", notices));
else
warn(std::format("{} record(s) in mcpp's package home point at nothing (first: {})\n"
" hint: `{} self doctor --fix` repairs them",
broken, example, provider->command_prefix()));
}
// 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. REPORTED PER ORIGIN, because it is chosen per origin
// (design §2.2): `msvc@system` searches the machine, a pinned toolset
// takes the payload installed with it. One unlabelled line here would
// be the same "one question, two answerers" shape the SDK axis exists
// to close — a user reading it would believe it applied to their
// pinned build, and it does not.
if (auto sdk = mcpp::toolchain::msvc::find_windows_sdk()) {
ok(std::format("Windows SDK (msvc@system) {} at {}", sdk->version,
sdk->root.string()));
} else {
warn("no Windows SDK found for msvc@system — native builds with "
"the machine's Visual Studio will fail (install the "
"'Windows 11 SDK' VS component)");
}
{
std::error_code sdkEc;
auto msvcRoot = mcpp::provider::layout().tool_in(
mcpp::provider::home_store().value_or(std::filesystem::path{}), "msvc");
for (auto& v : std::filesystem::directory_iterator(msvcRoot, sdkEc)) {
if (!v.is_directory(sdkEc)) continue;
auto ver = v.path().filename().string();
auto inst = mcpp::toolchain::msvc::installation_at(
v.path(), ver, /*identifyVersion=*/false);
if (!inst) continue;
// The SAME resolution a build performs, not a second one
// shaped like it: what doctor prints is what the user will
// believe, so the two must not be able to disagree.
auto choice = mcpp::toolchain::msvc::resolve_sdk_for(inst->clPath);
if (choice.sdk) {
ok(std::format("Windows SDK (msvc@{}) {} at {}",
ver, choice.sdk->version,
choice.sdk->root.string()));
} else {
warn(std::format(
"msvc@{} has no usable Windows SDK — reinstall it to "
"pull its `xim:windows-sdk` dependency", ver));
}
if (!choice.note.empty()) warn(choice.note);
}
}
mcpp::ui::status("Checking", "mingw (xim:mingw-gcc)");
{
auto pkgs = mcpp::provider::layout().tool_in(
mcpp::provider::home_store().value_or(std::filesystem::path{}), "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::provider::layout().tool_in(
mcpp::provider::home_store().value_or(std::filesystem::path{}), triple + "-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("{} {} 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");
// 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));
// Every index tree this mcpp reads, against the floor it declares.
// A run never reports this as an error (an index is data, and a run
// that resolves what it needs has nothing to report), so doctor is
// where the state is visible without a failing run.
mcpp::ui::status("Checking", "package indexes");
std::error_code idxEc;
bool anyIndex = false;
for (auto const& root : mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->index_roots()) {
if (!std::filesystem::exists(root / "pkgs", idxEc)) continue;
anyIndex = true;
const auto name = root.filename().string();
auto contract = mcpp::pm::read_index_contract(root);
if (!contract || contract->minMcpp.empty()) {
ok(std::format("index '{}' declares no mcpp floor", name));
} else if (mcpp::pm::index_usable(root)) {
ok(std::format("index '{}' requires mcpp >= {}", name, contract->minMcpp));
} else {
warn(std::format(
"index '{}' requires mcpp >= {}, and this is mcpp {}; the "
"packages it serves cannot be resolved until mcpp is "
"upgraded (xlings update mcpp). See `mcpp explain E0006`",
name, contract->minMcpp, mcpp::MCPP_VERSION));
}
}
if (!anyIndex) ok("no package index synced yet");
// Where the home is, and what it reuses (design 2026-10-16 §8, §10, §11).
mcpp::ui::status("Checking", "home");
if ((*cfg).systemHome) ok(std::format("{} is the machine's system home", (*cfg).mcppHome.string()));
if ((*cfg).upstreams.empty())
ok("self-contained: no upstream ([xlings] upstream)");
for (auto const& u : (*cfg).upstreams) ok(std::format("upstream {}", u.string()));
const auto caps = mcpp::platform::fs_caps((*cfg).mcppHome);
if (caps.native())
ok(std::format("{} on {}: every write is the filesystem's own",
(*cfg).mcppHome.string(), caps.type.empty() ? "this filesystem" : caps.type));
else
ok(std::format("{} on {}: {}{}{}", (*cfg).mcppHome.string(), caps.type.empty() ? "this filesystem" : caps.type,
caps.rename_no_replace ? "" : "publishes by hard link or a rename to a free name; ",
caps.rename_exchange ? "" : "exchanges by moving aside; ",
caps.symlinks ? "" : "no symbolic links (no reuse from an upstream)"));
if constexpr (mcpp::platform::is_windows)
ok(std::format("Windows long paths: {}", mcpp::platform::windows::long_paths_enabled()
? "on (LongPathsEnabled = 1)"
: "off (LongPathsEnabled = 0; mcpp does not change it)"));
}
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)));
}
// Reuse the verdict produced at the link seam. Doctor deliberately does
// not parse the artifact or inspect the current host: either would answer
// a different question after a SubOS/driver update and could contradict
// the exact RuntimeBinding the build used.
mcpp::ui::status("Checking", "last runtime closure verdict");
if (auto projectRoot = mcpp::project::find_manifest_root(
std::filesystem::current_path())) {
auto stored = mcpp::build::runtime_validation::latest_stored_verdict(
*projectRoot / "target");
if (!stored) {
ok("no linked ELF verdict stored yet (created on the next Linux link)");
} else {
using Status = mcpp::platform::elf::RuntimeVerdict::Status;
auto detail = stored->verdict.explain();
auto subject = std::format("{} (RuntimeBinding {})",
stored->artifact.string(), stored->contractHash);
if (stored->verdict.status == Status::Pass) {
ok(std::format("{}: pass", subject));
} else if (stored->verdict.status == Status::Inconclusive) {
warn(std::format("{}: inconclusive{}{}", subject,
detail.empty() ? "" : "\n", detail));
} else if (stored->verdict.status == Status::Unresolvable) {
err(std::format("{}: unresolvable runtime closure{}{}", subject,
detail.empty() ? "" : "\n", detail));
} else {
err(std::format("{}: proven mismatch{}{}", subject,
detail.empty() ? "" : "\n", detail));
}
}
} else {
ok("not in an mcpp project; no project runtime verdict to report");
}
// 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.
if (!inPackage) {
ok("(run inside a package to check its runtime capabilities)");
} else if (!pctx) {
ok("(not checked: the toolchain above is not ready)");
} 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.canonical(); 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 = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->store();
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 (const auto& entry : mcpp::provider::layout().entries(pkgsDir)) {
if (entry.ns != "xim") continue; // toolchains only
auto id = mcpp::toolchain::identify_xim_payload(entry.name);
if (!id) continue; // not a compiler pkg
{
const auto& vDir = entry.dir;
mcpp::toolchain::ToolchainSpec s;
s.family = id->family;
s.version = vDir.filename().string();
s.target = id->target;
// payload_frontend, not toolchain_frontend(root/"bin"):
// cl.exe is four levels down at
// VC/Tools/MSVC/<ver>/bin/Host<h>/<arch>/, so the `bin/`
// lookup finds nothing and `continue` drops every
// installed msvc toolset. That is the defect #436 fixed
// in `toolchain list`; doctor kept its own copy, so the
// two commands disagreed about the same machine.
auto binR = mcpp::toolchain::payload_frontend(
vDir, mcpp::toolchain::to_xim_package(s));
// DOCTOR IS THE COMMAND FOR EXACTLY THIS. A payload whose
// own description does not parse is a finding, and
// skipping it would make the one command that reports on
// an installation silent about the thing that breaks it.
if (!binR) { warn(binR.error()); continue; }
if (binR->empty()) continue;
const auto& bin = *binR;
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 the provider environment's lib — these
// point into payload lib dirs; a removed package leaves them broken.
auto subosLib = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->sysroot() / "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
// ─── Fixincludes-frozen headers in installed GCC payloads (mcpp#687) ────
//
// NOT GATED ON HOST OS, unlike the RUNPATH block above: this reads no
// ELF and spawns no `readelf`, so it runs the same way on every host
// that has a GCC-family payload installed, including a cross gcc/
// musl-gcc payload on a Linux, macOS or Windows mcpp.
mcpp::ui::status("Checking", "gcc include-fixed headers");
if (cfg) {
auto pkgsDir = mcpp::provider::open(mcpp::config::make_xlings_env(*cfg))->store();
std::error_code ec;
bool sawAnyGcc = false;
bool anyFrozen = false;
if (std::filesystem::exists(pkgsDir, ec)) {
// Same enumeration as the RUNPATH block: every `xim-x-<name>`
// directory `identify_xim_payload` recognises, filtered to the
// GCC family (native, musl-gcc, mingw-(cross-)gcc, and any
// `<triple>-gcc` cross payload all answer Family::Gcc).
for (const auto& entry : mcpp::provider::layout().entries(pkgsDir)) {
if (entry.ns != "xim") continue;
auto id = mcpp::toolchain::identify_xim_payload(entry.name);
if (!id || id->family != mcpp::toolchain::Family::Gcc) continue;
{
const auto& vDir = entry.dir;
sawAnyGcc = true;
auto findings = find_fixincludes_banners(vDir);
if (findings.empty()) continue;
anyFrozen = true;
mcpp::toolchain::ToolchainSpec s;
s.family = id->family;
s.version = vDir.filename().string();
s.target = id->target;
auto specStr = s.spec_str();
auto targetFlag = id->target.empty()
? std::string{}
: std::format(" --target {}", id->target.str());
std::string files;
std::string sources;
for (auto& f : findings) {
if (!files.empty()) files += ", ";
files += f.file.string();
if (!f.source.empty()
&& sources.find(f.source) == std::string::npos) {
if (!sources.empty()) sources += ", ";
sources += f.source;
}
}
warn(std::format(
"{}: {} carries a fixincludes copy of the BUILD "
"MACHINE's own header ({}), frozen from {}. mcpp's "
"`-idirafter` puts this ahead of the C library the "
"project actually resolves, so a header this frozen "
"can disagree with it (observed: a frozen "
"`pthread.h` breaking `<mutex>`/`<memory>`, "
"mcpp#687). Remedy: `mcpp index update`, then "
"`mcpp toolchain remove {}{}` and "
"`mcpp toolchain install {}{}`.",
s.display(), findings.size() == 1 ? "a file" :
std::format("{} files", findings.size()),
files, sources.empty() ? "an unrecorded source" : sources,
specStr, targetFlag, specStr, targetFlag));
}
}
}
if (sawAnyGcc && !anyFrozen)
ok("no fixincludes-frozen headers in installed gcc payloads");
else if (!sawAnyGcc)
ok("no installed gcc-family payloads to check");
}
// ── Build-policy knobs that are otherwise invisible ────────────────────
//
// Both of these change behaviour without changing anything a user can see
// in the output of a successful build, which is how "I set the key and
// nothing happened" becomes unanswerable. Report the EFFECTIVE value and,
// for the ones that have one, where it came from.
{
mcpp::ui::status("Checking", "build policy");
std::error_code pec;
auto manifestPath = std::filesystem::current_path(pec) / "mcpp.toml";
std::optional<mcpp::manifest::Manifest> m;
if (!pec && std::filesystem::exists(manifestPath, pec))
if (auto loaded = mcpp::manifest::load(manifestPath)) m = std::move(*loaded);
// Module-interface extensions: built-ins plus this project's additions.
{
auto table = mcpp::extension_table_for(
m ? m->buildConfig.moduleExtensions : std::vector<std::string>{});
std::string list;
for (auto const& e : table.moduleInterface) {
if (!list.empty()) list += ' ';
list += e;
}
const auto extra = table.moduleInterface.size() - 1; // built-in is .cppm
ok(std::format("module interfaces: {}{}", list,
extra ? std::format(" ({} from [build] module_extensions)", extra)
: " (built-in only)"));
}
// Run bound for build.mcpp, with its source named.
{
namespace pp = mcpp::build::program_protocol;
auto envSecs = pp::env_timeout_override();
auto manSecs = m ? m->buildConfig.buildProgramTimeoutSecs
: std::optional<int>{};
auto effective = pp::run_timeout(envSecs, manSecs).count() / 1000;
std::string_view from = envSecs ? "MCPP_BUILD_PROGRAM_TIMEOUT"
: manSecs ? "[build] build_program_timeout"
: "built-in default";
ok(std::format("build.mcpp run bound: {} (from {})",
effective ? std::format("{}s", effective)
: std::string("none — 0 disables it"),
from));
}
// Whether a deadline is actually enforced here. This used to be "no"
// on Windows while every knob claimed otherwise.
ok("process deadlines: enforced (POSIX SIGKILL / Windows job object)");
}
// No device-toolkit report here, on purpose. The bound a toolkit states
// for its host compiler, whether a device compiler can reach its own
// back-end, and whether the driver is new enough for the runtime are all
// facts about one vendor's tools, and the rule package that drives those
// tools is where they are read: it reports through `mcpp::warning` and
// states the driver relation through `mcpp::fact` / `mcpp::floor`, and the
// engine compares before the first compile. A machine with no rule
// package in its project has nothing vendor-specific to say here, and says
// nothing (`tests/unit/test_core_vendor_probes.cpp` holds that line).
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);
}
std::optional<std::pair<std::filesystem::path, nlohmann::json>>
latest_runtime_resolution(const std::filesystem::path& projectRoot) {
const auto target = projectRoot / "target";
std::error_code ec;
if (!std::filesystem::is_directory(target, ec)) return std::nullopt;
std::filesystem::path newest;
std::filesystem::file_time_type newestTime{};
bool found = false;
for (auto it = std::filesystem::recursive_directory_iterator(
target, std::filesystem::directory_options::skip_permission_denied, ec);
!ec && it != std::filesystem::recursive_directory_iterator{};
it.increment(ec)) {
if (!it->is_regular_file(ec)
|| it->path().filename() != "resolution.json") continue;
auto time = it->last_write_time(ec);
if (ec) { ec.clear(); continue; }
if (!found || time > newestTime
|| (time == newestTime && it->path() < newest)) {
found = true;
newest = it->path();
newestTime = time;
}
}
if (!found) return std::nullopt;
std::ifstream input(newest);
auto doc = nlohmann::json::parse(input, nullptr, false);
if (doc.is_discarded() || !doc.is_object()) return std::nullopt;
return std::pair{std::move(newest), std::move(doc)};
}
int print_stored_runtime_resolution() {
auto root = mcpp::project::find_manifest_root(std::filesystem::current_path());
if (!root) {
std::println(stderr,
"error: no mcpp.toml found; `mcpp why runtime` only reads a project's stored resolution");
return 2;
}
auto stored = latest_runtime_resolution(*root);
if (!stored) {
std::println(stderr,
"error: no stored runtime resolution; run `mcpp build` once");
return 2;
}
auto const& [path, doc] = *stored;
auto runtime = doc.find("runtime");
if (runtime == doc.end() || !runtime->is_object()) {
std::println(stderr, "error: {} has no runtime object", path.string());
return 2;
}
std::println("runtime resolution: {}", path.string());
if (auto binding = runtime->find("binding");
binding != runtime->end() && binding->is_object()) {
const bool declared = binding->value("declared", true);
std::println("binding: {} via {} (contract {}){}",
declared ? binding->value("runtime_id", "?") : "(undeclared)",
binding->value("provider_id", "?"),
binding->value("contract_hash", "?"),
declared ? "" : " — this SubOS did not describe itself");
// The note, when there is one. A degradation that only ever appeared
// once during the build it happened in is a degradation nobody can look
// up afterwards, and "why is my verdict inconclusive" is exactly the
// question this command exists to answer.
if (auto note = binding->value("note", std::string{}); !note.empty())
for (auto line : std::views::split(note, '\n'))