Repository navigation
Expand file tree
/
Copy pathproject.cppm
More file actions
1192 lines (1137 loc) · 60.8 KB
/
Copy pathproject.cppm
File metadata and controls
1192 lines (1137 loc) · 60.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// mcpp.project — project/workspace location + workspace-dependency merging.
//
// Shared by the CLI layer and the pm subsystem (which previously kept a
// private copy of find_manifest_root to avoid importing mcpp.cli).
// Bodies moved verbatim from the CLI layer. Zero behavior change.
module;
#include <cstdio>
#include <cstdlib>
export module mcpp.project;
import std;
import mcpp.manifest;
import mcpp.ui;
import mcpp.diag;
namespace mcpp::project {
// Locate mcpp.toml by walking upward from cwd.
export std::optional<std::filesystem::path> find_manifest_root(std::filesystem::path start) {
auto p = std::filesystem::absolute(start);
while (true) {
if (std::filesystem::exists(p / "mcpp.toml")) return p;
auto parent = p.parent_path();
if (parent == p) return std::nullopt;
p = parent;
}
}
// Find the workspace root by walking upward from a member directory.
// Returns empty if no workspace root found.
export std::filesystem::path find_workspace_root(const std::filesystem::path& memberRoot) {
auto p = memberRoot.parent_path();
while (true) {
if (std::filesystem::exists(p / "mcpp.toml")) {
auto m = mcpp::manifest::load(p / "mcpp.toml");
if (m && m->workspace.present) {
// Verify memberRoot is in members list
auto rel = std::filesystem::relative(memberRoot, p);
for (auto& member : m->workspace.members) {
if (rel == std::filesystem::path(member)) return p;
}
}
}
auto parent = p.parent_path();
if (parent == p) break;
p = parent;
}
return {};
}
// Merge workspace.dependencies into a member's deps (`x.workspace = true`).
//
// #224: this used to propagate only `version`, so a workspace-level
// `[workspace.dependencies] x = { path = "..." }` inherited by a member was
// silently treated as a version/index dep (empty version) and failed to
// resolve. Now the location fields (path/git/*) travel too — a dep spec is
// one of {version, path, git} so copying whichever the workspace declared
// is correct.
//
// `wsRoot` anchors a relative `path`: the workspace author wrote it
// relative to the WORKSPACE ROOT (where `[workspace.dependencies]` lives),
// not the inheriting member's own directory, so it is resolved to an
// absolute path here — downstream path-dep resolution (relative to the
// member root) then sees an already-absolute path and leaves it alone.
export void merge_workspace_deps(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot = {}) {
auto copy_from = [&](mcpp::manifest::DependencySpec& spec,
const mcpp::manifest::DependencySpec& wsSpec) {
spec.version = wsSpec.version;
spec.path = wsSpec.path;
spec.git = wsSpec.git;
spec.gitRev = wsSpec.gitRev;
spec.gitRefKind = wsSpec.gitRefKind;
// The features every member asks of it come first and a member's own
// follow; `default-features` is the member's when it states one
// (Cargo's rule). They were not copied, so a feature written in
// [workspace.dependencies] was dropped without a word (mcpp#797, W2).
std::vector<std::string> features = wsSpec.features;
for (auto const& f : spec.features)
if (std::ranges::find(features, f) == features.end()) features.push_back(f);
spec.features = std::move(features);
if (!spec.defaultFeaturesDeclared && wsSpec.defaultFeaturesDeclared) {
spec.defaultFeatures = wsSpec.defaultFeatures;
spec.defaultFeaturesDeclared = true;
}
if (!spec.path.empty() && !wsRoot.empty()) {
std::filesystem::path p(spec.path);
if (p.is_relative()) {
spec.path = std::filesystem::weakly_canonical(wsRoot / p).string();
}
}
spec.inheritWorkspace = false;
};
auto merge_map = [&](std::map<std::string, mcpp::manifest::DependencySpec>& deps) {
for (auto& [name, spec] : deps) {
if (!spec.inheritWorkspace) continue;
// Try exact key match first
auto it = workspace.workspace.dependencies.find(name);
if (it != workspace.workspace.dependencies.end()) {
copy_from(spec, it->second);
continue;
}
// Try short name for default-ns deps
auto shortIt = workspace.workspace.dependencies.find(spec.shortName);
if (shortIt != workspace.workspace.dependencies.end()) {
copy_from(spec, shortIt->second);
}
}
};
merge_map(member.dependencies);
merge_map(member.devDependencies);
merge_map(member.buildDependencies);
}
// Inherit the workspace root's `[indices]` when the member declares none.
// A relative `[indices].path` was written at the WORKSPACE ROOT, so it must
// resolve against `wsRoot` and not the member directory — otherwise every
// member needs its own `../`-prefixed copy of the same declaration (#224).
//
// Shared so every reader of `[indices]` sees the same effective map: the build
// path resolves dependencies through it, and `mcpp add` decides whether a
// package exists through it. Two copies of this rule is how the two ended up
// disagreeing about which packages are real.
export void inherit_workspace_indices(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot);
// Is `candidate` one of this workspace's declared members?
//
// The membership test is the workspace's OWN `members` list resolved against
// the workspace root — not "is this path under the workspace directory". A
// `path` dependency can live inside the tree without being a member (a vendored
// copy, an example, a scratch package), and a member's flags are exactly what
// it must not acquire.
export bool is_workspace_member(const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot,
const std::filesystem::path& candidate) {
if (!workspace.workspace.present) return false;
std::error_code ec;
auto want = std::filesystem::weakly_canonical(candidate, ec);
if (ec) { ec.clear(); want = candidate.lexically_normal(); }
for (auto const& m : workspace.workspace.members) {
auto member = std::filesystem::weakly_canonical(wsRoot / m, ec);
if (ec) { ec.clear(); member = (wsRoot / m).lexically_normal(); }
if (member == want) return true;
}
return false;
}
export void inherit_workspace_build(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot);
// The `[workspace.package]` half on its own — metadata, no paths, so no anchor
// argument. Second caller: a member reached as a sibling's `path` dependency,
// which may legally omit `version` because this table supplies it.
export void inherit_workspace_package(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace) {
const auto& inh = workspace.workspace.inherited;
// `standardDeclared` and not `standard != "c++23"`: a member that
// deliberately pins c++23 under a c++26 workspace must keep it, and that is
// indistinguishable from the default without the bit.
if (inh.standardDeclared && !member.package.standardDeclared) {
member.package.standard = inh.standard;
member.language.standard = inh.standard;
member.package.standardDeclared = true;
// `cppStandard` was normalised by the parser from the member's own
// value; it has to be re-derived, or the inherited spelling would sit
// in `package.standard` while every build surface kept reading the
// default out of the normalised copy.
if (auto cfg = mcpp::manifest::normalize_cpp_standard(inh.standard))
member.cppStandard = *cfg;
}
if (member.package.version.empty()) member.package.version = inh.version;
if (member.package.license.empty()) member.package.license = inh.license;
if (member.package.description.empty()) member.package.description = inh.description;
if (member.package.repo.empty()) member.package.repo = inh.repo;
if (member.package.authors.empty()) member.package.authors = inh.authors;
if (member.package.mcppFloor.empty()) member.package.mcppFloor = inh.mcppFloor;
}
// ─── The workspace layer and the position reading (SPEC-004 §9.10) ──────────
//
// A workspace root speaks to its members through `[workspace.X]` (the layer,
// `WorkspaceConfig::layer`) and nothing else (W1). The merge rule is the key's,
// not the table's (W3): a scalar the member declared is the member's, a list
// appends with the workspace first, a named table (`profile.<name>`,
// `target.<triple>`) is merged key by key, conditional rows are inherited row
// by row ahead of the member's own, and a relative path is anchored at the
// manifest that wrote it (#224).
//
// The root's own `[toolchain]`, `[indices]`, `[profile.*]`, `[target.*]` and
// `[xlings]` still reach members by position where they have no
// `[workspace.X]` (mcpp.pm.compat.workspace_position), with the old merge and
// a warning.
namespace detail {
bool lists(std::span<const std::string> names, std::string_view name) {
return std::ranges::find(names, name) != names.end();
}
// The position reading was used for `table`. A root without `[package]` has
// no other reading of these tables, so every use is reported; a root with one
// reads them as its own package's first, and only a member that actually
// received a value through the old rule is told.
//
// `report` is false where the value received changes nothing in the build: a
// member reached as a dependency of a build whose root is in the same
// workspace, which received the same entries itself (mcpp#797, R5). The use
// is still recorded for `mcpp why compat`.
void warn_position(const mcpp::manifest::Manifest& workspace,
const mcpp::manifest::Manifest& member,
std::string_view table, bool received, bool report = true) {
const bool virtualRoot = workspace.package.name.empty();
if (!virtualRoot && !received) return;
const auto text = mcpp::pm::compat::position_warning(
table, virtualRoot ? std::string_view{} : member.package.name);
mcpp::pm::compat::record_use("workspace-position", workspace.sourcePath.string(),
std::format("[{}] -> {}", table,
virtualRoot ? std::string("every member")
: member.package.name),
report);
if (report) mcpp::diag::warning("manifest/workspace-position", text);
}
// `[xlings]` entries and `[target.<sel>.xlings]` rows from `top` and `rows`,
// for a member. The workspace's entries come first and a member's own
// declaration of the same package wins, the "nearer the artifact" rule of
// SPEC-004 §4.5; identity is `(namespace, name)`. Feature-gated entries do
// not travel: a feature belongs to the package that declares it. Neither does
// the emitter's per-platform view (`workspaceByPlatform`), so a published
// member's descriptor states only what the member itself declared. Returns
// whether the member received anything.
//
// TWO PARTS, TWO POSITIONS (mcpp#797, R4). The entries say which payloads a
// package needs and travel in every position (W5). `[xlings.overrides]` says
// where a payload comes from on THIS machine: it belongs to the root of a
// build, and a dependency stating one is refused (`dependency_override_
// refusal`). Copying the workspace's overrides into a member reached as a
// dependency made a workspace's own statement fail its own build.
enum class XlingsPart { Entries, Overrides };
bool merge_xlings(mcpp::manifest::Manifest& member,
const mcpp::manifest::XlingsConfig* top,
std::span<const mcpp::manifest::ConditionalConfig> rows,
XlingsPart part) {
// `(namespace, name)`, the identity `mcpp.provider.address_set` defines,
// spelled here from the same parser rather than imported. Importing that
// module here makes GCC 16.1 fail with an internal compiler error
// (segmentation fault) at `import mcpp.cli;` in src/main.cpp. Measured.
auto package_key = [](std::string_view address) {
const auto e = mcpp::manifest::parse_address(address);
return (e.ns.empty() ? std::string("xim") : e.ns) + ":" + e.target;
};
std::set<std::string> own;
for (auto const& a : member.xlings.deps) own.insert(package_key(a));
for (auto const& cc : member.conditionalConfigs)
for (auto const& a : cc.xlings.deps) own.insert(package_key(a));
bool received = false;
// Copies the entries of `from` whose package the member does not declare,
// with the pin and the tier each address carries.
auto take = [&](const mcpp::manifest::XlingsConfig& from,
mcpp::manifest::XlingsConfig& to) {
if (part == XlingsPart::Overrides) {
// `[xlings.overrides]` states where a payload comes from on THIS
// machine, so a member built as the root of its own build takes
// the workspace's statement unless it makes its own (mcpp#755).
for (auto const& [pkg, o] : from.overrides)
if (to.overrides.try_emplace(pkg, o).second) received = true;
return;
}
std::vector<std::string> taken;
for (auto const& a : from.deps) {
if (own.contains(package_key(a))) continue;
taken.push_back(a);
const auto target = mcpp::manifest::parse_address(a).target;
if (auto pin = from.workspace.find(target); pin != from.workspace.end())
to.workspace.try_emplace(pin->first, pin->second);
if (auto w = from.depWhen.find(a); w != from.depWhen.end())
to.depWhen.try_emplace(a, w->second);
if (from.onRequest.contains(a)) to.onRequest.insert(a);
}
received = received || !taken.empty();
to.deps.insert(to.deps.begin(), taken.begin(), taken.end());
};
if (top) take(*top, member.xlings);
std::vector<mcpp::manifest::ConditionalConfig> taken;
for (auto const& cc : rows) {
if (cc.xlings.deps.empty() && cc.xlings.overrides.empty()) continue;
mcpp::manifest::ConditionalConfig row;
row.predicate = cc.predicate;
take(cc.xlings, row.xlings);
if (!row.xlings.deps.empty() || !row.xlings.overrides.empty())
taken.push_back(std::move(row));
}
member.conditionalConfigs.insert(member.conditionalConfigs.begin(),
std::make_move_iterator(taken.begin()),
std::make_move_iterator(taken.end()));
return received;
}
// W3 for the tables only a build's root reads: `[workspace.toolchain]`,
// `[workspace.indices]`, `[workspace.profile.<name>]` and the scalar rows of
// `[workspace.target.<triple>]`.
void merge_layer_root_position(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& layer,
const std::filesystem::path& wsRoot,
const std::filesystem::path& wsManifest) {
// A platform's entry: the member's own for that platform, else the
// member's own `default`, else the workspace's. The member's `default`
// is a statement about every platform, so it outranks the workspace's
// platform entries too.
auto& tc = member.toolchain;
if (!tc.byPlatform.contains("default")) {
for (auto const& [platform, spec] : layer.toolchain.byPlatform) {
if (tc.byPlatform.contains(platform)) continue;
tc.byPlatform[platform] = spec;
if (auto it = layer.toolchain.localByPlatform.find(platform);
it != layer.toolchain.localByPlatform.end())
tc.localByPlatform[platform] = it->second;
if (auto it = layer.toolchain.lineByPlatform.find(platform);
it != layer.toolchain.lineByPlatform.end())
tc.lineByPlatform[platform] = it->second;
tc.fileByPlatform[platform] = wsManifest;
}
}
if (tc.bootstrap.empty() && !layer.toolchain.bootstrap.empty()) {
tc.bootstrap = layer.toolchain.bootstrap;
tc.bootstrapLine = layer.toolchain.bootstrapLine;
}
for (auto const& [name, idx] : layer.indices) {
auto [it, inserted] = member.indices.try_emplace(name, idx);
if (inserted && it->second.is_local() && it->second.path.is_relative())
it->second.path = (wsRoot / it->second.path).lexically_normal();
}
for (auto const& [name, from] : layer.profiles) {
auto [it, inserted] = member.profiles.try_emplace(name, from);
if (inserted) continue;
auto& to = it->second;
if (!to.optDeclared && from.optDeclared) { to.optLevel = from.optLevel; to.optDeclared = true; }
if (!to.debugDeclared && from.debugDeclared) { to.debug = from.debug; to.debugDeclared = true; }
if (!to.ltoDeclared && from.ltoDeclared) { to.lto = from.lto; to.ltoDeclared = true; }
if (!to.stripDeclared && from.stripDeclared) { to.strip = from.strip; to.stripDeclared = true; }
if (!to.dependencyLinkageDeclared && from.dependencyLinkageDeclared) {
to.dependencyLinkage = from.dependencyLinkage;
to.dependencyLinkageDeclared = true;
}
to.cflags.insert(to.cflags.begin(), from.cflags.begin(), from.cflags.end());
to.cxxflags.insert(to.cxxflags.begin(), from.cxxflags.begin(), from.cxxflags.end());
to.ldflags.insert(to.ldflags.begin(), from.ldflags.begin(), from.ldflags.end());
}
for (auto const& [triple, from] : layer.targetOverrides) {
auto [it, inserted] = member.targetOverrides.try_emplace(triple, from);
if (inserted) continue;
auto& to = it->second;
if (to.toolchain.empty()) to.toolchain = from.toolchain;
if (to.linkage.empty()) to.linkage = from.linkage;
if (to.cxxRuntime.empty()) to.cxxRuntime = from.cxxRuntime;
if (!to.sysrootDeclared && from.sysrootDeclared) {
to.sysroot = from.sysroot;
to.sysrootDeclared = true;
}
if (to.minApiLevel == 0) to.minApiLevel = from.minApiLevel;
if (to.runner.empty()) to.runner = from.runner;
for (auto const& [n, argv] : from.namedRunners) to.namedRunners.try_emplace(n, argv);
}
}
} // namespace detail
// The workspace's `[xlings]` entries, for a member, at every position it is
// reached (W5): selected, a sibling's `path` dependency, a member of a
// git-hosted workspace, a host tool's sub-build root (#713).
//
// An xlings entry describes the environment a build runs in, so it is
// inherited without an opt-in; only dependencies, which are graph edges, need
// `.workspace = true` (`merge_workspace_deps`).
//
// The entries only: `[xlings.overrides]` is a root-position table
// (`inherit_workspace_xlings_overrides`). `reportPosition` is false for a
// member reached as a dependency of a build rooted in the same workspace, whose
// root already received these entries (R5).
export void inherit_workspace_xlings(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
bool reportPosition = true) {
static const std::vector<mcpp::manifest::ConditionalConfig> none;
using detail::XlingsPart;
if (auto const* layer = workspace.layer.get()) {
auto const& tables = workspace.workspace.layerTables;
detail::merge_xlings(member,
detail::lists(tables, "xlings") ? &layer->xlings : nullptr,
detail::lists(tables, "target") ? layer->conditionalConfigs : none,
XlingsPart::Entries);
}
// COMPAT(workspace-position): the root's own [xlings] and [target.<sel>.xlings].
const auto position = mcpp::pm::compat::position_tables(workspace);
const bool top = detail::lists(position, "xlings");
const bool rows = detail::lists(position, "target");
if (!top && !rows) return;
const bool received = detail::merge_xlings(member, top ? &workspace.xlings : nullptr,
rows ? workspace.conditionalConfigs : none,
XlingsPart::Entries);
if (top && !workspace.xlings.deps.empty())
detail::warn_position(workspace, member, "xlings", received, reportPosition);
}
// `[workspace.xlings.overrides]` and `[workspace.target.<sel>.xlings.overrides]`,
// and by position the root's own `[xlings.overrides]`, for a member that is
// the root of a build (mcpp#797, R4).
export void inherit_workspace_xlings_overrides(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace) {
static const std::vector<mcpp::manifest::ConditionalConfig> none;
using detail::XlingsPart;
if (auto const* layer = workspace.layer.get()) {
auto const& tables = workspace.workspace.layerTables;
detail::merge_xlings(member,
detail::lists(tables, "xlings") ? &layer->xlings : nullptr,
detail::lists(tables, "target") ? layer->conditionalConfigs : none,
XlingsPart::Overrides);
}
// COMPAT(workspace-position)
const auto position = mcpp::pm::compat::position_tables(workspace);
const bool top = detail::lists(position, "xlings");
const bool rows = detail::lists(position, "target");
if (!top && !rows) return;
const bool received = detail::merge_xlings(member, top ? &workspace.xlings : nullptr,
rows ? workspace.conditionalConfigs : none,
XlingsPart::Overrides);
if (top && !workspace.xlings.overrides.empty())
detail::warn_position(workspace, member, "xlings.overrides", received);
}
// The rows of `[workspace.target.<sel>]` -- `.build`, `.abi`, `.runtime` --
// for a member, at every position it is reached (W5), ahead of the member's
// own rows (W3). Their `.xlings` part is `inherit_workspace_xlings`'s.
// A relative path in them was written against the workspace root (#224).
export void inherit_workspace_layer_rows(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
auto const* layer = workspace.layer.get();
if (!layer || layer->conditionalConfigs.empty()) return;
auto anchor = [&](std::vector<std::filesystem::path>& dirs) {
for (auto& d : dirs)
if (d.is_relative()) d = (wsRoot / d).lexically_normal();
};
std::vector<mcpp::manifest::ConditionalConfig> rows;
for (auto cc : layer->conditionalConfigs) {
cc.xlings = {};
anchor(cc.inputs.includeDirs);
anchor(cc.inputs.includeDirsAfter);
anchor(cc.inputs.privateIncludeDirs);
anchor(cc.linkLibraryDirs);
rows.push_back(std::move(cc));
}
member.conditionalConfigs.insert(member.conditionalConfigs.begin(),
std::make_move_iterator(rows.begin()),
std::make_move_iterator(rows.end()));
}
// The tables only the root of a build reads -- `toolchain`, `indices`,
// `profile.<name>`, the scalar rows of `target.<triple>` -- for a member that
// is the root of one: selected by a command, or the root of a host tool's
// sub-build (#710). A member reached as somebody's dependency takes them from
// that build's root instead.
export void inherit_workspace_root_position(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
if (auto const* layer = workspace.layer.get())
detail::merge_layer_root_position(member, *layer, wsRoot, workspace.sourcePath);
inherit_workspace_xlings_overrides(member, workspace);
// COMPAT(workspace-position)
const auto position = mcpp::pm::compat::position_tables(workspace);
if (position.empty()) return;
const auto received = mcpp::pm::compat::inherit_root_position_by_position(
member, workspace, wsRoot, position);
for (auto const& table : position) {
if (table == "xlings") continue;
detail::warn_position(workspace, member, table, detail::lists(received, table));
}
}
// The effective `[indices]` of a member, for a reader that needs only them
// (`mcpp add`, the index router): `[workspace.indices]` by name (W3), and the
// root's own `[indices]` by position where the root has no
// `[workspace.indices]` (COMPAT(workspace-position)). A relative `path` was
// written at the workspace root and is anchored there (#224).
void inherit_workspace_indices(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
if (auto const* layer = workspace.layer.get())
for (auto const& [name, idx] : layer->indices) {
auto [it, inserted] = member.indices.try_emplace(name, idx);
if (inserted && it->second.is_local() && it->second.path.is_relative())
it->second.path = (wsRoot / it->second.path).lexically_normal();
}
const std::vector<std::string> indices{"indices"};
if (!detail::lists(mcpp::pm::compat::position_tables(workspace), "indices")) return;
const auto received = mcpp::pm::compat::inherit_root_position_by_position(
member, workspace, wsRoot, indices);
detail::warn_position(workspace, member, "indices", !received.empty());
}
// THE ROOT PACKAGE OF A WORKSPACE IS ONE OF ITS MEMBERS.
//
// A root that carries `[package]` beside `[workspace]` is selected as member
// `"."`, and it receives `[workspace.package]`, `[workspace.build]` and the
// workspace layer once, as every other member does, so that one manifest
// compiles the same way in every selection. Its own `[toolchain]`,
// `[target.*]`, ... are its own declarations and come first; its
// `[workspace.dependencies]` are merged explicitly by the caller.
//
// `asBuildRoot` is false when the package is reached as a dependency of a
// member's build: it then takes the workspace's entries and not its
// `[xlings.overrides]`, which belong to the build's root (R4).
export void inherit_as_root_package(mcpp::manifest::Manifest& root,
const std::filesystem::path& wsRoot,
bool asBuildRoot = true) {
if (!root.workspace.present || root.package.name.empty()) return;
if (root.inheritedAsRootPackage) return;
root.inheritedAsRootPackage = true;
const auto workspace = root;
inherit_workspace_package(root, workspace);
inherit_workspace_build(root, workspace, wsRoot);
if (auto const* layer = workspace.layer.get()) {
static const std::vector<mcpp::manifest::ConditionalConfig> none;
auto const& tables = workspace.workspace.layerTables;
detail::merge_layer_root_position(root, *layer, wsRoot, workspace.sourcePath);
inherit_workspace_layer_rows(root, workspace, wsRoot);
detail::merge_xlings(root,
detail::lists(tables, "xlings") ? &layer->xlings : nullptr,
detail::lists(tables, "target") ? layer->conditionalConfigs : none,
detail::XlingsPart::Entries);
if (asBuildRoot)
detail::merge_xlings(root,
detail::lists(tables, "xlings") ? &layer->xlings : nullptr,
detail::lists(tables, "target") ? layer->conditionalConfigs : none,
detail::XlingsPart::Overrides);
}
}
// EVERYTHING A MEMBER INHERITS FROM ITS WORKSPACE ROOT, IN ONE FUNCTION.
//
// There are two inheritance SITES in prepare_build — the command issued at the
// workspace root with `-p <member>`, and the command issued inside a member
// directory — and until this function existed they were two hand-copied lists
// of the same merges. A fifth key added to one of them is a defect that
// compiles, which is exactly how `[build]` came to be inherited by neither
// (#527 Bug 2).
//
// The discipline is stated on `WorkspaceInherited`: scalars are taken when the
// member did not DECLARE the key, vectors append with the workspace first, and
// dependencies keep their explicit `.workspace = true` opt-in because they are
// graph edges. The workspace layer follows the same rule (W3, above).
//
// `wsRoot` anchors relative paths: an `[indices].path` or a
// `[workspace.dependencies] path` was written against the WORKSPACE ROOT, and
// re-anchoring it to the member directory is #224.
export void inherit_workspace_config(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
merge_workspace_deps(member, workspace, wsRoot);
inherit_workspace_root_position(member, workspace, wsRoot);
inherit_workspace_xlings(member, workspace);
inherit_workspace_layer_rows(member, workspace, wsRoot);
// The two halves, each with a second caller of its own: a member reached
// as a sibling.s `path` dependency needs both, at two different points.
// (The "still missing after inheritance" refusal is
// `workspace_inheritance_error`, called by each site.)
inherit_workspace_package(member, workspace);
inherit_workspace_build(member, workspace, wsRoot);
}
// The `[workspace.build]` half on its own.
//
// SEPARATE BECAUSE IT HAS A SECOND CALLER. `inherit_workspace_config` runs for
// the manifest the command names; this runs additionally for every OTHER member
// pulled in as a `path` dependency — which is what workspace members are to each
// other, and therefore the ordinary case rather than an exotic one. Without the
// second call, `mcpp build -p appb` gave `appb` the workspace flags and gave the
// sibling `liba` none, while compiling both in the same command.
//
// `[workspace.package] standard` needs no second call: the standard is imposed
// graph-wide from the root for BMI-compatibility reasons, which is precisely
// why this gap stayed invisible until a `[build]` key became inheritable too.
export void inherit_workspace_build(mcpp::manifest::Manifest& member,
const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot) {
const auto& inh = workspace.workspace.inherited;
if (!inh.buildPresent) return;
auto& b = member.buildConfig;
const auto& w = inh.build;
{
auto prepend = [](auto& dst, const auto& src) {
if (src.empty()) return;
dst.insert(dst.begin(), src.begin(), src.end());
};
prepend(b.cflags, w.cflags);
prepend(b.cxxflags, w.cxxflags);
prepend(b.ldflags, w.ldflags);
prepend(b.defines, w.defines);
prepend(b.dialectCxxflags, w.dialectCxxflags);
// Where each inherited value was written, for the diagnostics that
// name a key (WS3): a word the workspace states is the workspace's.
for (auto const& [key, src] : {
std::pair{"cflags", &w.cflags}, std::pair{"cxxflags", &w.cxxflags},
std::pair{"ldflags", &w.ldflags}, std::pair{"defines", &w.defines},
std::pair{"dialect_cxxflags", &w.dialectCxxflags}}) {
if (src->empty()) continue;
auto& rec = b.inheritedFromWorkspace[key];
rec.insert(rec.end(), src->begin(), src->end());
}
// A RELATIVE INCLUDE DIRECTORY IN THE WORKSPACE MANIFEST WAS WRITTEN
// AGAINST THE WORKSPACE ROOT, and every member would otherwise resolve
// it against its own directory.
//
// This is #224 for a new key: `[indices].path` and
// `[workspace.dependencies] path` are anchored for exactly this reason,
// and a third relative-path key that skipped it would silently point at
// `<member>/shared/inc` for a directory that lives at
// `<workspace>/shared/inc`. The failure is a missing header, three
// members deep, naming neither the manifest that declared it nor the
// root it was declared against.
//
// Anchored rather than refused: an absolute include directory is
// already accepted by `expandIncludeDirs`, so the anchored form needs
// no new handling downstream.
auto anchored = [&](const std::vector<std::filesystem::path>& src) {
std::vector<std::filesystem::path> out;
out.reserve(src.size());
for (auto const& d : src)
out.push_back(d.is_absolute() ? d
: (wsRoot / d).lexically_normal());
return out;
};
prepend(b.includeDirs, anchored(w.includeDirs));
prepend(b.includeDirsAfter, anchored(w.includeDirsAfter));
prepend(b.privateIncludeDirs, anchored(w.privateIncludeDirs));
if (b.cStandard.empty()) b.cStandard = w.cStandard;
if (b.linkage.empty()) b.linkage = w.linkage;
if (b.target.empty()) b.target = w.target;
if (b.cxxRuntime.empty()) b.cxxRuntime = w.cxxRuntime;
if (b.dependencyLinkage.empty()) b.dependencyLinkage = w.dependencyLinkage;
if (b.macosDeploymentTarget.empty())
b.macosDeploymentTarget = w.macosDeploymentTarget;
if (b.iosDeploymentTarget.empty())
b.iosDeploymentTarget = w.iosDeploymentTarget;
}
}
// The required-field check, asked at the one point where it is answerable.
//
// `mcpp.manifest`'s parser cannot enforce `package.name` / `package.version` on
// a workspace member, because a member manifest carries no evidence that it is
// one. Deferring the check is not relaxing it: it runs here, after inheritance,
// and the message can name the workspace table that would have supplied the
// value — which the parser could not have done either.
export std::optional<std::string> workspace_inheritance_error(
const mcpp::manifest::Manifest& member,
const std::filesystem::path& memberDir) {
auto missing = [&](std::string_view field, std::string_view wsKey)
-> std::optional<std::string> {
return std::format(
"{}: missing required field '{}', and the workspace root does not "
"supply it either.\n"
" Declare it in the member, or once for every member:\n"
"\n"
" [workspace.package]\n"
" {} = \"...\"",
(memberDir / "mcpp.toml").string(), field, wsKey);
};
if (member.package.name.empty())
return std::format("{}: missing required field 'package.name'. "
"A workspace cannot supply it: members do not share "
"a name.", (memberDir / "mcpp.toml").string());
if (member.package.version.empty()) return missing("package.version", "version");
return std::nullopt;
}
// A `x.workspace = true` entry that no workspace resolved (#714).
//
// Inheritance replaces the entry with the workspace's declaration; an entry
// still marked afterwards names nothing. It used to fall through as a version
// dependency with an empty version, refused far downstream as "SemVer
// constraint '' ... run `mcpp index update`" -- an instruction about the index
// for a mistake in the manifest. Asked once, after inheritance, at each place a
// manifest enters a build: the root, a member built with `-p`, and every
// dependency load site. The two ways out are the two ways inheritance happens.
export std::optional<std::string>
unresolved_workspace_dependency_error(const mcpp::manifest::Manifest& m,
const std::filesystem::path& manifestDir) {
auto first = [](const std::map<std::string, mcpp::manifest::DependencySpec>& deps)
-> std::optional<std::string> {
for (auto const& [name, spec] : deps)
if (spec.inheritWorkspace) return name;
return std::nullopt;
};
std::optional<std::string> name;
std::string_view table;
if ((name = first(m.dependencies))) table = "dependencies";
else if ((name = first(m.devDependencies))) table = "dev-dependencies";
else if ((name = first(m.buildDependencies))) table = "build-dependencies";
if (!name) return std::nullopt;
return std::format(
"{}: [{}] {} = {{ workspace = true }}, but no workspace declares '{}'.\n"
" `workspace = true` is resolved against the [workspace.dependencies] "
"of the workspace whose `members` list this package.\n"
" fix: list this package in that workspace's [workspace] members, "
"or state the dependency's version, path or git source here.",
(manifestDir / "mcpp.toml").string(), table, *name, *name);
}
// THE EFFECTIVE MANIFEST OF A PROJECT DIRECTORY, FOR EVERY READER OUTSIDE
// `prepare_build`.
//
// `prepare_build` applies workspace inheritance where it loads the manifest a
// command names. Every other command that reads a project manifest (publish,
// pack routing, `emit xpkg`, `toolchain list`) used to call `manifest::load`
// directly and therefore saw the raw file: a member that omits `version`
// because `[workspace.package]` supplies it was refused, and a member without
// `[toolchain]` was reported against the global default while `mcpp build` in
// the same directory resolved the workspace's toolchain (#690, F5a and F6).
//
// The rule is the one `prepare_build` follows: a directory that its workspace
// lists as a member is loaded with `insideWorkspace` and receives
// `inherit_workspace_config` anchored at the workspace root, and the
// required-field check runs after inheritance. A workspace root that carries
// its own `[package]` receives the same inheritance as member `"."`
// (`inherit_as_root_package`). Any other directory is loaded as written.
export struct EffectiveManifest {
mcpp::manifest::Manifest manifest; // after inheritance
std::optional<mcpp::manifest::Manifest> workspace; // set when `member`
std::filesystem::path workspaceRoot; // empty unless `member`
bool member = false;
};
export std::expected<EffectiveManifest, std::string>
load_effective_manifest(const std::filesystem::path& dir) {
const auto manifestPath = dir / "mcpp.toml";
const auto wsRoot = find_workspace_root(std::filesystem::absolute(dir));
if (wsRoot.empty()) {
auto m = mcpp::manifest::load(manifestPath);
if (!m) return std::unexpected(m.error().format());
inherit_as_root_package(*m, std::filesystem::absolute(dir));
return EffectiveManifest{ std::move(*m), std::nullopt, {}, false };
}
auto m = mcpp::manifest::load(manifestPath, {.insideWorkspace = true});
if (!m) return std::unexpected(m.error().format());
auto ws = mcpp::manifest::load(wsRoot / "mcpp.toml");
if (!ws) return std::unexpected(ws.error().format());
inherit_workspace_config(*m, *ws, wsRoot);
if (auto bad = workspace_inheritance_error(*m, dir))
return std::unexpected(*bad);
return EffectiveManifest{ std::move(*m), std::move(*ws), wsRoot, true };
}
// One declared workspace member, with the identity its own manifest states.
//
// #725: `-p, --package <NAME>` promises a package (SPEC-001's `(namespace,
// name)`), so resolving it needs each member's OWN declaration, not only the
// directory `[workspace] members` names it with.
export struct WorkspaceMember {
std::string memberPath; // as written in [workspace] members
std::filesystem::path dir; // rootDir / memberPath
std::string namespace_; // "" when undeclared, or unreadable
std::string name; // "" when the manifest could not be
// read -- such a member still matches
// by directory or path (step 3 below)
};
// A member's qualified name the way SPEC-001 spells one: `<namespace>.<name>`.
// Meaningless (and not attempted by the resolver below) when the member
// declares no namespace -- SPEC-001 §3.1: an unnamespaced package's bare name
// already IS its full identity, so there is no separate dotted spelling to
// match against.
export std::string qualified_member_name(const WorkspaceMember& m) {
return m.namespace_.empty() ? m.name : m.namespace_ + "." + m.name;
}
// Every declared member, with its own package identity. Loaded the way a
// member is always loaded (`insideWorkspace = true`, so an omitted
// `package.version` is not refused here); a member whose manifest fails to
// parse keeps an empty name rather than aborting the listing, because `-p`
// still owes an answer about the members that DO parse.
export std::vector<WorkspaceMember>
workspace_members(const mcpp::manifest::Manifest& rootManifest,
const std::filesystem::path& rootDir) {
std::vector<WorkspaceMember> out;
out.reserve(rootManifest.workspace.members.size());
for (auto& mp : rootManifest.workspace.members) {
WorkspaceMember wm;
wm.memberPath = mp;
wm.dir = rootDir / mp;
if (auto mm = mcpp::manifest::load(wm.dir / "mcpp.toml", {.insideWorkspace = true})) {
wm.namespace_ = mm->package.namespace_;
wm.name = mm->package.name;
}
out.push_back(std::move(wm));
}
return out;
}
// Resolve which member directory a workspace command acts on, for the
// single-member case. Shared by every `-p`/`--package` reader (build, test,
// run, emit, and prepare_build's own root-level switch, #725), so
// `build -p X` and `test -p X` always agree on what X means.
//
// A value is resolved in this order, because the option names a package
// (SPEC-001) and a directory is a fallback spelling docs/07 §5.3 has always
// documented:
// 1. a member's qualified name, `<namespace>.<name>` (only attempted for a
// member that declares a namespace -- see `qualified_member_name`);
// 2. otherwise, a member's bare package name -- refused, naming every
// match's qualified name, when two or more members share it;
// 3. otherwise, a member's path as written in `[workspace] members`, or its
// directory's last segment. Two members sharing a directory basename
// keep today's first-match selection (a script written against it keeps
// working), with a warning naming the others and their paths.
// A value that is one member's package name (step 2) and a different
// member's directory (step 3) selects the step-2 member, with a warning
// naming the other member and its path.
//
// Returns:
// - the member dir when `package_filter` names a member,
// - empty path when no switch applies (not a workspace, or a rooted
// workspace with no filter → act on the root package),
// - error when the filter names no member, names more than one,
// or a *virtual* workspace is addressed with no filter
// (the caller must pick a member with -p or fan out with
// --workspace).
export std::expected<std::filesystem::path, std::string>
resolve_member_dir(const mcpp::manifest::Manifest& rootManifest,
const std::filesystem::path& rootDir,
std::string_view package_filter) {
if (!rootManifest.workspace.present) return std::filesystem::path{};
// "." and the rooted workspace's own package name select that package
// (workspace design 2026-09-29 §7.1): it is a member of every command
// that acts on the whole workspace, and `-p` names it like any other.
if (!rootManifest.package.name.empty()
&& (package_filter == "." || package_filter == rootManifest.package.name
|| (!rootManifest.package.namespace_.empty()
&& package_filter == rootManifest.package.namespace_ + "."
+ rootManifest.package.name)))
return rootDir;
if (package_filter.empty()) {
if (rootManifest.package.name.empty()) {
return std::unexpected(std::string(
"virtual workspace: specify -p <member> or --workspace"));
}
return std::filesystem::path{}; // rooted workspace, no filter → root
}
auto members = workspace_members(rootManifest, rootDir);
// Step 1: the qualified name. Unique by construction (SPEC-001 §3.3 asks
// a single index to keep `(namespace, name)` unique; two workspace
// members sharing one is a manifest defect this resolver does not
// adjudicate), so the first hit is taken without a warning.
for (auto const& m : members)
if (!m.namespace_.empty() && qualified_member_name(m) == package_filter)
return m.dir;
// Step 2: the bare package name, ignoring namespace.
std::vector<std::size_t> byName;
for (std::size_t i = 0; i < members.size(); ++i)
if (!members[i].name.empty() && members[i].name == package_filter)
byName.push_back(i);
if (byName.size() > 1) {
std::string names;
for (auto i : byName)
names += (names.empty() ? "" : ", ") + qualified_member_name(members[i]);
return std::unexpected(std::format(
"-p '{}' is ambiguous: it is the package name of {} members ({}). "
"Write the qualified name (<namespace>.<name>) to select one.",
package_filter, byName.size(), names));
}
// Step 3: the directory path or basename `[workspace] members` writes.
// The path is compared as a path, so `./modules/base/`, and on Windows
// `modules\base`, name the member written `modules/base`.
auto as_path = [](std::string_view s) {
auto p = std::filesystem::path(s).lexically_normal();
return (!p.has_filename() && p.has_parent_path()) ? p.parent_path() : p;
};
const auto filterPath = as_path(package_filter);
std::vector<std::size_t> byPath;
for (std::size_t i = 0; i < members.size(); ++i) {
auto basename = members[i].dir.filename().string();
if (basename == package_filter || as_path(members[i].memberPath) == filterPath)
byPath.push_back(i);
}
if (byName.size() == 1) {
const auto& picked = members[byName.front()];
if (!byPath.empty() && byPath.front() != byName.front()) {
const auto& other = members[byPath.front()];
mcpp::ui::warning(std::format(
"-p '{}' is the package name of member '{}' ({}) and also the "
"directory of member '{}' ({}); the package is selected. Write "
"'{}' to select the other member.",
package_filter, picked.memberPath, qualified_member_name(picked),
other.memberPath, qualified_member_name(other), other.memberPath));
}
return picked.dir;
}
if (!byPath.empty()) {
if (byPath.size() > 1) {
std::string others;
for (std::size_t k = 1; k < byPath.size(); ++k)
others += std::format("{}'{}'", others.empty() ? "" : ", ",
members[byPath[k]].memberPath);
mcpp::ui::warning(std::format(
"-p '{}' matches more than one member's directory; '{}' is "
"selected (the first listed in [workspace] members). Also "
"matched: {}.",
package_filter, members[byPath.front()].memberPath, others));
}
return members[byPath.front()].dir;
}
std::string list;
for (auto const& m : members) {
auto label = m.name.empty() ? std::string("?")
: (m.namespace_.empty() ? m.name
: std::format("{} ({})", m.name,
qualified_member_name(m)));
list += std::format("\n {} at '{}'", label, m.memberPath);
}
return std::unexpected(std::format(
"workspace member '{}' not found. [workspace] members:{}",
package_filter, list));
}
// ─── Workspace plans (workspace design 2026-09-29 §15) ──────────────────────
//
// A command on a workspace selects members and plans them under one root: the
// root package of a rooted workspace when it is selected, and otherwise a
// virtual root that holds the values one plan shares. The functions below are
// shared by the command layer, which groups the selected members into plans,
// and by the planner, which builds each plan's root; both read the same list
// of root-position values, so a group and its root cannot disagree about it.
// A selected member's effective manifest: loaded as a member, with the
// workspace's configuration inherited, exactly as the planner loads the member
// it builds.
export std::expected<mcpp::manifest::Manifest, std::string>
load_member_manifest(const mcpp::manifest::Manifest& workspace,
const std::filesystem::path& wsRoot,
std::string_view memberPath) {
const auto dir = wsRoot / std::string(memberPath);
if (memberPath == ".") {
auto m = workspace;
merge_workspace_deps(m, workspace, wsRoot);
inherit_as_root_package(m, wsRoot);
return m;
}
if (!std::filesystem::exists(dir / "mcpp.toml"))
return std::unexpected(std::format(
"workspace member '{}' has no mcpp.toml", memberPath));
auto mm = mcpp::manifest::load(dir / "mcpp.toml", {.insideWorkspace = true});
if (!mm) return std::unexpected(std::format(
"workspace member '{}': {}", memberPath, mm.error().format()));
// A relative `[indices].path` the member declares was written in the
// member's directory. The plan's root is the workspace root, so the path
// is anchored here, and two members that write the same relative path
// for different trees have different root-position keys.
for (auto& [_, idx] : mm->indices)
if (idx.is_local() && idx.path.is_relative())
idx.path = (dir / idx.path).lexically_normal();
inherit_workspace_config(*mm, workspace, wsRoot);
if (auto bad = workspace_inheritance_error(*mm, dir)) return std::unexpected(*bad);
return std::move(*mm);
}
// The values of a member's manifest that are one value per plan: the
// toolchain request and the target rows, the C++ standard, the graph-wide
// `[build]` keys (among them `linkage`, which chooses the C runtime every
// object is compiled against), the profiles and the indices. Members whose
// keys are equal are planned together; the key is a canonical string of those
// values and nothing else, so a member's own flags, sources and dependencies
// never separate it from another member.
//
// A conditional row's configuration values (`[target.<sel>.build]
// dialect_cxxflags`, `[target.<sel>.abi]`) count as they evaluate for this
// build (W4): `applies` answers whether a row's selector holds for the target
// the members are planned for, and it is the evaluator prepare uses
// (`cfgpred::matches` over `cfgpred::context_for`). Two members whose rows
// differ only on other targets therefore share a plan; without `applies` no
// row counts.