-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.zig
More file actions
4797 lines (4553 loc) · 254 KB
/
Copy pathbuild.zig
File metadata and controls
4797 lines (4553 loc) · 254 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
const std = @import("std");
// zig-libs — a curated collection of foundational Zig modules.
//
// Layout rationale: ONE build.zig at the repo root. `zig fetch` cannot target a
// subdirectory (ziglang/zig#23012), so a consumer fetches the whole repo and
// imports only the named module(s) it wants; the root build wires them up.
//
// Each module lives at modules/<name>/src/root.zig and is exposed as an
// importable module named <name>. `deps` lists sibling modules it imports.
// See CONVENTIONS.md for naming + the `meta` tag vocabulary.
const Module = struct {
name: []const u8,
deps: []const []const u8 = &.{},
/// Modules this one imports ONLY from its tests (in practice: `testkit`).
///
/// These are wired into the test binary and NOT into the module a
/// downstream consumer imports, so `@import("frost")` does not drag a test
/// harness along. That separation is the whole point of the field --
/// putting testkit in `deps` would work and would also publish it.
///
/// They DO appear in `module-graph`'s deps column, because that graph
/// exists to tell `scripts/test.sh` what to re-test, and a change to
/// testkit must re-test everything whose tests use it.
test_deps: []const []const u8 = &.{},
/// This module ships `example/main.zig`: a consumer binary built by
/// `zig build check-examples` against the PUBLISHED module — `deps` only,
/// no `test_deps`, no reach into anything the module does not export.
///
/// Declared here rather than probed from the tree so that both directions
/// are checkable: an example that stops building is red, and so is one
/// added without saying so (or a declaration whose file is gone).
///
/// Scope survey 2026-08-21: 78 of 229 modules already have an in-repo
/// consumer, and 93 more have a public surface under 25 functions (a third
/// of those anchored to published vectors, where an internal vector test
/// beats an example). The 57 with neither get one, widest surface first.
/// Which logical library (or libraries) this module belongs to. The
/// repository is `zig-libs`, plural: a `lib` is one of the six groupings a
/// consumer actually shops in, and this is where that grouping is written
/// down. FIRST entry is primary -- the section of the README catalog the
/// module's row lives under, which `check-libs` holds to agreement.
///
/// This is a hand-written note about where a module BELONGS, not a derived
/// fact, and it lives here rather than in README prose or in 231 separate
/// `meta` blocks for one reason: a taxonomy is reviewed as a list. You can
/// only notice that something is filed wrong by seeing its neighbours.
///
/// Extra entries are the point of the plural: a module can be worth
/// reaching for from more than one library, and saying so is how a
/// consumer on `net` finds out that `x509` is also theirs. Sixteen already
/// have a dependency edge crossing a library boundary, which is where the
/// initial second tags came from -- but the field is not limited to that
/// evidence, because "would be useful for" is not the same relation as
/// "is already imported by".
///
/// No default: a new module has to be filed, and copying `_template`
/// leaves a wrong answer rather than no answer, which the gate catches.
libs: []const []const u8,
/// Compute-bound: the tests are dominated by arithmetic (pairings,
/// hash-based signatures, FHE, scrypt, RSA), which an unoptimized Debug
/// build makes ~5x slower — `bls12_381` alone goes 35s -> 182s. These
/// modules ARE the test suite's critical path, so they are built at
/// ReleaseSafe when the requested mode is Debug. ReleaseSafe keeps every
/// safety check; only Debug-specific behaviour (0xAA-poisoned undefined
/// memory) is given up. Pass `-Dstrict-debug` to force real Debug — that
/// is what the CI matrix does. Threshold: >15s measured serially.
heavy: bool = false,
/// This module's tests talk to a REAL external peer -- a running server, a
/// foreign client -- not a fixture. Such tests are run serially, because
/// four of them at once turned a module that passes 98/98 alone into 3/3
/// failures: the contention is between the live peers, not the CPU.
///
/// Declared rather than probed: nothing in the tree distinguishes a socket
/// opened against a real peer from one opened against a fixture. It lives
/// HERE, next to `heavy` and `example`, because it is the same kind of fact
/// about the same thing, and `module-graph` publishes it so the shell
/// scripts stop keeping their own copy of the list.
///
/// `jinja` is deliberately absent: its live peer is a Python script it runs
/// to completion, not a network peer with a clock.
live: bool = false,
};
const module_list = [_]Module{
// Test-only harness. Consumers reach it through `test_deps`, never `deps`.
.{ .name = "testkit", .libs = &.{"os"} },
.{ .name = "netaddr", .libs = &.{ "net", "web" } },
// `workerpool` is a TEST-only dep: `h2_server.Options.dispatcher` is an
// injectable seam (a function pointer + a context pointer), so the
// published `http` module implements no pool at all — deliberately, since
// `websocket`/`accesslog`/`grpc`/`mcp-http`/… all depend on `http` and
// none of them should acquire threads by transitive accident. The tests
// wire a real `workerpool.WorkerPool` into that seam because the five
// concurrency invariants can only be exercised by real threads.
// `zig build check-testonly` proves the published module never needs it.
.{ .name = "http", .libs = &.{ "web", "crypto", "format", "net" }, .deps = &.{ "netaddr", "datefmt" }, .test_deps = &.{ "testkit", "workerpool" } },
.{ .name = "websocket", .libs = &.{"web"}, .deps = &.{"http"} },
.{ .name = "accesslog", .libs = &.{"web"}, .deps = &.{"http"} },
.{ .name = "staticfiles", .libs = &.{"web"}, .deps = &.{"http"} },
.{ .name = "brotli", .libs = &.{"web"}, .test_deps = &.{"testkit"} },
.{ .name = "dns", .libs = &.{"net"}, .deps = &.{ "netaddr", "http" } },
.{ .name = "ramcache", .libs = &.{ "storage", "net" } },
.{ .name = "router", .libs = &.{"web"}, .deps = &.{"http"} },
.{ .name = "ratelimit", .libs = &.{"web"}, .deps = &.{ "router", "http", "netaddr" } },
.{ .name = "abuseguard", .libs = &.{"web"}, .deps = &.{ "http", "netaddr", "router" } },
.{ .name = "throttle", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
// Importable as @import("security-headers") — module names are plain
// strings, the hyphen is fine (cf. the community's "known-folders").
.{ .name = "security-headers", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "cors", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "metrics", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "validate", .libs = &.{"web"}, .deps = &.{ "router", "http", "netaddr" } },
.{ .name = "openapi", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "health", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "requestid", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "linkheader", .libs = &.{"format"} },
.{ .name = "cookies", .libs = &.{"format"}, .deps = &.{"http"} },
.{ .name = "idempotency", .libs = &.{"web"}, .deps = &.{ "router", "http", "ramcache" } },
.{ .name = "webhooksig", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "tracecontext", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
// Importable as @import("aaa-gate") — hyphen OK, like security-headers.
.{ .name = "aaa-gate", .libs = &.{"web"}, .deps = &.{ "router", "http" } },
.{ .name = "resilience", .libs = &.{ "web", "net" } },
.{ .name = "acme", .libs = &.{"web"}, .deps = &.{ "http", "router", "entropy" } },
.{ .name = "netlink", .libs = &.{"net"}, .test_deps = &.{"testkit"} },
.{ .name = "genetlink", .libs = &.{"net"}, .deps = &.{"netlink"} },
.{ .name = "nl80211", .libs = &.{"net"}, .deps = &.{ "genetlink", "netlink" }, .test_deps = &.{"testkit"} },
.{ .name = "ethtool", .libs = &.{"net"}, .deps = &.{ "genetlink", "netlink" }, .test_deps = &.{"testkit"} },
.{ .name = "devlink", .libs = &.{"net"}, .deps = &.{ "genetlink", "netlink" }, .test_deps = &.{"testkit"} },
.{ .name = "decimal", .libs = &.{ "storage", "format" } },
.{ .name = "seqmap", .libs = &.{"net"} },
.{ .name = "icmp", .libs = &.{"net"}, .deps = &.{ "seqmap", "netaddr" } },
.{ .name = "mcp", .libs = &.{"os"} },
.{ .name = "mcp-http", .libs = &.{"os"}, .deps = &.{ "router", "http", "mcp" } },
.{ .name = "coap", .libs = &.{"net"} },
.{ .name = "kv", .libs = &.{"storage"} },
.{ .name = "kvtree", .libs = &.{ "storage", "net" }, .deps = &.{"kv"} },
.{ .name = "blobmsg", .libs = &.{"format"} },
.{ .name = "tar", .libs = &.{"format"} },
.{ .name = "latency-stats", .libs = &.{"net"} },
.{ .name = "pping", .libs = &.{"net"} },
.{ .name = "spf-ect", .libs = &.{"net"} },
.{ .name = "ethfrag", .libs = &.{"net"} },
.{ .name = "l2encap", .libs = &.{"net"} },
.{ .name = "l2forward", .libs = &.{"net"} },
.{ .name = "pbb", .libs = &.{"net"} },
.{ .name = "bumtree", .libs = &.{"net"}, .deps = &.{"spf-ect"} },
.{ .name = "spbfib", .libs = &.{"net"}, .deps = &.{"isis-spf"} },
.{ .name = "isis", .libs = &.{"net"} },
.{ .name = "isis-adj", .libs = &.{"net"}, .deps = &.{"isis"} },
.{ .name = "isis-dis", .libs = &.{"net"}, .deps = &.{"isis"} },
.{ .name = "isis-lsdb", .libs = &.{"net"}, .deps = &.{"isis"} },
.{ .name = "isis-flood", .libs = &.{"net"}, .deps = &.{ "isis", "isis-lsdb" } },
.{ .name = "isis-spf", .libs = &.{"net"}, .deps = &.{ "isis", "isis-lsdb", "spf-ect" } },
.{ .name = "isis-sim", .libs = &.{"net"}, .deps = &.{ "netsim", "isis", "isis-lsdb", "isis-flood", "isis-spf" } },
.{ .name = "aeadframe", .libs = &.{"crypto"}, .deps = &.{"chachapoly"} },
.{ .name = "tenantkex", .libs = &.{"crypto"}, .deps = &.{"noise"} },
.{ .name = "netsim", .libs = &.{"net"} },
.{ .name = "loopfree-reconv", .libs = &.{"net"}, .deps = &.{ "netsim", "spf-ect" } },
.{ .name = "df-elect", .libs = &.{"net"}, .deps = &.{"netsim"} },
.{ .name = "raft", .libs = &.{"net"}, .deps = &.{"netsim"} },
.{ .name = "liveness-hyst", .libs = &.{"net"}, .deps = &.{ "netsim", "latency-stats" } },
.{ .name = "loopix", .libs = &.{"net"}, .deps = &.{ "netsim", "sphinx" } },
.{ .name = "lockfree", .libs = &.{"net"} },
.{ .name = "workerpool", .libs = &.{"net"}, .deps = &.{"lockfree"} },
.{ .name = "shardstore", .libs = &.{"net"}, .deps = &.{"kvtree"} },
.{ .name = "writebehind", .libs = &.{"net"}, .deps = &.{ "ramcache", "workerpool", "jobqueue", "kvtree" } },
.{ .name = "pagecache", .libs = &.{"net"}, .deps = &.{ "kvtree", "ramcache" } },
.{ .name = "tsdb", .libs = &.{"storage"}, .deps = &.{"kvtree"} },
.{ .name = "entropy", .libs = &.{ "crypto", "web" } },
.{ .name = "hashdigest", .libs = &.{ "crypto", "storage" } },
.{ .name = "sealedbox", .libs = &.{"crypto"} },
.{ .name = "rsa", .libs = &.{ "crypto", "net", "web" }, .deps = &.{"montint"}, .heavy = true },
.{ .name = "blindrsa", .libs = &.{"crypto"}, .deps = &.{"rsa"} },
.{ .name = "ssh", .libs = &.{"net"}, .deps = &.{"rsa"}, .heavy = true, .live = true },
.{ .name = "netconf", .libs = &.{"net"}, .deps = &.{ "ssh", "xml" }, .test_deps = &.{"testkit"} },
.{ .name = "nftables", .libs = &.{"net"}, .deps = &.{"netlink"}, .test_deps = &.{"testkit"} },
.{ .name = "trie", .libs = &.{"storage"} },
.{ .name = "fuzzysearch", .libs = &.{"storage"}, .deps = &.{"trie"} },
.{ .name = "geoindex", .libs = &.{"storage"} },
.{ .name = "readthrough", .libs = &.{"net"}, .deps = &.{"ramcache"} },
.{ .name = "timelock_envelope", .libs = &.{"crypto"}, .deps = &.{ "tlock", "hqc", "chachapoly", "entropy" } },
.{ .name = "drand", .libs = &.{"crypto"}, .deps = &.{ "bls12_381", "tlock" } },
.{ .name = "tcplan", .libs = &.{"net"}, .deps = &.{"tc"} },
.{ .name = "modbus", .libs = &.{"net"} },
.{ .name = "iec104", .libs = &.{"net"}, .test_deps = &.{"testkit"} },
.{ .name = "fleetsim", .libs = &.{"net"}, .deps = &.{ "modbus", "dnp3", "iec104", "s7comm", "bacnet", "enip", "opcua", "netsim" }, .test_deps = &.{"testkit"} },
.{ .name = "smtp", .libs = &.{"net"}, .deps = &.{"netaddr"}, .test_deps = &.{"testkit"} },
.{ .name = "imap", .libs = &.{"net"}, .test_deps = &.{"testkit"}, .live = true },
.{ .name = "iec61850", .libs = &.{"net"}, .deps = &.{"xml"}, .test_deps = &.{"testkit"} },
.{ .name = "iec62351", .libs = &.{"net"}, .deps = &.{ "x509", "rsa" } },
.{ .name = "s7comm", .libs = &.{"net"}, .test_deps = &.{"testkit"} },
.{ .name = "enip", .libs = &.{"net"}, .deps = &.{"netaddr"}, .test_deps = &.{"testkit"} },
.{ .name = "bacnet", .libs = &.{"net"}, .deps = &.{ "netaddr", "websocket" }, .test_deps = &.{"testkit"} },
.{ .name = "whois", .libs = &.{"net"}, .deps = &.{"netaddr"} },
.{ .name = "uci", .libs = &.{"os"} },
.{ .name = "mqtt", .libs = &.{"net"} },
.{ .name = "snmp", .libs = &.{"net"}, .test_deps = &.{"testkit"} },
.{ .name = "wireguard", .libs = &.{"net"}, .deps = &.{ "netlink", "genetlink", "chachapoly", "entropy", "netaddr" } },
.{ .name = "tc", .libs = &.{"net"}, .deps = &.{"netlink"}, .test_deps = &.{"testkit"} },
.{ .name = "traceroute", .libs = &.{"net"}, .deps = &.{ "icmp", "netaddr", "latency-stats" } },
.{ .name = "probe", .libs = &.{"net"}, .deps = &.{ "netaddr", "latency-stats" }, .test_deps = &.{"testkit"} },
.{ .name = "pathmtu", .libs = &.{"net"}, .deps = &.{ "icmp", "netaddr" } },
.{ .name = "l2disco", .libs = &.{"net"}, .deps = &.{"netaddr"} },
.{ .name = "upstream", .libs = &.{"web"}, .deps = &.{ "resilience", "probe" } },
.{ .name = "jwt", .libs = &.{"web"}, .deps = &.{ "http", "router", "p256" } },
.{ .name = "rbac", .libs = &.{"web"} },
.{ .name = "xml", .libs = &.{ "web", "net" } },
.{ .name = "xmldsig", .libs = &.{"web"}, .deps = &.{ "xml", "rsa", "p256" } },
.{ .name = "saml", .libs = &.{"web"}, .deps = &.{ "xmldsig", "xml", "xmlenc", "rsa", "x509", "datefmt" }, .heavy = true },
.{ .name = "xmlenc", .libs = &.{"web"}, .deps = &.{ "xml", "rsa", "aescbc", "aeskw" }, .heavy = true },
.{ .name = "aescbc", .libs = &.{ "web", "crypto" } },
.{ .name = "aeskw", .libs = &.{"web"} },
.{ .name = "jwe", .libs = &.{"web"}, .deps = &.{ "rsa", "p256", "aescbc", "aeskw" } },
.{ .name = "rdap", .libs = &.{"net"}, .deps = &.{ "http", "netaddr" } },
.{ .name = "blobstore", .libs = &.{"storage"}, .deps = &.{"hashdigest"} },
.{ .name = "procnet", .libs = &.{"net"}, .deps = &.{"netaddr"} },
.{ .name = "diskfree", .libs = &.{"os"} },
.{ .name = "diskusage", .libs = &.{"os"} },
.{ .name = "conntrack", .libs = &.{"net"}, .deps = &.{ "netlink", "netaddr" }, .test_deps = &.{"testkit"} },
.{ .name = "procrun", .libs = &.{"os"}, .deps = &.{"argsafe"} },
.{ .name = "dataset", .libs = &.{"storage"} },
.{ .name = "tabular", .libs = &.{"storage"}, .deps = &.{"dataset"} },
.{ .name = "jsonshape", .libs = &.{"storage"}, .deps = &.{"dataset"} },
.{ .name = "finstats", .libs = &.{"storage"}, .deps = &.{"dataset"} },
.{ .name = "filestore", .libs = &.{"storage"} },
.{ .name = "framing", .libs = &.{ "format", "os" } },
.{ .name = "datefmt", .libs = &.{"format"} },
.{ .name = "diagnostics", .libs = &.{"os"} },
.{ .name = "json5", .libs = &.{"format"} },
.{ .name = "yaml", .libs = &.{"format"} },
.{ .name = "jinja", .libs = &.{"format"}, .test_deps = &.{"testkit"} },
.{ .name = "cbor", .libs = &.{"format"} },
.{ .name = "protobuf", .libs = &.{ "format", "web" }, .test_deps = &.{"testkit"} },
.{ .name = "grpc", .libs = &.{"web"}, .deps = &.{ "http", "protobuf" }, .test_deps = &.{"testkit"} },
.{ .name = "webauthn", .libs = &.{"crypto"}, .deps = &.{ "cbor", "rsa", "p256", "x509" } },
.{ .name = "zipstream", .libs = &.{"format"} },
.{ .name = "qr", .libs = &.{"format"} },
.{ .name = "qrscan", .libs = &.{"format"}, .deps = &.{"qr"} },
.{ .name = "tz", .libs = &.{"format"}, .deps = &.{"datefmt"} },
.{ .name = "pollworker", .libs = &.{"os"} },
.{ .name = "ipcbus", .libs = &.{"os"}, .deps = &.{"framing"} },
.{ .name = "csvstream", .libs = &.{"format"} },
.{ .name = "csvsafe", .libs = &.{"format"} },
.{ .name = "numparse", .libs = &.{"format"}, .deps = &.{"decimal"} },
.{ .name = "argsafe", .libs = &.{"os"} },
.{ .name = "sessions", .libs = &.{"web"}, .deps = &.{ "router", "http", "cookies", "ramcache", "entropy" } },
.{ .name = "jobqueue", .libs = &.{"storage"}, .deps = &.{"kv"} },
.{ .name = "reconcilable", .libs = &.{"net"}, .deps = &.{"resilience"} },
.{ .name = "llmclient", .libs = &.{"web"}, .deps = &.{"http"} },
.{ .name = "rawsock", .libs = &.{"net"}, .deps = &.{"netaddr"} },
.{ .name = "encoding", .libs = &.{"format"} },
.{ .name = "syslog", .libs = &.{"net"}, .deps = &.{"datefmt"} },
.{ .name = "sntp", .libs = &.{"net"} },
.{ .name = "stun", .libs = &.{"net"}, .deps = &.{"netaddr"} },
.{ .name = "opcua", .libs = &.{"net"}, .deps = &.{ "rsa", "x509" }, .test_deps = &.{"testkit"}, .heavy = true, .live = true },
.{ .name = "noise", .libs = &.{"crypto"}, .deps = &.{"chachapoly"} },
.{ .name = "x509", .libs = &.{ "crypto", "net" }, .deps = &.{ "rsa", "slhdsa" } },
.{ .name = "ocsp", .libs = &.{"crypto"}, .deps = &.{ "x509", "rsa", "p256" }, .heavy = true },
.{ .name = "ocspcache", .libs = &.{"crypto"}, .deps = &.{ "ocsp", "http", "x509" } },
.{ .name = "dnssec", .libs = &.{"net"}, .deps = &.{ "dns", "rsa" } },
.{ .name = "dnp3", .libs = &.{"net"}, .deps = &.{"aeskw"} },
.{ .name = "slhdsa", .libs = &.{"crypto"}, .heavy = true },
.{ .name = "falcon", .libs = &.{"crypto"} },
.{ .name = "hqc", .libs = &.{"crypto"}, .heavy = true },
.{ .name = "dtls", .libs = &.{"crypto"}, .deps = &.{ "rsa", "x509", "chachapoly" }, .test_deps = &.{"testkit"}, .live = true },
.{ .name = "tlsresume", .libs = &.{"crypto"} },
.{ .name = "quic-crypto", .libs = &.{"crypto"}, .deps = &.{"chachapoly"} },
.{ .name = "sandbox", .libs = &.{"os"} },
.{ .name = "bip340", .libs = &.{"crypto"}, .deps = &.{"k256"} },
.{ .name = "taproot", .libs = &.{"crypto"}, .deps = &.{ "bip340", "k256" } },
.{ .name = "bitcointx", .libs = &.{"crypto"}, .deps = &.{"bip340"} },
.{ .name = "psbt", .libs = &.{"crypto"}, .deps = &.{ "bitcointx", "bitcoinscript" } },
.{ .name = "bitcoinscript", .libs = &.{"crypto"}, .deps = &.{ "bitcointx", "k256", "bip340", "ripemd160" } },
.{ .name = "btcp2p", .libs = &.{"crypto"}, .deps = &.{"bitcointx"} },
.{ .name = "lnwire", .libs = &.{"crypto"} },
.{ .name = "lninvoice", .libs = &.{"crypto"}, .deps = &.{ "bech32", "k256", "lnwire", "bip340" } },
.{ .name = "musig2", .libs = &.{"crypto"}, .deps = &.{ "bip340", "k256" } },
.{ .name = "sphinx", .libs = &.{ "crypto", "net" }, .deps = &.{"k256"} },
.{ .name = "bolt8", .libs = &.{"crypto"}, .deps = &.{ "noise", "k256" } },
.{ .name = "bolt3", .libs = &.{"crypto"}, .deps = &.{"k256"} },
.{ .name = "hpke", .libs = &.{"crypto"}, .deps = &.{ "p256", "chachapoly", "entropy" } },
.{ .name = "adaptor", .libs = &.{"crypto"}, .deps = &.{ "bip340", "k256" } },
.{ .name = "frost", .libs = &.{"crypto"}, .deps = &.{ "bip340", "k256" } },
.{ .name = "oscore", .libs = &.{"crypto"} },
.{ .name = "spake2plus", .libs = &.{"crypto"}, .deps = &.{"p256"} },
.{ .name = "ct25519", .libs = &.{"crypto"} },
.{ .name = "voprf", .libs = &.{"crypto"}, .deps = &.{"ct25519"} },
.{ .name = "opaque", .libs = &.{"crypto"}, .deps = &.{ "voprf", "ct25519" } },
.{ .name = "bulletproofs", .libs = &.{"crypto"}, .deps = &.{"ct25519"} },
.{ .name = "xmss", .libs = &.{"crypto"}, .heavy = true },
.{ .name = "minisign", .libs = &.{"crypto"}, .deps = &.{"entropy"}, .heavy = true },
.{ .name = "otp", .libs = &.{"crypto"} },
.{ .name = "ctap2pin", .libs = &.{"crypto"}, .deps = &.{"p256"} },
.{ .name = "bls12_381", .libs = &.{"crypto"}, .deps = &.{"entropy"}, .heavy = true },
.{ .name = "bbs", .libs = &.{"crypto"}, .deps = &.{ "bls12_381", "entropy" } },
.{ .name = "coconut", .libs = &.{"crypto"}, .deps = &.{"bls12_381"}, .heavy = true },
.{ .name = "tlock", .libs = &.{"crypto"}, .deps = &.{ "bls12_381", "entropy" } },
// `tlock` is a TEST-only dep: `ibe/src/kat_test.zig` drives `ibe`'s own
// encrypt/decrypt through `ibe.Scheme` with drand's ciphersuite, to
// byte-compare against the genuine drand-Go-produced ciphertext `tlock`
// already has frozen. The published `ibe` module never imports it --
// `zig build check-testonly` proves that.
.{ .name = "ibe", .libs = &.{"crypto"}, .deps = &.{ "bls12_381", "entropy" }, .test_deps = &.{"tlock"}, .heavy = true },
.{ .name = "bn254", .libs = &.{"crypto"}, .heavy = true },
.{ .name = "ed448", .libs = &.{"crypto"}, .deps = &.{"entropy"} },
.{ .name = "decaf448", .libs = &.{"crypto"}, .deps = &.{"ed448"} },
.{ .name = "paillier", .libs = &.{"crypto"}, .deps = &.{"montint"}, .heavy = true },
.{ .name = "threshold_ecdsa", .libs = &.{"crypto"}, .deps = &.{ "paillier", "montint" }, .heavy = true },
.{ .name = "dkg", .libs = &.{"crypto"}, .deps = &.{ "threshold_ecdsa", "paillier" }, .heavy = true },
.{ .name = "vdf", .libs = &.{"crypto"}, .deps = &.{"montint"} },
.{ .name = "signal", .libs = &.{"crypto"}, .deps = &.{ "chachapoly", "ct25519", "entropy" } },
.{ .name = "mls", .libs = &.{"crypto"}, .deps = &.{"hpke"} },
.{ .name = "megolm", .libs = &.{"crypto"}, .deps = &.{ "aescbc", "entropy" } },
.{ .name = "ebpf", .libs = &.{"net"}, .deps = &.{"netlink"}, .test_deps = &.{"testkit"} },
.{ .name = "xdp-classifier", .libs = &.{"net"}, .deps = &.{"ebpf"} },
.{ .name = "ecvrf", .libs = &.{"crypto"}, .deps = &.{"ct25519"} },
.{ .name = "fss", .libs = &.{"crypto"} },
.{ .name = "pir", .libs = &.{"crypto"}, .deps = &.{"fss"} },
.{ .name = "bfv", .libs = &.{"crypto"}, .deps = &.{"entropy"} },
.{ .name = "groth16", .libs = &.{"crypto"}, .deps = &.{"bn254"} },
// Not heavy: the parameter derivation + all 30 tests run in 5s under
// -Dstrict-debug, well under the >15s threshold (and a Debug compile of
// this module is ~1s against ~27s at ReleaseSafe, so marking it heavy
// would cost more than it saves).
.{ .name = "poseidon", .libs = &.{"crypto"}, .deps = &.{ "bn254", "bls12_381" }, .test_deps = &.{"testkit"} },
// Not heavy, despite the inverse S-box (72 multiplies per element per
// half-round). Measured serially on this host: strict-Debug compile ~8.5s
// + run ~1.0s = 9.5s, under the >15s threshold — and a ReleaseSafe compile
// of this module is ~46s (comptime SHAKE256 derivation + heavily unrolled
// field code), so marking it heavy would cost 5x what it saves.
.{ .name = "rescue", .libs = &.{"crypto"} },
.{ .name = "tfhe", .libs = &.{"crypto"}, .deps = &.{"entropy"}, .heavy = true },
.{ .name = "montint", .libs = &.{"crypto"}, .heavy = true },
.{ .name = "chachapoly", .libs = &.{"crypto"} },
.{ .name = "k256", .libs = &.{"crypto"} },
.{ .name = "p256", .libs = &.{ "crypto", "web" } },
.{ .name = "ripemd160", .libs = &.{"crypto"} },
.{ .name = "bech32", .libs = &.{"crypto"}, .deps = &.{"ripemd160"} },
.{ .name = "bip32", .libs = &.{"crypto"}, .deps = &.{ "k256", "ripemd160", "bech32" } },
// Scaffold more here (copy modules/_template) — see CONVENTIONS.md
// "How to add a module" and the README "Roadmap / Non-goals" sections.
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const test_step = b.step("test", "Run every module's tests");
// `heavy` modules (see the Module doc comment) are compute-bound: Debug
// makes their tests ~5x slower and they are the suite's critical path, so
// a Debug request builds them at ReleaseSafe instead — same safety checks,
// a fraction of the wall clock. `-Dstrict-debug` opts back into real Debug
// for the CI matrix. Any explicit non-Debug mode is honoured as-is.
const strict_debug = b.option(
bool,
"strict-debug",
"Build compute-heavy modules at Debug too (much slower; the CI matrix uses this)",
) orelse false;
const heavy_optimize: std.builtin.OptimizeMode =
if (optimize == .Debug and !strict_debug) .ReleaseSafe else optimize;
// `-Dtest-filter=<substring>` — compile only the tests whose name contains
// one of these. Added for `scripts/fuzz-sweep.sh`: `--fuzz` puts every fuzz
// test of a module in ONE process, so the first harness to crash takes the
// rest of that module's harnesses down with it and they are reported as
// though they had run. The sweep re-runs a crashed module one harness at a
// time through this option, which is the only way to give the survivors a
// process (and a budget) of their own. Useful by hand too:
// `zig build test-http -Dtest-filter="fuzz: ChunkedReader"`.
const test_filters = b.option(
[]const []const u8,
"test-filter",
"Only build tests whose name contains this substring (repeatable)",
) orelse &.{};
// Gate for the "a body nothing references is never analysed" class — see
// the `force_mod` block in pass 2 for what it compiles and why.
const check_pubfn_reach = b.step("check-pubfn-reach", "Analyse every non-generic public declaration, including the ones no test reaches");
// `check-examples` — build `modules/<name>/example/main.zig` as a real
// consumer binary. See the `example` block in pass 2 for the one class it
// covers that no test in this repository can.
const check_examples = b.step("check-examples", "Build each module's example as an outside consumer would");
// `run-examples` — the same set, LINKED AND RUN. Compiling an example
// cannot see the class of defect examples exist to find: on 2026-08-23 a
// single sweep of this step found a dangling stack slice in `btcp2p`
// (whose own tests passed because they read the slice in the same
// expression), a leak in `ethfrag`, a wrong-tag union read in `iec104`,
// and twelve examples that had never run at all. It is a separate step
// from `check-examples` only because it costs a link plus a process per
// module; both are wired into the full lane.
const run_examples = b.step("run-examples", "Build AND run every module's example");
// CONVENTIONS.md 7.2: a module over either trigger must have an example.
// This used to also reconcile a `.example` bool in `module_list` against
// the tree -- a declaration that could drift, and did: eight example files
// sat unbuilt for an afternoon with `check-examples` green. The bool is
// gone; the file on disk is the declaration, so that class cannot recur
// and only the rule itself is left to check.
const check_example_rule = b.allocator.create(std.Build.Step) catch @panic("OOM");
check_example_rule.* = std.Build.Step.init(.{
.id = .custom,
.name = "check-example-rule",
.owner = b,
.makeFn = checkExampleRule,
});
check_examples.dependOn(check_example_rule);
// Also reachable on its own: which modules OWE an example has nothing to
// do with whether the existing ones compile, and it answers in
// milliseconds where the sweep takes minutes.
b.step("check-example-rule", "Check CONVENTIONS 7.2: every module over a trigger has an example").dependOn(check_example_rule);
// The `own_files` list `scripts/force-pubfn-reach.zig` needs, built ONCE per
// module and looked up by name. Shared rather than rebuilt at each use so
// the native gate and every cross-compiled pair cannot be handed different
// lists -- two copies of a derived fact is the shape this repository keeps
// finding to have drifted.
var own_files_mods = std.StringHashMap(*std.Build.Module).init(b.allocator);
for (module_list) |m| own_files_mods.put(m.name, ownFilesModule(b, m.name)) catch @panic("OOM");
// Pass 1: create each module so inter-module deps can be wired in pass 2.
var mods = std.StringHashMap(*std.Build.Module).init(b.allocator);
for (module_list) |m| {
const mod = b.addModule(m.name, .{
.root_source_file = b.path(b.fmt("modules/{s}/src/root.zig", .{m.name})),
.target = target,
.optimize = if (m.heavy) heavy_optimize else optimize,
});
mods.put(m.name, mod) catch @panic("OOM");
}
// Pass 2: wire deps + register a test build per module.
for (module_list) |m| {
const mod = mods.get(m.name).?;
for (m.deps) |dep| mod.addImport(dep, mods.get(dep).?);
// A module with test-only deps gets a SECOND module object over the
// same source, carrying the extra imports. `mod` -- the one
// `b.addModule` published above, and the one a consumer gets -- never
// sees them. Modules without test_deps test `mod` directly, so the
// common path is unchanged.
const test_root = if (m.test_deps.len == 0) mod else blk: {
const t = b.createModule(.{
.root_source_file = b.path(b.fmt("modules/{s}/src/root.zig", .{m.name})),
.target = target,
.optimize = if (m.heavy) heavy_optimize else optimize,
});
for (m.deps) |dep| t.addImport(dep, mods.get(dep).?);
for (m.test_deps) |dep| t.addImport(dep, mods.get(dep).?);
break :blk t;
};
// ⭐ `.name` defaults to "test", which made all 225 compilations
// indistinguishable in every place zig names a step: the progress tree
// said `compile test` whichever module was building, and so did
// `--summary`. Naming them after the module is what makes a build or a
// hang self-identifying — without it no amount of log plumbing can say
// WHICH module is the slow one.
const unit_tests = b.addTest(.{
.name = m.name,
.root_module = test_root,
.filters = test_filters,
});
const run = b.addRunArtifact(unit_tests);
test_step.dependOn(&run.step);
// ⭐ `zig build` COMPILES every module's tests and runs none of them.
//
// The default step used to be empty — nothing in this file installed an
// artifact, so `zig build` succeeded in milliseconds having done
// nothing, which is a confusing thing for the top-level command of a
// library collection to do.
//
// Giving it the Compile steps splits the gate's one opaque number in
// two. `zig build` is then the compile of all 225 test binaries and
// `zig build test` is, with that cache warm, close to pure test
// execution — and on 2026-08-14 nobody could say which of the two spent
// the five hours that got a tag's matrix killed by GitHub's 6h job cap.
//
// Deliberately NOT `installArtifact`: that adds a Step.InstallArtifact
// which COPIES each binary into zig-out/ (225 of them, for nothing) and
// it is the copy, not the build, that the install step then names. The
// dependency is on the Compile step itself, the same shape
// `check-testonly` already uses below for its probe objects.
b.getInstallStep().dependOn(&unit_tests.step);
// Per-module test step: `zig build test-<name>`.
const one = b.step(b.fmt("test-{s}", .{m.name}), b.fmt("Test the {s} module", .{m.name}));
one.dependOn(&run.step);
// `check-pubfn-reach`: compile a second root over the SAME module graph
// whose only job is to take a reference to every public declaration.
//
// ⭐ This is not redundant with `unit_tests` above, and that is the
// entire point. Zig analyses a function body only when something
// references it, so `unit_tests` — an `addTest` over the module root —
// walks straight past any `pub fn` no test calls. Proven by mutation on
// 2026-08-21: a deliberate type error injected into `nftables`
// `RuleBuilder.reject()` compiled AND linked a 20 MB test binary that
// exited 0 green, while this step went red on it.
//
// Measured the same day: 403 of 9626 public functions are unreachable
// from any test, spread over 106 modules, 90 of them declared in a
// module's own `root.zig` — i.e. on the published surface. All 403
// compile today, so this is a standing guard over a risk surface, not a
// burn-down of known breakage.
//
// Compile-only, like `check-testonly`'s probes: the forcing test has no
// behaviour to run, and a reference that reaches code generation has
// already proven what the step exists to prove.
const force_mod = b.createModule(.{
.root_source_file = b.path("scripts/force-pubfn-reach.zig"),
.target = target,
.optimize = if (m.heavy) heavy_optimize else optimize,
});
force_mod.addImport("m", test_root);
force_mod.addImport("own_files", own_files_mods.get(m.name).?);
const force_tests = b.addTest(.{
.name = b.fmt("force-{s}", .{m.name}),
.root_module = force_mod,
});
check_pubfn_reach.dependOn(&force_tests.step);
// ⭐ The one class nothing else here can cover: **is the published API
// sufficient to do the job?**
//
// Every test in this collection lives in the same file as the code it
// tests, so it reads private declarations freely and its build carries
// `test_deps` the published module never gets. It can therefore pass
// while a function is unreachable from outside, a type needed to call
// it is not exported, or an error is not nameable. That is not a
// hypothetical either — it is exactly how `diskfree` shipped two
// functions that did not compile: nothing had ever imported it.
//
// So the example is wired to `mod` — the module `b.addModule`
// published, with `deps` only and NO `test_deps`, exactly what
// `@import("<name>")` hands a downstream project.
//
// Demonstrated on `l2disco`, 2026-08-21, by dropping the `pub` from a
// type its public API needs: `zig build test-l2disco` stayed green
// (the tests are inside, they never cross the boundary) and so did
// `check-pubfn-reach` (the declaration still exists, it is just no
// longer public, so the walk silently covers less) — only this step
// went red. The three gates are complements, not overlaps.
//
// ⭐ NO LONGER compile-only, and the difference matters. Each example
// now also carries a `run-example-<name>` step (below), and a run step
// forces the artifact to be emitted and LINKED -- so this compile no
// longer passes `-fno-emit-bin`, and consumer-BINARY facts that used to
// sit outside it (link-time reach: a MIPS `PC16` fixup overflows at
// ±128 KB) are inside it now. What is still outside is what a module
// DRAGS IN by size (`http`'s TLS-vs-plaintext split was 334 KB), which
// needs a measured binary; `scripts/check-http-sizeprobe.sh` is the one
// place this repository does that.
//
// The comment this replaces said the opposite, and had said it since
// the run steps landed: a gate whose documented scope is narrower than
// its real one is the same defect as the reverse, just the lucky way
// round -- someone reads it and builds a second gate for a class this
// one already covers.
//
// Scoped, not universal (survey 2026-08-21): 78 of 229 modules already
// have an in-repo consumer, so their boundary is exercised by real
// code; 93 more have a public surface under 25 functions, a third of
// them anchored to published test vectors, where an internal vector
// test is strictly stronger than an example. The 57 that are left have
// no consumer AND a wide surface — those get examples, largest first.
if (moduleHasExample(b, m.name)) {
const example_mod = b.createModule(.{
.root_source_file = b.path(b.fmt("modules/{s}/example/main.zig", .{m.name})),
.target = target,
.optimize = optimize,
});
example_mod.addImport(m.name, mod);
// Its declared `deps` too — they are published modules, so a real
// consumer can depend on them exactly as this example does. What
// an example must NOT get is `test_deps` or any private
// declaration, and it gets neither. `conntrack` and `wireguard`
// are the honest case: their own docs describe `netlink` as the
// shared transport a caller reaches for, so forcing them to
// re-export its attribute codec would invent API to satisfy a
// rule rather than a consumer.
//
// ⚠ What that argument costs, measured 2026-08-23 rather than left
// implied: it was made about two modules and it applies to 151 —
// every module with any `.deps` — of which 65 examples really do
// import one. For those 65 this step no longer answers "is THIS
// module's published API sufficient?" but "is this module PLUS its
// published deps sufficient?", which is a weaker question. It is
// still the right trade (the alternative invents API), but the
// claim above should not be read as covering the other 63.
//
// ⚠ And the boundary is enforced at USE, not at declaration: a
// top-level `const x = @import("testkit");` an example never uses
// is not resolved, so it compiles. Only a used import goes red.
for (m.deps) |dep| example_mod.addImport(dep, mods.get(dep).?);
const example = b.addExecutable(.{
.name = b.fmt("example-{s}", .{m.name}),
.root_module = example_mod,
});
check_examples.dependOn(&example.step);
// Per-module entry point, so one example can be compiled on its
// own: `zig build example-<name>`. Without it the only way to
// check a single new example is the whole `check-examples` sweep,
// which fails on every OTHER module still missing one.
const one_example = b.step(b.fmt("example-{s}", .{m.name}), b.fmt("Build the {s} example", .{m.name}));
one_example.dependOn(&example.step);
// `zig build run-example-<name>` — and this one links and RUNS it.
// Worth its own step because compiling an example is not what
// examples are for: every leak this repository has found through
// one was found by running it under a leak-checking allocator
// (14 in `finstats` alone, 2026-08-23), and a compile cannot see
// any of them. Deliberately NOT wired into `check-examples`: some
// examples want a socket, a daemon or root, and the moment the
// sweep runs them it either fails on a laptop with no network or
// grows a hand-kept list of which ones are safe -- a declaration
// that would drift the way `.example` did.
const run_example = b.addRunArtifact(example);
b.step(b.fmt("run-example-{s}", .{m.name}), b.fmt("Build AND run the {s} example", .{m.name})).dependOn(&run_example.step);
run_examples.dependOn(&run_example.step);
}
}
// `zig build check-portable` — compile every module's TESTS for each
// real target it declares in `meta.targets` (CONVENTIONS.md §4), checked
// against a known-failures baseline keyed by (module, target).
//
// SCHEMA (2026-08-18). This gate used to sweep every `platform = .any`
// module for wasm32 alone -- one field conflating two different claims,
// where a module's AUTHOR intends it to run versus where anyone has
// PROVEN it runs, that a consumer reads as the latter. `meta.targets`
// replaces the guess with a set of concrete per-target claims
// (`PortableTarget` below); a module that never intended wasm32 (`http`,
// `aaa-gate`, `ratelimit`, `bbs` -- real std.Thread/libc/std.os.linux use,
// not portability defects) simply does not declare it and stops being
// swept for it -- the false-alarm shape this schema exists to remove.
// `meta.platform` is UNCHANGED and stays on every module: it is still the
// informal one-line claim behind ~40 prose references across
// SPEC.md/README.md/root.zig doc comments (grepped 2026-08-18; all prose,
// no other machine reader -- the only one was `declaresAnyPlatform`,
// replaced below by `parseMetaTargets`), but it is no longer
// gate-enforced. `meta.targets` is the enforceable claim from here on.
//
// WHY A BUILD STEP AND NOT A COMPTIME CHECK. The obvious idea is a
// `comptime` assertion inside the module, and it cannot work: comptime is
// evaluated FOR the target being built, so on an x86_64 build `usize` is 64
// bits at comptime too and there is nothing for an assertion to notice. The
// bug this catches -- `qr`'s BitWriter shifting a `usize` by a `u6`, legal
// on a 64-bit target and a compile error on a 32-bit one -- is invisible
// until something actually compiles for 32 bits. Nothing did: every lane in
// the CI matrix is 64-bit, arm64 included, so a module can claim a target
// for months while being unbuildable on half of what that claim covers.
//
// ⭐ `addTest`, NOT `addObject`. This gate originally used `zig build-obj`,
// and that was a structural blind spot: Zig analyses a container's function
// BODIES lazily, only once something calls them, and `build-obj` never
// calls anything -- it type-checks signatures and stops. Measured
// 2026-08-18: `modules/http/src/Server.zig`'s `formatHttpDate` indexes
// `day_names[day.day % 7]` where `day.day` is a `u47` -- a real compile
// error on any 32-bit target, since a `u47` cannot implicitly narrow to a
// 32-bit `usize` -- and `check-portable` reported 196/196 green anyway,
// because nothing in an object build ever CALLS `formatHttpDate`. A `zig
// build-obj` of the whole module can be 100% green while every public
// function in it is unbuildable. `addTest` compiles the module's own test
// binary -- the real entry point that calls the module's real functions --
// which is what forces the bodies to be analysed. It is deliberately NOT
// run (`--test-no-exec` shape: depend on the `Compile` step, never wrap it
// in `addRunArtifact`) -- none of the cross-compiled targets below have a
// host to run on anyway, and the gate only ever needed the compile+link to
// happen, not the result.
//
// wasm32-**wasi**, not wasm32-freestanding. `usize`/pointer width -- the
// property this axis exists to probe -- comes from the CPU arch
// (`wasm32`), not the OS tag, so this does not weaken the check. Freestanding
// has no OS at all, and the default Zig test runner needs one (it reaches
// `std.Io.Threaded` for its RNG seed, `posix.STDIN_FILENO`, argv, ...);
// `addObject` never instantiated that runner, so freestanding was never
// exercised against it before. Measured: an `addTest` at wasm32-freestanding
// fails to compile the STD TEST RUNNER itself (`posix.system` has no
// `getrandom`/`IOV_MAX`/`STDIN_FILENO` for freestanding) on a trivial module
// with no bug at all -- that is a gap in `std`'s freestanding surface, not a
// finding about any module here, and it would drown every real result.
// wasi gives the runner the OS surface it needs while keeping 32-bit
// pointers, which is the only property that axis measures.
//
// The declared set is read from each module's own source rather than
// repeated here. `pub const meta` is the canonical declaration
// (CONVENTIONS.md), and a second list in this file would drift from it
// silently -- the failure mode being a module that quietly stops being
// checked.
const portable_measure_all = b.option(
bool,
"portable-measure-all",
"check-portable: create a portable-<name>-<target> compile step for every module x cross-compiled target, regardless of meta.targets -- probe a target's true status before declaring it",
) orelse false;
const portable = b.step(
"check-portable",
"Compile every module's tests for each target in its meta.targets, checked against scripts/portable-known-failures.tsv",
);
// Pass 1: parse every module's declared set once, in module_list order,
// at graph-build time -- step CREATION below (pass 3) needs it to decide
// which `portable-<name>-<target>` steps to make. A module whose
// declaration cannot be read (no `meta` block, no `.targets` field, an
// unknown/duplicate token, or a set missing the mandatory `.linux64`)
// contributes no entry to `module_targets` and instead a message to
// `portable_decl_errors`, surfaced by `PortableBaselineStep.make` below --
// this is what makes "a module missing its declaration" a gate failure
// (bolt3, before this commit, had a `meta` block with no platform claim
// at all and nothing noticed).
var module_targets = std.StringHashMap([]const PortableTarget).init(b.allocator);
var portable_decl_errors: std.ArrayList([]const u8) = .empty;
for (module_list) |m| {
const parsed = parseMetaTargets(b, b.graph.io, m.name);
if (parsed.err) |e| {
portable_decl_errors.append(b.allocator, b.fmt("{s}: {s}", .{ m.name, e })) catch @panic("OOM");
continue;
}
module_targets.put(m.name, parsed.targets) catch @panic("OOM");
}
// Snapshot of `module_targets` as a plain, deterministically-sorted slice
// -- `check-portable-table` (below) renders a README table from this, and
// a hash map's iteration order is not something a checked-in file's row
// order should depend on. Same data as `module_targets`, just ordered.
var portable_decls: std.ArrayList(PortableDecl) = .empty;
{
var it = module_targets.iterator();
while (it.next()) |e| portable_decls.append(b.allocator, .{ .module = e.key_ptr.*, .targets = e.value_ptr.* }) catch @panic("OOM");
}
std.mem.sort(PortableDecl, portable_decls.items, {}, struct {
fn lessThan(_: void, x: PortableDecl, y: PortableDecl) bool {
return std.mem.lessThan(u8, x.module, y.module);
}
}.lessThan);
// Pass 2: one Module graph per cross-compiled target -- same shape as
// pass 1 in `build()` above (the native `mods` map), just once per
// `PortableTarget.query()`.
var cross_mods: [cross_compile_targets.len]std.StringHashMap(*std.Build.Module) = undefined;
var cross_resolved: [cross_compile_targets.len]std.Build.ResolvedTarget = undefined;
for (cross_compile_targets, 0..) |ct, ti| {
const rt = b.resolveTargetQuery(ct.query().?);
cross_resolved[ti] = rt;
var map = std.StringHashMap(*std.Build.Module).init(b.allocator);
for (module_list) |m| {
map.put(m.name, b.createModule(.{
.root_source_file = b.path(b.fmt("modules/{s}/src/root.zig", .{m.name})),
.target = rt,
.optimize = .ReleaseSmall,
})) catch @panic("OOM");
}
cross_mods[ti] = map;
}
for (cross_compile_targets, 0..) |_, ti| {
for (module_list) |m| {
const mod = cross_mods[ti].get(m.name).?;
for (m.deps) |dep| mod.addImport(dep, cross_mods[ti].get(dep).?);
}
}
// Pass 3: `portable-<name>-<target>` per (module, target) pair the
// module DECLARES (or, with `-Dportable-measure-all`, every pair --
// that flag is how this schema's own seed data was measured: create
// every step, sweep it by hand, THEN write `meta.targets` from the
// result instead of from optimism). Same shape as the old single-target
// gate's per-module step: deliberately UNGATED by the baseline below --
// this is the module's true, unmasked status, for
// `zig build portable-<name>-<target>` by hand.
var portable_pairs: std.ArrayList(PortablePair) = .empty;
for (cross_compile_targets, 0..) |ct, ti| {
for (module_list) |m| {
const declared = module_targets.get(m.name) orelse &.{};
var is_declared = false;
for (declared) |t| {
if (t == ct) {
is_declared = true;
break;
}
}
if (!is_declared and !portable_measure_all) continue;
const step_name = b.fmt("portable-{s}-{s}", .{ m.name, ct.label() });
// Same shape as the per-module `test-<name>` step in pass 2
// above: a module with `test_deps` gets a second module object
// carrying the extra imports, so the compile forces analysis of
// exactly the same test bodies `zig build test-<name>` does.
const test_root = if (m.test_deps.len == 0) cross_mods[ti].get(m.name).? else blk: {
const t = b.createModule(.{
.root_source_file = b.path(b.fmt("modules/{s}/src/root.zig", .{m.name})),
.target = cross_resolved[ti],
.optimize = .ReleaseSmall,
});
for (m.deps) |dep| t.addImport(dep, cross_mods[ti].get(dep).?);
for (m.test_deps) |dep| t.addImport(dep, cross_mods[ti].get(dep).?);
break :blk t;
};
const cross_test = b.addTest(.{ .name = step_name, .root_module = test_root });
const one = b.step(
step_name,
b.fmt("Compile {s}'s tests and public surface for {s} (true status, not baseline-checked)", .{ m.name, ct.label() }),
);
one.dependOn(&cross_test.step);
// ⭐ The cross compile above is an `addTest` over the module root,
// and therefore carries the SAME blind spot `check-pubfn-reach`
// exists to close on the native target: Zig analyses a function
// body only when something references it, so a `pub fn` no test
// reaches is never checked against this target at all. Measured
// 2026-08-21 on native: 403 of 9626 public functions are in that
// position. Without this, "module X compiles for Windows" means
// "the part of X some test happens to call compiles for Windows",
// which is exactly the kind of claim this repository keeps finding
// to be weaker than it reads.
//
// So each declared (module, target) pair also compiles the forcing
// root. Deliberately part of `portable-<name>-<target>` rather than
// a new gate: the pair is the unit `scripts/portable-known-failures.tsv`
// is keyed by, so a pair that already fails stays one row, and a
// pair that starts failing BECAUSE of this fails as itself rather
// than under a second baseline that would have to be kept in sync.
const cross_force_mod = b.createModule(.{
.root_source_file = b.path("scripts/force-pubfn-reach.zig"),
.target = cross_resolved[ti],
.optimize = .ReleaseSmall,
});
cross_force_mod.addImport("m", test_root);
cross_force_mod.addImport("own_files", own_files_mods.get(m.name).?);
const cross_force = b.addTest(.{
.name = b.fmt("force-{s}", .{step_name}),
.root_module = cross_force_mod,
});
one.dependOn(&cross_force.step);
if (is_declared) portable_pairs.append(b.allocator, .{ .module = m.name, .target = ct }) catch @panic("OOM");
}
}
// The gate itself does NOT `dependOn` the per-pair compile steps above --
// doing so would make `check-portable` red for every (module, target)
// already on the known-failures baseline, which is the opposite of what
// a baseline is for (CONVENTIONS.md has no baseline precedent for a
// whole-module failure, but `check-global-alloc`'s `global-alloc-ok:`
// markers set the standard this follows: an entry means "seen and
// accounted for", not "ignored", and a STALE entry -- one that now
// passes -- is itself a failure). Instead `PortableBaselineStep`
// re-invokes `zig build portable-<name>-<target>...` as a subprocess (the
// same shape `DarkTestsStep` uses to shell out to `dark-tests.sh`), reads
// the per-pair PASS/FAIL from `--summary all`, and diffs that against
// `scripts/portable-known-failures.tsv` itself.
const portable_baseline = b.allocator.create(PortableBaselineStep) catch @panic("OOM");
portable_baseline.* = .{
.step = std.Build.Step.init(.{
.id = .custom,
.name = "check-portable",
.owner = b,
.makeFn = PortableBaselineStep.make,
}),
.pairs = portable_pairs.items,
.decl_errors = portable_decl_errors.items,
};
portable.dependOn(&portable_baseline.step);
// `zig build check-portable-table` / `zig build gen-portable-table` — the
// consumer-facing half of the schema above. `check-portable` proves a
// claim; nothing until now let a consumer SEE it without reading 228
// `root.zig` files and a TSV. This renders the README's "Portability"
// table from exactly the two sources `check-portable` itself reads --
// `module_targets`' declared sets (snapshotted into `portable_decls`
// above) and `scripts/portable-known-failures.tsv` -- so the table can
// never assert a status the gate did not itself check, and a stale table
// (edited by hand, or left behind after a module's `.targets` changed)
// fails the build exactly as a stale baseline row does. Two named steps
// sharing one `make`: `check-portable-table` (the gate; `.write = false`)
// and `gen-portable-table` (the fix; `.write = true`), so "verify" and
// "regenerate" can never drift into two different rendering paths.
const portable_table_check = b.allocator.create(PortableTableStep) catch @panic("OOM");
portable_table_check.* = .{
.step = std.Build.Step.init(.{
.id = .custom,
.name = "check-portable-table",
.owner = b,
.makeFn = PortableTableStep.make,
}),
.decls = portable_decls.items,
.decl_errors = portable_decl_errors.items,
.write = false,
};
b.step(
"check-portable-table",
"Verify README.md's generated Portability table matches meta.targets + the known-failures baseline",
).dependOn(&portable_table_check.step);
const portable_table_gen = b.allocator.create(PortableTableStep) catch @panic("OOM");
portable_table_gen.* = .{
.step = std.Build.Step.init(.{
.id = .custom,
.name = "gen-portable-table",
.owner = b,
.makeFn = PortableTableStep.make,
}),
.decls = portable_decls.items,
.decl_errors = portable_decl_errors.items,
.write = true,
};
b.step(
"gen-portable-table",
"Regenerate README.md's Portability table from meta.targets + the known-failures baseline",
).dependOn(&portable_table_gen.step);
// `zig build check-libs-table` / `zig build gen-libs-table` — the
// consumer-facing half of `Module.libs`. `check-catalog` proves each
// module's PRIMARY library matches the section its row is printed under;
// this renders the six-library overview from the same field, so the
// cross-tags (the whole point of the plural) are visible without reading
// module_list, and a table left behind after a `.libs` edit fails the
// build exactly as a stale portability table does.
inline for (.{ .{ false, "check-libs-table", "Verify README.md's generated Libraries table matches module_list's `.libs`" }, .{ true, "gen-libs-table", "Regenerate README.md's Libraries table from module_list's `.libs`" } }) |cfg| {
const st = b.allocator.create(LibsTableStep) catch @panic("OOM");
st.* = .{
.step = std.Build.Step.init(.{ .id = .custom, .name = cfg[1], .owner = b, .makeFn = LibsTableStep.make }),
.write = cfg[0],
};
b.step(cfg[1], cfg[2]).dependOn(&st.step);
}
// `zig build gen-catalog` — the fix half of the grouping check
// inside `check-catalog`. Edit a module's `.libs`, run this, commit: the
// catalog row moves to the section the field names. Row TEXT is never
// generated (see CatalogSectionsStep's doc comment for the measurement
// that settled that), so this is a formatter for arrangement, not a
// renderer for content.
inline for (.{
.{ false, "check-catalog-table", "Verify README.md's module catalog matches module_list + each module's meta.doc/meta.platform_note" },
.{ true, "gen-catalog", "Regenerate README.md's module catalog from module_list + each module's meta.doc/meta.platform_note" },
}) |cfg| {
const st = b.allocator.create(CatalogStep) catch @panic("OOM");
st.* = .{
.step = std.Build.Step.init(.{ .id = .custom, .name = cfg[1], .owner = b, .makeFn = CatalogStep.make }),
.write = cfg[0],
};
b.step(cfg[1], cfg[2]).dependOn(&st.step);
}
// `zig build app-list` — the names in `example_apps`, one per line, so
// `scripts/check-apps.sh` reads the registry instead of globbing (a glob
// would silently include a directory nobody declared).
{
const st = b.allocator.create(std.Build.Step) catch @panic("OOM");
st.* = std.Build.Step.init(.{ .id = .custom, .name = "app-list", .owner = b, .makeFn = printAppList });
b.step("app-list", "Print the example-apps/ projects declared in build.zig, one per line").dependOn(st);
}
// `zig build check-package` — see `checkPackagePaths`.
{
const st = b.allocator.create(std.Build.Step) catch @panic("OOM");
st.* = std.Build.Step.init(.{ .id = .custom, .name = "check-package", .owner = b, .makeFn = checkPackagePaths });
b.step("check-package", "Verify build.zig.zon's .paths ships LICENSE and NOTICE").dependOn(st);
}
// `zig build check-scripts-doc` — see `checkScriptsDoc`.
{
const st = b.allocator.create(std.Build.Step) catch @panic("OOM");
st.* = std.Build.Step.init(.{ .id = .custom, .name = "check-scripts-doc", .owner = b, .makeFn = checkScriptsDoc });
b.step("check-scripts-doc", "Verify scripts/README.md names every file in scripts/").dependOn(st);
}
// `zig build check-testonly` — prove a test-only dep really is test-only.
//
// The claim `test_deps` makes is that the PUBLISHED module never needs
// them. Nothing checked it, and no ordinary build could: Zig analyses
// container-level decls lazily, so an `@import("testkit")` sitting unused
// in a module's non-test code is simply never looked at. Verified by
// planting `pub const leaked_probe = testkit.verbose_skip_env;` in
// `netlink` -- every dependent still built green.
//
// So force the analysis: for each such module, compile a consumer-shaped
// probe that imports ONLY the published module (deps, no test_deps) and
// calls `refAllDeclsRecursive` on it. A leak into public non-test code is
// then a compile error naming the missing module.
const testonly = b.step("check-testonly", "Prove each test_deps module isn't needed by the published module");
for (module_list) |m| {
if (m.test_deps.len == 0) continue;
const wf = b.addWriteFiles();
const src = wf.add(b.fmt("probe_{s}.zig", .{m.name}), b.fmt(
\\// Generated by build.zig's check-testonly step. See it for why.
\\//
\\// The reference walk is hand-rolled because `std.testing.refAllDecls`
\\// opens with `if (!builtin.is_test) return;` -- in a non-test build,
\\// which is exactly this probe, it does nothing at all. Using it here
\\// would have produced a check that always passes.
\\const published = @import("{s}");
\\
\\fn refAll(comptime T: type, comptime depth: u8) void {{
\\ @setEvalBranchQuota(200_000);
\\ switch (@typeInfo(T)) {{