Repository navigation
Expand file tree
/
Copy pathbuild.zig
More file actions
executable file
·7323 lines (6787 loc) · 368 KB
/
Copy pathbuild.zig
File metadata and controls
executable file
·7323 lines (6787 loc) · 368 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");
const stack_budget = @import("src/base/stack_budget.zig");
const builtin = @import("builtin");
const downstream_package = @import("src/build/downstream_package.zig");
const modules = @import("src/build/modules.zig");
const zig_test_suites = @import("src/build/zig_test_suites.zig");
const glibc_stub_build = @import("src/build/glibc_stub.zig");
const ci_steps = @import("src/build/ci_steps.zig");
const roc_target = @import("src/target/mod.zig");
const TestFixturePlan = @import("src/build/test_fixtures.zig").Plan;
const isImmutableNixStorePath = @import("src/build/nix_path.zig").isImmutableStorePath;
var test_fixtures: TestFixturePlan = undefined;
const Dependency = std.Build.Dependency;
const OptimizeMode = std.builtin.OptimizeMode;
const ResolvedTarget = std.Build.ResolvedTarget;
const Step = std.Build.Step;
var build_checks_exe: ?*Step.Compile = null;
fn buildChecksRun(b: *std.Build, command: []const u8) *Step.Run {
const exe = build_checks_exe orelse blk: {
// ReleaseFast on purpose. The source checks read every Zig file under
// src/: together they take about 0.3 s this way and 6 s as a Debug
// build, which is what Debug would save in compile time on a cold
// cache and a loss on every run after that.
const tool = b.addExecutable(.{
.name = "roc-build-checks",
.root_module = b.createModule(.{
.root_source_file = b.path("src/build/check_runner.zig"),
.target = hostToolTarget(b, .fast),
.optimize = .fast,
}),
});
build_checks_exe = tool;
break :blk tool;
};
const run = b.addRunArtifact(exe);
run.step.name = command;
run.addArg(command);
run.setCwd(b.path("."));
// Check commands operate on declared, staged source trees. Git/JJ and
// mutation commands intentionally execute every time.
if (std.mem.startsWith(u8, command, "check-") and
!std.mem.eql(u8, command, "check-snapshot-diff"))
{
const inputs = b.addWriteFiles();
_ = inputs.addCopyDirectory(b.path("src"), "src", .{ .include_extensions = &.{ ".zig", ".roc", ".pl" } });
_ = inputs.addCopyDirectory(b.path("test"), "test", .{ .include_extensions = &.{".roc"} });
_ = inputs.addCopyDirectory(b.path("ci"), "ci", .{ .exclude_extensions = &.{ ".pyc", ".pyo" } });
_ = inputs.addCopyFile(b.path("design.md"), "design.md");
run.setCwd(inputs.getDirectory());
run.expectExitCode(0);
}
return run;
}
// Cross-compile target definitions
/// Cross-compile target specification
const CrossTarget = struct {
name: []const u8,
query: std.Target.Query,
};
/// Musl-only cross-compile targets (static linking)
const musl_cross_targets = [_]CrossTarget{
.{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } },
.{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } },
};
/// Glibc cross-compile targets (dynamic linking)
const glibc_cross_targets = [_]CrossTarget{
.{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } },
.{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } },
};
/// Windows cross-compile targets
const windows_cross_targets = [_]CrossTarget{
.{ .name = "x64win", .query = .{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .msvc } },
.{ .name = "x64mingw", .query = .{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .gnu } },
.{ .name = "arm64win", .query = .{ .cpu_arch = .aarch64, .os_tag = .windows, .abi = .msvc } },
.{ .name = "arm64mingw", .query = .{ .cpu_arch = .aarch64, .os_tag = .windows, .abi = .gnu } },
};
/// MinGW C runtime files that a `*mingw` target must find next to `host.lib`.
///
/// A MinGW link passes `-lldmingw /nodefaultlib` and then only the inputs the
/// platform declares (see `src/cli/linker.zig`), so the startup object, the
/// compiled runtime archives, and the UCRT/Win32 import libraries all have to
/// live in the platform's `targets/<target>/` directory. `test/fx` holds the
/// checked-in copy (regenerate with `ci/vendor_mingw_runtime.py`) and the build
/// copies it into the other test platforms rather than committing duplicates.
const mingw_runtime_files = @import("src/echo_platform/mingw_runtime.zig").files;
/// Copies fx's checked-in MinGW C runtime into another test platform's
/// `platform/targets/<target_name>/` directory, so a `*mingw` link finds every
/// input the platform declares without committing more copies of the same
/// binaries. Returns the step so callers can order it after the host build.
fn copyMingwRuntimeToTestPlatform(
b: *std.Build,
platform_dir: []const u8,
target_name: []const u8,
) *Step {
const copy = b.addWriteFiles();
for (mingw_runtime_files) |runtime_filename| {
test_fixtures.copy(
copy,
b.path(b.pathJoin(&.{ "test/fx/platform/targets", target_name, runtime_filename })),
b.pathJoin(&.{ "test", platform_dir, "platform/targets", target_name, runtime_filename }),
);
}
return ©.step;
}
/// MSVC-ABI runtime files that a `*win` target must find next to `host.lib`.
///
/// An MSVC link passes `/nodefaultlib` and then only the inputs the platform
/// declares (see `src/cli/linker.zig`), exactly like a MinGW one, so nothing
/// comes from a Visual Studio or Windows SDK install. The import libraries
/// carry no compiled code, so they are the checked-in files of the matching
/// `*mingw` target; the startup archive is built from
/// `src/default_platform/msvc_runtime/`.
const msvc_runtime = @import("src/echo_platform/msvc_runtime.zig");
/// One startup archive per architecture, shared by every platform it is copied to.
var msvc_runtime_libs: [2]?*Step.Compile = .{ null, null };
/// The MSVC-ABI startup archive for `target_name` (`x64win` or `arm64win`):
/// process and DLL entry points, the TLS directory, the routines vcruntime
/// supplies, and compiler-rt, each in its own archive member so a link pulls
/// only what it references.
fn msvcRuntimeLib(b: *std.Build, target_name: []const u8) *Step.Compile {
const is_arm64 = std.mem.startsWith(u8, target_name, "arm64");
const slot = &msvc_runtime_libs[@intFromBool(is_arm64)];
if (slot.*) |lib| return lib;
const target = b.resolveTargetQuery(.{
.cpu_arch = if (is_arm64) .aarch64 else .x86_64,
.os_tag = .windows,
.abi = .msvc,
});
const arch_name = if (is_arm64) "arm64" else "x64";
const lib = b.addLibrary(.{
.name = b.fmt("msvc_runtime_{s}", .{arch_name}),
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path("src/default_platform/msvc_runtime/exe_startup.zig"),
.target = target,
.optimize = .ReleaseFast,
.strip = true,
.stack_check = false,
.link_libc = false,
}),
});
const Member = struct { name: []const u8, source: std.Build.LazyPath };
for ([_]Member{
.{ .name = "dll_startup", .source = b.path("src/default_platform/msvc_runtime/dll_startup.zig") },
.{ .name = "tls", .source = b.path("src/default_platform/msvc_runtime/tls.zig") },
.{ .name = "fltused", .source = b.path("src/default_platform/msvc_runtime/fltused.zig") },
.{ .name = "memcpy", .source = b.path("src/default_platform/msvc_runtime/memcpy.zig") },
.{ .name = "memmove", .source = b.path("src/default_platform/msvc_runtime/memmove.zig") },
.{ .name = "memset", .source = b.path("src/default_platform/msvc_runtime/memset.zig") },
.{ .name = "memcmp", .source = b.path("src/default_platform/msvc_runtime/memcmp.zig") },
}) |member| {
const obj = b.addObject(.{
.name = b.fmt("msvc_runtime_{s}_{s}", .{ member.name, arch_name }),
.root_module = b.createModule(.{
.root_source_file = member.source,
.target = target,
.optimize = .ReleaseFast,
.strip = true,
.stack_check = false,
.link_libc = false,
}),
});
lib.root_module.addObject(obj);
}
// Without libc, compiler-rt also defines `__chkstk`.
lib.bundle_compiler_rt = true;
slot.* = lib;
return lib;
}
/// Adds every MSVC runtime file for `target_name` to `copy`, landing in
/// `dest_dir`.
fn addMsvcRuntimeCopies(
b: *std.Build,
copy: *Step.WriteFile,
dest_dir: []const u8,
target_name: []const u8,
) void {
test_fixtures.copy(
copy,
msvcRuntimeLib(b, target_name).getEmittedBin(),
b.pathJoin(&.{ dest_dir, msvc_runtime.runtime_archive }),
);
const import_source = msvc_runtime.importLibrarySourceTarget(target_name);
for (msvc_runtime.import_libraries) |filename| {
test_fixtures.copy(
copy,
b.path(b.pathJoin(&.{ "test/fx/platform/targets", import_source, filename })),
b.pathJoin(&.{ dest_dir, filename }),
);
}
}
/// Puts the C runtime a Windows link needs into a test platform's
/// `platform/targets/<target_name>/` directory. Returns null when the target
/// needs no copy: it is not a Windows target, or it is fx's own checked-in
/// MinGW runtime.
fn copyWindowsRuntimeToTestPlatform(
b: *std.Build,
platform_dir: []const u8,
target_name: []const u8,
) ?*Step {
if (std.mem.endsWith(u8, target_name, "mingw")) {
if (std.mem.eql(u8, platform_dir, "fx")) return null;
return copyMingwRuntimeToTestPlatform(b, platform_dir, target_name);
}
if (std.mem.endsWith(u8, target_name, "win")) {
const copy = b.addWriteFiles();
addMsvcRuntimeCopies(b, copy, b.pathJoin(&.{ "test", platform_dir, "platform/targets", target_name }), target_name);
return ©.step;
}
return null;
}
/// BSD cross-compile targets
const bsd_cross_targets = [_]CrossTarget{
.{ .name = "x64freebsd", .query = .{ .cpu_arch = .x86_64, .os_tag = .freebsd, .abi = .none } },
.{ .name = "x64openbsd", .query = .{ .cpu_arch = .x86_64, .os_tag = .openbsd, .abi = .none } },
.{ .name = "x64netbsd", .query = .{ .cpu_arch = .x86_64, .os_tag = .netbsd, .abi = .none } },
};
/// All Linux cross-compile targets (musl + glibc)
const linux_cross_targets = musl_cross_targets ++ glibc_cross_targets;
comptime {
// The prebuilt builtins objects these targets produce are what
// `roc build --opt=dev --target=X` links, and `BuiltinsObjects.forTarget`
// hands the same object to a `v1` target as to its default twin. That is
// only sound while these queries name no CPU model, because Zig then
// resolves them to the architecture baseline. Naming a model here would
// put instructions the baseline lacks into every `v1` binary, so it has to
// come with per-CPU-level objects instead.
for (musl_cross_targets ++ glibc_cross_targets ++ windows_cross_targets ++ bsd_cross_targets) |cross_target| {
if (cross_target.query.cpu_model != .determined_by_arch_os) {
@compileError("cross-compile target " ++ cross_target.name ++
" names a CPU model; baseline (v1) targets would link non-baseline builtins");
}
if (!cross_target.query.cpu_features_add.isEmpty()) {
@compileError("cross-compile target " ++ cross_target.name ++
" adds CPU features; baseline (v1) targets would link non-baseline builtins");
}
}
}
/// Test platform directories that need host libraries built
const all_test_platform_dirs = [_][]const u8{ "str", "int", "fx", "fx-open", "dylib", "archive", "alloc-count", "box-model-uniqueness" };
fn mustUseLlvm(target: ResolvedTarget) bool {
return target.result.os.tag == .macos and target.result.cpu.arch == .x86_64;
}
fn isGlibcTestHost(target: ResolvedTarget) bool {
return target.result.os.tag == .linux and target.result.abi == .gnu;
}
fn testHostNeedsLlvm(target: ResolvedTarget) bool {
// macOS x86_64 must use the LLVM backend. All Linux test hosts use it too:
// the symbol-ABI tests need the ELF @export visibility metadata Zig's LLVM
// backend emits, and glibc hosts additionally rely on LLVM for DCE.
return mustUseLlvm(target) or target.result.os.tag == .linux;
}
fn testHostNeedsCompilerRt(target: ResolvedTarget) bool {
return target.result.os.tag == .linux or
mustUseLlvm(target) or
(target.result.os.tag == .windows and target.result.cpu.arch == .aarch64);
}
/// Every executable that links the whole compiler emits more `.text` than an
/// ARM32 branch can reach. LLD places range-extension thunks *between* input
/// sections, so a monolithic `.text` leaves it nowhere to put one and the link
/// fails with "InputSection too large for range extension thunk". Splitting per
/// function and per datum gives LLD those insertion points, and lets the final
/// link discard unused compiler code on every target.
fn splitCompilerSections(step: *Step.Compile) void {
step.link_function_sections = true;
step.link_data_sections = true;
}
fn configureBackend(step: *Step.Compile, target: ResolvedTarget) void {
if (mustUseLlvm(target)) {
step.use_llvm = true;
}
}
/// The watch module uses real macOS FSEvents (CoreFoundation/CoreServices) when building
/// natively for macOS, and kernel32 for directory-change notifications on Windows. Any step
/// that compiles the watch module (the roc exe and the tests that import it) must link these.
fn linkWatchPlatformLibs(step: *Step.Compile, target: ResolvedTarget) void {
if (target.result.os.tag == .macos and
builtin.target.os.tag == .macos and
target.result.cpu.arch == builtin.target.cpu.arch and
target.result.abi == builtin.target.abi)
{
step.root_module.linkFramework("CoreFoundation", .{});
step.root_module.linkFramework("CoreServices", .{});
} else if (target.result.os.tag == .windows) {
step.root_module.linkSystemLibrary("kernel32", .{});
}
}
const TestHostOptions = struct {
uses_stack_handler: bool = false,
/// Extra source files compiled as their own objects and added to the host
/// archive as separate members, for platforms that test multi-member hosts.
extra_sources: []const []const u8 = &.{},
};
/// The dylib host keeps its hosted functions in a second archive member that
/// only the app references, so `run-test-dylib` fails if a link ever stops
/// rooting the hosted symbols it uses (see test/dylib/platform/host_hosted.zig).
fn testPlatformExtraHostSources(platform_dir: []const u8) []const []const u8 {
if (std.mem.eql(u8, platform_dir, "dylib")) return &.{"test/dylib/platform/host_hosted.zig"};
return &.{};
}
fn testPlatformUsesStackHandler(platform_dir: []const u8) bool {
return std.mem.eql(u8, platform_dir, "fx") or std.mem.eql(u8, platform_dir, "fx-open");
}
fn testPlatformRequiresSectionDceHost(platform_dir: []const u8) bool {
return std.mem.eql(u8, platform_dir, "dylib") or std.mem.eql(u8, platform_dir, "archive");
}
/// The v1 twin that needs its own test-platform host build.
fn muslBaselineTestTargetName(target_name: []const u8) ?[]const u8 {
if (std.mem.eql(u8, target_name, "x64musl")) return "x64v1musl";
if (std.mem.eql(u8, target_name, "arm64musl")) return "arm64v1musl";
return null;
}
fn testHostNeedsLibc(options: TestHostOptions, target: ResolvedTarget) bool {
if (!options.uses_stack_handler) return false;
return switch (target.result.os.tag) {
.linux,
.macos,
.ios,
.tvos,
.watchos,
.visionos,
.freebsd,
.dragonfly,
.netbsd,
.openbsd,
=> true,
.freestanding,
.other,
.contiki,
.fuchsia,
.hermit,
.managarm,
.haiku,
.hurd,
.illumos,
.plan9,
.rtems,
.serenity,
.driverkit,
.maccatalyst,
.windows,
.uefi,
.wiiu,
.@"switch",
.gba,
.psx,
.tios,
.ashetos,
.@"3ds",
.ps3,
.ps4,
.ps5,
.psp,
.vita,
.emscripten,
.wasi,
.amdhsa,
.amdpal,
.cuda,
.mesa3d,
.nvcl,
.opencl,
.opengl,
.vulkan,
=> false,
};
}
fn isNativeishOrMusl(target: ResolvedTarget) bool {
return target.result.cpu.arch == builtin.target.cpu.arch and
target.result.os.tag == builtin.target.os.tag and
(target.query.isNativeAbi() or target.result.abi.isMusl());
}
const NativeSharedArchiveTarget = struct {
resolved: ResolvedTarget,
roc_name: []const u8,
};
fn nativeSharedArchiveTarget(b: *std.Build, target: ResolvedTarget) NativeSharedArchiveTarget {
if (target.result.os.tag == .linux) {
const arch = target.result.cpu.arch;
return if (arch == .x86_64) .{
.resolved = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu }),
.roc_name = "x64glibc",
} else if (arch == .aarch64) .{
.resolved = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu }),
.roc_name = "arm64glibc",
} else .{
.resolved = target,
.roc_name = roc_target.RocTarget.fromStdTarget(target.result).toName(),
};
}
return .{
.resolved = target,
.roc_name = roc_target.RocTarget.fromStdTarget(target.result).toName(),
};
}
fn withRocMacosDeploymentTarget(b: *std.Build, target: ResolvedTarget) ResolvedTarget {
if (target.result.os.tag != .macos) return target;
return b.resolveTargetQuery(roc_target.macos_deployment.query(target.result.cpu.arch));
}
/// Returns the target query for release builds on the current host.
///
/// `-Dtarget` and `-Dcpu` select the release target, so a packager can trade
/// portability for speed. With neither flag, the defaults are:
/// - Linux: Uses musl for fully static binaries
/// - x86_64 and aarch64: Uses the baseline CPU, so a single released binary
/// runs on every CPU of that architecture
///
/// What that floor costs the parts of this binary which aren't declared as
/// dependencies in build.zig.zon:
///
/// - Zig's standard library has no runtime CPU dispatch anywhere. Its vector
/// width is picked at comptime by `std.simd.suggestVectorLength` from
/// `builtin.cpu`: SSE gives 128 bits, AVX2 gives 256, AVX-512 gives 512. So a
/// baseline build halves the width used by `std.mem`, `std.unicode`,
/// `std.crypto.blake3` (which is what `roc bundle` hashes with),
/// `std.compress.flate`, and `std.http.HeadParser`. It does not make them
/// scalar.
/// - musl has no ifunc and no x86_64 string assembly, so it is unaffected.
/// - compiler_rt has no dispatch and nothing above baseline to give up.
/// - Zig string primitives use scalar/word operations and standard-library
/// helpers. Roc SIMD operations go through the compiler's explicit lowering.
/// SHA rounds separately use target-specific assembly with runtime dispatch.
///
/// build.zig.zon carries the same accounting for each declared dependency.
fn getReleaseTargetQuery(b: *std.Build, target: ResolvedTarget) std.Target.Query {
// A `-Dtarget` triple names the entire target, ABI included, so it replaces
// every default below.
if (b.user_input_options.contains("target")) return target.query;
// `-Dcpu` names only the CPU, so it carries over onto the release ABI
// defaults rather than replacing them.
const explicit_cpu = b.user_input_options.contains("cpu");
var query: std.Target.Query = if (explicit_cpu) target.query else .{};
// Use musl on Linux for static linking
if (builtin.target.os.tag == .linux) {
query.abi = .musl;
}
if (!explicit_cpu) {
// An otherwise-empty query means "native CPU", which bakes the build
// machine's CPU features into the released binary. Pin the CPU model
// explicitly so releases stay portable.
const arch = builtin.target.cpu.arch;
if (arch == .x86_64 or arch == .aarch64) {
// Baseline x86-64 is the 2003 instruction set, which every x86-64
// CPU supports. A higher floor makes the binary die of SIGILL
// before it can print anything on any CPU below that floor: pinning
// x86_64_v3 requires AVX2 and BMI2, which excludes Intel's Celeron
// and Pentium Silver lines along with everything pre-Haswell.
query.cpu_model = .baseline;
// Baseline aarch64 is armv8.0-a, which every aarch64 device supports.
// Building natively on an armv9 CI runner emitted SVE (plus LSE and
// RCPC) instructions, which crashed with SIGILL on older phones and
// Raspberry Pis. On macOS, baseline is apple_m1, i.e. all Apple Silicon.
}
addSha256Floor(&query);
}
return query;
}
/// `addSha256Floor` applied to the target `zig build` compiles for, unless the
/// query named a CPU model explicitly (`-Dcpu`), in which case the caller's
/// choice stands and `TypeDigestHasher` reports a missing feature at compile
/// time.
///
/// A Debug build for an x86_64 target other than macOS also gets the SHA
/// extension, with the SSSE3 the rounds use (`debugBuildsWithSha`). Debug uses
/// Zig's self-hosted x86_64 backend, which encodes only instructions in the
/// target CPU's feature set, so without the feature such a build has no
/// hardware rounds at all. The feature is added to the generic x86-64 model,
/// which the rounds read as a floor rather than a CPU (`x86_floor` in
/// src/builtins/sha256.zig): the compiler's digests choose rounds with CPUID
/// (`dispatches_at_runtime` in src/base/sha256.zig), so the build runs on
/// CPUs without the extension, and every machine resolves the same target, so
/// CI's shared build is a cache hit on every runner.
fn withSha256Floor(b: *std.Build, target: ResolvedTarget, optimize: std.builtin.OptimizeMode) ResolvedTarget {
var query = target.query;
switch (query.cpu_model) {
.determined_by_arch_os, .baseline => {},
.native, .explicit => return target,
}
addSha256Floor(&query);
if (optimize == .debug and debugBuildsWithSha(target)) {
query.cpu_features_add.addFeature(@backingInt(std.Target.x86.Feature.sha));
query.cpu_features_add.addFeature(@backingInt(std.Target.x86.Feature.ssse3));
}
return b.resolveTargetQuery(query);
}
/// `target` without the SHA-256 instructions `withSha256Floor` adds, for the
/// native builtins objects. Those objects are linked into programs for Roc
/// targets whose CPU contract may lack the instructions (`BuiltinsObjects.forTarget`
/// in src/cli/main.zig hands a `v1` target its default twin's object), and the
/// `Crypto` builtins compress with the SHA-256 instructions exactly when their
/// object is compiled with them (see `Rounds` in src/builtins/sha256.zig). A
/// `-Dcpu` choice stands, as it does for `withSha256Floor`.
fn withoutSha256Floor(b: *std.Build, target: ResolvedTarget) ResolvedTarget {
var query = target.query;
switch (query.cpu_model) {
.determined_by_arch_os, .baseline => {},
.native, .explicit => return target,
}
switch (roc_target.classifyCpuArch(target.result.cpu.arch)) {
.x86_64 => query.cpu_features_add.removeFeature(@backingInt(std.Target.x86.Feature.sha)),
.aarch64 => query.cpu_features_add.removeFeature(@backingInt(std.Target.aarch64.Feature.sha2)),
.aarch64_be, .arm, .wasm32, .other => return target,
}
return b.resolveTargetQuery(query);
}
/// Whether a Debug build for `target` gets the x86 SHA extension and SSSE3:
/// every x86_64 target but macOS, whose Intel Macs compute digests with the
/// portable rounds (`uses_software_rounds` in src/base/sha256.zig). The
/// answer depends on the target alone, never on the machine running the
/// build, so a build one machine makes for another resolves the same target.
fn debugBuildsWithSha(target: ResolvedTarget) bool {
return target.result.cpu.arch == .x86_64 and target.result.os.tag != .macos;
}
/// The target for everything that is compiled to run on this machine during
/// the build: tools, generators and their tests. It is this machine's OS and
/// ABI at the CPU the default target gets, baseline x86-64 with the Debug
/// floor of `withSha256Floor`, rather than the CPU `b.graph.host` detected.
///
/// Zig keys each compilation on its target CPU, so a tool compiled for the
/// detected CPU is a cache miss on a machine with a different one, and so is
/// every step that consumes what the tool generates: `builtin_compiler` bakes
/// the builtins most test binaries embed. CI compiles on one runner and tests
/// on others, and x86_64 runners do not all have the same CPU, so those test
/// jobs recompiled the tools and most test binaries instead of reusing them.
/// ci/tidy.zig bans compiling for `b.graph.host` for that reason.
///
/// Other architectures keep the detected CPU, as the default target does.
fn hostToolTarget(b: *std.Build, optimize: OptimizeMode) ResolvedTarget {
if (builtin.target.cpu.arch != .x86_64) return b.graph.host;
return withSha256Floor(b, b.resolveTargetQuery(.{ .cpu_model = .baseline }), optimize);
}
/// Raise an aarch64 compiler target's CPU floor to include the SHA-256
/// instructions. Type digests are cryptographic SHA-256 (see
/// `src/base/TypeDigestHasher.zig` for why) and aarch64 computes them in
/// hardware with no software rounds, so an aarch64 CPU without these
/// instructions is not a supported host for the compiler. This is the only
/// feature added above the architecture baseline: the `sha2` crypto extension,
/// present on all Apple Silicon, Graviton, Ampere and Raspberry Pi 5, absent on
/// the Cortex-A53/A72 in Raspberry Pi 4 and earlier. A `-Dcpu` that omits the
/// feature fails to compile `Sha256` rather than silently getting a
/// slower binary.
///
/// x86_64 gets no floor here. Its SHA extension is missing from Intel's
/// 2015-2020 Skylake through Comet Lake cores, which are every Intel Mac and
/// still a common Linux and Windows machine, so the binary stays at the
/// architecture baseline and the rounds are chosen for the CPU it runs on: at
/// runtime by CPUID on every x86_64 target except macOS (see
/// `dispatches_at_runtime` in src/base/sha256.zig), and always the
/// portable rounds on x86_64 macOS (see `uses_software_rounds` there). x86_64
/// Debug builds carry the extension as a floor, in `withSha256Floor`.
fn addSha256Floor(query: *std.Target.Query) void {
const arch = query.cpu_arch orelse builtin.target.cpu.arch;
switch (roc_target.classifyCpuArch(arch)) {
.aarch64 => query.cpu_features_add.addFeature(@backingInt(std.Target.aarch64.Feature.sha2)),
.x86_64, .aarch64_be, .arm, .wasm32, .other => {},
}
}
const TestsSummaryStep = struct {
step: *Step,
run: *Step.Run,
mutable_reports: std.Build.LazyPath,
producers: std.StringHashMapUnmanaged(void) = .empty,
serialize_runs: bool = false,
last_run: ?*Step = null,
fn create(b: *std.Build, test_filters: []const []const u8, forced_passes: usize) *TestsSummaryStep {
const self = b.allocator.create(TestsSummaryStep) catch @panic("OOM");
const run = buildChecksRun(b, "tests-summary");
run.addArg(b.fmt("{d}", .{forced_passes}));
run.addArg(b.fmt("{d}", .{test_filters.len}));
run.addArgs(test_filters);
const mutable_reports = b.addWriteFiles();
mutable_reports.mode = .tmp;
self.* = .{ .step = &run.step, .run = run, .mutable_reports = mutable_reports.getDirectory() };
return self;
}
fn setRunSerialization(self: *TestsSummaryStep) void {
self.serialize_runs = true;
}
fn addRun(self: *TestsSummaryStep, run_step: *Step) void {
const run: *Step.Run = @fieldParentPtr("step", run_step);
const b = run.step.owner;
const entry = self.producers.getOrPut(b.allocator, run.producer.?.name) catch @panic("OOM");
if (entry.found_existing) std.debug.panic("duplicate test report producer: {s}", .{run.producer.?.name});
const basename = b.fmt("{s}.tsv", .{run.producer.?.name});
// Test runs always execute because they accept dynamic passthrough.
// Each summary owns a temporary destination so concurrent invocations
// never mutate the same report. Retain an immutable copy only after
// the test writer succeeds, before the summary reads its contents.
run.addDirectoryArg2(self.mutable_reports, .{
.prefix = "--roc-test-report-dir=",
.make_absolute = true,
});
run.addArg(b.fmt("--roc-test-report-name={s}", .{basename}));
const retained_reports = b.addWriteFiles();
const report = retained_reports.addCopyFile(self.mutable_reports.path(b, basename), basename);
retained_reports.step.dependOn(run_step);
self.run.addArg(run.producer.?.name);
self.run.addFileArg(report);
if (self.serialize_runs) {
if (self.last_run) |last_run| run_step.dependOn(last_run);
self.last_run = run_step;
}
self.step.dependOn(run_step);
}
};
/// One environment variable, applied identically to both runs of a test suite.
const TestSuiteEnv = struct {
key: []const u8,
value: []const u8,
};
/// Everything that differs between the Zig test suites registered with
/// `TestsSummaryStep`. Anything not expressible here is applied uniformly by
/// `TestSuiteRegistry.register`. If a suite ever needs a knob that is missing
/// (a working directory, `has_side_effects`, ...), add a field here rather than
/// hand-wiring that one suite at its call site -- see the note on `register`.
const TestSuiteSpec = struct {
/// Suffix appended to `run-test-zig-`. This is also the MiniCI job name in
/// `src/build/minici.zig`, so it must stay stable.
step_suffix: []const u8,
/// Description for the public `run-test-zig-<step_suffix>` step.
description: []const u8,
/// The test binary. Both runs execute this one `Step.Compile`, so it is
/// compiled once no matter which step was asked for.
compile: *Step.Compile,
/// Extra edges both runs need: copied host libraries, installed prebuilt
/// apps, the `roc` CLI, and so on. These are build work, so `build-test-zig`
/// depends on them as well.
deps: []const *Step = &.{},
/// Environment variables both runs need.
env: []const TestSuiteEnv = &.{},
/// Whether a bare `zig build` should also build this test binary.
default_step: bool = false,
/// Run fixture consumers in a private prepared project directory.
fixture_root: bool = false,
/// Which inventory lists this suite: a per-module suite comes from the
/// module inventory in `src/build/modules.zig`; every other suite must be
/// listed in `src/build/zig_test_suites.zig`, which says where CI runs it.
inventory: enum { module_tests, zig_test_suites } = .zig_test_suites,
};
/// Configures Roc's registered Zig unit tests to use Zig's stock runner with
/// stack-trace capture controlled by `-Ddebug-gpa-traces`.
const UnitTestRunner = struct {
path: std.Build.LazyPath,
build_options: *std.Build.Module,
zig_default_test_runner: *std.Build.Module,
fn configure(self: UnitTestRunner, compile: *Step.Compile) void {
compile.root_module.addImport("build_options", self.build_options);
compile.root_module.addImport("zig_default_test_runner", self.zig_default_test_runner);
compile.test_runner = .{
.path = self.path,
.mode = runnerMode(compile),
};
}
/// Keep in sync with `std.Build.addRunArtifact` and Zig's stock test
/// runner. These backends use the simple exit-code protocol; all others
/// use `std.zig.Server`.
fn runnerMode(compile: *Step.Compile) @FieldType(Step.Compile.TestRunner, "mode") {
if (compile.use_llvm == false) {
const arch = compile.rootModuleTarget().cpu.arch;
return if (arch == .aarch64 or
arch == .aarch64_be or
arch == .powerpc or
arch == .powerpcle or
arch == .powerpc64 or
arch == .powerpc64le or
arch == .riscv64) .simple else .server;
}
return .server;
}
};
/// Owns every cross-cutting edge a Zig test suite needs, so that a suite is
/// wired up in exactly one call.
///
/// The failure this type exists to prevent: on Windows `TestsSummaryStep`
/// chains its registered runs (see `setRunSerialization`) so that test binaries
/// start one at a time. If a granular `run-test-zig-<suite>` step is pointed at
/// the *same* `Step.Run` that the chain owns, it inherits the entire prefix of
/// that chain. `run-test-zig-minici` is a std-only suite of ~11 string-parsing
/// tests; wired that way it re-ran 41 unrelated test binaries and took 441s
/// instead of ~2s. MiniCI runs each granular step as its own `zig build`
/// process, so it replayed that prefix once per affected job -- which is what
/// pushed the Windows CI job past its two-hour limit.
///
/// `register` builds *both* runs from one spec, so the summary's run and the
/// public step's run can neither be the same object nor drift apart in their
/// configuration. `assertGranularTestStepsAreIsolated` re-checks the finished
/// graph, in case a suite is ever wired by hand anyway.
const TestSuiteRegistry = struct {
b: *std.Build,
summary: *TestsSummaryStep,
build_test_zig_step: *Step,
unit_test_runner: UnitTestRunner,
/// Non-null only while the test-wiring check is enumerating test binaries.
wiring_run: ?*Step.Run,
/// Args forwarded from `zig build -- <args>`, e.g. `--test-filter`.
run_args: []const []const u8,
fn register(self: TestSuiteRegistry, spec: TestSuiteSpec) void {
const b = self.b;
if (spec.inventory == .zig_test_suites and zig_test_suites.find(spec.step_suffix) == null) {
std.debug.panic("run-test-zig-{s} is not listed in src/build/zig_test_suites.zig", .{spec.step_suffix});
}
self.unit_test_runner.configure(spec.compile);
// Build-side wiring, uniform for every suite. `deps` are built here
// too: left to the run steps alone, each MiniCI shard compiled them
// (the fx suite's host libraries) instead of reusing `build-ci`.
self.build_test_zig_step.dependOn(&spec.compile.step);
for (spec.deps) |dep| self.build_test_zig_step.dependOn(dep);
if (spec.default_step) b.default_step.dependOn(&spec.compile.step);
if (self.wiring_run) |wiring| wiring.addArtifactArg(spec.compile);
// Two runs over one compile. Only the summary's run joins the
// serialization chain (currently enabled only on Windows); the public
// step gets a run with no chain edges.
self.summary.addRun(&self.configuredRun(spec).step);
const public_step = b.step(
b.fmt("run-test-zig-{s}", .{spec.step_suffix}),
spec.description,
);
public_step.dependOn(&self.configuredRun(spec).step);
}
/// Both runs are produced by this one function from one spec, so there is
/// never a second copy of the configuration to fall out of sync.
fn configuredRun(self: TestSuiteRegistry, spec: TestSuiteSpec) *Step.Run {
const run = self.b.addRunArtifact(spec.compile);
if (self.run_args.len != 0) run.addArgs(self.run_args);
run.addPassthruArgs();
for (spec.env) |entry| run.setEnvironmentVariable(entry.key, entry.value);
for (spec.deps) |dep| run.step.dependOn(dep);
if (spec.fixture_root) run.setCwd(test_fixtures.mutableRoot(spec.deps));
return run;
}
};
/// Build step that checks for forbidden patterns in the type checker code.
///
/// During type checking, we NEVER do string or byte comparisons because:
/// 1. They take linear time, which can cause performance issues
/// 2. They are brittle to changes that type-checking should not be sensitive to
///
/// Instead, we always compare indices - either into node stores or to interned string indices.
/// This step enforces that rule by failing the build if `std.mem.` is found in src/canonicalize/, src/check/, src/layout/, or src/eval/.
const CheckTypeCheckerPatternsStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-type-checker-patterns");
}
};
/// Build step that checks for @enumFromInt(0) usage in all .zig files.
///
/// We forbid @enumFromInt(0) because it hides bugs and makes them harder to debug.
/// If we need a placeholder value that we believe will never be read, we should
/// use `undefined` instead - that way our intent is clear, and it can fail in a
/// more obvious way if our assumption is incorrect.
const CheckEnumFromIntZeroStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-enum-from-int-zero");
}
};
/// Build step that checks for unused variable suppression patterns.
///
/// In this codebase, we don't use `_ = variable;` to suppress unused variable warnings.
/// Instead, we delete the unused variable/argument and update all call sites as necessary.
const CheckUnusedSuppressionStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-unused-suppression");
}
};
/// Build step that checks for deleted post-check architecture APIs being reintroduced.
///
/// This enforces the cor-style lowering contract:
/// - no output/canonicalization layer in post-check lowering
/// - no workspace/source-var remapping layer in monotype
/// - no canonical-source specialization lookup in compilation stages
const CheckPostcheckArchitectureStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-postcheck-architecture");
}
};
/// Build step that fails when tracked snapshots differ from the freshly regenerated
/// output. Detects whether the build root is backed by Git or JJ and invokes the
/// matching diff command directly. This deliberately avoids shelling out through
/// `sh -c`, which is not available on Windows (it fails to spawn with FileNotFound).
const CheckSnapshotDiffStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-snapshot-diff");
}
};
/// Build step that checks for @panic and std.debug.panic usage in interpreter and builtins.
///
/// In Roc's design philosophy, compile-time errors become runtime errors with helpful messages.
/// Users can run apps despite errors, and we provide actionable feedback. Using @panic unwinds
/// the stack and prevents showing helpful error messages.
///
/// Additionally, in WASM builds, @panic compiles to the `unreachable` instruction with no
/// message output, making debugging impossible. All runtime code must use roc_ops.crash()
/// to ensure error messages are properly displayed.
const CheckPanicStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-panic-usage");
}
};
/// Build step that requires every `unreachable` under src/ to carry a
/// same-line comment naming the invariant that makes the case impossible.
const CheckUnreachableRationaleStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-unreachable-rationale");
}
};
/// Build step that checks for global stdio usage in CLI code.
///
/// The CLI code uses a context-based I/O pattern where stdout/stderr are accessed
/// through `ctx.io.stdout()` and `ctx.io.stderr()`. This prepares for Zig's upcoming
/// I/O interface changes where I/O is passed through functions (like Allocator).
///
/// This step enforces that pattern by failing the build if direct global stdio
/// access is found in src/cli/main.zig.
const CheckCliGlobalStdioStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-cli-global-stdio");
}
};
/// Build step that parses kcov JSON output and prints coverage summary.
/// Used by the `coverage` build step to report parser code coverage statistics.
const CoverageSummaryStep = struct {
fn create(b: *std.Build, dir: []const u8, exe: []const u8) *Step.Run {
return createWithOptions(b, dir, exe, "PARSER", 28.0);
}
fn createWithOptions(b: *std.Build, dir: []const u8, exe: []const u8, label: []const u8, minimum: f64) *Step.Run {
const run = buildChecksRun(b, "coverage-summary");
run.addArgs(&.{ dir, exe, label, b.fmt("{d}", .{minimum}) });
return run;
}
};
const CheckTestAssetCoverageStep = struct {
fn create(b: *std.Build) *Step.Run {
return buildChecksRun(b, "check-test-asset-coverage-inner");
}
};
/// Separate processes are essential: ASLR-dependent data can remain stable
/// across multiple bakes within a single process.
const CheckBuiltinBakeReproducibleStep = struct {
fn create(b: *std.Build, exe: *Step.Compile) *Step.Run {
const inputs = b.addWriteFiles();
const sources = inputs.addCopyDirectory(b.path("src/build/roc"), "roc", .{});
const compare = buildChecksRun(b, "check-builtin-bake-reproducible");
for (0..3) |index| {
const bake = b.addRunArtifact(exe);
bake.addFileArg(sources.path(b, "Builtin.roc"));
for ([_][]const u8{ "Builtin.bin", "builtin_indices.zig", "Builtin.artifact.bin" }) |name| {
// Distinct declared output names produce distinct Run keys.
// Identical keys could reuse one process's output three times,
// defeating this ASLR reproducibility check.
compare.addFileArg(bake.addOutputFileArg(b.fmt("bake-{d}-{s}", .{ index, name })));
}
}
return compare;
}
};
/// Size budget for echo.wasm, the browser compiler on www.roc-lang.org. The website
/// rejects a module over Cloudflare's 25 MiB asset limit (`MAX_ASSET_SIZE` in
/// roc-lang/www.roc-lang.org `ci_scripts/prepare_compiler_wasm.mjs`). The website
/// measures after `wasm-opt -Oz`, which in practice only shrinks the module (by about
/// 10%), so checking the raw ReleaseSmall build against the full limit is conservative
/// and needs no Binaryen in CI.
const echo_wasm_size_budget: u64 = 26_214_400;
/// Fails when a wasm module exceeds its size budget, and warns from 90% of it.
const CheckWasmSizeStep = struct {
fn create(b: *std.Build, compile: *Step.Compile, budget: u64, budget_name: []const u8) *Step.Run {
const run = buildChecksRun(b, "wasm-size-budget");
run.addFileArg(compile.getEmittedBin());
run.addArgs(&.{ b.fmt("{d}", .{budget}), budget_name });
return run;
}
};
const BuiltinCompilerRun = struct {
exe: *Step.Compile,
run: *Step.Run,
builtin_bin: std.Build.LazyPath,
builtin_indices_zig: std.Build.LazyPath,
builtin_artifact_bin: std.Build.LazyPath,
};
/// zstd frames of the builtin blobs, for browser modules that must stay under the
/// website's asset size limit. A root module opts in with
/// `pub const roc_compressed_builtins = @import("compressed_builtins");` and calls
/// `compiled_builtins.decompress()` before compiling. Level 9 takes about a second
/// even with the Debug zstd of default builds; levels 18+ save another 1 MB but take
/// tens of seconds there, on the critical path of the wasm compile.
fn addCompressedBuiltins(
b: *std.Build,
compress_exe: *Step.Compile,
builtin_compiler: BuiltinCompilerRun,
builtin_roc: std.Build.LazyPath,
) *std.Build.Module {
const files = b.addWriteFiles();
const inputs = [_]struct { std.Build.LazyPath, []const u8 }{
.{ builtin_compiler.builtin_bin, "Builtin.bin.zst" },
.{ builtin_roc, "Builtin.roc.zst" },
.{ builtin_compiler.builtin_artifact_bin, "Builtin.artifact.bin.zst" },
};
for (inputs) |input| {
const compress = b.addRunArtifact(compress_exe);
compress.addFileArg(input[0]);
const output = compress.addOutputFileArg(input[1]);
compress.addArg("9");
_ = files.addCopyFile(output, input[1]);
}
const source = files.add("compressed_builtins.zig",
\\pub const builtin_bin = @embedFile("Builtin.bin.zst");
\\pub const builtin_source = @embedFile("Builtin.roc.zst");
\\pub const builtin_artifact_bin = @embedFile("Builtin.artifact.bin.zst");
\\
);
return b.createModule(.{ .root_source_file = source });
}
fn createAndRunBuiltinCompiler(
b: *std.Build,
roc_modules: modules.RocModules,
flag_enable_tracy: ?[]const u8,
roc_files: []const []const u8,
) BuiltinCompilerRun {