-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtoml.cppm
More file actions
3007 lines (2867 loc) · 154 KB
/
Copy pathtoml.cppm
File metadata and controls
3007 lines (2867 loc) · 154 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.manifest:toml — load and validate mcpp.toml.
export module mcpp.manifest.toml;
import mcpp.manifest.types;
import mcpp.targetside;
import std;
import mcpp.source_kind;
import mcpp.libs.toml;
import mcpp.pm.dep_spec;
import mcpp.version_req;
import mcpp.pm.dependency_selector;
import mcpp.pm.index_spec;
import mcpp.platform;
import mcpp.platform.axis; // the one macos/macosx spelling rule
// ⚠️ ANONYMOUS NAMESPACE, AND THIS COST TWO WINDOWS JOBS TO LEARN.
//
// The first version of this helper sat at namespace scope in the module
// purview, which makes its declaration part of what this module's interface
// records. Under clang
// with the MSVC standard library that was enough to break every downstream
// translation unit that constructs one:
//
// MSVC\include\optional:307: error: no matching constructor for
// initialization of '_SMF_control<_Optional_construct_base<basic_string…
//
// The errors named test files that this change never touched, which is the
// signature of the hazard: a std type in a newly-exported interface poisons the
// importers' module files rather than failing where it was written. The Linux
// jobs stayed green throughout.
//
// Nothing outside this file calls it, so nothing outside this file should be
// able to see it.
namespace {
// A dependency's version requirement, checked with the parser that will later
// be asked to match it.
//
// ⚠️ THE PARSER EXISTED AND THIS PATH DID NOT USE IT.
//
// `version_req::parse_req` is what decides which published version satisfies a
// requirement. The dependency reader handed its string straight to the
// installer instead, so a requirement the matcher could never satisfy reached
// the network — and came back as
//
// E_NOT_FOUND: package 'compat.std-freestanding-alloc-libc@0.1.x' not
// found in the synced index
//
// which names the PACKAGE. The package exists; the requirement is what does
// not parse. Measured 2026-08-20, from a form that this repository's own
// documentation recommended (`docs/05` §2.8.2 said `compat.openblas = "0.3.x"`).
//
// Checking here converts a network round-trip and a misleading answer into a
// message that names the actual problem, at the point where the text was
// written.
//
// ⚠️ A WARNING AND NOT AN ERROR, AND THE FIRST VERSION GOT THIS WRONG.
//
// Rejecting the manifest breaks every consumer of a PUBLISHED package that
// carries such a string — including one where the offending entry belongs to a
// feature nobody activates. Measured: with the check as an error, a project
// pinned to `std-freestanding` 0.3.0 stopped loading entirely, although the
// half of that package it used was unaffected.
//
// This is the mirror of the rule the index already follows. Published data must
// not invalidate a running program; equally, a new program must not invalidate
// published data. A manifest check has no standing to do so over an entry that
// may never be reached.
// ⚠️ RETURNS A PLAIN STRING, EMPTY MEANING "NO PROBLEM", AND NOT AN
// `std::optional<std::string>`. The optional was the obvious spelling and cost
// two rounds of Windows CI: see the note on `TargetEntry::sysroot` for what
// that specialisation does to importers under clang with the MSVC standard
// library. Nothing here needs to distinguish an absent problem from an empty
// one, so nothing is lost.
std::string version_req_problem(std::string_view spec) {
if (spec.empty()) return {}; // path/git/workspace deps
if (auto r = mcpp::version_req::parse_req(spec); !r) return r.error();
return {};
}
} // namespace
export namespace mcpp::manifest {
// WHAT A MEMBER MANIFEST IS ALLOWED TO LEAVE OUT.
//
// `package.name` and `package.version` are required, and the parser cannot see
// that a manifest is a workspace MEMBER: a member has no `[workspace]` table of
// its own, so the file that would relax the rule is the one above it. Passing
// the fact in keeps the required-field check where it is while letting
// `[workspace.package]` actually be inheritable — a key that nothing could
// consume would be a recorded field with no reader, which is the defect these
// tables exist to remove rather than one to add.
//
// The requirement does not disappear. `inherit_workspace_config`'s caller
// raises it after inheritance, where "still missing" is knowable and the
// message can name both files.
struct LoadContext {
bool insideWorkspace = false;
};
// ─── `[xlings]`: mcpp's surface for xlings' local project mechanism ─────────
//
// `[xlings]` is not a schema of mcpp's own. It is what a project writes into
// xlings' project `.xlings.json`, and every rule below is that file's rule.
//
// A value is a string or an object keyed by platform, and xlings resolves the
// object against the host it runs on: the host's key wins, `default` is the
// fallback, and no match with no default means the entry is absent there
// (`resolve_platform_workspace_value_`, xlings `src/core/xvm/db.cppm`). The
// keys are xlings' own OS names — `linux`, `macosx`, `windows` — plus
// `default`; `macos` is accepted as an alias because mcpp spells it that way
// elsewhere. An unknown key is an error rather than a dropped entry: a
// mis-typed platform that silently declared nothing is the shape #531 was
// filed for.
//
// Resolved for THIS host at load, so every downstream reader stays on a flat
// list. The unresolved declaration is kept beside it in
// `XlingsConfig::workspaceByPlatform`, because the descriptor emitter needs
// every platform at once and cannot re-derive what was already collapsed.
//
// ⚠️ `macos` AND `macosx` ARE ONE PLATFORM, AND THE RULE IS NOT WRITTEN HERE.
// mcpp's triple vocabulary says `macos`; a descriptor and xlings' project file
// say `macosx`. `mcpp::platform::xpkg_platform_key_for` is the one place that
// knows, and `xpkg_platform` is the host in the same vocabulary — an earlier
// draft of this section hand-rolled both, which is the second copy of a rule
// this file exists to avoid.
inline std::string_view host_platform_key() {
return mcpp::platform::xpkg_platform;
}
// Split `<scope>:<rest>` on the FIRST colon. xlings writes a namespace this
// way on a version (`"mcpp": "xim:2026.8.30.2"` in a real subos file) and mcpp
// additionally accepts it on the key, so one splitter serves both halves.
inline std::pair<std::string, std::string> split_scope(std::string_view s) {
auto pos = s.find(':');
if (pos == std::string_view::npos) return {"", std::string(s)};
return {std::string(s.substr(0, pos)), std::string(s.substr(pos + 1))};
}
// Every platform a value speaks for. A plain string yields the single key
// `"*"`, meaning "on every platform"; an object yields its own keys, canonical
// (`macos` folded to `macosx`), with `default` kept as itself.
inline std::expected<std::vector<std::pair<std::string, std::string>>, std::string>
platform_values(const mcpp::libs::toml::Value& v) {
std::vector<std::pair<std::string, std::string>> out;
if (v.is_string()) { out.emplace_back("*", v.as_string()); return out; }
if (!v.is_table())
return std::unexpected(std::string(
"expected a string or a { <platform> = \"...\" } table"));
for (auto& [k, val] : v.as_table()) {
auto canon = k == "default"
? std::optional<std::string_view>("default")
: mcpp::platform::xpkg_platform_key_for(k);
if (!canon)
return std::unexpected(std::format(
"unknown platform key '{}'; expected one of linux, macosx "
"(or macos), windows, default", k));
if (!val.is_string())
return std::unexpected(std::format("platform key '{}' must be a string", k));
out.emplace_back(std::string(*canon), val.as_string());
}
return out;
}
// The value that applies on `platform`, or nullopt when the entry is absent
// there. `"*"` outranks nothing: a plain string is the whole answer.
inline std::optional<std::string>
value_for_platform(const std::vector<std::pair<std::string, std::string>>& vals,
std::string_view platform) {
// Folded on BOTH sides: a caller may name the host `macos` (mcpp's triple
// spelling) while the stored key is already canonical.
const std::string_view want =
mcpp::platform::xpkg_platform_key_for(platform).value_or(platform);
auto pick = [&](std::string_view k) -> std::optional<std::string> {
for (auto const& [key, v] : vals) if (key == k) return v;
return std::nullopt;
};
if (auto v = pick("*")) return v;
if (auto v = pick(want)) return v;
return pick("default");
}
// Retained so the pre-#544 spelling of a `deps` entry keeps parsing while the
// key is deprecated.
inline std::expected<std::optional<std::string>, std::string>
resolve_host_value(const mcpp::libs::toml::Value& v, std::string_view host) {
auto vals = platform_values(v);
if (!vals) return std::unexpected(vals.error());
return value_for_platform(*vals, host);
}
// One `[xlings.workspace]` entry, normalised.
//
// `target` is the xvm target the shim looks up and the key the file carries;
// `ns` is the index namespace, which qualifies where a version comes from and
// may be written on either half; `version` is empty when the entry asks only
// for presence, which is what `""` means in an authored project file.
struct XlingsEntry {
std::string ns, target, version;
// `[<ns>:]<target>[@<version>]` — what `install_packages` is asked for.
std::string address() const {
std::string a = ns.empty() ? target : ns + ":" + target;
if (!version.empty()) a += "@" + version;
return a;
}
// `[<ns>:]<version>` — what the file's `workspace` object carries. A scope
// qualifies a version, so with no version there is nothing to qualify.
std::string pin() const {
if (version.empty()) return {};
return ns.empty() ? version : ns + ":" + version;
}
// The RECOMMENDED authored spelling, which is not the same thing as the
// materialised one. A manifest names a package and then says which version
// of it, so the namespace belongs to the package and rides the key:
//
// "xim:qemu-riscv" = "9.2.4-1"
//
// The other position is accepted and is what the file itself carries,
// because a `.xlings.json` key is an xvm target and the scope there
// qualifies the version. Two vocabularies, one entry; mcpp suggests the
// one an author writes.
std::string authored_line() const {
auto key = ns.empty() ? target : std::format("\"{}:{}\"", ns, target);
return std::format("{} = \"{}\"", key, version);
}
};
// The inverse: `[<ns>:]<target>[@<version>]` back into its parts. Used by the
// `deps` compatibility path, which receives an address and has to say what the
// equivalent `[xlings.workspace]` line is — and to compare it against one.
inline XlingsEntry parse_address(std::string_view address) {
auto at = address.find('@');
auto head = at == std::string_view::npos ? address : address.substr(0, at);
auto version = at == std::string_view::npos ? std::string_view{}
: address.substr(at + 1);
auto [ns, target] = split_scope(head);
return XlingsEntry{ ns, target, std::string(version) };
}
// Combine the two halves a namespace may be written on. Both may carry it;
// disagreeing is an error rather than a precedence rule, because a precedence
// rule would make one of the two spellings silently ineffective.
inline std::expected<XlingsEntry, std::string>
make_xlings_entry(std::string_view key, std::string_view value) {
auto [keyNs, target] = split_scope(key);
auto [valNs, version] = split_scope(value);
if (target.empty())
return std::unexpected(std::string("names no package"));
if (!keyNs.empty() && !valNs.empty() && keyNs != valNs)
return std::unexpected(std::format(
"namespace '{}' on the key and '{}' on the version disagree; "
"write it once", keyNs, valNs));
return XlingsEntry{ keyNs.empty() ? valNs : keyNs, target, version };
}
std::expected<Manifest, ManifestError> parse_string(std::string_view content,
const std::filesystem::path& origin = "mcpp.toml",
LoadContext ctx = {});
std::expected<Manifest, ManifestError> load(const std::filesystem::path& path,
LoadContext ctx = {});
// For `mcpp new` scaffolding.
std::string default_template(std::string_view packageName);
// Shared source-preserving editor used by both `mcpp add` and scaffold
// self-dependency injection. Identity is structured; formatting is emitted in
// the canonical default table / namespace-subtable form.
struct DependencyTextEdit {
std::string namespace_;
std::string shortName;
std::string version;
std::vector<std::string> features;
bool dev = false;
};
std::expected<std::string, std::string>
upsert_dependency_text(std::string_view source,
const DependencyTextEdit& edit);
// Every path the lib-root convention would accept, in extension-table order
// (`.cppm` first, then whatever `[build] module_extensions` declares). An
// explicit `[lib] path` collapses this to that one entry.
std::vector<std::filesystem::path> lib_root_candidates(const Manifest& manifest);
// The lib root that EXISTS under `projectRoot`. `mcpp.manifest.types` has the
// non-probing form, which answers with the conventional NAME and is the right
// one for a diagnostic; this is the right one for "which file is actually
// there", and a project whose interfaces are `.ixx` needs it.
std::filesystem::path resolve_lib_root_path(const Manifest& manifest,
const std::filesystem::path& projectRoot);
} // namespace mcpp::manifest
namespace mcpp::manifest {
namespace t = mcpp::libs::toml;
namespace {
ManifestError error(const std::filesystem::path& origin,
const std::string& msg,
t::Position pos = {0, 0}) {
return ManifestError{msg, origin, pos.line, pos.column};
}
// #227 follow-up: libs/toml.cppm's `[[dotted.path]]` support is intentionally
// schema-agnostic — it accepts an array-of-tables at ANY dotted path, so a
// doubled-bracket typo like `[[dependencies]]` (single brackets meant) or
// `[[toolchain]]` parses cleanly as an Array Value there. Every mcpp consumer
// reads such sections via get_table(), which returns nullptr for a non-table
// Value, so the section silently reads as ABSENT (e.g. all dependencies
// silently dropped) with no parse error and no warning. Close the grammar
// here, at the manifest layer, instead of hardcoding mcpp section names into
// the generic TOML layer: the only legitimate array-of-tables in mcpp.toml
// today is `[[build.flags]]`.
bool is_array_of_tables(const t::Value& v) {
if (!v.is_array()) return false;
auto& arr = v.as_array();
if (arr.empty()) return false;
for (auto& e : arr) if (!e.is_table()) return false;
return true;
}
// #253: shared parser for the per-glob flags array shape
// `[{ glob = "...", cflags/cxxflags/asmflags/defines = [...] }, ...]` —
// one entry grammar for `[build].flags` and `[features].<name>.flags`.
// `ctxLabel` names the anchoring key in error messages. Entries append to
// `dst` in declaration order (order is the override semantics). Returns an
// error message, or nullopt on success.
std::optional<std::string> parse_glob_flags_value(
const t::Value& fv, std::string_view ctxLabel, std::vector<GlobFlags>& dst)
{
if (!fv.is_array()) {
return std::format(
"{} must be an array of inline tables "
"(flags = [{{ glob = \"...\", cxxflags = [...] }}, ...])", ctxLabel);
}
for (auto& ev : fv.as_array()) {
if (!ev.is_table()) {
return std::format(
"{} entries must be inline tables with a `glob` key", ctxLabel);
}
auto& et = ev.as_table();
GlobFlags gf;
for (auto& [k, v] : et) {
auto read_list = [&](std::vector<std::string>& out) -> bool {
if (!v.is_array()) return false;
for (auto& s : v.as_array())
if (s.is_string()) out.push_back(s.as_string());
return true;
};
bool ok = false;
if (k == "glob") { ok = v.is_string(); if (ok) gf.glob = v.as_string(); }
else if (k == "cflags") ok = read_list(gf.cflags);
else if (k == "cxxflags") ok = read_list(gf.cxxflags);
else if (k == "asmflags") ok = read_list(gf.asmflags);
else if (k == "defines") ok = read_list(gf.defines);
if (!ok) {
return std::format(
"{}: invalid key '{}' (expected glob = \"...\" "
"plus cflags/cxxflags/asmflags/defines arrays)", ctxLabel, k);
}
}
if (gf.glob.empty()) {
return std::format("{} entry is missing its `glob` key", ctxLabel);
}
dst.push_back(std::move(gf));
}
return std::nullopt;
}
// Allowlist entries are dotted paths whose segments may be the wildcard `*`,
// matching exactly one path segment — needed for #253's `features.<name>.flags`,
// whose middle segment (the feature name) is author-chosen.
bool aot_path_matches(std::string_view pattern, std::string_view path) {
while (true) {
auto pDot = pattern.find('.');
auto sDot = path.find('.');
auto pSeg = pattern.substr(0, pDot);
auto sSeg = path.substr(0, sDot);
if (pSeg != "*" && pSeg != sSeg) return false;
if (pDot == std::string_view::npos || sDot == std::string_view::npos)
return pDot == std::string_view::npos && sDot == std::string_view::npos;
pattern.remove_prefix(pDot + 1);
path.remove_prefix(sDot + 1);
}
}
std::optional<std::string> find_disallowed_array_of_tables(
const t::Table& tbl, const std::string& prefix,
std::span<const std::string_view> allowlist)
{
for (auto& [k, v] : tbl) {
std::string path = prefix.empty() ? k : std::format("{}.{}", prefix, k);
if (is_array_of_tables(v)) {
bool allowed = false;
for (auto a : allowlist) if (aot_path_matches(a, path)) { allowed = true; break; }
if (!allowed) return path;
} else if (v.is_table()) {
if (auto found = find_disallowed_array_of_tables(v.as_table(), path, allowlist))
return found;
}
}
return std::nullopt;
}
} // namespace
std::expected<Manifest, ManifestError> parse_string(std::string_view content,
const std::filesystem::path& origin,
LoadContext ctx) {
auto doc = t::parse(content);
if (!doc) {
return std::unexpected(error(origin, doc.error().message, doc.error().where));
}
// Closed-grammar guard: reject any array-of-tables whose dotted path
// isn't explicitly allowlisted, BEFORE any section is read. See
// find_disallowed_array_of_tables above.
static constexpr std::string_view kAllowedArraysOfTables[] = {
"build.flags",
"features.*.flags", // #253 — the middle segment is the feature name
"target.*.build.flags", // #258 — middle segment is the cfg predicate
"runtime.requirements",
"runtime.artifacts",
// #544: `deps = [{ linux = "..." }]` — every entry a per-platform
// table — is the same Value shape as `[[xlings.deps]]`, and the guard
// cannot tell the inline form from the doubled-bracket typo. The
// reader below type-checks every entry, so nothing is silently
// dropped on this path either way.
"xlings.deps",
};
if (auto badPath = find_disallowed_array_of_tables(doc->root(), "", kAllowedArraysOfTables)) {
return std::unexpected(error(origin, std::format(
"[[{}]] (array-of-tables) is not allowed for section '{}'; "
"array-of-tables syntax is only supported for [[build.flags]], "
"[[features.<name>.flags]], [[runtime.requirements]], "
"[[runtime.artifacts]], and [xlings] deps entries",
*badPath, *badPath)));
}
Manifest m;
m.sourcePath = origin;
// [package] — required unless [workspace] is present (virtual workspace).
auto* pkg_t = doc->get_table("package");
bool has_workspace = (doc->get_table("workspace") != nullptr);
if (!pkg_t && !has_workspace)
return std::unexpected(error(origin, "missing required [package] section"));
auto name = doc->get_string("package.name");
if (!name && !has_workspace && !ctx.insideWorkspace)
return std::unexpected(error(origin, "missing required field 'package.name'"));
if (name) m.package.name = *name;
// 0.0.6+: explicit namespace field (xpkg V1 style).
// If present, [package].name is the short name.
// If absent, compat.cppm::resolve_package_name infers from dotted name.
if (auto v = doc->get_string("package.namespace")) m.package.namespace_ = *v;
auto version = doc->get_string("package.version");
if (!version && !has_workspace && !ctx.insideWorkspace)
return std::unexpected(error(origin, "missing required field 'package.version'"));
if (version) m.package.version = *version;
if (auto v = doc->get_string("package.description")) m.package.description = *v;
if (auto v = doc->get_string("package.license")) m.package.license = *v;
if (auto v = doc->get_string("package.repo")) m.package.repo = *v;
if (auto v = doc->get_string_array("package.authors")) m.package.authors = *v;
if (auto v = doc->get_string_array("package.platforms")) m.package.platforms = *v;
// [package].standard (M5.0 new home)
if (auto v = doc->get_string("package.standard")) {
m.package.standard = *v;
// Recorded HERE, where the key's presence is a fact rather than an
// inference. Both spellings count as a declaration; the deprecated
// `[language] standard` below is the same statement in an older place.
m.package.standardDeclared = true;
} else if (auto n = doc->get_int("package.standard")) {
// `standard = 26` — WRITTEN BY USERS AND SILENTLY IGNORED UNTIL NOW.
//
// The key is documented as a string, `get_string` returns nothing for a
// bare integer, and the project compiled at the default with no
// diagnostic. Measured on the released engine: `standard = 26` produced
// `-std=c++23`. Issue #527 writes it that way in three of its examples,
// so a reader following the issue got a build that ignored the line
// they were told to add.
//
// Accepted rather than refused because the mapping is unambiguous and
// the intent is not in question; an integer that is not a standard
// level still goes through `normalize_cpp_standard` below and is
// refused there, with that function's list of accepted spellings.
m.package.standard = std::format("c++{}", *n);
m.package.standardDeclared = true;
}
// [language] (M5.0: deprecated, kept for backward compat — drop in M6)
// Reads to old fields AND mirrors to new package.standard if [package].standard not set.
bool had_language_section = (doc->get_table("language") != nullptr);
if (auto v = doc->get_string("language.standard")) {
m.language.standard = *v;
// mirror to new home only if [package].standard wasn't explicitly set
if (!doc->get_string("package.standard")) m.package.standard = *v;
m.package.standardDeclared = true;
} else {
m.language.standard = m.package.standard; // keep old field consistent with new
}
if (auto v = doc->get_bool("language.modules")) m.language.modules = *v;
if (auto v = doc->get_bool("language.import_std")) m.language.importStd = *v;
// Validation on the unified standard. Store the canonical spelling so all
// downstream build surfaces consume one active value.
auto stdCfg = normalize_cpp_standard(m.package.standard);
if (!stdCfg) return std::unexpected(error(origin, stdCfg.error()));
m.cppStandard = *stdCfg;
m.package.standard = m.cppStandard.canonical;
m.language.standard = m.cppStandard.canonical;
if (had_language_section && !m.language.modules) {
return std::unexpected(error(origin,
"language.modules must be true (mcpp is modules-only)"));
}
// [build].sources (M5.0 new home) + [modules].sources (deprecated, compat)
//
// `sourcesDeclared` records PRESENCE, not content: `sources = []` has to
// mean "compile nothing", and only the key's existence can say that (see
// BuildConfig::sourcesDeclared). Set from either spelling, because the
// legacy one has to be able to express it too.
if (auto v = doc->get_string_array("build.sources")) {
m.buildConfig.sources = *v;
m.buildConfig.sourcesDeclared = true;
}
if (auto v = doc->get_string_array("modules.sources")) {
m.modules.sources = *v;
// If [build].sources wasn't set, mirror legacy field into new field.
if (!m.buildConfig.sourcesDeclared) {
m.buildConfig.sources = *v;
m.buildConfig.sourcesDeclared = true;
}
}
// Mirror new → legacy so existing code reading manifest.modules.sources keeps working.
if (m.modules.sources.empty()) m.modules.sources = m.buildConfig.sources;
if (auto v = doc->get_string_array("modules.exports")) m.modules.exports_ = *v;
if (auto v = doc->get_bool("modules.strict")) m.modules.strict = *v;
// [build].include_dirs (M5.0 new field)
if (auto v = doc->get_string_array("build.include_dirs")) {
for (auto& s : *v) m.buildConfig.includeDirs.emplace_back(s);
}
// [build].include_dirs_after (#249) — searched after system dirs (-idirafter).
if (auto v = doc->get_string_array("build.include_dirs_after")) {
for (auto& s : *v) m.buildConfig.includeDirsAfter.emplace_back(s);
}
// [build].private_include_dirs — of `include_dirs`, the ones a consumer
// must NOT receive. See BuildInputs::privateIncludeDirs for why it is a
// subset of that list rather than a second ordered list.
if (auto v = doc->get_string_array("build.private_include_dirs")) {
for (auto& s : *v) m.buildConfig.privateIncludeDirs.emplace_back(s);
}
// [targets.*] — M5.0: now optional. If absent, defer to auto-inference (in load()).
// [profile.<name>] — bundled build settings.
if (auto* profile_table = doc->get_table("profile");
profile_table && !profile_table->empty()) {
for (auto& [pname, pval] : *profile_table) {
if (!pval.is_table()) continue;
auto& tt = pval.as_table();
Profile pr;
if (auto it = tt.find("opt"); it != tt.end()) {
if (it->second.is_string()) pr.optLevel = it->second.as_string();
else if (it->second.is_int()) pr.optLevel = std::to_string(it->second.as_int());
}
if (auto it = tt.find("debug"); it != tt.end() && it->second.is_bool()) pr.debug = it->second.as_bool();
if (auto it = tt.find("lto"); it != tt.end() && it->second.is_bool()) pr.lto = it->second.as_bool();
if (auto it = tt.find("strip"); it != tt.end() && it->second.is_bool()) pr.strip = it->second.as_bool();
auto read_list = [&](const char* key, std::vector<std::string>& out) {
if (auto it = tt.find(key); it != tt.end() && it->second.is_array())
for (auto& v : it->second.as_array())
if (v.is_string()) out.push_back(v.as_string());
};
if (auto it = tt.find("dependency_linkage");
it != tt.end() && it->second.is_string())
pr.dependencyLinkage = it->second.as_string();
read_list("cflags", pr.cflags);
read_list("cxxflags", pr.cxxflags);
read_list("ldflags", pr.ldflags);
m.profiles[pname] = pr;
}
}
// [features] — feature name → implied features. "default" lists the
// default-active set. Two accepted shapes (Feature System v2):
// array form (shorthand): name = ["implied", ...]
// table form (full): name = { implies = [...], defines = [...] }
// The table form lets a feature contribute package-owned defines (Stage 1);
// `requires`/`provides`/`deps` keys are reserved for later stages.
if (auto* features_table = doc->get_table("features");
features_table && !features_table->empty()) {
auto read_str_array = [](const auto& tbl, std::string_view key,
std::vector<std::string>& out) {
if (auto it = tbl.find(std::string(key));
it != tbl.end() && it->second.is_array())
for (auto& v : it->second.as_array())
if (v.is_string()) out.push_back(v.as_string());
};
for (auto& [fname, fval] : *features_table) {
std::vector<std::string> implied;
std::vector<std::string> forwardTokens;
if (fval.is_array()) {
for (auto& v : fval.as_array())
if (v.is_string()) implied.push_back(v.as_string());
} else if (fval.is_table()) {
auto& ft = fval.as_table();
read_str_array(ft, "implies", implied);
// #243: a feature may forward features to its dependencies
// (Cargo `dep/feat`). Two equivalent spellings, one data model:
// `dep/feat` tokens mixed into `implies` (Cargo parity), or a
// dedicated self-documenting `forward = ["dep/feat", ...]` key.
read_str_array(ft, "forward", forwardTokens);
std::vector<std::string> defs;
read_str_array(ft, "defines", defs);
if (!defs.empty()) m.buildConfig.featureDefines[fname] = std::move(defs);
// Feature-gated source globs — same semantics as the index
// descriptor's `sources` key (one data model, two grammars):
// listed globs leave the default build and compile only when
// the feature is active.
std::vector<std::string> fsrcs;
read_str_array(ft, "sources", fsrcs);
if (!fsrcs.empty()) m.buildConfig.featureSources[fname] = std::move(fsrcs);
std::vector<std::string> reqs, provs;
read_str_array(ft, "requires", reqs);
read_str_array(ft, "provides", provs);
if (!reqs.empty()) m.featureRequires[fname] = std::move(reqs);
if (!provs.empty()) m.featureProvides[fname] = std::move(provs);
// #253: per-feature per-glob compile flags — same entry grammar
// as [build].flags (shared parse_glob_flags_value), gated by
// this feature and folded in AFTER base globFlags at activation
// so feature rules win via "last flag wins". Both spellings
// reach here: the inline array and [[features.X.flags]] AOT
// (allowlisted via the features.*.flags pattern, mirroring
// #227's build.flags decision — libs/toml builds one shape).
if (auto it = ft.find(std::string("flags")); it != ft.end()) {
if (auto err = parse_glob_flags_value(
it->second,
std::format("[features].{}.flags", fname),
m.buildConfig.featureFlags[fname])) {
return std::unexpected(error(origin, *err));
}
}
}
// #243: split `dep/feat` tokens out of `implies` into featureForwards
// (raw depKey shares the `dependencies`/`featureDeps` keyspace); the
// dedicated `forward` key is always forwards. Plain names stay implies.
std::vector<std::string> localImplies;
for (auto& tok : implied) {
if (auto fwd = mcpp::pm::split_feature_forward_token(tok))
m.featureForwards[fname].push_back(std::move(*fwd));
else
localImplies.push_back(std::move(tok));
}
for (auto& tok : forwardTokens)
if (auto fwd = mcpp::pm::split_feature_forward_token(tok))
m.featureForwards[fname].push_back(std::move(*fwd));
m.featuresMap[fname] = std::move(localImplies);
// #540: the table form is the ONE structured manifest section that
// had no schema check, so `include_dirs` written inside a feature
// built successfully with zero diagnostics — while the identical
// misplacement in `[build]` or `[target.<pred>.build]` is reported.
//
// MUST stay in sync with the reads above. Warning, not error, and
// root-manifest-only in effect (prepare surfaces schemaWarnings for
// the root before any dependency manifest is loaded), so a package
// may adopt a future key before its consumers upgrade — the same
// property #515 measured for `[build] private_include_dirs`.
if (fval.is_table()) {
static constexpr std::string_view kKnownFeatureKeys[] = {
"defines", "flags", "forward", "implies", "provides",
"requires", "sources",
};
for (auto& [fkey, fignored] : fval.as_table()) {
(void)fignored;
if (std::ranges::find(kKnownFeatureKeys, fkey)
!= std::end(kKnownFeatureKeys)) continue;
// `deps` is named apart because it is RESERVED rather than
// wrong: the comment above this block has promised it since
// Feature System v2 and nothing reads it yet. Saying
// "unsupported" would deny a documented plan; saying nothing
// is what let it look implemented.
if (fkey == "deps") {
// ⚠️ THE SPELLING NAMED HERE HAS TO EXIST. The first
// draft of this message offered `optional = true`,
// which mcpp has never had — a diagnostic that sends
// its reader to a key the parser does not know is the
// same defect as the warnings this release removes,
// just one layer out. `[feature-deps.<name>]` is the
// documented mechanism (docs/05 §2.8.2).
m.schemaWarnings.push_back(std::format(
"[features].{}.deps is reserved for a later stage "
"and is not read yet (ignored). To pull in a "
"dependency when this feature is active, declare it "
"under [feature-deps.{}].", fname, fname));
continue;
}
std::string supported;
for (auto k : kKnownFeatureKeys) {
if (!supported.empty()) supported += ", ";
supported += k;
}
m.schemaWarnings.push_back(std::format(
"[features].{} has unsupported key '{}' (ignored). "
"Supported keys: {}. A feature contributes build INPUTS "
"through `sources`, `defines` and `flags`; include "
"directories and compiler flags belong to [build] or to "
"a `flags` entry, not directly to the feature.",
fname, fkey, supported));
}
}
}
}
// [package] provides — package-level capabilities (Feature System v2 S3).
//
// Two populations share this array, and only one of them is mcpp's. Names
// under the reserved `mcpp:` prefix are target-side layers the engine
// resolves and acts on, so they are a closed set and a misspelling is an
// error here. Every other name belongs to the packages themselves — the
// feature system matches `requires` against `provides` without the engine
// having an opinion — so those pass through untouched.
//
// Validating the whole array instead would reject `freestanding-allocator`,
// which already ships. Validating none of it is what shipped until now, and
// its cost is that a single wrong letter in a layer name disables the
// behaviour it was meant to select while the build still reports success.
if (auto v = doc->get_string_array("package.provides")) {
for (auto const& entry : *v)
if (auto cap = mcpp::targetside::parse_capability(entry); !cap)
m.unknownCapabilities.push_back(entry);
m.provides = *v;
}
// [package] requires — validated exactly like `provides`: names under the
// reserved prefix are a closed set, everything else passes through.
if (auto v = doc->get_string_array("package.requires")) {
for (auto const& entry : *v)
if (auto cap = mcpp::targetside::parse_capability(entry); !cap)
m.unknownCapabilities.push_back(entry);
m.requires_ = *v;
}
// std-module / std-compat-module / std-module-flags.
//
// ⚠️ THEY BELONG UNDER `[build]`, AND `[package]` IS THE OLDER SPELLING.
// The module source is one of this package's translation units in every way
// that matters: it is compiled with the package's include directories and
// its definitions, and it is a `.cppm` file like any other. Keeping it in
// `[package]` cost the one thing that placement decides — `[build]` is
// conditional and `[package]` is not, so a package supporting several C
// libraries could not vary the flags its std module needs. `-D_GNU_SOURCE`
// is right for musl and glibc and wrong for picolibc, and there was no
// spelling for that.
//
// Read `[package]` first so `[build]` wins, and so a manifest carrying both
// during the transition behaves the way its author would expect.
if (auto v = doc->get_string("package.std-module")) m.stdModule = *v;
if (auto v = doc->get_string("package.std-compat-module"))
m.stdCompatModule = *v;
if (auto v = doc->get_string_array("package.std-module-flags"))
m.buildConfig.stdModuleFlags = *v;
if (auto v = doc->get_string("build.std-module")) m.stdModule = *v;
if (auto v = doc->get_string("build.std-compat-module"))
m.stdCompatModule = *v;
if (auto v = doc->get_string_array("build.std-module-flags"))
m.buildConfig.stdModuleFlags = *v;
// [capabilities] cap = "provider" — root-only provider pins.
if (auto* caps = doc->get_table("capabilities"); caps && !caps->empty()) {
for (auto& [cap, cval] : *caps)
if (cval.is_string()) m.capabilityPins[cap] = cval.as_string();
}
// [tools.overrides] "<pkg>:<tool>" = "<path>" — #355 escape hatch. Use an
// existing host binary instead of building the dependency's tool target.
// Root-only, like [capabilities]: it is the consumer's environment being
// described, and a dependency has no business overriding it.
if (auto* tovr = doc->get_table("tools.overrides"); tovr && !tovr->empty()) {
for (auto& [k, v] : *tovr)
if (v.is_string()) m.toolOverrides[k] = v.as_string();
}
// [generated_files] — "relative/path" = "file contents" (multiline
// strings supported). Same mechanism as the index descriptor's
// generated_files key: materialized into the package root before glob
// expansion, content folded into the package fingerprint. Paths are
// validated again at materialize time; checking here gives the error a
// manifest location.
if (auto* gf = doc->get_table("generated_files"); gf && !gf->empty()) {
for (auto& [rel, val] : *gf) {
if (!val.is_string()) {
return std::unexpected(error(origin, std::format(
"[generated_files].\"{}\" must be a string (file contents)", rel)));
}
std::filesystem::path p(rel);
// has_root_path, not is_absolute: on Windows "/x" is root-relative
// (not absolute) yet still escapes the project root.
bool escapes = rel.empty() || p.has_root_path();
// const&: libc++'s path iterator dereferences to a temporary
// path (libstdc++ hands out a reference) — auto& won't bind.
for (auto const& part : p.lexically_normal())
if (part == "..") { escapes = true; break; }
if (escapes) {
return std::unexpected(error(origin, std::format(
"[generated_files] path '{}' must be relative and stay "
"inside the project root", rel)));
}
m.buildConfig.generatedFiles.emplace(std::move(p), val.as_string());
}
}
// [scan_overrides."<glob>"] — author-asserted scan results (see
// manifest:types ScanOverride). provides/imports are string arrays.
if (auto* so_table = doc->get_table("scan_overrides");
so_table && !so_table->empty()) {
for (auto& [glob, val] : *so_table) {
if (!val.is_table()) {
return std::unexpected(error(origin,
std::format("[scan_overrides.\"{}\"] must be a table", glob)));
}
manifest::ScanOverride ov;
auto& st = val.as_table();
auto read_names = [&](const char* key, std::vector<std::string>& out)
-> std::optional<std::string> {
auto it = st.find(key);
if (it == st.end()) return std::nullopt;
if (!it->second.is_array())
return std::format("scan_overrides.\"{}\".{} must be an array", glob, key);
for (auto& v : it->second.as_array()) {
if (!v.is_string() || v.as_string().empty())
return std::format("scan_overrides.\"{}\".{} entries must be non-empty strings", glob, key);
out.push_back(v.as_string());
}
return std::nullopt;
};
if (auto msg = read_names("provides", ov.provides))
return std::unexpected(error(origin, *msg));
if (auto msg = read_names("imports", ov.imports))
return std::unexpected(error(origin, *msg));
if (ov.provides.empty() && ov.imports.empty()) {
return std::unexpected(error(origin, std::format(
"scan_overrides.\"{}\" declares neither provides nor imports", glob)));
}
m.modules.scanOverrides.emplace(glob, std::move(ov));
}
}
auto* targets_table = doc->get_table("targets");
if (targets_table && !targets_table->empty()) {
for (auto& [tname, tval] : *targets_table) {
if (!tval.is_table()) {
return std::unexpected(error(origin,
std::format("[targets.{}] must be a table", tname)));
}
Target t;
t.name = tname;
auto& tt = tval.as_table();
auto kit = tt.find("kind");
if (kit == tt.end() || !kit->second.is_string()) {
return std::unexpected(error(origin,
std::format("targets.{}.kind missing or not a string", tname)));
}
const auto& kind_s = kit->second.as_string();
if (kind_s == "lib" || kind_s == "library") t.kind = Target::Library;
else if (kind_s == "bin" || kind_s == "binary") t.kind = Target::Binary;
else if (kind_s == "shared" || kind_s == "dylib"
|| kind_s == "so" || kind_s == "shlib") t.kind = Target::SharedLibrary;
else return std::unexpected(error(origin,
std::format("targets.{}.kind must be 'bin', 'lib' or 'shared'; got '{}'", tname, kind_s)));
if (t.kind == Target::Binary) {
auto mit = tt.find("main");
if (mit == tt.end() || !mit->second.is_string()) {
return std::unexpected(error(origin,
std::format("targets.{} (kind=bin) requires 'main' field", tname)));
}
t.main = mit->second.as_string();
}
if (auto sit = tt.find("soname"); sit != tt.end()) {
if (!sit->second.is_string()) {
return std::unexpected(error(origin,
std::format("targets.{}.soname must be a string", tname)));
}
t.soname = sit->second.as_string();
}
if (auto msg = validate_target_soname(t, std::format("targets.{}.", tname))) {
return std::unexpected(error(origin, *msg));
}
// Per-target flags (entry-scoped) + required-features gate.
auto read_list = [&](const char* key, std::vector<std::string>& out) {
if (auto it = tt.find(key); it != tt.end() && it->second.is_array())
for (auto& v : it->second.as_array())
if (v.is_string()) out.push_back(v.as_string());
};
read_list("cflags", t.cflags);
read_list("cxxflags", t.cxxflags);
read_list("defines", t.defines);
read_list("required_features", t.requiredFeatures);
// Guard: -std=... belongs to [package].standard, not per-target flags
// (same rule as [build].cxxflags). Reject early with a clear message.
for (auto const& flag : t.cxxflags) {
if (starts_with_std_flag(flag)) {
return std::unexpected(error(origin, std::format(
"targets.{}.cxxflags contains '{}'; use [package].standard to "
"configure the C++ language standard", tname, flag)));
}
}
// Surface unsupported keys instead of silently dropping them — the
// historic footgun behind issue #131 (a `[targets.x] cxxflags` typo on
// an older mcpp just vanished). Per-target arbitrary build config that
// must reach SHARED code is intentionally not a target key; point users
// at the right axis (workspace / features / profile).
static constexpr std::string_view kKnownTargetKeys[] = {
"kind", "main", "soname",
"cflags", "cxxflags", "defines", "required_features",
};
for (auto& [key, _] : tt) {
bool known = false;
for (auto k : kKnownTargetKeys) if (key == k) { known = true; break; }
if (!known) {
m.schemaWarnings.push_back(std::format(
"[targets.{}] has unsupported key '{}' (ignored). Per-target keys: "
"kind, main, soname, cflags, cxxflags, defines, required_features. "
"For config that must affect shared code, split into a workspace "
"member or use [features]; for a whole-build mode use [profile.*].",
tname, key));
}
}
m.targets.push_back(std::move(t));
}
} // close `if (targets_table && !targets_table->empty())`
// [dependencies] / [dev-dependencies]
//
// Three accepted forms (M5.x):
//
// (1) flat / default-ns
// [dependencies]
// gtest = "1.15.2" ⇒ (mcpp, gtest)
// frob = { path = "..." } ⇒ (mcpp, frob) inline spec
//
// (2) namespaced subtable (TOML-native, no quotes)
// [dependencies.mcpplibs]
// cmdline = "0.0.2" ⇒ (mcpplibs, cmdline)
// tmpl = { version = "0.0.1", features = [...] }
//
// (3) legacy quoted dotted form (deprecated, still parsed)
// [dependencies]
// "mcpplibs.cmdline" = "0.0.2" ⇒ (mcpplibs, cmdline) + warning
//
// The map key remains the fully-qualified `<ns>.<name>` for non-default
// namespaces (so existing fetcher / lockfile lookups by composite name
// keep working) and the bare `<name>` for the default namespace (so the
// common case stays unchanged).
// MUST list every key `fill_inline_spec` below reads.
// `Manifest.EveryDependencySpecKeyIsAccepted` holds the two in sync.
auto is_dep_spec_key = [](std::string_view k) {
return k == "path" || k == "version" || k == "git"
|| k == "rev" || k == "tag" || k == "branch"
|| k == "features" || k == "default-features"
|| k == "workspace" || k == "visibility"
|| k == "backend" || k == "tools"
|| k == "host-module" || k == "reexport"
|| k == "linkage";
};
// What makes a table an inline dep spec is that it names a SOURCE. This
// used to be "every key is known", which quietly coupled two unrelated
// things: the discriminator (spec vs nested namespace table) and the
// vocabulary (which keys mean something).
//
// The coupling is a compatibility hazard, not a style problem. A manifest
// using a key introduced after the reader was built did not get "unknown
// option" — the table failed the discriminator, was taken for a NAMESPACE,
// and the user was told their `reexport = true` "must be a string, inline
// dep table, or nested table". Worse, a published package cannot adopt a
// new key at all, because every older client fails to load it outright
// rather than ignoring what it does not understand. That is the same
// property #349 established for the index floor: data must not be able to
// decide whether the program works.
//
// An identity key is an unambiguous discriminator: a nested namespace
// table's keys are PACKAGE names, and no package is named `version` /
// `path` / `git` / `workspace`.
auto looks_like_inline_dep_spec = [](const t::Table& sub) {
if (sub.empty()) return false;
for (auto& [sk, sv] : sub)
if (sk == "path" || sk == "version" || sk == "git" || sk == "workspace")