-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathplan.cppm
More file actions
1807 lines (1709 loc) · 91.4 KB
/
Copy pathplan.cppm
File metadata and controls
1807 lines (1709 loc) · 91.4 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.build.plan — backend-agnostic representation of "what to build".
//
// The pipeline is:
// manifest + modgraph + toolchain + fingerprint → BuildPlan → Backend.build()
export module mcpp.build.plan;
import std;
import mcpp.build.graph_shape;
import mcpp.build.loader_contract;
import mcpp.manifest;
import mcpp.source_kind;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.toolchain.cppfly;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
import mcpp.toolchain.linkmodel;
import mcpp.toolchain.triple;
import mcpp.pack.prebuilt; // is_distribution_package — a shipped shared lib is not rebuilt
import mcpp.platform;
import mcpp.platform.runtime_binding;
import mcpp.platform.runtime_env_contract;
import mcpp.platform.runtime_search;
import mcpp.platform.xlings.subos_info;
export namespace mcpp::build {
struct CompileUnit {
std::filesystem::path source;
// The unit's ROLE. Copied from the SourceUnit the scanner classified, and
// read by the object namer, the link-object collectors, the ninja rule
// picker, the CDB emitter and the assembly dialect check — none of which
// look at the extension any more. See mcpp.source_kind.
//
// Declared second so a hand-built unit reads `.source` then `.kind`. There
// is no safe default: a unit whose kind was never set would route to the
// generic C++ rule, which is a silent misroute rather than an error. Any
// producer other than make_plan (i.e. a test) must state it.
mcpp::SourceKind kind = mcpp::SourceKind::Other;
std::filesystem::path object; // relative to plan.outputDir
std::string packageName;
std::vector<std::filesystem::path> localIncludeDirs;
// #249: emitted as -idirafter (searched after the toolchain's system
// dirs) — a dep source root on this list can't shadow standard headers.
std::vector<std::filesystem::path> localIncludeDirsAfter;
std::vector<std::string> packageCflags;
std::vector<std::string> packageCxxflags;
std::vector<std::string> packageAsmflags; // per-glob asmflags (G4)
std::optional<std::string> providesModule; // logical name, if .cppm export
std::vector<std::string> imports; // logical names imported
// Unit came from a scan_overrides declaration — plan-vs-ddi
// verification is mandatory for it (ninja_backend emits --expect-*).
bool scanOverridden = false;
// This unit's outputs are already in the global cache: the backend emits
// `stage_file` edges from the cache instead of a compile edge (and skips
// the P1689 scan for it entirely). The unit itself stays in the plan so
// compile_commands.json keeps an entry for it and clangd does not lose the
// dependency's sources.
bool servedFromCache = false;
std::filesystem::path cachedObject; // absolute, inside the cache
std::filesystem::path cachedBmi; // absolute; empty if no module
// mcpp#344: this object's address INSIDE a global-cache entry — relative to
// `<entry>/obj/`, and a pure function of the owning package (its source's
// path relative to its own package root). Distinct from `object`, which is
// a build-dir path and therefore depends on which other packages this
// particular build contains.
//
// Empty means "no admissible cache address": the root package (never
// cached), or a dependency source that could not be anchored to anything
// machine-independent. prepare.cppm drops the WHOLE package out of the
// cache when any of its units has an empty address — a half-staged package
// mixes cached and freshly built BMIs, which is the mismatch GCC reports as
// a CRC error three edges later.
std::filesystem::path packageObjectRel;
};
struct LinkUnit {
std::string targetName;
enum Kind { Binary, StaticLibrary, SharedLibrary, TestBinary } kind = Binary;
// Normally relative to plan.outputDir. A `role = "object"` action's outputs
// land here ABSOLUTE, on purpose: ninja identifies a file by the string an
// edge declares, and the action edge declares whatever prepare_actions
// produced — respelling it here would create a second node and "missing and
// no known rule to make it". Do not normalise this vector.
std::vector<std::filesystem::path> objects;
std::vector<std::filesystem::path> implicitInputs; // relative to plan.outputDir
std::vector<std::string> linkFlags; // per-link edge flags
// The loader-tag flag for THIS unit's form (mcpp.build.loader_contract).
// Separate from linkFlags because its correctness is positional: gcc specs
// and clang config files hand ld `--enable-new-dtags`, last occurrence
// wins, so the emitter puts this after every other linker argument.
// Deciding it stays here; placing it is the emitter's business.
std::string loaderTagFlag;
std::filesystem::path output; // relative to plan.outputDir
// The import library a PE shared library also produces — empty on ELF and
// Mach-O, and empty for every non-shared unit. It is a SECOND output of the
// link edge, declared implicitly so the consumer that links it has a
// producer; without that, ninja reports "no known rule to make it" naming a
// file the link command does in fact write.
std::filesystem::path importLibrary; // relative to plan.outputDir
// The generated module-definition file, on the MSVC ABI only.
//
// MSVC exports nothing from a DLL without `__declspec(dllexport)` or a
// `.def`, so without this the import library is empty and every consumer
// fails with unresolved externals for symbols that are visibly in the
// objects. MinGW's linker auto-exports and needs none of this.
std::filesystem::path defFile; // relative to plan.outputDir
std::string soname; // ABI name for shared libraries
std::vector<std::filesystem::path> runtimeAliases; // relative aliases, e.g. bin/libfoo.so.1
std::optional<std::filesystem::path> entryMain; // src path of main.cpp for bin
};
// One Windows resource script compiled into one linkable resource artifact
// (mcpp#365).
//
// Deliberately NOT a CompileUnit. A `.rc` has no module semantics, so putting it
// there would drag it into the module graph, the topological order, the cache
// key and compile_commands.json — where clangd would be handed a file no C++
// frontend can parse. It is its own edge whose output joins `LinkUnit::objects`,
// which is the one thing `[build].ldflags` could never do: ldflags is a flat
// string in the link command, so a `.res` named there is invisible to ninja and
// changing it produced "no work to do".
struct ResourceUnit {
std::filesystem::path source; // absolute; synthesised ones live under outputDir
std::filesystem::path output; // relative to plan.outputDir
// The `.rc`'s own inputs: quoted #includes and the data files named by its
// resource statements. Neither windres nor llvm-rc can emit a depfile
// (verified against llvm-rc 22.1.8: /I, /D, no dependency output), so these
// come from a text scan plus `[resources].extra-inputs`.
std::vector<std::filesystem::path> implicitInputs;
};
struct RuntimeCapabilityProvider {
std::string capability;
mcpp::manifest::PackageId provider;
};
struct ResolvedRuntimeContract {
std::vector<mcpp::manifest::RuntimeRequirement> requirements;
std::vector<mcpp::manifest::RuntimeArtifact> artifacts;
mcpp::manifest::LinkIntent linkIntent;
std::vector<RuntimeCapabilityProvider> providers;
};
// Normalize structured and legacy runtime metadata and stamp every fact with
// the exact package identity that supplied it. Exported as a pure seam for
// contract tests and machine-readable tooling; it performs no host probing.
ResolvedRuntimeContract resolve_runtime_contract(
const std::vector<mcpp::modgraph::PackageRoot>& packages);
struct BuildPlan {
mcpp::manifest::Manifest manifest;
mcpp::toolchain::Toolchain toolchain;
mcpp::toolchain::Fingerprint fingerprint;
// Which graph this plan will write into build.ninja. The fingerprint does
// NOT cover dev-deps or test targets, so `mcpp build` and `mcpp test`
// share an output directory and overwrite each other's graph; this is what
// lets a fast path tell them apart (mcpp#407, mcpp.build.graph_shape).
GraphShape graphShape = GraphShape::Normal;
// The module-edge schedule this plan will emit, resolved ONCE (see
// mcpp.build.schedule.policy). The backend writes the graph in this shape,
// the graph records the tag, and the fast path compares against it — three
// readers, one derivation. Deriving it separately in the backend and in the
// executor is how the BMI-equivalence check and the job count drifted into
// disagreeing about what a module edge is.
std::string scheduleTag = "none";
// What to hand ninja. Under detach-codegen a compiler stops holding a slot
// when it publishes, so this must exceed the real compiler cap or the ready
// frontier starves — see the hazard note in schedule/detach_codegen.
int scheduleNinjaJobs = 0;
int scheduleCompilerCap = 0;
// One immutable snapshot selected before workspace member substitution.
// Build/run/test and cache fast paths consume this value; none may re-read
// xlings active/current state.
mcpp::platform::runtime::RuntimeBinding runtimeBinding;
std::string cppStandard = "c++23";
std::string cppStandardFlag = "-std=c++23";
// Module-graph-global dialect flags (issue #210), pre-joined with a
// leading space per flag (e.g. " -freflection"). Rides -std='s channels:
// global $cxxflags (all TUs incl. deps), std BMI prebuild, scans.
std::string dialectFlags;
std::filesystem::path projectRoot; // where mcpp.toml lives
std::filesystem::path outputDir; // target/<triple>/<fp>/
// Where compile_commands.json goes. Carried rather than derived from
// projectRoot: under BuildOverrides::work_dir the package root is a shared
// (possibly read-only) registry directory, and deriving the path would put
// an IDE database there. Empty → projectRoot, the historical default.
std::filesystem::path compileDbPath;
// GCC only: a specs file that replaces the pristine `*link:`, so the
// payload's own (patched by every home that ever installed against it)
// cannot inject rpath entries into this build's artifacts. Empty for
// clang, which bypasses its cfg with --no-default-config instead.
std::filesystem::path gccCleanSpecs;
std::filesystem::path stdBmiPath; // absolute path to prebuilt std.gcm
std::filesystem::path stdObjectPath; // absolute path to prebuilt std.o
std::filesystem::path stdCompatBmiPath; // absolute path to prebuilt std.compat.pcm
std::filesystem::path stdCompatObjectPath; // absolute path to prebuilt std.compat.o
std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only)
// NASM assembly (.asm sources). Both resolved in prepare AFTER the plan
// exists — only when the plan actually contains .asm units (lazy, hard
// failure when unavailable; never a silent skip).
std::filesystem::path nasmPath; // nasm binary (empty → no .asm units)
std::string nasmFormat; // -f value derived from the target triple
// Windows resources (mcpp#365). Resolved in prepare AFTER the plan exists
// and only when the plan actually has resource units — same lazy, hard-fail
// shape as nasm above: a resource that silently vanished would show up as
// "my icon is gone" with nothing to attribute it to.
std::vector<ResourceUnit> resourceUnits;
std::filesystem::path rcPath; // windres / llvm-rc / rc.exe
// "gnu" → windres, emits a COFF object (ld cannot consume a .res)
// "msvc" → rc.exe / llvm-rc, emits a .res (link.exe and lld-link take it)
std::string rcStyle;
std::vector<std::string> rcFlags; // -I / -D, target-shaped
std::vector<CompileUnit> compileUnits; // topologically sorted
std::vector<LinkUnit> linkUnits;
// Build-graph nodes declared by build programs (`mcpp:action=`). Paths are
// absolute and engine variables already substituted by the time they get
// here, so the backend only has to spell edges.
std::vector<mcpp::manifest::BuildAction> actions;
std::vector<std::filesystem::path> runtimeLibraryDirs;
// ONLY the dependency packages' [runtime] library_dirs (not toolchain/
// payload dirs). These are the dirs that must be baked into the produced
// binary's RUNPATH (e.g. compat.glx-runtime). Kept separate so static/musl
// links don't pull the glibc payload dir.
std::vector<std::filesystem::path> depRuntimeLibraryDirs;
std::vector<mcpp::manifest::RuntimeRequirement> runtimeRequirements;
std::vector<mcpp::manifest::RuntimeArtifact> runtimeArtifacts;
mcpp::manifest::LinkIntent linkIntent;
// The complete run-time search closure of the artifacts this plan will
// produce, in loader order and tagged with where each directory came from
// (`mcpp.platform.runtime_search`).
//
// This is the RECORD — what resolution.json publishes and `mcpp why
// runtime` explains. Emission is still owned by each origin's existing
// producer (payloads by the toolchain link model, package dirs by
// LinkIntent), with one exception: SubosFarm entries have no other
// producer, so `flags.cppm` renders them from here, appended last.
//
// ⚠️ The farm deliberately does NOT enter `runtimeLibraryDirs`. That
// vector becomes LD_LIBRARY_PATH for `mcpp run`, which is inherited by
// every child process including host binaries — measured to kill
// `xdg-open`/`notify-send` outright when a private libc is on it. The farm
// is reachable PER OBJECT (DT_RPATH) and must stay that way.
std::vector<mcpp::platform::search::Dir> runtimeSearch;
// Windows runtime-DLL deployment. On PE (`supports_rpath` is false) a
// directly-launched .exe cannot RUNPATH-locate a dependency's DLL, so each
// *.dll found in a dependency's [runtime] library_dir is copied beside the
// produced executable (into bin/). The filter is the *.dll extension, not a
// platform `if constexpr`: a real Linux/macOS dependency ships .so/.dylib
// (never .dll), so this list is empty there and non-Windows builds are
// byte-for-byte unchanged; only a Windows prebuilt-DLL package (or a test
// that ships a .dll) populates it. dest is relative to outputDir.
struct DeployFile {
std::filesystem::path source; // absolute source DLL
std::filesystem::path dest; // relative to outputDir, e.g. bin/libopenblas.dll
};
std::vector<DeployFile> runtimeDeployFiles;
// Aggregated host-runtime requirements from dependency packages'
// [runtime] metadata. Capability/provider-driven — no platform special-casing
// in mcpp: providers (e.g. compat.glx-runtime) declare these per platform.
std::vector<std::string> runtimeDlopenLibs; // union of deps' dlopen sonames
std::vector<std::string> runtimeCapabilities; // union of host capabilities
// (capability, provider package). A named aggregate instead of std::pair:
// musl-gcc 15.1 modules failed to emit vector<pair<string,string>>'s
// move-ctor instantiation across the module boundary (release link error).
std::vector<RuntimeCapabilityProvider> runtimeProviders;
};
// Merge the generic facts exported by the already-selected xlings
// RuntimeBinding. This is data ingestion only: xlings has already selected
// providers and materialized artifacts before mcpp sees this snapshot.
void merge_runtime_binding_contract(
BuildPlan& plan,
const mcpp::platform::runtime::RuntimeBinding& binding);
// The run-time search closure for this plan, in loader order and tagged with
// provenance. Exported so a test can exercise the guards (cross target, non-ELF
// format, undeclared SubOS) without linking a binary for each.
std::vector<mcpp::platform::search::Dir> runtime_search_closure(
const BuildPlan& plan,
const mcpp::platform::runtime::RuntimeBinding& binding);
// Is `p` inside one of `roots`, judged LEXICALLY?
//
// Lexical is the whole point (mcpp#344). std::filesystem::relative() runs
// weakly_canonical on both sides and therefore RESOLVES SYMLINKS, and a payload
// store whose entries are symlinks into another store is ordinary — e2e's
// _inherit_toolchain.sh builds one, and so does any CI cache that links a warm
// payload tree into a fresh MCPP_HOME. Under canonicalization those packages
// stop looking like store packages and silently drop out of the build cache.
// Both the cacheability gate and the cache-address anchor ask "where was this
// installed", which is a question about the path, not about the inode — and
// they must answer it the same way, so there is one function.
bool path_is_under_any(const std::filesystem::path& p,
const std::vector<std::filesystem::path>& roots);
// True if a source file defines a top-level `int main(`/`auto main(` entry,
// ignoring comments and string/raw-string literals. Drives the archive-vs-inline
// choice for kind="lib" dependencies (see plan.cppm).
bool source_defines_main(const std::filesystem::path& src);
// Build a BuildPlan from already-validated inputs. Fails (mcpp#233) only
// when the object-path uniqueness assertion below finds a residual
// collision after the relPath-mirroring scheme — a would-be ninja
// "multiple rules generate X" turned into a diagnosable mcpp error.
std::expected<BuildPlan, std::string>
make_plan(const mcpp::manifest::Manifest& manifest,
const mcpp::toolchain::Toolchain& tc,
const mcpp::toolchain::Fingerprint& fp,
const mcpp::modgraph::Graph& graph,
const std::vector<std::size_t>& topoOrder,
const std::vector<mcpp::modgraph::PackageRoot>& packages,
const std::filesystem::path& projectRoot,
const std::filesystem::path& outputDir,
const std::filesystem::path& stdBmiPath,
const std::filesystem::path& stdObjectPath,
// Roots of the immutable xpkgs payload stores (there is more than
// one: the global registry, plus the project-local `.mcpp/**/data`
// roots a custom git index installs into). Used ONLY to anchor the
// cache address of a dependency source that lives outside its own
// package root (a build.mcpp OUT_DIR product). Empty is legal and
// simply makes those units uncacheable.
const std::vector<std::filesystem::path>& storeRoots = {});
// Expand one manifest `include_dirs` entry against the project root — the
// #249 consistency join + the expand_dir_glob the dep path uses. Exported
// (like modgraph's glob_literal_prefix) so unit tests can assert its
// native-separator contract directly; see the definition below.
std::vector<std::filesystem::path>
expand_manifest_include_entry(const std::filesystem::path& root,
const std::filesystem::path& inc);
} // namespace mcpp::build
namespace mcpp::build {
namespace {
std::string sanitize_for_path(std::string_view module_name) {
std::string s;
s.reserve(module_name.size());
for (char c : module_name) {
if (c == ':') s.push_back('-');
else s.push_back(c);
}
return s;
}
// Both the naming POLICY and its formatting now live in mcpp.source_kind, so
// the planner and `mcpp pack` cannot answer "what is this object called"
// differently. This alias keeps the local spelling every call site below uses.
using mcpp::object_filename_for;
std::string qualified_package_name(const mcpp::manifest::Manifest& manifest) {
if (!manifest.package.namespace_.empty()
&& manifest.package.name.starts_with(manifest.package.namespace_ + ".")) {
return manifest.package.name;
}
if (manifest.package.namespace_.empty()) return manifest.package.name;
return manifest.package.namespace_ + "." + manifest.package.name;
}
std::vector<std::string> dependency_name_candidates(
const std::string& depName,
const mcpp::manifest::DependencySpec& spec)
{
std::vector<std::string> out;
auto push = [&](std::string value) {
if (value.empty()) return;
if (std::find(out.begin(), out.end(), value) == out.end())
out.push_back(std::move(value));
};
push(depName);
if (!spec.shortName.empty()) push(spec.shortName);
if (!spec.namespace_.empty() && !spec.shortName.empty()) {
push(spec.namespace_ + "." + spec.shortName);
}
return out;
}
// The naming this MACHINE would use for its own binaries. Correct only for a
// host-target build; passed to artifact_naming() as the fallback for an empty
// triple, and never consulted directly when a target triple is present.
mcpp::toolchain::triple::ArtifactNaming host_artifact_naming() {
return {
.exeSuffix = mcpp::platform::exe_suffix,
.libPrefix = mcpp::platform::lib_prefix,
.staticLibExt = mcpp::platform::static_lib_ext,
.sharedLibExt = mcpp::platform::shared_lib_ext,
.sharedNeedsImportLib = mcpp::platform::is_windows,
};
}
mcpp::toolchain::triple::ArtifactNaming naming_for(const mcpp::toolchain::Toolchain& tc) {
auto t = mcpp::toolchain::triple::parse(tc.targetTriple);
return mcpp::toolchain::triple::artifact_naming(
t ? *t : mcpp::toolchain::triple::Triple{}, host_artifact_naming());
}
// What the artifact is CALLED — a property of the target, not of this machine.
// Reading the host constants here made ninja declare an output the compiler
// never writes (Linux -> PE: declared `bin/foo`, produced `bin/foo.exe`), so
// the link edge could never be satisfied and reran on every build.
std::filesystem::path target_output(const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n) {
if (t.kind == mcpp::manifest::Target::Library) {
return std::filesystem::path("bin") /
std::format("{}{}{}", n.libPrefix, t.name, n.staticLibExt);
}
if (t.kind == mcpp::manifest::Target::SharedLibrary) {
return std::filesystem::path("bin") /
std::format("{}{}{}", n.libPrefix, t.name, n.sharedLibExt);
}
return std::filesystem::path("bin") /
std::format("{}{}", t.name, n.exeSuffix);
}
std::vector<std::filesystem::path> runtime_aliases_for_target(
const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n) {
std::vector<std::filesystem::path> aliases;
if (t.kind != mcpp::manifest::Target::SharedLibrary || t.soname.empty()) {
return aliases;
}
auto output = target_output(t, n);
if (t.soname != output.filename().string()) {
aliases.push_back(output.parent_path() / t.soname);
}
return aliases;
}
// A unit whose object is linked because it CONTRIBUTES CODE, as opposed to a
// module interface (linked unconditionally, because its global initializers
// can matter even when no symbol of it is referenced).
//
// Kind-based rather than extension-based. One incidental fix comes with it:
// `.mm` (Objective-C++) was missing from the old list, so an Objective-C++
// object was compiled and then never linked.
bool is_implementation_source(mcpp::SourceKind kind) {
return kind == mcpp::SourceKind::Cxx || kind == mcpp::SourceKind::C
|| kind == mcpp::SourceKind::GasAsm || kind == mcpp::SourceKind::NasmAsm;
}
// The import library a PE shared target produces, and empty everywhere else.
//
// PE splits a shared library into two files: the `.dll` the loader opens and a
// small archive of stubs the LINKER consumes. Nothing else models that, so the
// name lives here — the link rule writes it, the consumer links it, and the
// packer ships it, all from this one answer.
//
// The two spellings are the toolchains' own conventions, not a choice:
// mingw lib<name>.dll.a (ld --out-implib; keeps `.a` so -l finds it)
// msvc <name>.lib (link /IMPLIB)
// The msvc spelling is the same as a static library's, which is fine because a
// target is `lib` OR `shared`, never both.
std::filesystem::path import_library_for(const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n) {
if (t.kind != mcpp::manifest::Target::SharedLibrary || !n.sharedNeedsImportLib)
return {};
const bool msvc = n.libPrefix.empty(); // "" prefix + ".lib" is the msvc row
return std::filesystem::path("bin") /
(msvc ? std::format("{}{}", t.name, n.staticLibExt)
: std::format("{}{}{}{}", n.libPrefix, t.name,
n.sharedLibExt, n.staticLibExt));
}
// How a CONSUMER links against a shared library. Also a target property: PE has
// no rpath and wants an import library, Mach-O uses @loader_path, ELF uses
// $ORIGIN. Keying this on the host pointed it the wrong way under cross builds.
//
// PE now links the IMPORT LIBRARY rather than the `.dll` itself. Passing the
// `.dll` is something mingw's ld tolerates and MSVC's link.exe rejects outright,
// so the tolerant case was hiding the broken one — and "it works on the
// toolchain we happened to test" is the whole reason this path went unverified
// for so long.
std::vector<std::string> shared_library_link_flags(
const mcpp::manifest::Target& t,
const mcpp::toolchain::triple::ArtifactNaming& n,
const mcpp::toolchain::triple::Triple& target) {
std::vector<std::string> flags;
const bool pe = n.sharedNeedsImportLib;
const bool macho = target.empty() ? bool(mcpp::platform::is_macos)
: target.os == "macos";
if (pe) {
flags.push_back(import_library_for(t, n).generic_string());
} else {
flags.push_back("-L" + target_output(t, n).parent_path().generic_string());
flags.push_back(macho ? "-Wl,-rpath,@loader_path"
: "-Wl,-rpath,'$$ORIGIN'");
flags.push_back("-l" + t.name);
}
return flags;
}
} // namespace
// #249 consistency fix: expand include_dirs entries with the same
// `expand_dir_glob` the dep path (prepare.cppm) uses, so a main-manifest
// `include_dirs = ["*/include"]` glob works identically here. For a literal
// (wildcard-free) entry expand_dir_glob only returns EXISTING directories,
// whereas this helper historically joined unconditionally — keep the plain
// join as a fallback so an -I for a dir created later (e.g. by a build
// step) isn't silently dropped.
//
// Deliberately OUTSIDE the anonymous namespace: it is exported for its unit
// test (like modgraph's glob_literal_prefix), and the two
// local_include_dirs_*_for_manifest consumers below ride along so a single
// namespace split serves the whole trio.
std::vector<std::filesystem::path>
expand_manifest_include_entry(const std::filesystem::path& root,
const std::filesystem::path& inc)
{
if (inc.is_absolute()) {
// A TOML value like `C:/SDL2/include` keeps its `/` on MSVC — make
// it native so the CDB's -I (via local_include_args) is uniform.
auto n = inc;
n.make_preferred();
return { std::move(n) };
}
const auto glob = inc.generic_string();
auto expanded = mcpp::modgraph::expand_dir_glob(root, glob);
if (expanded.empty() && glob.find('*') == std::string::npos) {
// Same native-spelling rule for the bare join (see above): `root / p`
// with a multi-segment `generated/inc` is MIXED on MSVC, and this
// fallback exists precisely for dirs like `generated/` that a later
// build step creates — the #390 shape.
auto joined = root / inc;
joined.make_preferred();
expanded.push_back(std::move(joined));
}
return expanded;
}
std::vector<std::filesystem::path>
local_include_dirs_for_manifest(const std::filesystem::path& root,
const mcpp::manifest::Manifest& manifest)
{
std::vector<std::filesystem::path> dirs;
for (auto const& inc : manifest.buildConfig.includeDirs) {
for (auto& d : expand_manifest_include_entry(root, inc))
dirs.push_back(std::move(d));
}
return dirs;
}
// #249: same, for the -idirafter channel.
std::vector<std::filesystem::path>
local_include_dirs_after_for_manifest(const std::filesystem::path& root,
const mcpp::manifest::Manifest& manifest)
{
std::vector<std::filesystem::path> dirs;
for (auto const& inc : manifest.buildConfig.includeDirsAfter) {
for (auto& d : expand_manifest_include_entry(root, inc))
dirs.push_back(std::move(d));
}
return dirs;
}
namespace {
void append_unique_path(std::vector<std::filesystem::path>& out,
std::filesystem::path path)
{
if (path.empty()) return;
if (std::find(out.begin(), out.end(), path) == out.end())
out.push_back(std::move(path));
}
} // namespace
ResolvedRuntimeContract resolve_runtime_contract(
const std::vector<mcpp::modgraph::PackageRoot>& packages)
{
ResolvedRuntimeContract out;
auto append_string = [](std::vector<std::string>& values, std::string value) {
if (!value.empty() && std::ranges::find(values, value) == values.end())
values.push_back(std::move(value));
};
auto absolute_from = [](const std::filesystem::path& root,
const std::filesystem::path& value) {
return (value.is_absolute() ? value : root / value).lexically_normal();
};
auto append_requirement = [&](mcpp::manifest::RuntimeRequirement value) {
const bool duplicate = std::ranges::any_of(out.requirements,
[&](auto const& existing) {
return existing.kind == value.kind
&& existing.value == value.value
&& existing.phase == value.phase
&& existing.requester == value.requester
&& existing.required == value.required;
});
if (!duplicate) out.requirements.push_back(std::move(value));
};
for (auto const& package : packages) {
const auto id = mcpp::manifest::package_id(package.manifest.package);
auto const& runtime = package.manifest.runtimeConfig;
for (auto requirement : runtime.requirements) {
requirement.requester = id;
append_requirement(std::move(requirement));
}
// One compatibility train: old soname/capability lists are normalized
// into the structured requirement shape. They do NOT imply provider
// ownership; only `provides` below creates a provider fact.
for (auto const& soname : runtime.dlopenLibs) {
append_requirement({
.kind = "soname", .value = soname, .phase = "run",
.requester = id, .required = true,
});
}
for (auto const& capability : runtime.capabilities) {
append_requirement({
.kind = "capability", .value = capability, .phase = "run",
.requester = id, .required = true,
});
}
for (auto artifact : runtime.artifacts) {
artifact.provider = id;
artifact.path = absolute_from(package.root, artifact.path);
const bool duplicate = std::ranges::any_of(out.artifacts,
[&](auto const& existing) {
return existing.role == artifact.role
&& existing.provider == artifact.provider
&& existing.path == artifact.path
&& existing.provenance == artifact.provenance
&& existing.abi == artifact.abi
&& existing.digest == artifact.digest
&& existing.hostFingerprint == artifact.hostFingerprint;
});
if (!duplicate) out.artifacts.push_back(std::move(artifact));
}
for (auto const& capability : runtime.provides) {
const bool duplicate = std::ranges::any_of(out.providers,
[&](auto const& existing) {
return existing.capability == capability
&& existing.provider == id;
});
if (!duplicate) out.providers.push_back({capability, id});
}
for (auto const& library : runtime.linkIntent.libraries) {
const std::filesystem::path asPath(library);
const bool explicitPath = !library.starts_with('-')
&& (asPath.is_absolute() || asPath.has_parent_path()
|| asPath.has_extension());
append_string(out.linkIntent.libraries,
explicitPath ? absolute_from(package.root, asPath).string()
: library);
}
for (auto const& framework : runtime.linkIntent.frameworks)
append_string(out.linkIntent.frameworks, framework);
auto append_paths = [&](auto const& input, auto& output) {
for (auto const& path : input)
append_unique_path(output, absolute_from(package.root, path));
};
append_paths(runtime.linkIntent.linkLibraryDirs,
out.linkIntent.linkLibraryDirs);
append_paths(runtime.linkIntent.transitiveNeededDirs,
out.linkIntent.transitiveNeededDirs);
append_paths(runtime.linkIntent.runtimeSearchDirs,
out.linkIntent.runtimeSearchDirs);
append_paths(runtime.linkIntent.deployFiles,
out.linkIntent.deployFiles);
// Legacy library_dirs means run-time discovery only. It deliberately
// does not enter linkLibraryDirs; callers that need -L must opt into
// the structured field.
append_paths(runtime.libraryDirs, out.linkIntent.runtimeSearchDirs);
}
return out;
}
// The run-time search closure of everything this plan will link, in the order
// the loader will consult it, tagged with where each directory came from.
//
// ONE assembly, three producers. Payload directories come from the toolchain
// link model, package directories from the resolved LinkIntent, and the SubOS
// farm from the RuntimeBinding — and the farm is the addition that closes the
// gap this whole change exists for: mcpp already passes `--sysroot=<subos>` on
// the compile AND link lines, so `-lGL` resolves out of `<subos>/lib` with no
// flags from the user, while the RUN-time path was derived from payload
// directories alone. Link succeeded, the artifact could not start.
//
// FARM LAST, and it is the only invariant here. `<subos>/lib` is a symlink
// view rewritten by every `xlings install`; payload directories are written
// once and never touched. Payload-first keeps libc / libm / libstdc++ resolving
// from the pinned payload and leaves the farm to supply only what nothing else
// does. Farm-first would let a later install silently change which libc an
// ALREADY LINKED artifact loads. `search::ordered` is what enforces it, and
// e2e 219 asserts it on the produced ELF rather than on this code.
std::vector<mcpp::platform::search::Dir> runtime_search_closure(
const BuildPlan& plan,
const mcpp::platform::runtime::RuntimeBinding& binding) {
using mcpp::platform::search::Dir;
using mcpp::platform::search::Origin;
// PAYLOAD DIRECTORIES COME FROM THE SAME FUNCTION THAT EMITS THEM.
//
// `resolve_link_model` is a pure function of the toolchain and is what
// `flags.cppm` renders as `-L`/`-Wl,-rpath` for the C runtime; asking it
// here is how the record and the artifact stay the same list. Deriving
// them a second way is what made the first version of this record show a
// one-entry closure while the artifact carried three — `linkRuntimeDirs`
// is populated for CLANG ONLY, so on GCC it is simply empty and the
// payloads arrive through the link model instead.
//
// Both are read, in the order `flags.cppm` concatenates them.
std::vector<Dir> closure;
for (auto const& dir : mcpp::toolchain::resolve_link_model(plan.toolchain).libDirs)
closure.push_back({dir, Origin::Payload});
for (auto const& dir : plan.toolchain.linkRuntimeDirs)
closure.push_back({dir, Origin::Payload});
for (auto const& dir : plan.linkIntent.runtimeSearchDirs)
closure.push_back({dir, Origin::Package});
// TWO GUARDS, both about "will this artifact ever run here".
//
// format DT_RPATH exists on ELF only. Mach-O and PE get nothing rather
// than a branch in every consumer — the same shape
// `loader_contract` uses for the tag half of this contract.
// host The farm belongs to THIS host's SubOS, and a SubOS is a
// (os, arch, libc) triple's worth of libraries. A cross target
// must match all three or the path is inert at best and points
// at the wrong architecture's — or the wrong C library's —
// objects at worst. `x86_64-linux-musl` is the case that makes
// the libc axis load-bearing: same OS, same arch, and a glibc
// farm on a musl program's search path is exactly the payload
// mixing rule B exists to prevent. (Today that target is also
// `linkage = "static"`, so the flag is inert — measured: no
// dynamic section at all. The guard is for the day it is not.)
const auto triple = [&] {
auto t = mcpp::toolchain::triple::parse(plan.toolchain.targetTriple);
return t ? *t : mcpp::toolchain::triple::Triple{};
}();
const bool elfTarget = triple.empty()
? bool(mcpp::platform::is_linux)
: (triple.os != "macos" && triple.os != "windows");
// THE ARTIFACT'S OWN DIRECTORY — `$ORIGIN` (#415).
//
// It is emitted onto the link line by `shared_library_link_flags`, on a
// per-unit channel that never came through here, so the record and the
// artifact's DT_RPATH were STRUCTURALLY not comparable: e2e 219 had to
// special-case `$ORIGIN` to compare them at all, and that exception was
// the shape of the gap rather than a detail of the test.
//
// Recorded, not emitted: the link flag still comes from the same place it
// did. This closes the record, not the producer — making the closure the
// sole rpath producer means teaching it per-unit scope, which is a much
// larger change for a much smaller gain.
//
// ELF only, matching the guard below: `$ORIGIN` is the ELF spelling, and a
// format that gets no DT_RPATH gets no entry either.
//
// ⚠️ AND ONLY WHEN SOMETHING ACTUALLY EMITS IT. `$ORIGIN` comes from
// `shared_library_link_flags`, i.e. per CONSUMER of a shared library — a
// project with no shared library never gets one. Recording it
// unconditionally would swap this issue's asymmetry for its mirror image:
// the record would carry an entry the artifact does not, and e2e 219 would
// still need an exception to compare them. The point is that no exception
// is needed in either direction.
const bool buildsSharedLib = std::ranges::any_of(
plan.linkUnits, [](const LinkUnit& lu) {
return lu.kind == LinkUnit::SharedLibrary;
});
if (elfTarget && buildsSharedLib && !plan.outputDir.empty())
closure.push_back({plan.outputDir / "bin", Origin::Artifact});
// The binding names its libc as `<family>@<version>`; the triple names it
// as an ABI env (`gnu` ⇒ glibc). A MISMATCH must be PROVEN, not assumed:
// an undeclared SubOS has no runtime identity at all, and refusing its own
// library view because it did not describe itself would be the same
// absence-read-as-contradiction this change exists to remove. Unknown on
// either side ⇒ no evidence of a mismatch ⇒ the os/arch guards decide.
const auto bindingLibcFamily =
binding.runtimeId.substr(0, binding.runtimeId.find('@'));
const auto targetLibcFamily = triple.env.empty()
? std::string{} : (triple.env == "gnu" ? "glibc" : triple.env);
const bool libcMismatch = !bindingLibcFamily.empty()
&& !targetLibcFamily.empty()
&& targetLibcFamily != bindingLibcFamily;
const bool hostTarget = binding.platform == "linux"
&& !libcMismatch
&& (triple.empty()
|| (triple.os == "linux"
&& (triple.arch.empty() || triple.arch == binding.arch)));
if (elfTarget && hostTarget)
for (auto const& dir : binding.searchDirs)
closure.push_back({dir, Origin::SubosFarm});
return mcpp::platform::search::ordered(std::move(closure));
}
void merge_runtime_binding_contract(
BuildPlan& plan,
const mcpp::platform::runtime::RuntimeBinding& binding) {
auto package_id = [](const mcpp::xlings::subos::PackageIdentity& value) {
return mcpp::manifest::PackageId{
.namespace_ = value.namespace_,
.name = value.name,
.version = value.version,
.sourceProvenance = value.source,
};
};
std::vector<RuntimeCapabilityProvider> selected;
for (auto const& provider : binding.runtimeProviders) {
RuntimeCapabilityProvider value{
.capability = provider.capability,
.provider = package_id(provider.provider),
};
if (std::ranges::none_of(selected, [&](auto const& existing) {
return existing.capability == value.capability
&& existing.provider == value.provider;
}))
selected.push_back(std::move(value));
}
// xlings' selected providers are authoritative and therefore precede
// descriptor-declared fallback/provider facts.
for (auto it = selected.rbegin(); it != selected.rend(); ++it) {
if (std::ranges::none_of(plan.runtimeProviders, [&](auto const& existing) {
return existing.capability == it->capability
&& existing.provider == it->provider;
}))
plan.runtimeProviders.insert(plan.runtimeProviders.begin(), *it);
}
for (auto const& artifact : binding.runtimeArtifacts) {
mcpp::manifest::RuntimeArtifact value{
.role = artifact.role,
.provider = package_id(artifact.provider),
.path = artifact.path.lexically_normal(),
.provenance = artifact.provenance,
.abi = artifact.abi,
.digest = artifact.digest,
.hostFingerprint = artifact.hostFingerprint,
};
if (std::ranges::none_of(plan.runtimeArtifacts, [&](auto const& existing) {
return existing.role == value.role
&& existing.provider == value.provider
&& existing.path == value.path
&& existing.provenance == value.provenance
&& existing.abi == value.abi
&& existing.digest == value.digest
&& existing.hostFingerprint == value.hostFingerprint;
}))
plan.runtimeArtifacts.push_back(std::move(value));
}
plan.runtimeSearch = runtime_search_closure(plan, binding);
}
// True if `src` defines a top-level `int main(` / `auto main(` entry point.
// Comments and string/char/raw-string literals are stripped first, so test
// fixtures that embed `"int main() {...}"` or R"(int main(){})" don't
// false-positive (that misfire chose archive linking for a no-main test →
// gtest_main.o not pulled by MSVC lld-link → LNK1561). Heuristic but robust;
// worst case is a sub-optimal archive-vs-inline choice, never a miscompile.
bool path_is_under_any(const std::filesystem::path& p,
const std::vector<std::filesystem::path>& roots)
{
// Empty = unrelated (different roots/drives). ".." or a "../" prefix =
// outside. Everything else — including "." for the root itself — is in.
auto inside = [](const std::filesystem::path& a,
const std::filesystem::path& b) {
auto s = a.lexically_normal()
.lexically_relative(b.lexically_normal())
.generic_string();
return !s.empty() && s != ".." && !s.starts_with("../");
};
for (auto const& root : roots) {
if (root.empty()) continue;
if (inside(p, root)) return true;
// Retry on canonicalized paths. Lexical is the PRIMARY answer (it is
// the only one that survives a symlinked store), but it also requires
// the two paths to be spelled the same way, and mcpp's home is reached
// through more than one spelling on Windows (HOME vs USERPROFILE, drive
// letter case, 8.3 names). Both comparisons answer the same question
// under different equivalence relations, and either "yes" is sufficient
// evidence that the payload was installed into a store — so a spelling
// difference degrades to a slower build, never to a wrong one.
std::error_code e1, e2;
auto cp = std::filesystem::weakly_canonical(p, e1);
auto cr = std::filesystem::weakly_canonical(root, e2);
if (!e1 && !e2 && inside(cp, cr)) return true;
}
return false;
}
bool source_defines_main(const std::filesystem::path& src) {
std::ifstream is(src);
if (!is) return false;
std::string raw((std::istreambuf_iterator<char>(is)),
std::istreambuf_iterator<char>());
std::string code;
code.reserve(raw.size());
enum State { Normal, Line, Block, Str, Chr, RawStr } st = Normal;
std::string rawEnd; // ")delim\"" terminator for the active raw string
for (std::size_t i = 0; i < raw.size(); ++i) {
char c = raw[i];
char n = (i + 1 < raw.size()) ? raw[i + 1] : '\0';
switch (st) {
case Normal:
if (c == 'R' && n == '"') {
std::size_t j = i + 2;
std::string delim;
while (j < raw.size() && raw[j] != '(') delim.push_back(raw[j++]);
rawEnd = ")" + delim + "\"";
st = RawStr;
i = j; // sit on '(' ; loop ++ moves past
} else if (c == '/' && n == '/') { st = Line; ++i; }
else if (c == '/' && n == '*') { st = Block; ++i; }
else if (c == '"') { st = Str; }
else if (c == '\'') { st = Chr; }
else { code.push_back(c); }
break;
case Line: if (c == '\n') { st = Normal; code.push_back(c); } break;
case Block: if (c == '*' && n == '/') { st = Normal; ++i; } break;
case Str: if (c == '\\') ++i; else if (c == '"') st = Normal; break;
case Chr: if (c == '\\') ++i; else if (c == '\'') st = Normal; break;
case RawStr:
if (raw.compare(i, rawEnd.size(), rawEnd) == 0) {
st = Normal;
i += rawEnd.size() - 1;
}
break;
}
}
auto isws = [](char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v';
};
for (std::size_t i = 0; i + 4 <= code.size(); ++i) {
if (code.compare(i, 4, "main") != 0) continue;
std::size_t p = i;
bool sawWs = false;
while (p > 0 && isws(code[p - 1])) { --p; sawWs = true; }
bool prevOk = sawWs && (
(p >= 3 && code.compare(p - 3, 3, "int") == 0) ||
(p >= 4 && code.compare(p - 4, 4, "auto") == 0));
std::size_t q = i + 4;
while (q < code.size() && isws(code[q])) ++q;
bool nextOk = q < code.size() && code[q] == '(';
if (prevOk && nextOk) return true;
}
return false;
}
std::expected<BuildPlan, std::string>
make_plan(const mcpp::manifest::Manifest& manifest,
const mcpp::toolchain::Toolchain& tc,
const mcpp::toolchain::Fingerprint& fp,
const mcpp::modgraph::Graph& graph,
const std::vector<std::size_t>& topoOrder,
const std::vector<mcpp::modgraph::PackageRoot>& packages,
const std::filesystem::path& projectRoot,
const std::filesystem::path& outputDir,
const std::filesystem::path& stdBmiPath,
const std::filesystem::path& stdObjectPath,
const std::vector<std::filesystem::path>& storeRoots)
{
BuildPlan plan;
plan.manifest = manifest;
plan.toolchain = tc;
plan.fingerprint = fp;
// The ROOT package's extension table. Only the synthesized entry main
// needs it — every scanned unit arrives with its kind already set by the
// scanner, using its OWN package's table.
const auto rootExtTable =
mcpp::extension_table_for(manifest.buildConfig.moduleExtensions);
// Artifact naming and shared-library link shape are properties of the
// TARGET. Resolved once here from tc.targetTriple (empty = host target, in
// which case the host constants ARE the right answer) and threaded down,
// so nothing below reaches for mcpp::platform to describe an output.
const auto targetTriple = [&] {
auto t = mcpp::toolchain::triple::parse(tc.targetTriple);
return t ? *t : mcpp::toolchain::triple::Triple{};
}();
const auto naming = naming_for(tc);
// The loader-tag contract exists only where DT_RPATH/DT_RUNPATH do.
// Mach-O and PE have neither, so they get no flag rather than a branch in
// every consumer.
const bool elfTarget = targetTriple.empty()
? bool(mcpp::platform::is_linux)
: (targetTriple.os != "macos" && targetTriple.os != "windows");
auto loader_tag_flag = [&](LinkUnit::Kind kind) -> std::string {
if (!elfTarget) return {};
using mcpp::build::loader::Form;
Form form;
switch (kind) {
case LinkUnit::Binary: