-
-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathbuild.zig
More file actions
1209 lines (1093 loc) · 58.5 KB
/
build.zig
File metadata and controls
1209 lines (1093 loc) · 58.5 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 zip = @import("src/deps/zip/build.zig");
const dvui = @import("dvui");
const velopack = @import("velopack_zig");
const content_dir = "assets/";
const ProcessAssetsStep = @import("src/tools/process_assets.zig");
const update = @import("update.zig");
const GitDependency = update.GitDependency;
fn update_step(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
const deps = &.{
GitDependency{
// zig_objc
.url = "https://github.com/foxnne/zig-objc",
.branch = "main",
},
GitDependency{
// zigwin32 (kristoff-it fork has the zig 0.16 fix branch)
.url = "https://github.com/kristoff-it/zigwin32",
.branch = "fix/zig16",
},
GitDependency{
// icons
.url = "https://github.com/foxnne/zig-lib-icons",
.branch = "dvui",
},
GitDependency{
// dvui
.url = "https://github.com/foxnne/dvui-dev",
.branch = "main",
},
};
try update.update_dependency(step.owner.allocator, step.owner.graph.io, deps);
}
/// Installed artifacts go under `zig-out/<this>/…` so `packageall` and parallel targets never clobber each other.
/// Uses `arm64` (not `aarch64`) for Apple Silicon / arm64 Linux and Windows to match the six release triples.
///
/// Segment separator is `-` only: `vpk pack --channel` is merged into filenames that get parsed as NuGet
/// versions (e.g. `1.2.3-<channel>-full.nupkg`), and NuGet prerelease labels must not contain `_`.
fn zigOutSubdirForTarget(b: *std.Build, rt: std.Build.ResolvedTarget) []const u8 {
const arch_name: []const u8 = switch (rt.result.cpu.arch) {
.x86_64 => "x86-64",
.aarch64 => "arm64",
else => @tagName(rt.result.cpu.arch),
};
const os_name: []const u8 = switch (rt.result.os.tag) {
.windows => "windows",
.linux => "linux",
.macos => "macos",
else => @tagName(rt.result.os.tag),
};
const base = b.fmt("{s}-{s}", .{ arch_name, os_name });
if (std.mem.indexOfScalar(u8, base, '_') == null)
return base;
const buf = b.allocator.alloc(u8, base.len) catch @panic("OOM");
@memcpy(buf, base);
for (buf) |*byte| {
if (byte.* == '_') byte.* = '-';
}
return buf;
}
/// SDL (via dvui → lazy `sdl3`) requires SDK layout when `-Dtarget=*-macos` is not "native"
/// (`target.query.isNative()` is false). Do not set the root `b.sysroot` for that: it skews
/// the main link (objc, libc paths). Forward include / framework / lib paths into dvui instead.
const MacosSdlPaths = struct {
include: std.Build.LazyPath,
framework: std.Build.LazyPath,
lib: std.Build.LazyPath,
};
fn resolveMacosSdkPath(b: *std.Build) ![]const u8 {
if (b.graph.environ_map.get("SDKROOT")) |sdk| {
const trimmed = std.mem.trim(u8, sdk, " \t\r\n");
if (trimmed.len > 0) {
return b.dupePath(trimmed);
}
}
const argv: []const []const u8 = &.{
"xcrun",
"--sdk",
"macosx",
"--show-sdk-path",
};
const run = try std.process.run(b.allocator, b.graph.io, .{
.argv = argv,
.stdout_limit = std.Io.Limit.limited(4096),
.stderr_limit = std.Io.Limit.limited(4096),
});
defer {
b.allocator.free(run.stdout);
b.allocator.free(run.stderr);
}
switch (run.term) {
.exited => |code| if (code != 0) {
std.log.err("SDL on macOS: explicit -Dtarget=*-macos needs an SDK path. xcrun exited with code {d}. Install Xcode Command Line Tools or set SDKROOT.", .{code});
return error.MacosSdkPath;
},
else => {
std.log.err("SDL on macOS: xcrun --show-sdk-path failed", .{});
return error.MacosSdkPath;
},
}
const path = std.mem.trimEnd(u8, run.stdout, " \t\r\n");
if (path.len == 0) return error.MacosSdkPath;
return b.dupePath(path);
}
fn macosSdlPathsForExplicitTarget(b: *std.Build, target: std.Build.ResolvedTarget) !?MacosSdlPaths {
if (target.result.os.tag != .macos) return null;
if (b.graph.host.result.os.tag != .macos) return null;
if (target.query.isNative()) return null;
const sdk = try resolveMacosSdkPath(b);
return MacosSdlPaths{
.include = .{ .cwd_relative = b.pathJoin(&.{ sdk, "usr/include" }) },
.framework = .{ .cwd_relative = b.pathJoin(&.{ sdk, "System/Library/Frameworks" }) },
.lib = .{ .cwd_relative = b.pathJoin(&.{ sdk, "usr/lib" }) },
};
}
pub fn build(b: *std.Build) !void {
const windows_msvc_libc_opt = b.option([]const u8, "windows-msvc-libc", "zig libc manifest for *-windows-msvc when cross-compiling; forwarded by packageall for Windows children") orelse null;
// Default depends on host+target and is computed below once `target` is resolved.
// Pass `-Dfetch-msvc=false` on a Windows host to opt out of the auto-download and
// fall back to Zig's system-MSVC auto-detection (if you have Visual Studio installed).
const fetch_msvc_opt = b.option(bool, "fetch-msvc", "If *-windows-msvc libc is missing under .velopack-msvc/, run msvcup-setup first (downloads MSVC+SDK; requires network). Defaults to true on Windows hosts targeting *-windows-msvc.");
// macOS `vpk pack` codesigning / notarization. Optional: when omitted, packaging produces an
// unsigned bundle. Set all three to sign + notarize a release build.
const macos_sign_app_identity = b.option([]const u8, "macos-sign-app", "macOS codesign identity for the app bundle (e.g. 'Developer ID Application: NAME (TEAMID)')") orelse
b.graph.environ_map.get("FIZZY_MACOS_SIGN_APP");
const macos_sign_install_identity = b.option([]const u8, "macos-sign-installer", "macOS codesign identity for the installer pkg (e.g. 'Developer ID Installer: NAME (TEAMID)')") orelse
b.graph.environ_map.get("FIZZY_MACOS_SIGN_INSTALLER");
const macos_notary_profile = b.option([]const u8, "macos-notary-profile", "notarytool keychain profile name (run `xcrun notarytool store-credentials <name>` first)") orelse
b.graph.environ_map.get("FIZZY_MACOS_NOTARY_PROFILE");
const target = b.standardTargetOptions(.{});
// Artifacts install to `zig-out/<arch>-<os>/` (e.g. arm64-macos, x86-64-windows). Pass `-Dtarget=…` as usual.
const optimize = b.standardOptimizeOption(.{});
const macos_sdl_paths = try macosSdlPathsForExplicitTarget(b, target);
const zig_out_subdir = zigOutSubdirForTarget(b, target);
const zig_out_install_dir: std.Build.InstallDir = .{ .custom = zig_out_subdir };
const target_is_windows_msvc = target.result.os.tag == .windows and target.result.abi == .msvc;
const cross_win_msvc = target_is_windows_msvc and b.graph.host.result.os.tag != .windows;
// Auto-fetch defaults: on Windows hosts targeting *-windows-msvc, downloading the
// MSVC SDK into .velopack-msvc/ is the deterministic path — Zig's auto-detection
// of a system Visual Studio install picks up whatever's currently installed, which
// makes packaged release builds non-reproducible. The same .velopack-msvc/ tree is
// used on macOS/Linux cross-compile hosts, so all three triples land on the same
// SDK headers + libs. Explicit `-Dfetch-msvc=false` opts out (use system VS); an
// explicit `-Dwindows-msvc-libc=...` overrides the discovery entirely.
const fetch_msvc = fetch_msvc_opt orelse (target_is_windows_msvc and windows_msvc_libc_opt == null);
const win_libc = velopack.resolveWindowsMsvcLibc(b, target, .{
.explicit_path = windows_msvc_libc_opt,
.install_dir_name = ".velopack-msvc",
.fetch_if_missing = fetch_msvc,
});
var effective_win_libc: ?[]const u8 = win_libc.libc_path;
if (effective_win_libc == null) {
if (cross_win_msvc) effective_win_libc = b.libc_file;
}
// Velopack in the dev/install exe is opt-in (`-Dvelopack=true`). Release
// packaging (`zig build package`) still links Velopack when the ABI supports
// it via a second compile, so `zig build` / `run` / `test` never pull dotnet
// or the static Velopack lib unless you ask. Windows *-gnu targets are
// unchanged (no Velopack prebuilt for that ABI).
const velopack_supported_for_target = !(target.result.os.tag == .windows and target.result.abi != .msvc);
const velopack_enabled = b.option(
bool,
"velopack",
"Link Velopack runtime in the install/run exe (auto-update). Default: false. `package` still produces a Velopack-linked binary when supported.",
) orelse false;
if (velopack_enabled and !velopack_supported_for_target) {
std.log.err(
"-Dvelopack=true is unsupported for target ABI {s}: Velopack on Windows requires -Dtarget=x86_64-windows-msvc or -Dtarget=aarch64-windows-msvc.",
.{@tagName(target.result.abi)},
);
return error.WindowsMsvcAbiRequired;
}
// Fail loudly when the *-windows-msvc target has no headers/libs to compile against.
// On a non-Windows host this happens whenever `.velopack-msvc/` is missing and the
// user didn't pass `-Dfetch-msvc` or `-Dwindows-msvc-libc=…`. On a Windows host the
// auto-fetch default makes this unreachable unless the user explicitly opted out
// with `-Dfetch-msvc=false` — in which case Zig falls back to system Visual Studio
// auto-detection, which we can't validate here.
const velopack_required_fail: ?*std.Build.Step = if (cross_win_msvc and effective_win_libc == null)
&b.addFail(
\\*-windows-msvc needs MSVC + Windows SDK headers/libs.
\\ One-shot install (macOS/Linux/Windows): zig build msvcup-setup
\\ Then: zig build package -Dtarget=x86_64-windows-msvc (auto-uses .velopack-msvc/zig-libc-x64.ini)
\\ Or auto-download in this build: add -Dfetch-msvc (default on Windows hosts; forwards through packageall)
\\ Or pass: --libc path.ini / -Dwindows-msvc-libc=path.ini
).step
else
null;
const no_emit = b.option(bool, "no-emit", "Check for compile errors without emitting any code") orelse false;
const app_version_opt = b.option([]const u8, "app_version", "App version for vpk packVersion and startup log; defaults to VERSION file");
// GitHub repo URL baked into the binary so Velopack's auto-update can find
// the latest release via the GitHub Releases API. Override at build time
// with `-Drepo-url=...` (e.g. when shipping a fork). At runtime, the env
// var `FIZZY_AUTOUPDATE_URL` still overrides this for local feed testing.
const app_repo_url = b.option([]const u8, "repo-url", "GitHub repo URL used by Velopack auto-update (e.g. https://github.com/fizzyedit/fizzy)") orelse "https://github.com/fizzyedit/fizzy";
// Comma-separated fallback repo URLs checked (in order) after `app_repo_url`
// yields no update. Lets a build survive a repo move/rename: ship a binary
// whose primary points at the new home and whose fallback points at the old
// one (where the transitional release is published), then transfer the repo.
// Empty by default (no fallback).
const app_repo_url_fallback = b.option([]const u8, "repo-url-fallback", "Comma-separated fallback GitHub repo URLs for Velopack auto-update, tried after -Drepo-url") orelse "";
var version_owned: ?[]u8 = null;
defer if (version_owned) |buf| b.allocator.free(buf);
const app_version: []const u8 = if (app_version_opt) |v| v else blk: {
const raw = b.build_root.handle.readFileAlloc(b.graph.io, "VERSION", b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e});
version_owned = raw;
break :blk std.mem.trimEnd(u8, raw, "\r\n");
};
const build_opts = b.addOptions();
build_opts.addOption([]const u8, "app_version", app_version);
build_opts.addOption([]const u8, "app_repo_url", app_repo_url);
build_opts.addOption([]const u8, "app_repo_url_fallback", app_repo_url_fallback);
build_opts.addOption(bool, "velopack_enabled", velopack_enabled);
const step = b.step("update", "update git dependencies");
step.makeFn = update_step;
const msvcup_before_compile = velopack.addMsvcupSetupStep(b, ".velopack-msvc");
const msvcup_setup_step = b.step("msvcup-setup", "Download MSVC SDK into .velopack-msvc/ via velopack-zig (writes zig-libc-*.ini)");
msvcup_setup_step.dependOn(&msvcup_before_compile.step);
const zip_pkg = zip.package(b, .{});
const accesskit = b.option(dvui.AccesskitOptions, "accesskit", "Enable accesskit") orelse .off;
const assetpack = @import("assetpack");
const assets_module = assetpack.pack(b, b.path("assets"), .{});
// Generated atlas / asset stubs (`src/generated/*.zig`) are imported
// unconditionally by `fizzy.zig`, so the process-assets step has to
// run before any target that touches fizzy.zig — exe, integration
// tests, etc.
const assets_processing = try ProcessAssetsStep.init(b, "assets", "src/generated/");
const process_assets_step = b.step("process-assets", "generates struct for all assets");
process_assets_step.dependOn(&assets_processing.step);
// ---------------------------------------------------------------
// Web (wasm) build — entirely separate from the native exe so it can't disturb
// packaging / SDL / Velopack paths. `zig build web` produces `zig-out/web/{web.wasm,
// web.js, index.html, NotoSansKR-Regular.ttf}`, deployable as-is to a static host.
//
// Checkpoint A: minimal placeholder app, no fizzy editor code yet. Later checkpoints
// will incrementally pull fizzy modules in, gating each native-only path behind a
// `arch != .wasm32` check.
// ---------------------------------------------------------------
{
const web_target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .freestanding,
.cpu_features_add = std.Target.wasm.featureSet(&.{
.atomics,
.multivalue,
.bulk_memory,
}),
});
const dvui_web_dep = b.dependency("dvui", .{
.target = web_target,
.optimize = optimize,
.backend = .web,
.freetype = false,
});
const web_exe = b.addExecutable(.{
.name = "web",
.root_module = b.createModule(.{
.root_source_file = b.path("src/web_main.zig"),
.target = web_target,
.optimize = optimize,
.link_libc = false,
.single_threaded = true,
.strip = optimize == .ReleaseFast or optimize == .ReleaseSmall,
}),
});
web_exe.entry = .disabled;
web_exe.root_module.addImport("dvui", dvui_web_dep.module("dvui_web"));
web_exe.root_module.addImport("web-backend", dvui_web_dep.module("web"));
// Extra wasm exports beyond dvui's own (`dvui_init`/`dvui_update`/etc.). The wasm
// linker only emits symbols listed here, so `export fn` in Zig isn't enough on its
// own — without this line our trackpad pinch entry point would compile cleanly but
// be missing from `instance.exports`, and the JS bootstrap in `web/shell.html`
// would never be able to forward pinch deltas into the canvas widget.
web_exe.root_module.export_symbol_names = &[_][]const u8{
"FizzyWebTrackpadMagnification",
};
// `icons` (pure-Zig icon data) is referenced at file scope in
// `src/dvui.zig` and `src/editor/Infobar.zig`. Wired in so any future
// wasm-reachable code that pulls those files in compiles cleanly.
if (b.lazyDependency("icons", .{ .target = web_target, .optimize = optimize })) |dep| {
web_exe.root_module.addImport("icons", dep.module("icons"));
}
// `assets` is generated at build time by assetpack (pure `@embedFile`s,
// target-independent). Same instance as native — no extra build cost.
web_exe.root_module.addImport("assets", assets_module);
// `build_opts` (app_version, app_repo_url, velopack_enabled) — shared
// with native. velopack_enabled is whatever was passed via `-Dvelopack`;
// wasm path is gated by `arch != .wasm32` in `auto_update.impl`.
web_exe.root_module.addOptions("build_opts", build_opts);
// `zip` — Zig decls + miniz/zip.c compiled for wasm with `fizzy_zip_libc.c`
// (malloc → dvui_c_alloc). Enables `zip_stream_*` for .fiz open/save in browser.
web_exe.root_module.addImport("zip", zip_pkg.module);
zip.linkWasm(web_exe);
// `known-folders` is referenced at file scope in a few editor files
// (AboutFizzy, Editor settings paths). It's a pure-Zig wrapper for
// OS-specific user-directory APIs — the file compiles fine on wasm even
// though runtime calls would fail (which we'll never reach on web).
const known_folders_web = b.dependency("known_folders", .{
.target = web_target,
.optimize = optimize,
}).module("known-folders");
web_exe.root_module.addImport("known-folders", known_folders_web);
// Three editor files have `const sdl3 = @import("backend").c;` at file
// scope. After refactoring all `sdl3.SDL_DialogFileFilter` references
// to `fizzy.backend.DialogFileFilter`, those decls became dead — Zig's
// lazy analysis skips file-scope consts that no reachable body uses.
// So no `backend` module is wired in for the web build.
// `zstbi` for the web build. The C sources include `<stdlib.h>` /
// `<assert.h>` only when `STBI_NO_STDLIB` is undefined; with the flag
// set, `zstbi.c` routes alloc + qsort through `fizzy_stbi_libc.c`
// (which forwards to DVUI's `dvui_c_alloc` / `dvui_c_free`). Lets the
// Packer compile + run on wasm against the currently-open files.
const zstbi_web_lib = b.addLibrary(.{
.name = "zstbi-web",
.root_module = b.addModule("zstbi_web", .{
.target = web_target,
.optimize = optimize,
.root_source_file = b.path("src/deps/stbi/zstbi.zig"),
.link_libc = false,
.single_threaded = true,
}),
});
const zstbi_web_cflags = [_][]const u8{
"-DSTBI_NO_STDLIB=1",
"-DSTBI_NO_SIMD=1",
};
zstbi_web_lib.root_module.addCSourceFile(.{
.file = std.Build.path(b, "src/deps/stbi/zstbi.c"),
.flags = &zstbi_web_cflags,
});
zstbi_web_lib.root_module.addCSourceFile(.{
.file = std.Build.path(b, "src/deps/stbi/fizzy_stbi_libc.c"),
.flags = &zstbi_web_cflags,
});
web_exe.root_module.addImport("zstbi", zstbi_web_lib.root_module);
const msf_gif_web_lib = b.addLibrary(.{
.name = "msf_gif-web",
.root_module = b.addModule("msf_gif_web", .{
.target = web_target,
.optimize = optimize,
.root_source_file = b.path("src/deps/msf_gif/msf_gif.zig"),
.link_libc = false,
.single_threaded = true,
}),
});
const msf_gif_wasm_cflags = [_][]const u8{"-Isrc/deps/msf_gif/wasm_shim"};
msf_gif_web_lib.root_module.addCSourceFile(.{
.file = std.Build.path(b, "src/deps/msf_gif/fizzy_msf_gif_wasm.c"),
.flags = &msf_gif_wasm_cflags,
});
web_exe.root_module.addImport("msf_gif", msf_gif_web_lib.root_module);
const web_install_dir: std.Build.InstallDir = .{ .custom = "web" };
const install_wasm = b.addInstallArtifact(web_exe, .{
.dest_dir = .{ .override = web_install_dir },
});
// Cache-buster: stamps a 64-char hash into the index.html / web.js placeholders so
// the browser picks up new wasm builds without manual hard-reloads. Re-implements
// upstream DVUI's `addWebExample` machinery so we don't have to invoke its step.
const cb = b.addExecutable(.{
.name = "cacheBuster",
.root_module = b.createModule(.{
.root_source_file = dvui_web_dep.path("src/cacheBuster.zig"),
.target = b.graph.host,
}),
});
const cb_run = b.addRunArtifact(cb);
cb_run.addFileArg(b.path("web/shell.html"));
cb_run.addFileArg(dvui_web_dep.path("src/backends/web.js"));
cb_run.addFileArg(web_exe.getEmittedBin());
const index_html_with_hash = cb_run.captureStdOut(.{});
const web_step = b.step("web", "Build the fizzy web (wasm) app into zig-out/web/");
web_step.dependOn(&install_wasm.step);
web_step.dependOn(&b.addInstallFileWithDir(
index_html_with_hash,
web_install_dir,
"index.html",
).step);
web_step.dependOn(&b.addInstallFileWithDir(
dvui_web_dep.path("src/backends/web.js"),
web_install_dir,
"web.js",
).step);
web_step.dependOn(&b.addInstallFileWithDir(
dvui_web_dep.path("src/fonts/NotoSansKR-Regular.ttf"),
web_install_dir,
"NotoSansKR-Regular.ttf",
).step);
// Compile-only smoke check for the wasm target. Pairs with `check` (unit
// tests). Catches regressions where someone reaches a wasm-incompatible
// code path (thread spawn, std.posix surface, missing module import)
// from the wasm root. No install — just compile.
const check_web_step = b.step("check-web", "Compile fizzy web (wasm) without installing artifacts");
check_web_step.dependOn(&web_exe.step);
// Copy zig-out/web into web/app/ for local preview at the production
// `/app/` path: `cd web && python3 -m http.server` then open
// http://localhost:8000/app/. The landing page lives in fizzyedit/website.
const web_docs_step = b.step("web-docs", "Build web app and copy into web/app/ for local /app/ preview");
web_docs_step.dependOn(web_step);
const cp_web_to_docs = b.addSystemCommand(&.{ "sh", "-c" });
cp_web_to_docs.addArg("mkdir -p web/app && cp -R zig-out/web/. web/app/");
cp_web_to_docs.step.dependOn(web_step);
web_docs_step.dependOn(&cp_web_to_docs.step);
const serve_web_cmd = b.addSystemCommand(&.{ "sh", "scripts/serve-web.sh" });
serve_web_cmd.step.dependOn(web_step);
_ = b.step(
"serve-web",
"Serve zig-out/web at http://127.0.0.1:8765/ (builds web first; frees stale :8765)",
).dependOn(&serve_web_cmd.step);
}
const main_fizzy = try addFizzyExecutableForTarget(b, target, optimize, accesskit, build_opts, zip_pkg, assets_module, process_assets_step, macos_sdl_paths, velopack_enabled);
const exe = main_fizzy.exe;
const zstbi_module = main_fizzy.zstbi_module;
const msf_gif_module = main_fizzy.msf_gif_module;
const known_folders = main_fizzy.known_folders;
const exe_for_package: *std.Build.Step.Compile = package_blk: {
if (velopack_enabled) break :package_blk exe;
if (!velopack_supported_for_target) break :package_blk exe;
const pack_opts = b.addOptions();
pack_opts.addOption([]const u8, "app_version", app_version);
pack_opts.addOption([]const u8, "app_repo_url", app_repo_url);
pack_opts.addOption([]const u8, "app_repo_url_fallback", app_repo_url_fallback);
pack_opts.addOption(bool, "velopack_enabled", true);
const pack_fizzy = try addFizzyExecutableForTarget(b, target, optimize, accesskit, pack_opts, zip_pkg, assets_module, process_assets_step, macos_sdl_paths, true);
break :package_blk pack_fizzy.exe;
};
if (no_emit) {
b.getInstallStep().dependOn(&exe.step);
} else {
const install_artifact = b.addInstallArtifact(exe, .{
.dest_dir = .{ .override = zig_out_install_dir },
});
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run the app (does not run Velopack)");
run_cmd.step.dependOn(&install_artifact.step);
run_step.dependOn(&run_cmd.step);
b.getInstallStep().dependOn(&install_artifact.step);
}
const package_step = b.step("package", "Velopack release artifacts (strip + vpk); not part of install or run");
// The default native target on a Windows host resolves to x86_64-windows-gnu,
// for which `velopack_supported_for_target` is false — exe_for_package falls
// back to the plain (Velopack-less) exe. vpk would still wrap it as a Velopack
// installer, but the install hook never runs: Setup.exe hangs with "the
// application install hook failed". Fail loudly instead of shipping that trap.
const windows_non_msvc = target.result.os.tag == .windows and target.result.abi != .msvc;
if (velopack_required_fail) |fail_step| {
package_step.dependOn(fail_step);
} else if (windows_non_msvc) {
package_step.dependOn(&b.addFail(
\\`zig build package` for Windows requires the MSVC ABI so Velopack is linked.
\\The default native target resolves to x86_64-windows-gnu, which builds a binary
\\WITHOUT the Velopack runtime. vpk would still wrap it as a Velopack installer, but
\\the install hook never runs and Setup.exe hangs ("the application install hook failed").
\\
\\Build with the MSVC target instead:
\\ zig build package -Dtarget=x86_64-windows-msvc -Dfetch-msvc
\\(needs Windows SDK 10.0.26100+ for SDL's GameInput backend.)
).step);
} else if (no_emit) {
package_step.dependOn(&b.addFail("cannot run `package` with -Dno-emit").step);
} else switch (target.result.os.tag) {
.linux, .macos, .windows => {
// Host strip can't process foreign object files when cross-compiling.
const cross_os = target.result.os.tag != b.graph.host.result.os.tag;
// Same-OS / different-arch (e.g. aarch64-linux from x86_64-linux) also
// breaks host strip — it errors with "Unable to recognise the format".
const cross_for_strip = cross_os or target.result.cpu.arch != b.graph.host.result.cpu.arch;
// Windows hosts don't ship `strip` or `touch`. Skip the external strip
// step entirely there — Zig's linker already drops debug info in
// release builds. Use `cmd /c exit 0` as the no-op and keep the
// dependency on exe_for_package via the step graph.
const host_is_windows = b.graph.host.result.os.tag == .windows;
const skip_strip = host_is_windows or optimize == .Debug or cross_for_strip;
const strip_release_sh = if (host_is_windows) blk: {
const sh = b.addSystemCommand(&.{ "cmd", "/c", "exit", "0" });
sh.step.dependOn(&exe_for_package.step);
break :blk sh;
} else blk: {
const sh = b.addSystemCommand(&.{if (skip_strip) "touch" else "strip"});
sh.addFileArg(exe_for_package.getEmittedBin());
break :blk sh;
};
//const dotnet_tool_restore = velopack.addDotnetToolRestoreStep(b);
//const vpk_vendor_repair = velopack.addVpkVendorRepairStep(b);
//vpk_vendor_repair.step.dependOn(&dotnet_tool_restore.step);
const vpk_pkg_sh = b.addSystemCommand(&.{"dotnet"});
vpk_pkg_sh.addArg("vpk");
// When packaging a foreign-OS bundle, vpk needs an OS directive (e.g. `vpk [win] pack ...`)
// because by default it auto-detects from the host OS.
if (cross_os) {
vpk_pkg_sh.addArg(switch (target.result.os.tag) {
.windows => "[win]",
.linux => "[linux]",
.macos => "[osx]",
else => unreachable,
});
}
vpk_pkg_sh.addArg("pack");
vpk_pkg_sh.addArg("--packId");
vpk_pkg_sh.addArg("fizzy");
vpk_pkg_sh.addArg("--packVersion");
vpk_pkg_sh.addArg(app_version);
// Channel = zig-out subdir (`<arch>-<os>`, NuGet-safe — no underscores). Baked into
// the binary by vpk; the updater matches this to release assets. Distinct per triple
// so parallel `vpk pack` runs don't collide on RELEASES / nupkg names.
vpk_pkg_sh.addArg("--channel");
vpk_pkg_sh.addArg(zig_out_subdir);
vpk_pkg_sh.addArg("--mainExe");
vpk_pkg_sh.addArg(switch (target.result.os.tag) {
.windows => "fizzy.exe",
else => "fizzy",
});
vpk_pkg_sh.addArg("--delta");
vpk_pkg_sh.addArg("None");
vpk_pkg_sh.addArg("--yes");
vpk_pkg_sh.addArg("--outputDir");
// `addOutputDirectoryArg` takes a basename — Zig manages the actual
// path under the run step's cache dir. The `addInstallDirectory`
// below copies that into zig-out/<channel>/. Previously this passed
// the full install path, which produced `.zig-cache\o\<hash>\C:\...`
// on Windows (BadPathName).
const vpk_pkg_out_dir = vpk_pkg_sh.addOutputDirectoryArg("desktop");
vpk_pkg_sh.addArg("--packDir");
vpk_pkg_sh.addDirectoryArg(exe_for_package.getEmittedBin().dirname());
switch (target.result.os.tag) {
.windows => {
// Sets the installer's icon and the Start Menu shortcut icon. The
// exe's own icon is already embedded via assets/windows/fizzy.rc.
vpk_pkg_sh.addArg("--icon");
const ico_path = b.path("assets/windows/fizzy.ico").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("ico path: {}", .{e});
vpk_pkg_sh.addArg(ico_path);
// Velopack's installer is silent (no shortcut-choice UI). Default is
// Desktop,StartMenu; restrict to StartMenu so we don't drop an
// unrequested icon on the user's desktop.
vpk_pkg_sh.addArg("--shortcuts");
vpk_pkg_sh.addArg("StartMenu");
},
.macos => {
vpk_pkg_sh.addArg("--packTitle");
vpk_pkg_sh.addArg("fizzy");
// Bundle id / document types / versions: assets/macos/info.plist (vpk rejects --bundleId with --plist).
vpk_pkg_sh.addArg("--plist");
const plist_path = b.path("assets/macos/info.plist").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("plist path: {}", .{e});
vpk_pkg_sh.addArg(plist_path);
vpk_pkg_sh.addArg("--icon");
const icns_path = b.path("assets/macos/fizzy.icns").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("icns path: {}", .{e});
vpk_pkg_sh.addArg(icns_path);
if (macos_sign_app_identity) |id| {
vpk_pkg_sh.addArg("--signAppIdentity");
vpk_pkg_sh.addArg(id);
// Required for notarization: enables hardened runtime + secure timestamp on
// every nested binary (vpk forwards the file to `codesign --entitlements`).
// Without this, Apple's notary service rejects with "signature does not
// include a secure timestamp" / "hardened runtime not enabled".
vpk_pkg_sh.addArg("--signEntitlements");
const entitlements_path = b.path("assets/macos/Fizzy.entitlements").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("entitlements path: {}", .{e});
vpk_pkg_sh.addArg(entitlements_path);
}
if (macos_sign_install_identity) |id| {
vpk_pkg_sh.addArg("--signInstallIdentity");
vpk_pkg_sh.addArg(id);
}
if (macos_notary_profile) |profile| {
vpk_pkg_sh.addArg("--notaryProfile");
vpk_pkg_sh.addArg(profile);
}
},
else => {},
}
vpk_pkg_sh.setEnvironmentVariable("DOTNET_ROLL_FORWARD", "Major");
// Stream vpk's stdout/stderr live so failures surface their actual
// diagnostic instead of just an exit-code-N message from the build
// runner. With `addOutputDirectoryArg` in play, `infer_from_args`
// can otherwise capture+drop stdio on certain runner configs.
vpk_pkg_sh.stdio = .inherit;
try velopack.attachMksquashfsToVpkRun(b, vpk_pkg_sh, target);
//vpk_pkg_sh.step.dependOn(&vpk_vendor_repair.step);
vpk_pkg_sh.step.dependOn(&strip_release_sh.step);
const build_package_install = b.addInstallDirectory(.{
.source_dir = vpk_pkg_out_dir,
.install_dir = zig_out_install_dir,
.install_subdir = "",
});
package_step.dependOn(&build_package_install.step);
},
else => {
package_step.dependOn(&b.addFail("Velopack packaging is only supported for Linux, macOS, and Windows targets").step);
},
}
const desktop_step = b.step("desktop", "Alias for `zig build package`");
desktop_step.dependOn(package_step);
const packageall_step = b.step("packageall", "Six zig build package runs; use -Dwindows-msvc-libc= or -Dfetch-msvc for Windows children from macOS/Linux");
if (no_emit) {
packageall_step.dependOn(&b.addFail("cannot run `packageall` with -Dno-emit").step);
} else {
const packageall_optimize_arg = b.fmt("-Doptimize={s}", .{@tagName(optimize)});
// Build order is deliberately fail-fast: Windows first (most likely to
// fail on a fresh CI runner because of MSVC SDK setup, libc.ini paths,
// and cross-compile ABI surprises), then Linux (mksquashfs / AppImage
// packaging quirks), then macOS last (native, lowest risk). When a
// release run is going to break, this ordering surfaces the failure
// 5-10 minutes sooner than the alphabetical order did.
const packageall_triples = [_][]const u8{
"x86_64-windows-msvc",
"aarch64-windows-msvc",
"x86_64-linux-gnu",
"aarch64-linux-gnu",
"x86_64-macos",
"aarch64-macos",
};
var prev_step: ?*std.Build.Step = null;
for (packageall_triples) |triple| {
const zig_pkg_run = b.addSystemCommand(&.{
b.graph.zig_exe,
"build",
"package",
packageall_optimize_arg,
b.fmt("-Dtarget={s}", .{triple}),
});
if (std.mem.endsWith(u8, triple, "-windows-msvc")) {
if (windows_msvc_libc_opt) |libc_path| {
zig_pkg_run.addArg(b.fmt("-Dwindows-msvc-libc={s}", .{libc_path}));
}
if (fetch_msvc) zig_pkg_run.addArg("-Dfetch-msvc");
}
zig_pkg_run.setCwd(b.path("."));
if (prev_step) |p| {
zig_pkg_run.step.dependOn(p);
}
prev_step = &zig_pkg_run.step;
}
packageall_step.dependOn(prev_step.?);
}
// ---------------------------------------------------------------
// Tests
// ---------------------------------------------------------------
//
// Fizzy has two test layers (see tests/README.md):
//
// 1. Unit tests — pure-logic only (math, palette parsing, layer
// order). The test root imports nothing but std + the pure
// modules under test, so it compiles in well under a second
// and never needs dvui/SDL/assets.
//
// 2. Integration tests (added in Phase 2 of the testing plan)
// will use dvui's testing backend and exercise real fizzy
// drawing functions in a headless Window.
//
// Both share the same `zig build test` and `zig build check`
// entry points.
const test_filters = b.option(
[]const []const u8,
"test-filter",
"Skip tests that do not match any filter",
) orelse &[0][]const u8{};
const tests_module = b.addModule("fizzy-tests", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/root.zig"),
});
// Wire each pure-logic source file as a named module on the test
// target. Zig 0.15 disallows importing source files outside the test
// module's own directory via relative paths, so we expose them by
// name. Each of these files imports only `std`, so they remain free
// of dvui / SDL / globals.
inline for (.{
.{ "fizzy-direction", "src/math/direction.zig" },
.{ "fizzy-easing", "src/math/easing.zig" },
.{ "fizzy-layer-order", "src/internal/layer_order.zig" },
.{ "fizzy-palette-parse", "src/internal/palette_parse.zig" },
.{ "fizzy-layout-anchor", "src/math/layout_anchor.zig" },
.{ "fizzy-reduce", "src/algorithms/reduce.zig" },
.{ "fizzy-grid-validate", "src/internal/grid_layout_validate.zig" },
.{ "fizzy-animation", "src/Animation.zig" },
}) |entry| {
tests_module.addAnonymousImport(entry[0], .{
.root_source_file = b.path(entry[1]),
.target = target,
.optimize = optimize,
});
}
const unit_tests = b.addTest(.{
.name = "fizzy-unit-tests",
.root_module = tests_module,
.filters = test_filters,
});
// `zig build test` is the CI entry point and must stay self-contained: pure
// unit tests only, no dvui/SDL/Velopack/MSVC. Integration tests live under
// `zig build test-integration` (Velopack + dvui-testing + comctl32 on Windows
// → needs MSVC SDK on Windows hosts). `zig build test-all` runs both.
const test_step = b.step("test", "Run fizzy unit tests (pure-logic only, no dvui/SDL/Velopack)");
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
// `check` mirrors the split so editor compile-error checking matches CI.
const check_step = b.step("check", "Compile fizzy unit tests without running them");
check_step.dependOn(&unit_tests.step);
// ---------------------------------------------------------------
// Layer 2: headless integration tests against dvui's testing
// backend. Wired under separate `test-integration` / `check-integration`
// steps so `zig build test` stays MSVC-free on Windows CI runners. Skipped
// when cross-compiling to *-windows-msvc without an MSVC libc INI.
// ---------------------------------------------------------------
const test_integration_step = b.step("test-integration", "Run fizzy headless integration tests (dvui-testing; needs MSVC on Windows)");
const check_integration_step = b.step("check-integration", "Compile fizzy integration tests without running them");
const test_all_step = b.step("test-all", "Run unit + integration tests");
test_all_step.dependOn(test_step);
test_all_step.dependOn(test_integration_step);
if (velopack_required_fail) |fail_step| {
test_integration_step.dependOn(fail_step);
check_integration_step.dependOn(fail_step);
return;
}
const dvui_testing_dep = b.dependency("dvui", .{
.target = target,
.optimize = optimize,
.backend = .testing,
.accesskit = accesskit,
});
// Build a module rooted at `src/fizzy.zig` carrying all the same
// imports the production exe carries. Because fizzy.zig's transitive
// imports (App.zig, Editor.zig, …) reference `dvui`, `assets`,
// `known-folders`, etc. by name, those names must be wired here.
// We point dvui at the *testing* backend so calling drawing
// functions doesn't try to open a real OS window.
const fizzy_test_module = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/fizzy.zig"),
});
fizzy_test_module.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
fizzy_test_module.addImport("backend", dvui_testing_dep.module("testing"));
fizzy_test_module.addImport("assets", assets_module);
fizzy_test_module.addImport("known-folders", known_folders);
fizzy_test_module.addOptions("build_opts", build_opts);
fizzy_test_module.addImport("zstbi", zstbi_module);
fizzy_test_module.addImport("msf_gif", msf_gif_module);
fizzy_test_module.addImport("zip", zip_pkg.module);
if (b.lazyDependency("icons", .{ .target = target, .optimize = optimize })) |dep| {
fizzy_test_module.addImport("icons", dep.module("icons"));
}
if (target.result.os.tag == .macos) {
if (b.lazyDependency("zig_objc", .{ .target = target, .optimize = optimize })) |dep| {
fizzy_test_module.addImport("objc", dep.module("objc"));
}
} else if (target.result.os.tag == .windows) {
if (b.lazyDependency("zigwin32", .{})) |dep| {
fizzy_test_module.addImport("win32", dep.module("win32"));
}
}
const integration_module = b.addModule("fizzy-integration-tests", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/integration.zig"),
});
integration_module.addImport("fizzy", fizzy_test_module);
integration_module.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
const integration_tests = b.addTest(.{
.name = "fizzy-integration-tests",
.root_module = integration_module,
.filters = test_filters,
});
if (target.result.os.tag == .windows) {
integration_tests.root_module.linkSystemLibrary("comctl32", .{});
}
// Zig's bundled libc++/libcxxabi cannot compile against MSVC headers from
// --libc (vcruntime_typeinfo.h vs libc++ type_info, etc.), so libc++ must be
// off for the msvc ABI regardless of host (cross or native Windows).
integration_tests.root_module.link_libcpp = !target_is_windows_msvc;
zip.link(integration_tests);
if (velopack_enabled) {
try velopack.linkVelopack(b, integration_tests, .{ .target = target, .optimize = optimize });
}
integration_tests.step.dependOn(process_assets_step);
test_integration_step.dependOn(&b.addRunArtifact(integration_tests).step);
check_integration_step.dependOn(&integration_tests.step);
if (win_libc.needs_setup) {
exe.step.dependOn(&msvcup_before_compile.step);
if (!velopack_enabled and velopack_supported_for_target) {
exe_for_package.step.dependOn(&msvcup_before_compile.step);
}
integration_tests.step.dependOn(&msvcup_before_compile.step);
unit_tests.step.dependOn(&msvcup_before_compile.step);
}
if (target.result.os.tag == .windows and target.result.abi == .msvc) {
var roots: [4]*std.Build.Step.Compile = undefined;
var n: usize = 0;
roots[n] = exe;
n += 1;
roots[n] = unit_tests;
n += 1;
roots[n] = integration_tests;
n += 1;
if (!velopack_enabled and velopack_supported_for_target) {
roots[n] = exe_for_package;
n += 1;
}
// Always apply the translate-c shim + SIZE_MAX define for windows-msvc, regardless of
// whether we're using a downloaded SDK or the host's system MSVC. translate-c uses aro
// (not MSVC cl.exe), and aro rejects literals like `0xffffffffffffffffui64` from MSVC's
// <stdint.h>. The shim shadows stdint.h via `-I` (search order beats `-isystem`); the
// defineCMacro adds belt-and-suspenders by predefining SIZE_MAX before any include so
// MSVC's stdint.h `#ifndef SIZE_MAX` skips its own definition entirely.
applyMsvcTranslateCShim(b, roots[0..n]) catch |e| {
std.debug.panic("MSVC translate-c shim wiring failed: {s}", .{@errorName(e)});
};
if (effective_win_libc) |ini| {
if (cross_win_msvc) b.libc_file = null;
const libc_lp: std.Build.LazyPath = .{ .cwd_relative = ini };
velopack.applyWindowsMsvcLibcRecursive(b, roots[0..n], libc_lp);
const ini_exists = blk: {
b.build_root.handle.access(b.graph.io, ini, .{}) catch break :blk false;
break :blk true;
};
if (ini_exists) {
// Adds explicit MSVC/UCRT/SDK `-isystem` paths from the libc INI to each reachable
// translate-c step. Only relevant when cross-compiling with .velopack-msvc/; on a
// Windows host with system MSVC, Zig auto-discovers these paths itself.
applyMsvcIncludesToReachableTranslateC(b, roots[0..n], ini) catch |e| {
std.debug.panic("MSVC translate-c include fixup failed: {s}", .{@errorName(e)});
};
} else {
// The INI is written by `msvcup-setup` (a make-phase step), but the translate-c
// `-isystem` paths embed the SDK version subdir, which is only known after the SDK
// is installed — so they must be wired at configure time, before that step runs.
// A one-shot `zig build package -Dfetch-msvc` against a clean .velopack-msvc can't
// satisfy that ordering. Fail only the compiles that need it (not `msvcup-setup`,
// which has no such dependency), so running setup first still works.
const fail = &b.addFail(
\\*-windows-msvc has no .velopack-msvc/zig-libc INI yet, so translate-c can't be wired.
\\The SDK install must run as its own step before packaging (it can't be done in one
\\pass — the translate-c include paths depend on the installed SDK version):
\\ zig build msvcup-setup
\\ zig build package -Dtarget=x86_64-windows-msvc
).step;
for (roots[0..n]) |rc| rc.step.dependOn(fail);
}
}
}
}
/// Apply the always-on translate-c fixups for windows-msvc targets: the stdint.h shim
/// (so aro doesn't choke on MSVC's `ui64` literal suffix) and a predefined SIZE_MAX.
/// Runs whether or not we have a downloaded SDK — the shim is purely an `-I` injection
/// and a `-D` flag, so it works equally on cross-compile and native windows-host builds.
fn applyMsvcTranslateCShim(b: *std.Build, roots: []const *std.Build.Step.Compile) !void {
var seen = std.AutoHashMap(*std.Build.Step.TranslateC, void).init(b.allocator);
defer seen.deinit();
for (roots) |root_compile| {
const graph = root_compile.root_module.getGraph();
for (graph.modules) |mod| {
const root_src = mod.root_source_file orelse continue;
const gen = switch (root_src) {
.generated => |g| g,
else => continue,
};
const dep_step = gen.file.step;
if (dep_step.id != .translate_c) continue;
const tc: *std.Build.Step.TranslateC = @fieldParentPtr("step", dep_step);
const gop = try seen.getOrPut(tc);
if (gop.found_existing) continue;
const rt = tc.target.result;
if (rt.os.tag != .windows or rt.abi != .msvc) continue;
// `-I` searches before `-isystem`, so this shim wins over MSVC's <stdint.h>.
tc.addIncludePath(b.path("src/tools/msvc_translatec_shim"));
// Pre-define SIZE_MAX so MSVC's stdint.h `#ifndef SIZE_MAX` block — which would
// otherwise install a `0xff…ui64` literal — skips itself. Belt-and-suspenders
// to the shim: covers the case where another header includes <stdint.h> through
// a path that bypasses our shim.
tc.defineCMacro("SIZE_MAX", switch (rt.ptrBitWidth()) {
32 => "4294967295U",
64 => "18446744073709551615ULL",
else => "UINT_MAX",
});
}
}
}
/// Finds every `Step.TranslateC` reachable from each root compile's Zig module graph and adds
/// MSVC / Windows SDK `-isystem` paths from the zig-libc INI. We walk `Module.getGraph()` (imports)
/// rather than `Step.dependencies`: Zig wires `root_source_file` → `TranslateC` only in
/// `createModuleDependencies`, which runs after `build()` returns, so a step BFS from `Compile`
/// would miss DVUI's `dvui-c` / `sdl3-c` translate steps during Configure.
fn applyMsvcIncludesToReachableTranslateC(
b: *std.Build,
roots: []const *std.Build.Step.Compile,
libc_ini_path: []const u8,
) !void {
// `libc_ini_path` is absolute (resolved via `b.pathFromRoot`), so any Dir works as the base.
const data = try b.build_root.handle.readFileAlloc(b.graph.io, libc_ini_path, b.allocator, .unlimited);
var include_dir: ?[]const u8 = null;
var sys_include_dir: ?[]const u8 = null;
var line_it = std.mem.splitScalar(u8, data, '\n');
while (line_it.next()) |raw| {
const line = std.mem.trim(u8, raw, " \r\t");
if (std.mem.startsWith(u8, line, "include_dir=")) {
include_dir = std.mem.trim(u8, line["include_dir=".len..], " \r\t");
} else if (std.mem.startsWith(u8, line, "sys_include_dir=")) {
sys_include_dir = std.mem.trim(u8, line["sys_include_dir=".len..], " \r\t");
}
}
if (include_dir == null or sys_include_dir == null) return;
// `include_dir` points at `.../Windows Kits/10/Include/<ver>/ucrt`. The Windows SDK's
// um/shared/winrt headers live as siblings of the `ucrt` directory.
const sdk_inc_root = std.fs.path.dirname(include_dir.?) orelse return;
const um_dir = try std.fs.path.join(b.allocator, &.{ sdk_inc_root, "um" });
const shared_dir = try std.fs.path.join(b.allocator, &.{ sdk_inc_root, "shared" });
const winrt_dir = try std.fs.path.join(b.allocator, &.{ sdk_inc_root, "winrt" });
var seen_translate_c = std.AutoHashMap(*std.Build.Step.TranslateC, void).init(b.allocator);