-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathdirectives.cppm
More file actions
1190 lines (1113 loc) · 64.8 KB
/
Copy pathdirectives.cppm
File metadata and controls
1190 lines (1113 loc) · 64.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.build.directives — the ONE definition of what a `build.mcpp` directive is.
//
// WHY THIS MODULE EXISTS
//
// A directive used to be defined in nine places: the Directives struct field,
// parse_line's dispatch, write_cache's emit, read_cache's parse, apply's fold
// into the manifest, cache_fresh's declared-output check, prepare.cppm's
// DirectiveMark field, markDirectiveTail, and foldDirectiveTailIntoPrivateBuild
// — plus the bundled `mcpp` module's typed wrapper. prepare.cppm's own comment
// admitted the split was still incomplete ("Link/source/fingerprint residues
// stay at the call sites"). That is the "same decision derived in N places"
// shape this codebase has paid for repeatedly (#233/#240/#242/#344): it does
// not fail when you add the directive, it fails later, somewhere else.
//
// Here a directive is ONE row in kTable. Parsing, cache serialization, cache
// deserialization, application to the manifest, the declared-output contract,
// and the private-scope fold are all driven off that row.
//
// WHY IT IS A SEPARATE MODULE RATHER THAN MORE OF build_program.cppm
//
// Not taste — a miscompile. build_program.cppm's anonymous namespace corrupts
// its own neighbours under clang 22 + C++20 modules + -O2: PR#332 established
// that an UNUSED helper added there was enough to break `contract_env`, and
// PR#334 reproduced it. mcpp.build.hostprogram was split out for exactly this
// reason and says so in its header. The rule is "stop growing that namespace",
// so the table lives here.
//
// SCOPE IS A REQUIRED FIELD, ON PURPOSE
//
// Every row must state its Scope. `include-dir` being PackagePrivate is not a
// style choice — it is the supply-chain rule that a build-time program must
// not silently widen a package's public interface (Cargo discipline). Making
// Scope a field means the next directive cannot be added without someone
// answering that question.
//
// See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4 (S5).
export module mcpp.build.directives;
import std;
import mcpp.build.program_protocol;
import mcpp.libs.json;
import mcpp.manifest;
import mcpp.source_kind;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint; // hash_string for the glob fingerprint
import mcpp.modgraph.glob; // the one path-glob matcher
export namespace mcpp::build::directives {
// ── Protocol terms ─────────────────────────────────────────────────────────
//
// The protocol version, the cache epoch and the run bound have moved to
// `mcpp.build.program_protocol`. They are the terms both sides agree on BEFORE
// any directive is exchanged, they have consumers that need nothing else from
// this file (hostprogram stamps the version; build_program applies the bound),
// and keeping them here meant importing the whole directive table to ask one
// number.
//
// Re-exported under the old names so existing readers (`dirs::kCacheEpoch` in
// build_program.cppm, `dirs::kProtocolVersion` in the tests) keep working —
// this is one contract seen through two namespaces, not two contracts.
using mcpp::build::program_protocol::kProtocolVersion;
using mcpp::build::program_protocol::kCacheEpoch;
using mcpp::build::program_protocol::kDefaultRunTimeoutSecs;
using mcpp::build::program_protocol::env_timeout_override;
using mcpp::build::program_protocol::run_timeout;
using mcpp::build::program_protocol::run_timeout_for;
// ── The table ──────────────────────────────────────────────────────────────
// Where a directive's value accumulates. One slot may be fed by several wire
// names (link-lib and link-search both produce link flags).
enum class Slot : std::size_t {
// How to EXECUTE the artifact. Its own slot, not a corner of LdFlags: it
// is neither a compile input nor a link input, and putting it in LdFlags
// would put an emulator's argv on the linker command line.
Runner,
// A NAMED WAY OF REACHING THE ARTEFACT. ONE SLOT, ANY NUMBER OF NAMES.
//
// `Runner` above is the default — how the artefact is EXECUTED. Writing it
// to a device, watching what it prints, starting a debug server, deploying
// it, serving it: all the same shape, an argv the PACKAGE supplies, and the
// only thing that distinguishes them is a name.
//
// THE NAME IS DATA. An earlier version gave `flash`, `monitor` and
// `debug` their own slots — which put EMBEDDED vocabulary in the engine,
// so a web package could not add `serve` nor a cluster package `submit`
// without an engine release. The value here is `<name>:<token>`, one token
// per line as argv requires, and the engine never learns a name.
NamedRunner,
// Marks a named runner as having no natural end (`<name>`). Declared rather
// than derived: `openocd -c "program … exit"` terminates and
// `openocd -c "init"` does not, spelled alike up to the argument the
// package chose, and deriving it from a name would work only for names the
// engine knows.
RunnerLongLived,
// Not an argv: a package stating that this target's device admits one user
// at a time. See `BuildConfig::runExclusive`.
RunExclusive,
CxxFlags,
CFlags,
LdFlags,
Defines,
Generated,
Sources,
IncludeDirs,
IncludeDirsAfter,
RerunFiles,
RerunEnv,
// #359: an input that is a SET of files rather than one file. The
// fingerprint is the sorted list of matching relative paths — never their
// contents, sizes or timestamps. A program that globs (`proto/**/*.proto`)
// otherwise cannot express "re-run me when a file appears", because no
// declared file's hash changes and the new file is silently never built.
RerunGlobs,
// Build-graph nodes (`mcpp:action=`). The value is a JSON payload rather
// than a scalar: an action has six fields, and a flat `key=value` line
// cannot carry them. The bundled `mcpp` module owns the encoding, which
// is exactly why the typed API is the only surface that grows (S4).
Actions,
// A sentence for the USER. Not a build input at all — see Scope::Advisory
// for why this could not be folded into any existing slot.
Warnings,
// A CLAIM ABOUT THE MACHINE, OR ABOUT WHAT THIS PACKAGE NEEDS OF IT.
//
// `fact` carries `<name>=<version>`: something the program established
// about the machine, by whatever means the package owns (a driver's
// version, read from the driver's own library). `floor` carries
// `<name> >= <version>`: what the package needs of that quantity. The
// engine compares the two before anything is compiled and refuses with
// both values when the floor is unmet (`version-floor-unmet`); a floor
// nobody stated a fact for is silent. Neither string means anything to
// this file -- the name is data flowing through -- which is what keeps
// vendor knowledge in the package that has it and out of the engine.
Facts,
Floors,
// A DISTRIBUTION FORMAT THIS PACKAGE PROVIDES (`mcpp:pack-format=`).
//
// `mcpp pack --format <name>` resolves `<name>` through the graph the same
// way `--target` reaches a triple: the engine holds the DISPATCH and no
// format. The value is a bare name and means nothing to this file, which is
// what keeps dpkg's control fields, WiX's schema and Apple's notarisation
// out of an engine whose release would otherwise be coupled to theirs.
//
// COLLECTED FROM A BUILD THAT ASKED FOR NOTHING, which is why it is a slot
// and not a side effect of the request. `mcpp pack --format bogus` names
// what is available and `--help` says "plus any format the resolved graph
// provides"; both read this set on a pass where `MCPP_PACK_FORMAT` is
// empty. See `mcpp::provides_pack_format` for the author-facing half of the
// same rule -- declare unconditionally, submit conditionally.
PackFormats,
// A NAMED EXECUTABLE'S PE SUBSYSTEM AND ENTRY (#618), as `<target>:<value>`.
// Two slots rather than one parsed pair, because each is a field of
// `manifest::Target` with its own set of accepted values.
WindowsSubsystem,
WindowsEntry,
// A FILE THIS PROGRAM PRODUCED OR SELECTED, PLACED BESIDE THE ARTIFACT
// (#622 A4), as `<from>\t<to>`. The build-program form of `[runtime]
// deploy` (#615): `from` may be an action's own absolute declared output,
// which the manifest key can never name because it refuses an absolute
// path. One slot, not two, because `Transform::Deploy` resolves `from`
// against the package root at parse time and stores the pair together --
// splitting it into two slots would let a build with N deploy directives
// pair them up wrong the moment N > 1.
Deploy,
Count
};
inline constexpr std::size_t kSlotCount = static_cast<std::size_t>(Slot::Count);
// THERE IS DELIBERATELY NO LIST OF ACTION NAMES HERE.
//
// An earlier version carried `kDeviceSlots`, `device_slot_name()` and a
// `Semantics` function switching on four hardcoded values. Every one of them
// was a place the engine decided which domains were expressible. What replaced
// them is a map keyed by whatever a package wrote, and a `longLived` flag the
// package sets — so `flash`, `serve`, `submit` and `logcat` are the same kind
// of thing to this file, which is to say: nothing it knows about.
// Who sees the value. The field that must be answered for every new directive.
enum class Scope {
PackagePrivate, // only this package's own TUs — never propagated to consumers
LinkGlobal, // reaches the final link of whatever consumes this package
// Reaches how the consumer RUNS the artifact. Parallel to LinkGlobal in
// propagation and deliberately NOT the same value: the two have different
// conflict rules. Link flags from two dependencies concatenate and that is
// correct; two runners cannot, so this scope carries an exactly-one-
// provider check that LinkGlobal must not inherit.
RunGlobal,
SourceSet, // joins the compile set
RerunKey, // not a build input at all; only feeds the re-run key
GraphNode, // declares an edge in the build graph; see manifest::BuildAction
// REACHES THE USER RATHER THAN THE BUILD, AND THAT IS WHY IT IS A
// SEVENTH VALUE RATHER THAN A REUSED ONE.
//
// Every other scope answers "which part of the build sees this". An
// advisory is seen by nobody in the build: it changes no compile line, no
// link line and no source set, and `apply` therefore does not read its
// slot. Spelling it `PackagePrivate` would have been the cheap move and
// would have said something false — that it reaches this package's own
// translation units.
//
// `RerunKey` is the closest existing value (also "not a build input"), and
// it is still wrong: a re-run key feeds a MACHINE decision, an advisory
// feeds a person.
Advisory,
// A statement the engine COMPARES at prepare time. It reaches no compile
// line, no link line, no source set and no person directly; the verdict
// of the comparison does. Persisted, so a cached run replays the claim --
// right for a floor, and the reason a program stating a FACT about the
// machine must also declare what would change it (`rerun_if_changed` on
// the file the fact was read from), or the fact outlives the machine.
Claim,
// REACHES THE LINK OF ONE TARGET OF THIS PACKAGE, NAMED IN THE VALUE.
//
// Not `LinkGlobal`, which reaches every consumer's link, and not
// `PackagePrivate`, which reaches this package's translation units and no
// link at all. A subsystem is a property of one executable: applied to a
// consumer, a test binary or a second executable of the same package, it is
// the defect #618 reports against `[build] ldflags`.
TargetLink,
};
// How the raw wire value is normalized before it is stored. Applied ONCE, at
// parse time, so the cache holds the already-spelled form (safe: the cache key
// hashes the compiler identity, so a dialect switch invalidates the entry
// before any old spelling could be replayed under a new dialect).
enum class Transform {
Verbatim,
LibFlag, // dialect lib_flag_for (-lfoo | foo.lib)
LibSearchPath, // dialect libSearchPrefix + absolute path
DefinePrefix, // dialect definePrefix + value
AbsPath, // absolute, lexically normal
LinkerScript, // "-T <absolute path>" — freestanding link layout
// `<from>\t<to>` (#622 A4). `from` is resolved against the package root
// exactly as AbsPath does -- absolute stays absolute, relative resolves --
// and `to` is left verbatim, because it is relative to an executable this
// package has not seen and `deploy_path_problem` (not this table) is what
// checks its shape. TAB rather than `:` (the `windows-subsystem` pair
// separator): an absolute `from` is a Windows path on that platform, and
// `C:\...` contains a colon.
Deploy,
};
struct Def {
std::string_view wire; // the `mcpp:<wire>=` name
std::string_view tag; // cache-record tag; empty = not persisted as a directive
Slot slot;
Scope scope;
Transform transform;
// Declared-output contract: the value names a file that MUST exist after
// the program ran, and whose disappearance invalidates the cache.
bool mustExistAfterRun;
// The diagnostic when it does not, as "build.mcpp <prefix> '<path>'
// <suffix>". Two fields rather than one generic sentence because the two
// output-shaped directives mean genuinely different things — `generated=`
// says "I WROTE this", `source=` says "I SELECTED this pre-existing file"
// — and a user debugging one needs to be told which contract they broke.
// Required (non-empty) whenever mustExistAfterRun is set.
std::string_view missingPrefix;
std::string_view missingSuffix;
int sinceProtocol;
};
inline constexpr std::array<Def, 26> kTable{{
// wire tag slot scope transform must missingPrefix missingSuffix since
{"cxxflag", "cxxflag", Slot::CxxFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1},
{"cflag", "cflag", Slot::CFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1},
{"link-lib", "ldflag", Slot::LdFlags, Scope::LinkGlobal, Transform::LibFlag, false, "", "", 1},
{"link-search", "ldflag", Slot::LdFlags, Scope::LinkGlobal, Transform::LibSearchPath, false, "", "", 1},
{"cfg", "define", Slot::Defines, Scope::PackagePrivate, Transform::DefinePrefix, false, "", "", 1},
{"generated", "generated", Slot::Generated, Scope::SourceSet, Transform::Verbatim, true, "declared generated source", "but it does not exist after the run", 1},
{"source", "source", Slot::Sources, Scope::SourceSet, Transform::Verbatim, true, "selected source", "(mcpp:source=) but no such file exists", 1},
// LinkGlobal, and that is the whole reason this row exists.
//
// A board-support package is the one thing that knows a board's memory
// layout, and a linker script is how that layout is expressed. Every other
// way of getting one onto the link line is package-private (`cxxflag`) or
// cannot express the flag at all (`link-lib` emits `-l`, `link-search`
// emits `-L`), so before this row a BSP could supply the C library and the
// startup code and still not supply the layout — leaving the one thing a
// user cannot write for themselves as the one thing they had to.
//
// The supply-chain rule the PackagePrivate rows enforce is not weakened:
// this widens the LINK, which `link-lib`/`link-search` already do, and not
// the public compile interface.
//
// Single-valued in practice — two scripts on one line is an lld error, and
// that error names both, which is a better diagnostic than anything a
// conflict check here would produce.
// `mustExistAfterRun` is FALSE, and not by oversight. That contract
// assumes the directive's value IS a path (`generated=`, `source=`), and
// this one's transformed value is `-T <path>` — so the check would test
// the wrong string and reject a script that is right there. Special-casing
// the contract for one row would cost more than it buys: lld's own error
// is already exact ("cannot find linker script <path>"), which is the
// condition the contract exists to make legible.
// One argv TOKEN per line, in emission order.
//
// argv is an ordered list and a directive is one line = one value, so the
// list is built by repetition. The alternative — a JSON array, as `action`
// uses — would introduce an escaping contract for a payload that never
// nests, and `action` pays that cost only because it has six fields.
//
// Verbatim: a runner token is not a path to normalize (it may be `-bios`),
// and the producer already resolved the emulator absolutely, because a
// bare name resolves through PATH to a shim that dispatches against its
// OWNER home — measured in CI as `xlings: '…' is not installed` from a job
// where the same name had answered `--version` two steps earlier.
{"runner", "runner", Slot::Runner, Scope::RunGlobal, Transform::Verbatim, false, "", "", 4},
{"runner-named", "runner-named", Slot::NamedRunner, Scope::RunGlobal, Transform::Verbatim, false, "", "", 6},
{"runner-longlived", "runner-longlived", Slot::RunnerLongLived, Scope::RunGlobal, Transform::Verbatim, false, "", "", 6},
{"run-exclusive", "run-exclusive", Slot::RunExclusive, Scope::RunGlobal, Transform::Verbatim, false, "", "", 6},
{"link-script", "ldflag", Slot::LdFlags, Scope::LinkGlobal, Transform::LinkerScript, false, "", "", 3},
// THE OUTLET THE LINK FAMILY WAS MISSING (v8).
//
// `link-lib`, `link-search` and `link-script` each name one KIND of thing.
// A flag the program COMPUTED belongs to none of them: a generated version
// script (`-Wl,--version-script=`), `-Wl,--wrap=malloc` for a runtime that
// takes over a C-library symbol, `-Wl,--exclude-libs,ALL` so a statically
// absorbed third party does not become part of this package's ABI.
//
// Scope::LinkGlobal, AND THAT IS THE CORRECTION OF AN EARLIER DESIGN.
// The design doc first ruled it PackagePrivate by analogy with
// `include-dir`. The analogy is false. `include-dir` is private because a
// compile interface has a declarative public counterpart
// (`[build] include_dirs`) and a build-time program must not widen it
// behind the manifest's back. Link flags have no such split: the
// declarative `[build] ldflags` ALREADY propagates to consumers, and
// `linkUsage.ldflags` is a copy of `buildConfig.ldflags`. A private link
// flag is not a policy this engine can express today, and making the
// computed form behave differently from its declarative twin would be the
// inconsistency, not the safeguard.
//
// The consequence is stated rather than hidden: a dependency emitting
// `-Wl,--version-script=` puts it on the consumer's link too. That hazard
// is not new -- a dependency writing the same flag in `[build] ldflags`
// has always done this -- so this row widens who can compute the value,
// not what the value can reach.
//
// Verbatim: the engine does not parse linker flags. `-Wl,` forms, `-z`
// pairs and vendor spellings are the linker's vocabulary, not this table's.
{"link-flag", "ldflag", Slot::LdFlags, Scope::LinkGlobal, Transform::Verbatim, false, "", "", 8},
{"include-dir", "include-dir", Slot::IncludeDirs, Scope::PackagePrivate, Transform::AbsPath, false, "", "", 1},
{"include-dir-after", "include-dir-after", Slot::IncludeDirsAfter, Scope::PackagePrivate, Transform::AbsPath, false, "", "", 1},
{"rerun-if-changed", "", Slot::RerunFiles, Scope::RerunKey, Transform::Verbatim, false, "", "", 1},
{"rerun-if-env-changed","", Slot::RerunEnv, Scope::RerunKey, Transform::Verbatim, false, "", "", 1},
{"rerun-if-changed-glob","", Slot::RerunGlobs, Scope::RerunKey, Transform::Verbatim, false, "", "", 2},
// THE ONE THING A BUILD PROGRAM COULD NOT DO BEFORE: SUCCEED AND STILL
// SAY SOMETHING.
//
// mcpp captures a build program and prints what it captured only on a
// NON-ZERO exit. So a program that finished its job but found something
// the user needs to know had no channel: a `std::cerr` note printed
// nothing on exactly the builds that needed it, which is worse than no
// note at all because it looks like a fix. Failing instead is not an
// option either — the build succeeded.
//
// The motivating case: a manifest's `[xlings] deps` is a DECLARATION, not
// an install trigger, so on a machine that has not installed the emulator
// `xpkg_dir` returns empty and the program configures no runner. Correct,
// silent, and indistinguishable to the user from a package that forgot.
//
// `tag` IS NON-EMPTY, AND THAT IS LOAD-BEARING. A build program's
// result is cached and a hit does not re-run it, so an advisory that lived
// only on the run path would appear once and never again — the same
// failure shape as the note that was deleted, arrived at from the other
// side. A non-empty tag puts it in the cache record, and `serialize` /
// `accept_cache_record` are table-driven, so the replay costs nothing.
//
// kCacheEpoch is deliberately NOT bumped for this row. Entries written
// before it carry no `d warning` line, and the programs that wrote them
// could not emit one — so replaying them yields exactly what the program
// said, and the entry is still correct. Bumping would re-run every build
// program in every project on upgrade and buy nothing. The reverse
// direction is already safe: an older engine reading a newer entry hits
// the unknown-tag path and discards the whole record.
{"warning", "warning", Slot::Warnings, Scope::Advisory, Transform::Verbatim, false, "", "", 5},
{"action", "action", Slot::Actions, Scope::GraphNode, Transform::Verbatim, false, "", "", 1},
// The probe channel: a rule package measures, the engine compares. See
// Slot::Facts for the shape of each value.
{"fact", "fact", Slot::Facts, Scope::Claim, Transform::Verbatim, false, "", "", 7},
{"floor", "floor", Slot::Floors, Scope::Claim, Transform::Verbatim, false, "", "", 7},
// `tag` IS NON-EMPTY FOR THE REASON `warning`'S IS, AND IT MATTERS MORE
// HERE. A build program's result is cached and a hit does not re-run it, so
// a declaration that lived only on the run path would be present on the
// first build of a project and absent on every later one -- and the pass
// that reads it is `mcpp pack`, which is never the first build. The set
// would then be empty exactly when a user asks for a format, and the
// refusal would name nothing.
//
// kCacheEpoch is NOT bumped. An entry written before this row carries no
// `d pack-format` line, and the program that wrote it could not emit one,
// so replaying it yields what that program said. An older engine reading a
// newer entry already discards the whole record through the unknown-tag
// path.
{"pack-format", "pack-format", Slot::PackFormats, Scope::Claim, Transform::Verbatim, false, "", "", 9},
// v10 (#618). The value names a target, and `target_directive_error`
// refuses a name this package does not declare as an executable before
// anything is applied. Persisted like every row but the re-run keys, so a
// cached run applies what the program said.
{"windows-subsystem", "windows-subsystem", Slot::WindowsSubsystem, Scope::TargetLink, Transform::Verbatim, false, "", "", 10},
{"windows-entry", "windows-entry", Slot::WindowsEntry, Scope::TargetLink, Transform::Verbatim, false, "", "", 10},
// v11 (#622 A4). Scope::LinkGlobal because this joins `LinkIntent`, the
// same struct `link-lib`/`link-search`/`link-script`/`link-flag` feed, and
// is merged into a CONSUMER's `bin/` by `resolve_runtime_contract` exactly
// as the manifest key `[runtime] deploy` already is -- it is that field,
// reached from a build program instead of TOML. `mustExistAfterRun` is
// FALSE: `from` routinely names an action's output, and the action has not
// run yet when build.mcpp exits -- it is a ninja edge scheduled after this
// process, not a side effect of it. `deploy_directive_error` is the `to`
// check the manifest reader also runs (`deploy_path_problem`), checked
// before `apply` on both the run path and the cache-hit path, so a cached
// replay refuses exactly what a fresh run would.
{"deploy", "deploy", Slot::Deploy, Scope::LinkGlobal, Transform::Deploy, false, "", "", 11},
}};
// ── Collected output of one run ────────────────────────────────────────────
struct Directives {
std::array<std::vector<std::string>, kSlotCount> slots{};
// The protocol the program announced. 0 = it never announced one, which
// means a hand-written `printf("mcpp:...")` program (the frozen surface).
int protocol = 0;
// `mcpp:` keys this engine does not know. Whether that is fatal depends on
// `protocol` — see unknown_directive_error().
std::vector<std::string> unknownKeys;
std::vector<std::string>& at(Slot s) { return slots[static_cast<std::size_t>(s)]; }
const std::vector<std::string>& at(Slot s) const { return slots[static_cast<std::size_t>(s)]; }
};
// ── Lookups ────────────────────────────────────────────────────────────────
const Def* find_by_wire(std::string_view wire);
const Def* find_by_tag(std::string_view tag);
// ── Path helper (shared with the caller's existence checks) ────────────────
std::string abs_against(const std::filesystem::path& base, std::string_view p);
// ── Parse ──────────────────────────────────────────────────────────────────
enum class LineResult {
NotADirective, // ordinary program chatter
Accepted,
Protocol, // `mcpp:protocol=<N>`
Unknown, // a `mcpp:` key this engine does not know
};
// Parse ONE stdout line into `d`. `root` resolves relative paths for the
// path-shaped transforms; `dial` spells the link/define flags.
LineResult accept_line(Directives& d, const mcpp::toolchain::CommandDialect& dial,
const std::filesystem::path& root, std::string_view raw);
void accept_output(Directives& d, const mcpp::toolchain::CommandDialect& dial,
const std::filesystem::path& root, std::string_view out);
// Non-empty when the run must be rejected: either the program speaks a newer
// protocol than this engine, or it declared a protocol and still emitted a
// directive this engine does not know (inside a version both sides agree on,
// an unknown key is a bug, not a forward-compat situation).
//
// A program that never announced a protocol keeps the historical
// warn-and-ignore behaviour: it is a hand-written printf program, frozen at
// protocol 1, and its unknown keys are typos rather than future syntax.
std::optional<std::string> protocol_error(const Directives& d);
// ── Advisories ─────────────────────────────────────────────────────────────
//
// The `mcpp:warning=` lines a program emitted, each already prefixed with the
// package it came from.
//
// THE FORMATTING LIVES HERE AND THE PRINTING DOES NOT, for two reasons that
// pull the same way. This module deliberately imports no UI — a directive
// table that knew how to draw would be a different kind of thing. And there
// are TWO call sites, a run and a cache hit, which is exactly the shape that
// drifts: one source for the wording means they cannot disagree about what
// they say, and a test covers whether they both say it.
//
// The package name comes from the caller because a build program cannot spell
// it reliably — in a workspace it would have to know which member it is.
std::vector<std::string> advisories(std::string_view packageName, const Directives& d);
// ── Cache serialization ────────────────────────────────────────────────────
// `d <tag> <value>` lines, in table order.
void serialize(std::ostream& os, const Directives& d);
// One `d <tag> <value>` record. Returns false for an unknown tag (a cache
// written by a newer mcpp) — the caller treats that as a stale entry.
bool accept_cache_record(Directives& d, std::string_view tag, std::string_view value);
// ── Glob inputs (#359) ─────────────────────────────────────────────────────
//
// The fingerprint of `rerun-if-changed-glob=<pattern>`: the SORTED SET of
// relative paths matching the pattern under `root`, and nothing else.
//
// Deliberately not contents, size or mtime:
// * contents are already covered — a file whose bytes matter is declared as
// an ordinary `rerun-if-changed` input, and size is a strictly weaker
// signal than the hash that entry already carries;
// * mtime is unstable across git checkout, container builds and rsync, and
// this project has already paid for treating a timestamp as identity
// (the file_time_type epoch in the dependency cache).
// The question a glob input asks is "which files are here", so the answer is
// the path set, exactly.
//
// `root`-relative, generic_string, byte-ordered — otherwise the same tree
// fingerprints differently depending on the platform's directory-iteration
// order and separator.
//
// `outputDirName` (typically "target") and ".git" are never walked. A build
// program writes its outputs INSIDE the project, so a pattern like `**` would
// otherwise include what the previous run produced and the set would change on
// every build — a permanent re-run loop, and the classic Cargo footgun. This
// is enforced here rather than left to the author's pattern.
std::string glob_fingerprint(const std::filesystem::path& root,
std::string_view pattern,
std::string_view outputDirName);
// ── Apply ──────────────────────────────────────────────────────────────────
// Fold the collected directives into the manifest's buildConfig. The single
// place that knows which manifest channel each slot feeds.
void apply(mcpp::manifest::Manifest& m, const Directives& d);
// Decode one `mcpp:action=` JSON payload. nullopt = malformed.
std::optional<mcpp::manifest::BuildAction> decode_action(std::string_view payload);
// Non-empty when any declared action is malformed. A separate pass so the
// caller can refuse BEFORE applying anything — a half-applied action set is
// worse than none.
std::string action_error(const Directives& d);
// Non-empty when a `windows-subsystem` or `windows-entry` directive names
// something `apply` cannot honour: a value that is not `<target>:<value>`, a
// value outside the accepted set, a target this package does not declare, a
// target that is not an executable, or a value that contradicts mcpp.toml or an
// earlier directive of the same program. Checked before `apply` on both the run
// path and the cache-hit path, so the two apply one rule.
std::string target_directive_error(const mcpp::manifest::Manifest& m, const Directives& d);
// Non-empty when a `deploy` directive names something `apply` cannot honour
// (#622 A4): a wire value that is not `<from>\t<to>`, or a `to` that fails the
// same rule `deploy_path_problem` enforces for the manifest key. `from` is
// never checked here — it was already resolved to an absolute path by
// `Transform::Deploy`, and a directive-sourced `from` is allowed to be one (an
// action's own declared output), unlike the manifest key's. Checked before
// `apply` on both the run path and the cache-hit path, and the message names
// both the directive and the declaring package, like `target_directive_error`.
std::string deploy_directive_error(const mcpp::manifest::Manifest& m, const Directives& d);
// Resolve an action's paths against `pkgRoot` and make its Source outputs
// exist, so the ordinary source scan can see them.
//
// A placeholder rather than a synthesised CompileUnit, because that reuses
// every existing mechanism: the glob finds it, the scanner reads it, the plan
// gives it an object path, and ninja overwrites it with the real content
// before the compile edge runs (the compile depends on the action's output).
//
// For a module interface the placeholder carries the DECLARED interface —
// `export module X;` plus its imports — so the prepare-time scan agrees with
// what the generator will emit. That is the same assertion-plus-verification
// trade `[modules].scan_overrides` makes: the declaration is checked against
// the compiler's own P1689 output at build time, so a wrong one is caught
// rather than silently believed.
//
// Never truncates an existing file: after the first build the real content is
// there, and rewriting it would make ninja think the input changed on every
// prepare.
//
// ONLY FOR OUTPUTS THAT ARE TRANSLATION UNITS, which is why this needs the
// table. A placeholder exists so the SCAN has something to read, and the scan
// never reads a header — but writing one anyway turned "the generator did not
// run" into "the header is empty", and mcpp#534 was diagnosed as a race for
// exactly that reason: the file was on disk, so the action looked like it had
// run. A missing file is the honest report, and after the ordering fix the
// generator runs before anything reads it either way.
void prepare_actions(std::vector<mcpp::manifest::BuildAction>& actions,
const std::filesystem::path& pkgRoot,
const mcpp::ExtensionTable& extensions);
// Does this action output belong in the COMPILE set?
//
// A `source` action routinely emits companion files that must exist but must
// not be compiled — protoc writes `foo.pb.cc` AND `foo.pb.h`, and the header
// is an include, not a translation unit. Adopting everything gave both the
// same object path and tripped the uniqueness assertion
// ("object path collision after uniqueness pass").
//
// The non-source outputs are still declared to ninja, so the edge still
// produces them and anything that includes them still waits for the generator.
bool is_compilable_output(const std::filesystem::path& p,
const mcpp::ExtensionTable& t);
// ── Private-scope fold (was prepare.cppm's DirectiveMark / fold pair) ──────
//
// Lives here because "which compile-visible channels a PackagePrivate
// directive lands in" is a property of the table, not of the call site. The
// caller records a Mark before running the program and folds the tail after.
struct Mark {
std::size_t cflags = 0, cxxflags = 0, includeDirs = 0, includeDirsAfter = 0;
};
Mark mark(const mcpp::manifest::Manifest& m);
// Fold the PackagePrivate tail into a UsageRequirements-shaped destination.
// Templated so this module does not have to import the scanner (which would
// close a module cycle) — the destination only needs the four vectors.
template <class Usage>
void fold_private_tail(Usage& dst, const mcpp::manifest::Manifest& ran, const Mark& t) {
auto const& bc = ran.buildConfig;
dst.cflags.insert(dst.cflags.end(),
bc.cflags.begin() + static_cast<std::ptrdiff_t>(t.cflags),
bc.cflags.end());
dst.cxxflags.insert(dst.cxxflags.end(),
bc.cxxflags.begin() + static_cast<std::ptrdiff_t>(t.cxxflags),
bc.cxxflags.end());
auto append_unique = [](auto& v, const std::filesystem::path& p) {
if (std::find(v.begin(), v.end(), p) == v.end()) v.push_back(p);
};
for (auto it = bc.includeDirs.begin() + static_cast<std::ptrdiff_t>(t.includeDirs);
it != bc.includeDirs.end(); ++it)
append_unique(dst.includeDirs, *it);
for (auto it = bc.includeDirsAfter.begin() + static_cast<std::ptrdiff_t>(t.includeDirsAfter);
it != bc.includeDirsAfter.end(); ++it)
append_unique(dst.includeDirsAfter, *it);
}
} // namespace mcpp::build::directives
namespace mcpp::build::directives {
namespace fs = std::filesystem;
const Def* find_by_wire(std::string_view wire) {
for (auto const& d : kTable)
if (d.wire == wire) return &d;
return nullptr;
}
const Def* find_by_tag(std::string_view tag) {
if (tag.empty()) return nullptr;
for (auto const& d : kTable)
if (d.tag == tag) return &d; // first row wins; rows sharing a tag share a slot
return nullptr;
}
std::string abs_against(const fs::path& base, std::string_view p) {
// Native spelling (see mcpp::modgraph::native_path_from_generic): a
// directive path like `generated/modules/x` would otherwise stay mixed
// on MSVC and leak into include flags / the CDB.
fs::path pp = mcpp::modgraph::native_path_from_generic(p);
if (pp.is_relative()) pp = base / pp;
return pp.lexically_normal().string();
}
namespace {
std::string trim(std::string_view s) {
std::size_t b = 0, e = s.size();
while (b < e && (s[b] == ' ' || s[b] == '\t' || s[b] == '\r')) ++b;
while (e > b && (s[e - 1] == ' ' || s[e - 1] == '\t' || s[e - 1] == '\r')) --e;
return std::string(s.substr(b, e - b));
}
std::string transformed(const Def& def, std::string_view raw,
const mcpp::toolchain::CommandDialect& dial,
const fs::path& root) {
switch (def.transform) {
case Transform::Verbatim: return std::string(raw);
case Transform::LibFlag: return mcpp::toolchain::lib_flag_for(dial, raw);
case Transform::LibSearchPath: return std::string(dial.libSearchPrefix)
+ abs_against(root, raw);
case Transform::DefinePrefix: return std::string(dial.definePrefix) + std::string(raw);
case Transform::AbsPath: return abs_against(root, raw);
// Absolute on purpose: the link runs in the build directory, so a
// relative script path resolves against the wrong root and lld
// answers "cannot find linker script link.ld" — measured.
case Transform::LinkerScript: return "-T " + abs_against(root, raw);
// Resolve `from` NOW, while `root` (this build.mcpp's package root) is
// still in hand -- `apply` is never given it. `to` is left untouched:
// it is a destination relative to an executable this package has not
// seen, and its shape is `deploy_directive_error`'s question, not
// this transform's.
case Transform::Deploy: {
const auto tab = raw.find('\t');
if (tab == std::string_view::npos) return std::string(raw);
return abs_against(root, raw.substr(0, tab)) + '\t'
+ std::string(raw.substr(tab + 1));
}
}
return std::string(raw);
}
} // namespace
LineResult accept_line(Directives& d, const mcpp::toolchain::CommandDialect& dial,
const fs::path& root, std::string_view raw) {
std::string line = trim(raw);
constexpr std::string_view kPfx = "mcpp:";
if (!line.starts_with(kPfx)) return LineResult::NotADirective;
std::string_view body = std::string_view(line).substr(kPfx.size());
auto eq = body.find('=');
std::string key = std::string(body.substr(0, eq));
std::string val = eq == std::string_view::npos ? std::string()
: std::string(body.substr(eq + 1));
if (key == "protocol") {
int n = 0;
auto* first = val.data();
auto* last = val.data() + val.size();
if (std::from_chars(first, last, n).ec == std::errc{}) d.protocol = n;
return LineResult::Protocol;
}
const Def* def = find_by_wire(key);
if (!def) {
if (std::find(d.unknownKeys.begin(), d.unknownKeys.end(), key)
== d.unknownKeys.end())
d.unknownKeys.push_back(key);
return LineResult::Unknown;
}
d.at(def->slot).push_back(transformed(*def, val, dial, root));
return LineResult::Accepted;
}
void accept_output(Directives& d, const mcpp::toolchain::CommandDialect& dial,
const fs::path& root, std::string_view out) {
std::size_t pos = 0;
while (pos <= out.size()) {
std::size_t nl = out.find('\n', pos);
std::string_view ln = out.substr(
pos, nl == std::string_view::npos ? std::string_view::npos : nl - pos);
accept_line(d, dial, root, ln);
if (nl == std::string_view::npos) break;
pos = nl + 1;
}
}
std::optional<std::string> protocol_error(const Directives& d) {
if (d.protocol > kProtocolVersion) {
return std::format(
"build.mcpp speaks directive protocol {}, but this mcpp understands "
"at most {}.\n"
" The package was written for a newer mcpp — upgrade with "
"`mcpp self update`.\n"
" (Continuing would silently drop directives this build "
"depends on.)",
d.protocol, kProtocolVersion);
}
if (d.protocol > 0 && !d.unknownKeys.empty()) {
std::string list;
for (auto const& k : d.unknownKeys)
list += (list.empty() ? "" : ", ") + ("mcpp:" + k);
// NOT "so it must be a typo".
//
// That is what this said, and adding `link-script` in protocol 3
// proved it wrong: a package written against a newer mcpp reaches an
// older one with the OLDER engine's protocol number stamped on it —
// the announcement is substituted at build.mcpp compile time by
// whichever engine is running, not carried by the package. So the two
// numbers agreeing says nothing about whether the KEY is from the
// future, and an old mcpp cannot tell the two cases apart. Naming both
// is the only honest thing it can do, and the upgrade is the cheaper
// one to try first.
return std::format(
"build.mcpp emitted directive(s) this mcpp does not know: {}.\n"
" Either the package was written for a newer mcpp (try "
"`mcpp self update`),\n"
" or the directive is misspelled. This mcpp speaks protocol "
"{}; the protocol number\n"
" cannot distinguish the two, because it is stamped by "
"whichever mcpp compiled\n"
" the program, not by the package.",
list, kProtocolVersion);
}
return std::nullopt;
}
void serialize(std::ostream& os, const Directives& d) {
// Table order, and one pass per row rather than per slot: rows sharing a
// slot (link-lib / link-search) share a tag, so emitting per row would
// duplicate them.
std::array<bool, kSlotCount> done{};
for (auto const& def : kTable) {
if (def.tag.empty()) continue;
auto idx = static_cast<std::size_t>(def.slot);
if (done[idx]) continue;
done[idx] = true;
for (auto const& v : d.at(def.slot))
os << "d " << def.tag << ' ' << v << '\n';
}
}
bool accept_cache_record(Directives& d, std::string_view tag, std::string_view value) {
const Def* def = find_by_tag(tag);
if (!def) return false;
d.at(def->slot).emplace_back(value);
return true;
}
std::string glob_fingerprint(const std::filesystem::path& root,
std::string_view pattern,
std::string_view outputDirName) {
namespace fs = std::filesystem;
std::vector<std::string> hits;
std::error_code ec;
// skip_permission_denied only: symlinked directories are NOT followed, the
// same rule the source scan uses, so a self-referential link cannot make
// this walk diverge.
fs::recursive_directory_iterator it(
root, fs::directory_options::skip_permission_denied, ec);
if (ec) return {};
for (; it != fs::recursive_directory_iterator(); it.increment(ec)) {
if (ec) break;
const auto& p = it->path();
std::error_code dec;
if (it->is_directory(dec)) {
auto name = p.filename().string();
if (name == ".git" || (!outputDirName.empty() && name == outputDirName)) {
it.disable_recursion_pending();
continue;
}
if (it->is_symlink(dec)) it.disable_recursion_pending();
continue;
}
if (!mcpp::modgraph::path_matches_glob(p, root, pattern)) continue;
std::string rel;
try {
rel = p.lexically_relative(root).generic_string();
} catch (const std::exception&) {
continue; // unspellable name — see path_matches_glob
}
hits.push_back(std::move(rel));
}
std::ranges::sort(hits);
std::string joined;
for (auto const& h : hits) { joined += h; joined.push_back('\n'); }
return mcpp::toolchain::hash_string(joined);
}
std::vector<std::string> advisories(std::string_view packageName, const Directives& d) {
std::vector<std::string> out;
for (auto const& w : d.at(Slot::Warnings)) {
// Unnamed rather than "<unnamed package>": a workspace member always
// has a name, and an unnamed root is a single-package build where the
// prefix would be noise.
out.push_back(packageName.empty()
? std::string(w)
: std::format("{}: {}", packageName, w));
}
return out;
}
void apply(mcpp::manifest::Manifest& m, const Directives& d) {
auto& bc = m.buildConfig;
auto const& cxx = d.at(Slot::CxxFlags);
auto const& c = d.at(Slot::CFlags);
auto const& ld = d.at(Slot::LdFlags);
auto const& runner = d.at(Slot::Runner);
auto const& defines = d.at(Slot::Defines);
bc.cxxflags.insert(bc.cxxflags.end(), cxx.begin(), cxx.end());
bc.cflags.insert(bc.cflags.end(), c.begin(), c.end());
bc.ldflags.insert(bc.ldflags.end(), ld.begin(), ld.end());
// Appended in emission order — the tokens ARE the argv.
bc.runner.insert(bc.runner.end(), runner.begin(), runner.end());
// `<name>:<token>`, split on the FIRST colon. A token may contain colons
// (a Windows path, a URL); a name may not, which is what makes the first
// one unambiguous.
for (auto const& entry : d.at(Slot::NamedRunner)) {
auto sep = entry.find(':');
if (sep == std::string::npos || sep == 0) continue;
bc.namedRunners[entry.substr(0, sep)].argv.push_back(entry.substr(sep + 1));
}
for (auto const& name : d.at(Slot::RunnerLongLived))
bc.namedRunners[name].longLived = true;
// ANY non-empty value sets it, and nothing can unset it. Exclusivity is a
// claim about the DEVICE: if one package knows the target is a mutex, it is
// one, and a later package saying nothing must not relax that.
if (!d.at(Slot::RunExclusive).empty()) bc.runExclusive = true;
// cfg defines colour BOTH language channels — the one slot that fans out.
bc.cflags.insert(bc.cflags.end(), defines.begin(), defines.end());
bc.cxxflags.insert(bc.cxxflags.end(), defines.begin(), defines.end());
// Generated + selected sources join the source set. BOTH lists: the
// scanner walks the legacy modules.sources mirror, so pushing only
// bc.sources leaves a generated file outside the base globs invisible to
// the scan (latent since L3).
for (auto slot : {Slot::Generated, Slot::Sources}) {
for (auto const& s : d.at(slot)) {
bc.sources.push_back(s);
m.modules.sources.push_back(s);
}
}
// Already absolute from the AbsPath transform. PRIVATE by design: for the
// root these join buildConfig before the package snapshot; for a
// dependency the caller mirrors them into privateBuild only, never into
// publicUsage.
for (auto const& p : d.at(Slot::IncludeDirs))
bc.includeDirs.emplace_back(p);
for (auto const& p : d.at(Slot::IncludeDirsAfter))
bc.includeDirsAfter.emplace_back(p);
// Claims join the runtime declarations the manifest could have carried
// itself, so the version-floor check in prepare reads ONE list and never
// learns which spelling a claim arrived in.
for (auto const& f : d.at(Slot::Facts))
m.runtimeConfig.provides.push_back(f);
for (auto const& fl : d.at(Slot::Floors)) {
mcpp::manifest::RuntimeRequirement req;
req.kind = "version-floor";
req.value = fl;
req.phase = "build";
m.runtimeConfig.requirements.push_back(std::move(req));
}
// A NAME, CARRIED AND NOT INTERPRETED. The engine compares it against
// `--format` and hands the request to whoever claimed it; nothing here
// parses it, because a distribution format has less claim to a name in the
// engine than a language does, and Slang is already supported without one.
for (auto const& f : d.at(Slot::PackFormats))
bc.packFormats.push_back(f);
// A named executable's subsystem and entry. `target_directive_error` has
// refused every value that names no executable, so the conditions below
// only keep this function total. #622 A3: `is_program()`, not `Binary`
// alone — `windows_subsystem`/`windows_entry` accept `app` exactly as
// they accept `bin` (PE has no Android row, so this never meets an
// `Application` whose form is a shared object in practice).
for (auto const& entry : d.at(Slot::WindowsSubsystem)) {
const auto sep = entry.rfind(':');
if (sep == std::string::npos) continue;
const auto name = entry.substr(0, sep);
for (auto& t : m.targets)
if (t.name == name && t.is_program())
t.windowsSubsystem = entry.substr(sep + 1);
}
for (auto const& entry : d.at(Slot::WindowsEntry)) {
const auto sep = entry.rfind(':');
if (sep == std::string::npos) continue;
const auto name = entry.substr(0, sep);
for (auto& t : m.targets)
if (t.name == name && t.is_program())
t.windowsEntry = entry.substr(sep + 1);
}
// Build-graph nodes. Decoded here rather than at parse time so the cache
// stores the payload verbatim and a replay is byte-identical to a run.
for (auto const& payload : d.at(Slot::Actions)) {
if (auto a = decode_action(payload)) bc.actions.push_back(std::move(*a));
}
// `[runtime] deploy` from a build program (#622 A4). `from` was resolved
// to absolute by Transform::Deploy at parse time; `deploy_directive_error`
// has refused every `to` this function cannot honour, so the missing-tab
// guard below only keeps this function total.
for (auto const& entry : d.at(Slot::Deploy)) {
const auto tab = entry.find('\t');
if (tab == std::string::npos) continue;
m.runtimeConfig.linkIntent.deploy.push_back(
{std::filesystem::path(entry.substr(0, tab)), entry.substr(tab + 1)});
}
}
std::optional<mcpp::manifest::BuildAction> decode_action(std::string_view payload) {
try {
auto j = nlohmann::json::parse(payload);
mcpp::manifest::BuildAction a;
a.id = j.value("id", std::string{});
auto role = j.value("role", std::string{"source"});
a.role = role == "check" ? mcpp::manifest::BuildAction::Role::Check
: role == "artifact" ? mcpp::manifest::BuildAction::Role::Artifact
: role == "object" ? mcpp::manifest::BuildAction::Role::Object
: mcpp::manifest::BuildAction::Role::Source;
auto arr = [&](const char* k, std::vector<std::string>& dst) {
if (auto it = j.find(k); it != j.end() && it->is_array())
for (auto const& v : *it)
if (v.is_string()) dst.push_back(v.get<std::string>());
};
arr("inputs", a.inputs);
arr("outputs", a.outputs);
arr("command", a.command);
arr("provides", a.provides);
arr("imports", a.imports);
arr("targets", a.targets);
a.blocking = j.value("blocking", false);
a.depfile = j.value("depfile", std::string{});
a.description = j.value("description", std::string{});
if (a.command.empty() || a.outputs.empty()) return std::nullopt;
if (a.id.empty()) a.id = a.outputs.front();
return a;
} catch (...) {
return std::nullopt;
}
}