-
Notifications
You must be signed in to change notification settings - Fork 93
Expand file tree
/
Copy pathbuild.zig
More file actions
3516 lines (3377 loc) · 188 KB
/
Copy pathbuild.zig
File metadata and controls
3516 lines (3377 loc) · 188 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
// we do these things not because they are easy, but because we thought they were going to be easy
const std = @import("std");
const String = []const u8;
// LLVM-implicating targets (qtarcan, gamescope, llama-cli, and Phase 3+
// afsrv_bun) live in build_llvm/ as a standalone Zig project — invoked
// via `cd build_llvm && zig build <target>`. They are intentionally NOT
// reachable from this top-level build script and their deps are NOT in
// this top-level build.zig.zon, so a self-host-only toolchain checking
// out arcan never sees them. See build_llvm/build.zig{,.zon}.
const build_xarcan = @import("build_xarcan.zig");
const build_helpers = @import("build_helpers.zig");
const build_external = @import("build_external.zig");
const a12_version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 };
const shmif_version = std.SemanticVersion{ .major = 0, .minor = 18, .patch = 0 };
const Opts = struct {
platform_header: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
static: bool,
strip: ?bool,
pic: ?bool,
build_shmif_server: bool,
build_shmif: bool,
build_tui: bool,
build_a12: bool,
build_arcan_db: bool,
build_arcan_frameserver: bool,
build_afsrv_terminal: bool,
build_afsrv_decode: bool,
build_afsrv_encode: bool,
build_afsrv_net: bool,
build_arcan_net: bool,
build_arcan_net_session: bool,
build_afsrv_remoting: bool,
build_afsrv_game: bool,
build_afsrv_avfeed: bool,
build_afsrv_bun: bool,
build_afsrv_probe: bool,
build_arcan_vk: bool,
build_shmif_ext: bool,
build_aclip: bool,
build_shmmon: bool,
build_acfgfs: bool,
build_xarcan: bool,
build_ghostty_tests: bool = false,
prebuilt_musl: bool = false,
libc_file: ?std.Build.LazyPath = null,
static_deps: bool = false,
ext: build_external.ExternalDeps = .{},
};
// Shared include path constants
const shmif_include_paths: []const String = &.{
"src/shmif", "src/shmif/tui", "src/shmif/tui/lua", "src/shmif/tui/widgets",
"src/shmif/platform", "src/engine", "src/platform",
};
const shmif_tui_include_paths: []const String = &.{
"src/frameserver", "src/engine", "src/engine/external", "src/shmif",
};
const a12_include_paths: []const String = &.{
"src/a12", "src/a12/external/blake3", "src/a12/external/zstd",
"src/a12/external/zstd/common", "src/a12/external/mono",
"src/a12/external/mono/optional", "src/a12/external", "src/engine", "src/shmif",
};
const compositor_include_paths: []const String = &.{
"src/engine", "src/engine/external", "src/platform", "src/shmif",
"src/shmif/tui", "src/frameserver", "external",
};
const fsrv_a12_include_paths: []const String = &.{
"src/frameserver/util", "src/a12", "src/a12/external", "src/a12/external/blake3",
"src/a12/external/mono", "src/a12/external/mono/optional", "src/engine", "src/frameserver",
};
// Helper functions
fn createExe(b: *std.Build, name: []const u8, opts: Opts) *std.Build.Step.Compile {
// src/zig_panic_root.zig installs `pub const panic = std.debug.simple_panic`,
// bypassing the default DWARF-walking panic handler that recursively crashes
// on our compressed .zdebug_* sections. See the file for full rationale.
const exe = b.addExecutable(.{ .use_llvm = use_llvm_default, .name = name, .root_module = b.createModule(.{
.root_source_file = b.path("src/zig_panic_root.zig"),
.target = opts.target, .optimize = opts.optimize,
}) });
addLibC(exe, opts);
addPlatformDefinitions(exe, opts);
exe.root_module.addCMacro("PLATFORM_HEADER", opts.platform_header);
addDarwinLibcBridge(exe, b, opts);
return exe;
}
// On macOS every exe needs the glibc↔Darwin libc bridges (stdio globals,
// __errno_location, __assert_fail, __sigsetjmp, mremap/setfs*). One object
// per exe — no cross-object symbol collisions.
fn addDarwinLibcBridge(exe: *std.Build.Step.Compile, b: *std.Build, opts: Opts) void {
switch (opts.target.result.os.tag) {
.ios, .macos, .watchos, .tvos => {},
else => return,
}
// LLVM backend: the stdio __mod_init_func constructor uses an @export
// with an explicit mach-o section, which the self-hosted backend does
// not yet implement (ExportOptions.section).
const obj = b.addObject(.{ .use_llvm = true, .name = "libc_darwin", .root_module = b.createModule(.{
.root_source_file = b.path("src/platform/darwin/libc_darwin.zig"),
.target = opts.target, .optimize = opts.optimize,
}) });
addLibC(obj, opts);
exe.addObject(obj);
}
fn createLibrary(b: *std.Build, name: []const u8, version: std.SemanticVersion, opts: Opts) *std.Build.Step.Compile {
return b.addLibrary(.{
.linkage = if (opts.static) .static else .dynamic,
.name = name, .version = version,
.use_llvm = use_llvm_default,
.root_module = b.createModule(.{
.target = opts.target, .optimize = opts.optimize, .pic = opts.pic, .strip = opts.strip,
}),
});
}
fn addIncludes(step: *std.Build.Step.Compile, b: *std.Build, paths: []const []const u8) void {
for (paths) |p| step.addIncludePath(b.path(p));
}
// SH backend is the default for everything in this build.zig.
// LLVM-required builds (ghostty bridge variants, Qt/gamescope/llama
// integrations) live under build_llvm/ and have their own entry points.
// Kept as a function (rather than inlining `false`) so future per-file
// overrides have a single chokepoint to extend.
fn useLlvmForSource(b: *std.Build, zig_path: []const u8) ?bool {
_ = b;
_ = zig_path;
return use_llvm_default;
}
// Backend selection: self-hosted for the native linux/bsd builds, LLVM when
// targeting darwin — the SH-backend Mach-O objects trip relocation overflows
// in the self-hosted linker. Set once at the top of build().
var use_llvm_default: bool = false;
fn debugPrefixFlag(b: *std.Build) []const u8 {
return b.fmt("-fdebug-prefix-map={s}/=", .{b.build_root.path orelse "."});
}
fn cSourceFlags(b: *std.Build, extra: []const []const u8) []const []const u8 {
// `&.{debugPrefixFlag(b)}` with a runtime value produces a stack-local
// 1-element array; returning a pointer to it is a dangling slice.
// LLVM often hides this by not reusing the stack; the self-hosted
// aarch64 backend overwrites it aggressively, so allocate on b.allocator
// unconditionally.
const flags = b.allocator.alloc([]const u8, extra.len + 1) catch @panic("OOM");
@memcpy(flags[0..extra.len], extra);
flags[extra.len] = debugPrefixFlag(b);
return flags;
}
fn addCSources(step: *std.Build.Step.Compile, b: *std.Build, paths: []const []const u8) void {
const dbg = &.{debugPrefixFlag(b)};
for (paths) |p| step.addCSourceFile(.{ .file = b.path(p), .flags = dbg });
}
// Call step.linkLibC() and, when -Dlibc_file is given, also point the step
// at that Zig libc file so Zig's own musl-from-source compile is skipped in
// favour of the prebuilt artifacts it describes. Replaces all direct
// step.linkLibC() calls so the flag is honoured uniformly.
fn addLibC(step: *std.Build.Step.Compile, opts: Opts) void {
step.linkLibC();
if (opts.libc_file) |p| step.setLibCFile(p);
}
fn linkFsrvStdlib(exe: *std.Build.Step.Compile) void {
// On Darwin, m/rt/dl/pthread/atomic/util all live in libSystem (linked via
// libc); there are no standalone .dylibs to name, so requesting them fails
// the link. Only Linux/BSD split them into separate libraries.
switch (exe.rootModuleTarget().os.tag) {
.ios, .macos, .watchos, .tvos => {},
// windows: m/atomic come from compiler-rt, dl/pthread/rt from the win
// substrate; none exist as standalone import libs. (windows port)
.windows => {},
else => for ([_][]const u8{ "m", "rt", "dl", "pthread", "atomic" }) |lib| exe.linkSystemLibrary(lib),
}
}
fn linkUtil(exe: *std.Build.Step.Compile) void {
switch (exe.rootModuleTarget().os.tag) {
.ios, .macos, .watchos, .tvos => {}, // forkpty/openpty are in libSystem
.windows => {}, // no forkpty on windows (windows port)
else => exe.linkSystemLibrary("util"),
}
}
fn createMod(b: *std.Build, path: []const u8, opts: Opts) *std.Build.Module {
return b.createModule(.{
.root_source_file = b.path(path),
.target = opts.target, .optimize = opts.optimize,
});
}
// a12_types re-exports shmif-layer types from shmif_types (arcan_event,
// arcan_shmif_cont, shmifsrv_client, arg_arr) so a12 consumers and shmif
// consumers agree on the underlying Zig type through the dispatch-struct
// pattern. Use this helper rather than createMod when a12_types is
// constructed locally.
fn createA12TypesMod(b: *std.Build, opts: Opts, shmif_mod: *std.Build.Module) *std.Build.Module {
const m = createMod(b, "src/a12/a12_types.zig", opts);
m.addImport("shmif_types", shmif_mod);
return m;
}
// anet_types re-exports concrete extern-struct types from shmif_types,
// a12_types, and posix_libc (struct_arcan_shmif_cont, struct_arcan_event,
// struct_a12_state, pthread_mutex_t, ...) so its own module graph must see
// those siblings. Use this helper rather than createMod when the anet_types
// module is constructed locally.
fn createAnetTypesMod(
b: *std.Build,
opts: Opts,
shmif_mod: *std.Build.Module,
a12_mod: *std.Build.Module,
libc_mod: *std.Build.Module,
) *std.Build.Module {
const m = createMod(b, "src/a12/net/anet_types.zig", opts);
m.addImport("shmif_types", shmif_mod);
m.addImport("a12_types", a12_mod);
m.addImport("posix", libc_mod);
return m;
}
// The four hand-written replacement modules the T46 @cImport→@import sweep
// rewrites consumers against. Any Zig compile step that might end up
// containing (directly or through future rewrites) a call site spelled
// `@import("posix_libc")` / `@import("shmif_types")` / `@import("a12_types")`
// / `@import("anet_types")` needs these four modules available as named
// imports. Module graphs are lazy, so adding them to a compile step that
// doesn't actually use them is free.
const CoreMods = struct {
posix_libc: *std.Build.Module,
shmif_types: *std.Build.Module,
a12_types: *std.Build.Module,
anet_types: *std.Build.Module,
lua54_api: *std.Build.Module,
};
fn coreMods(b: *std.Build, opts: Opts) CoreMods {
const shmif_types = createMod(b, "src/shmif/shmif_types.zig", opts);
const a12_types = createA12TypesMod(b, opts, shmif_types);
const posix_libc = createMod(b, "src/platform/posix/libc.zig", opts);
return .{
.posix_libc = posix_libc,
.shmif_types = shmif_types,
.a12_types = a12_types,
.anet_types = createAnetTypesMod(b, opts, shmif_types, a12_types, posix_libc),
.lua54_api = createMod(b, "src/lua54/api.zig", opts),
};
}
fn coreImports(mods: CoreMods) [5]std.Build.Module.Import {
return .{
.{ .name = "posix", .module = mods.posix_libc },
.{ .name = "shmif_types", .module = mods.shmif_types },
.{ .name = "a12_types", .module = mods.a12_types },
.{ .name = "anet_types", .module = mods.anet_types },
.{ .name = "lua_api", .module = mods.lua54_api },
};
}
const NamePath = struct { name: String, path: String };
fn addZigObjects(
exe: *std.Build.Step.Compile, b: *std.Build, opts: Opts,
sources: []const NamePath, imports: []const std.Build.Module.Import,
// Pointer-to-slice (1 reg slot) rather than slice-by-value (2 reg slots).
// The 6-param signature's 9th register slot (the 2nd slice's .len) was
// landing on the stack, and the aarch64 self-hosted backend was not
// writing that stack slot — so the callee's `includes.len` came through
// as 0xaaaa... (undef) and `addIncludes`/`Build.path` crashed reading
// garbage. Pointer-to-slice keeps us at 8 register slots.
includes: *const []const []const u8,
) void {
for (sources) |src| {
const obj = b.addObject(.{ .use_llvm = use_llvm_default, .name = src.name, .root_module = b.createModule(.{
.root_source_file = b.path(src.path), .target = opts.target,
.optimize = opts.optimize, .imports = imports,
}) });
addLibC(obj, opts);
addIncludes(obj, b, includes.*);
exe.addObject(obj);
}
}
// Compile src/lua54/lua_all_embed.zig as a shared Zig object providing the
// Lua 5.4 runtime (pure-Zig port) for userspace binaries. This is a slim
// variant of lua_all.zig that excludes seL4/boot-environment modules (lrepl,
// lunix, serialize, visitor, llock, lnotice, ltests, and the luaencode*/
// luaparse*/luapush*/etc helpers). The full lua_all.zig is reserved for the
// freestanding boot build. Call once per binary — each call produces a
// distinct Object so Zig's linker can dedupe symbols per-binary.
fn addLua54AllObject(b: *std.Build, exe: *std.Build.Step.Compile, opts: Opts) void {
const lua54_all_obj = b.addObject(.{ .use_llvm = use_llvm_default,
.name = "lua54_all",
.root_module = b.createModule(.{
.root_source_file = b.path("src/lua54/lua_all_embed.zig"),
.target = opts.target, .optimize = opts.optimize, .pic = opts.pic,
}),
});
addLibC(lua54_all_obj, opts);
exe.addObject(lua54_all_obj);
}
// ════════════════════════════════════════════════════════════════════
// build() — main entry point
// ════════════════════════════════════════════════════════════════════
pub fn build(b: *std.Build) void {
// Ticket 0157 — refuse to run as root unless explicitly opted in.
// Root-owned files in zig-out/ silently break subsequent user-level
// installs (the directory copy doesn't surface EPERM). The opt-in
// is for legitimate system-prefix installs ("zig build install
// -p /opt/arcan-system" with ARCAN_ALLOW_ROOT_INSTALL=1).
if (@import("builtin").os.tag == .linux) {
if (std.os.linux.geteuid() == 0) {
// std.posix.getenv walks the initial environ snapshot taken
// at program start; for the build binary that snapshot DOES
// include vars set in the parent shell environment before
// `zig build` was invoked. Direct walk of std.os.environ is
// identical and avoids a libc dep. Lookup must match key
// exactly including the trailing '='.
var ok: ?[]const u8 = null;
for (std.os.environ) |raw| {
const e = std.mem.span(raw);
if (std.mem.startsWith(u8, e, "ARCAN_ALLOW_ROOT_INSTALL=")) {
ok = e["ARCAN_ALLOW_ROOT_INSTALL=".len..];
break;
}
}
if (ok == null or !std.mem.eql(u8, ok.?, "1")) {
std.debug.print(
\\
\\refusing to build as root (EUID 0).
\\
\\Root-owned files in zig-out/ break subsequent user-level
\\installs because the directory copy doesn't surface
\\EPERM as an error — the install silently skips affected
\\files and you end up running stale code.
\\
\\If you intend a system-prefix install, set
\\ ARCAN_ALLOW_ROOT_INSTALL=1
\\and use
\\ zig build install -p /opt/arcan-system
\\(or another non-zig-out/ prefix). Never sudo into
\\zig-out/. See CLAUDE.md "Don'ts" + ticket 0157.
\\
, .{});
std.process.exit(1);
}
}
}
const platform_header_path = b.path("./src/platform/platform.h").getPath(b);
const build_all = b.option(bool, "build_all", "Build all targets (default: false)") orelse false;
const pic: ?bool = b.option(bool, "pic", "Produce Position Independent Code");
const want_static_deps = b.option(bool, "static_deps", "Build OpenAL/libdrm from source instead of using system pkg-config (default: true)") orelse true;
const target = b.standardTargetOptions(.{ .default_target = .{
.cpu_arch = .aarch64,
.os_tag = .linux,
.abi = .musl,
} });
use_llvm_default = switch (target.result.os.tag) {
// darwin: SH Mach-O relocations overflow. windows: SH has no Win64
// codegen for several constructs (var-args, some atomics) — LLVM is
// the only working backend for the x86_64-windows target.
.ios, .macos, .watchos, .tvos, .windows => true,
else => false,
};
// Bug 0162: setting `preferred_optimize_mode = .Debug` here disables
// user-selectable release modes — the fork's standardOptimizeOption
// (lib/std/Build.zig:1348) returns the preferred mode whenever
// `--release` or `release_mode != .off` is set, ignoring which mode
// the user actually asked for. Without preferred_optimize_mode the
// function falls through to its release_mode switch (Debug if .off,
// ReleaseSafe / ReleaseFast / ReleaseSmall otherwise) which is what
// most projects want and what `--release=safe` actually means.
//
// Practical effect: `zig build install --release=safe` now produces
// a ReleaseSafe build instead of silently staying Debug. Workaround
// for 0162's a12int_append_out 7.8 MB stack frame until the
// codegen-side fix lands.
const optimize = b.standardOptimizeOption(.{});
const ext: build_external.ExternalDeps = if (want_static_deps)
build_external.resolveAll(b, target, optimize, shmif_include_paths)
else
.{};
// Optional: point the build at a pre-existing libc installation via a
// standard Zig libc file (see `zig libc`) to skip Zig's internal musl
// compile. Off unless a path is passed.
const libc_file_path = b.option([]const u8, "libc_file", "Path to a Zig libc file describing a prebuilt libc to link instead of compiling musl from source");
const libc_file: ?std.Build.LazyPath = if (libc_file_path) |p| .{ .cwd_relative = p } else null;
const opts = Opts{
.platform_header = b.fmt("\"{s}\"", .{platform_header_path}),
.target = target,
.optimize = optimize,
.static = b.option(bool, "static", "Statically linked build (default: true)") orelse true,
.strip = b.option(bool, "strip", "Omit debug information"),
.pic = pic,
.build_shmif_server = b.option(bool, "build_shmif_server", "Build arcan_shmif_server library (default: true)") orelse true,
.build_shmif = b.option(bool, "build_shmif", "Build arcan_shmif library (default: true)") orelse true,
.build_tui = b.option(bool, "build_tui", "Build arcan_tui library (default: true)") orelse true,
// a12/net is deferred on windows (needs the posix fd/socket substrate);
// the compositor path links without it. (windows port)
.build_a12 = b.option(bool, "build_a12", "Build arcan_a12 library (default: true)") orelse (target.result.os.tag != .windows),
.build_arcan_db = b.option(bool, "build_arcan_db", "Build arcan_db database tool (default: false)") orelse build_all,
.build_arcan_frameserver = b.option(bool, "build_arcan_frameserver", "Build arcan_frameserver chainloader (default: true)") orelse (target.result.os.tag != .windows),
.build_afsrv_terminal = b.option(bool, "build_afsrv_terminal", "Build afsrv_terminal frameserver (default: true)") orelse (target.result.os.tag != .windows),
.build_afsrv_decode = b.option(bool, "build_afsrv_decode", "Build afsrv_decode frameserver (default: true)") orelse (target.result.os.tag != .windows),
.build_afsrv_encode = b.option(bool, "build_afsrv_encode", "Build afsrv_encode frameserver (default: false, needs a12 fixes)") orelse build_all,
.build_afsrv_net = b.option(bool, "build_afsrv_net", "Build afsrv_net frameserver (default: false, needs a12 fixes)") orelse build_all,
.build_arcan_net = b.option(bool, "build_arcan_net", "Build arcan-net directory/bridge binary (pure Zig)") orelse (target.result.os.tag != .windows),
.build_arcan_net_session = b.option(bool, "build_arcan_net_session", "Build arcan-net-session binary (pure Zig)") orelse (target.result.os.tag != .windows),
.build_afsrv_remoting = b.option(bool, "build_afsrv_remoting", "Build afsrv_remoting frameserver (default: false, needs a12 fixes)") orelse build_all,
.build_afsrv_game = b.option(bool, "build_afsrv_game", "Build afsrv_game frameserver (default: true)") orelse (target.result.os.tag != .windows),
.build_afsrv_avfeed = b.option(bool, "build_afsrv_avfeed", "Build afsrv_avfeed frameserver (default: true)") orelse (target.result.os.tag != .windows),
.build_afsrv_bun = b.option(bool, "build_afsrv_bun", "Build afsrv_bun frameserver — embedded Bun host for JS/TS shmif clients (default: false, see bugs/0036)") orelse false,
.build_afsrv_probe = b.option(bool, "build_afsrv_probe", "Build afsrv_probe frameserver (drives a12 coverage probes)") orelse (target.result.os.tag != .windows),
.build_arcan_vk = b.option(bool, "build_arcan_vk", "Build arcan Vulkan VK_KHR_display compositor (default: true)") orelse true,
.build_shmif_ext = b.option(bool, "build_shmif_ext", "Build arcan_shmif_ext stub library (default: false)") orelse build_all,
.build_aclip = b.option(bool, "build_aclip", "Build aclip clipboard tool (default: false)") orelse build_all,
.build_shmmon = b.option(bool, "build_shmmon", "Build shmmon monitor tool (default: false)") orelse build_all,
.build_acfgfs = b.option(bool, "build_acfgfs", "Build acfgfs FUSE config filesystem (default: false)") orelse build_all,
.build_xarcan = b.option(bool, "build_xarcan", "Build Xarcan X server with arcan backend (default: false)") orelse build_all,
.build_ghostty_tests = b.option(bool, "build_ghostty_tests", "Expose the test-ghostty step; pulls the ghostty dep into the build graph (default: false)") orelse false,
.prebuilt_musl = libc_file != null,
.libc_file = libc_file,
.static_deps = want_static_deps,
.ext = ext,
};
switch (opts.target.result.os.tag) {
.linux, .ios, .macos, .watchos, .tvos, .freebsd, .dragonfly, .openbsd, .netbsd, .windows => {},
else => |t| std.debug.panic("Unsupported platform: {s} — arcan targets Linux/BSD/macOS/Windows", .{@tagName(t)}),
}
// PIC: only forced when caller passes -Dpic=true. qtarcan (now in
// build_llvm/) sets PIC on its own shared lib internally.
// Core libraries
// Public "arcan" API module for downstream package consumers. Pure-Zig
// type mirror (same file the engine uses internally); replaces the old
// translate-C of src/arcan.h.
_ = b.addModule("arcan", .{
.root_source_file = b.path("src/engine/arcan_zig_types.zig"),
.target = opts.target,
.optimize = opts.optimize,
});
const arcan_shmif_server = createArcanShmifServer(b, opts);
if (opts.build_shmif_server) b.installArtifact(arcan_shmif_server);
if (opts.build_shmif and !opts.build_shmif_server) @panic("Can not build Arcan shmif library without shmif server");
const arcan_shmif = createArcanShmif(b, opts);
arcan_shmif.linkLibrary(arcan_shmif_server);
if (opts.build_shmif) b.installArtifact(arcan_shmif);
if (opts.build_tui and !opts.build_shmif) @panic("Can not build Arcan TUI library without shmif");
const arcan_tui = createArcanTui(b, opts);
arcan_tui.linkLibrary(arcan_shmif);
if (opts.build_tui) b.installArtifact(arcan_tui);
const arcan_a12 = createArcanA12(b, opts);
if (opts.build_shmif) { arcan_a12.linkLibrary(arcan_shmif); arcan_a12.linkLibrary(arcan_shmif_server); }
if (opts.build_a12) b.installArtifact(arcan_a12);
const arcan_shmif_ext = createArcanShmifExt(b, opts);
if (opts.build_shmif_ext) b.installArtifact(arcan_shmif_ext);
// Install data directories
const install_resources = b.addInstallDirectory(.{ .source_dir = b.path("data/resources"), .install_dir = .{ .custom = "share/arcan/resources" }, .install_subdir = "" });
const install_scripts = b.addInstallDirectory(.{ .source_dir = b.path("data/scripts"), .install_dir = .{ .custom = "share/arcan/scripts" }, .install_subdir = "" });
const install_appls = b.addInstallDirectory(.{ .source_dir = b.path("data/appl"), .install_dir = .{ .custom = "share/arcan/appl" }, .install_subdir = "" });
// Install source tree for debug info
const install_src = b.addInstallDirectory(.{ .source_dir = b.path("src"), .install_dir = .{ .custom = "src" }, .install_subdir = "" });
// Bundled appls (durden, cat9)
// Policy: install upstream durden as-is. Any fix or debug hook we need
// goes in via a SMALL, NAMED overlay file placed next to this block —
// not a wholesale fork of the tree. The prior monolithic overlay at
// src/sel4-zig/durden_appl/ was deleted in favour of this approach so
// we stay reviewable against upstream and so our "shmif-monitoring-style"
// probes can land as isolated drop-ins rather than diffs against a
// quietly-drifting fork.
const install_durden = if (b.lazyDependency("durden", .{})) |dep| blk: {
const dir_step = b.addInstallDirectory(.{ .source_dir = dep.path("durden"), .install_dir = .{ .custom = "share/arcan/appl/durden" }, .install_subdir = "" });
// Engine builtin Lua files (debug/string/table/keyboard/mouse/json/…)
// live in data/scripts/builtin and are NOT included with the durden
// package. Durden expects them at appl/durden/builtin/ (top-level
// system_load in durden.lua) AND at resources/builtin/ (shared with
// welcome/console etc).
const install_builtins_appl = b.addInstallDirectory(.{ .source_dir = b.path("data/scripts/builtin"), .install_dir = .{ .custom = "share/arcan/appl/durden/builtin" }, .install_subdir = "" });
install_builtins_appl.step.dependOn(&dir_step.step);
const install_builtins_shared = b.addInstallDirectory(.{ .source_dir = b.path("data/scripts/builtin"), .install_dir = .{ .custom = "share/arcan/resources/builtin" }, .install_subdir = "" });
install_builtins_shared.step.dependOn(&install_builtins_appl.step);
// JetBrains Mono Variable as the ONLY font (GPU Slug renderer
// contract — we do not fall back to bitmap/TTF raster). Overwrites
// every font file durden ships with the variable one so aliased
// names keep resolving.
const jb_font = b.path("data/resources/fonts/jetbrain_variable.font");
const font_targets = [_][]const u8{ "default.ttf", "hack.ttf", "emoji.ttf", "hyperlegible.otf" };
var last_step: *std.Build.Step = &install_builtins_shared.step;
for (font_targets) |fname| {
const step = b.addInstallFile(jb_font, b.fmt("share/arcan/appl/durden/fonts/{s}", .{fname}));
step.step.dependOn(last_step);
last_step = &step.step;
}
// Durden overlay: small, named drop-ins that land atop upstream
// durden. Each file is one ~self-contained patch; this keeps our
// changes reviewable against upstream and makes it obvious when
// an "overlay" is a debug aid vs a legitimate fix.
const install_overlay_autorun = b.addInstallFile(
b.path("data/scripts/durden_overlay/autorun.lua"),
"share/arcan/appl/durden/autorun.lua",
);
install_overlay_autorun.step.dependOn(last_step);
// bug 0017: suppl.lua holds the bug 0006 math.floor coercion
// around string.utf8ralign before passing to string.sub —
// without this, the firstrun-wizard text input crashes Lua.
const install_overlay_suppl = b.addInstallFile(
b.path("data/scripts/durden_overlay/suppl.lua"),
"share/arcan/appl/durden/suppl.lua",
);
install_overlay_suppl.step.dependOn(&install_overlay_autorun.step);
// bug 0021: browse.lua's load_image_asynch callback expects
// status as a TABLE; defensive `type(status) == "table"` guard
// prevents an arcan-killing chain (lua runtime error → alt_call
// → bug 0008 alignment panic) on previewable file selection.
const install_overlay_browse = b.addInstallFile(
b.path("data/scripts/durden_overlay/browse.lua"),
"share/arcan/appl/durden/menus/browse.lua",
);
install_overlay_browse.step.dependOn(&install_overlay_suppl.step);
break :blk &install_overlay_browse.step;
} else null;
const install_cat9 = if (b.lazyDependency("cat9", .{})) |dep| blk: {
const cat9_dir = b.addInstallDirectory(.{ .source_dir = dep.path("."), .install_dir = .{ .custom = "share/arcan/appl/durden/lash" }, .install_subdir = "", .exclude_extensions = &.{ ".md", "LICENSE" } });
const wrapper = b.addWriteFiles();
_ = wrapper.add("default.lua",
\\-- generated by build.zig: load cat9 as default lash shell
\\local path = lash.scriptdir .. "cat9.lua"
\\local fn, err = loadfile(path)
\\if fn then return fn()
\\else table.insert(lash.messages, "cat9 load error: " .. tostring(err)) end
\\
);
// Override dev.lua descriptor: cat9.lua's load_builtins("dev")
// ALWAYS pre-loads the default set first (line 207-209), so we
// do NOT need to re-list ../default/* here — that caused dispatch
// breakage on this build (shell-jobs stopped firing). Just system
// bridges + the dev-specific files.
_ = wrapper.add("cat9/dev.lua",
\\return {
\\ '../system/cd.lua',
\\ '../system/term.lua',
\\ 'scm.lua',
\\ 'debug.lua',
\\ 'build.lua',
\\ 'graph.lua',
\\ '_helpers.lua',
\\ 'read.lua',
\\ 'head.lua',
\\ 'tail.lua',
\\ 'wc.lua',
\\ 'write.lua',
\\ 'paste.lua',
\\ 'edit.lua',
\\ 'grep.lua',
\\ 'glob.lua',
\\ 'find.lua',
\\ 'run.lua',
\\ 'zigbuild.lua',
\\ 'compile.lua',
\\ 'git.lua',
\\ 'edits.lua',
\\ 'disasm.lua',
\\ 'sheet.lua',
\\ 'selfhost.lua',
\\ 'bugs.lua',
\\ 'metrics.lua',
\\ 'hilbert.lua',
\\ 'snippets.lua',
\\ 'dashboard.lua',
\\ 'dwarf.lua',
\\ 'dietree.lua',
\\ 'atlas.lua',
\\ 'memcloud.lua',
\\ 'diegraph.lua',
\\ 'time.lua',
\\ 'refactor.lua',
\\ 'status.lua',
\\ 'proc.lua',
\\ 'fs.lua',
\\ 'screenshot.lua',
\\ 'bun.lua',
\\ 'claude.lua',
\\ 'region.lua',
\\ 'fossil.lua',
\\}
\\
);
// Generate build.lua with embedded source/zig paths
// Pipeworld-like: each build/run spawns a tied terminal window (handover
// with join-r hint) so every stage is a visible cell in the tiler.
_ = wrapper.add("cat9/dev/build.lua", b.fmt(
\\return
\\function(cat9, root, builtins, suggest, views, builtin_cfg)
\\local srcdir = "{s}"
\\local zigbin = "{s}"
\\local outdir = srcdir .. "/zig-out"
\\local lwa_bin = outdir .. "/bin/arcan"
\\
\\local function escape_cell(v)
\\ local s = tostring(v)
\\ s = string.gsub(s, '"', '\\"')
\\ return string.format('"%s"', s)
\\end
\\
\\local function make_spread(title, headers, rows)
\\ local ob = cat9.builtin_name
\\ cat9.builtins["builtin"]("spreadsheet")
\\ cat9.parse_string(cat9.readline, "new")
\\ local spread = cat9.latestjob
\\ if not spread then
\\ cat9.add_message("build: spreadsheet builtin not available")
\\ cat9.builtins["builtin"](ob)
\\ return
\\ end
\\ spread.short = title
\\ local hdr_parts = {{}}
\\ for _, h in ipairs(headers) do table.insert(hdr_parts, escape_cell(h)) end
\\ cat9.parse_string(cat9.readline,
\\ string.format("insert #%d 1 %s", spread.id, table.concat(hdr_parts, " ")))
\\ for i, row in ipairs(rows) do
\\ local parts = {{}}
\\ for _, v in ipairs(row) do table.insert(parts, escape_cell(v)) end
\\ cat9.parse_string(cat9.readline,
\\ string.format("insert #%d %d %s", spread.id, i + 1, table.concat(parts, " ")))
\\ end
\\ cat9.builtins["builtin"](ob)
\\ return spread
\\end
\\
\\-- Run build inline in cat9 cell. Output appears in current cell.
\\-- When on_done is provided, fires on success (exit code 0).
\\local function do_build(targets, on_done, build_opts)
\\ build_opts = build_opts or {{}}
\\ local argv = {{zigbin, "zig", "build"}}
\\ for _, v in ipairs(targets) do table.insert(argv, v) end
\\ local env = cat9.table_copy_shallow(cat9.env)
\\ local old_dir = root:chdir()
\\ root:chdir(build_opts.dir or srcdir)
\\ local job = cat9.setup_shell_job(argv, "re", env,
\\ "zig build " .. table.concat(targets, " "), {{close = true}})
\\ if job then
\\ job.short = "build:" .. (targets[1] or "default")
\\ if on_done then
\\ table.insert(job.hooks.on_finish, on_done)
\\ table.insert(job.hooks.on_fail, function()
\\ cat9.add_message("build failed: " .. table.concat(targets, " "))
\\ end)
\\ end
\\ end
\\ root:chdir(old_dir)
\\ return job
\\end
\\
\\-- target name -> output path mapping (relative to zig-out)
\\local target_outputs = {{
\\ ["arcan_vk"] = "bin/arcan_vk",
\\ ["arcan_frameserver"]= "bin/arcan_frameserver",
\\ ["afsrv_terminal"] = "bin/afsrv_terminal",
\\ ["arcan_db"] = "bin/arcan_db",
\\ ["afsrv_decode"] = "bin/afsrv_decode",
\\ ["afsrv_encode"] = "bin/afsrv_encode",
\\ ["afsrv_net"] = "bin/afsrv_net",
\\ ["afsrv_remoting"] = "bin/afsrv_remoting",
\\ ["afsrv_game"] = "bin/afsrv_game",
\\ ["afsrv_avfeed"] = "bin/afsrv_avfeed",
\\ ["afsrv_bun"] = "bin/afsrv_bun",
\\ ["aclip"] = "bin/aclip",
\\ ["shmmon"] = "bin/shmmon",
\\ ["acfgfs"] = "bin/acfgfs",
\\ ["arcan_shmif"] = "lib/libarcan_shmif.a",
\\ ["arcan_shmif_server"]="lib/libarcan_shmif_server.a",
\\ ["arcan_tui"] = "lib/libarcan_tui.a",
\\ ["arcan_a12"] = "lib/libarcan_a12.a",
\\ ["arcan_shmif_ext"] = "lib/libarcan_shmif_ext.a",
\\ ["xarcan"] = "bin/Xarcan",
\\ ["init"] = "bin/init",
\\ ["callgraph"] = "bin/callgraph",
\\}}
\\
\\local function file_age(path)
\\ local f = io.open(path, "r")
\\ if not f then return nil end
\\ f:close()
\\ return true
\\end
\\
\\local function parse_zig_help(lines)
\\ local opts = {{}}
\\ local integrations = {{}}
\\ local targets = {{}}
\\ local section = ""
\\ for _, line in ipairs(lines) do
\\ if line:match("^Steps:") then section = "steps"
\\ elseif line:match("^Project%-Specific Options:") then section = "opts"
\\ elseif line:match("^System Integration Options:") then section = "sysint"
\\ elseif line:match("^Available System Integrations:") then section = "avail"
\\ elseif line:match("^General Options:") then section = "general"
\\ elseif line:match("^Advanced Options:") then section = "advanced"
\\ elseif section == "steps" then
\\ local name, desc = line:match("^%s+(%S+).-(%u.+)")
\\ if name then table.insert(targets, {{name, desc}}) end
\\ elseif section == "opts" then
\\ local name, typ, desc = line:match("^%s+%-D(%S+)=%[(%a+)%]%s+(.*)")
\\ if name then
\\ local def = desc:match("%(default:%s*(.-)%)")
\\ table.insert(opts, {{name, typ, def or "", desc:gsub("%s*%(default:.-%)",""):gsub("^%s+",""):gsub("%s+$","")}})
\\ end
\\ elseif section == "avail" then
\\ local pkg, enabled = line:match("^%s+(%S+)%s+(yes.*)")
\\ if not pkg then pkg, enabled = line:match("^%s+(%S+)%s+(no.*)") end
\\ if pkg then table.insert(integrations, {{pkg, enabled}}) end
\\ end
\\ end
\\ return targets, opts, integrations
\\end
\\
\\local function do_config()
\\ local argv = {{zigbin, "zig", "build", "--help"}}
\\ local old_dir = root:chdir()
\\ root:chdir(srcdir)
\\ local _, outf, errf, pid = root:popen(argv, "re")
\\ root:chdir(old_dir)
\\ if not pid then
\\ cat9.add_message("build config: could not run zig build --help")
\\ return
\\ end
\\ local lines = {{}}
\\ local job = cat9.add_background_job(outf, pid, {{lf_strip = true, err = errf}},
\\ function(job, code)
\\ if code ~= 0 then
\\ cat9.add_message("build config: zig build --help exited " .. tostring(code))
\\ return
\\ end
\\ local targets, opts, integrations = parse_zig_help(lines)
\\
\\ -- targets spreadsheet with built/not-built status
\\ local trows = {{}}
\\ for _, t in ipairs(targets) do
\\ local opath = target_outputs[t[1]]
\\ local status = ""
\\ if opath then
\\ local full = outdir .. "/" .. opath
\\ status = file_age(full) and "built" or ""
\\ end
\\ table.insert(trows, {{t[1], status, opath or "", t[2]}})
\\ end
\\ make_spread("Build Targets",
\\ {{"Target", "Status", "Output", "Description"}}, trows)
\\
\\ -- options spreadsheet
\\ make_spread("Build Options",
\\ {{"Option", "Type", "Default", "Description"}}, opts)
\\
\\ -- system integrations spreadsheet
\\ if #integrations > 0 then
\\ make_spread("System Integrations",
\\ {{"Package", "Enabled"}}, integrations)
\\ end
\\ end)
\\ table.insert(job.hooks.on_data, function(line)
\\ if line then table.insert(lines, line) end
\\ end)
\\end
\\
\\local function lwa_env()
\\ local env = cat9.table_copy_shallow(cat9.env)
\\ env["ARCAN_APPLBASEPATH"] = outdir .. "/share/arcan/appl"
\\ env["ARCAN_RESOURCEPATH"] = outdir .. "/share/arcan/resources"
\\ env["ARCAN_SCRIPTPATH"] = outdir .. "/share/arcan/scripts"
\\ env["ARCAN_BINPATH"] = outdir .. "/bin/arcan_frameserver"
\\ env["ARCAN_LIBPATH"] = outdir .. "/lib"
\\ return env
\\end
\\
\\local function parse_mode(args)
\\ local cmode = "embed"
\\ if type(args[1]) == "table" and args[1].parg then
\\ local t = table.remove(args, 1)
\\ for _, v in ipairs(t) do
\\ if v == "v" then cmode = "join-d"
\\ elseif v == "h" then cmode = "join-r"
\\ elseif v == "tab" then cmode = "tab"
\\ elseif v == "embed" then cmode = "embed"
\\ end
\\ end
\\ end
\\ return cmode
\\end
\\
\\builtins.hint["build"] = "Build arcan inline (zig build [target...] | run | xarcan | config)"
\\function builtins.build(...)
\\ local args = {{...}}
\\ local set = {{}}
\\ local ok, msg = cat9.expand_arg(set, args)
\\ if not ok then return false, msg end
\\
\\ local cmode = parse_mode(set)
\\
\\ if set[1] == "config" then return do_config() end
\\
\\ -- "build run [appl]": build inline, then launch LWA compositor embedded.
\\ -- "build (h) run durden": tile LWA to the right instead of embed.
\\ if set[1] == "run" then
\\ local appl = set[2] or "durden"
\\ return do_build({{}}, function()
\\ cat9.shmif_handover(cmode, "e", lwa_bin, lwa_env(),
\\ {{"arcan(lwa:" .. appl .. ")", appl}})
\\ end)
\\ end
\\
\\ -- "build xarcan [app]": build Xarcan then launch embedded.
\\ if set[1] == "xarcan" then
\\ local xapp = set[2] or "xterm"
\\ return do_build({{"-Dbuild_xarcan=true", "xarcan"}}, function()
\\ local env = cat9.table_copy_shallow(cat9.env)
\\ cat9.shmif_handover(cmode, "e", outdir .. "/bin/Xarcan", env,
\\ {{"Xarcan", "-ac", "-retro", xapp}})
\\ end)
\\ end
\\
\\ -- "build [target...]": build inline in cat9 cell
\\ return do_build(set)
\\end
\\
\\builtins.hint["game"] = "Launch app via gamescope (game [app...])"
\\function builtins.game(...)
\\ local args = {{...}}
\\ local set = {{}}
\\ local ok, msg = cat9.expand_arg(set, args)
\\ if not ok then return false, msg end
\\
\\ local cmode = parse_mode(set)
\\ local gs_args = {{}}
\\ for _, v in ipairs(set) do table.insert(gs_args, v) end
\\ if #gs_args == 0 then gs_args = {{"chromium-browser"}} end
\\
\\ -- gamescope is built from the build_llvm/ sub-tree (LLVM-only
\\ -- target). Output lands in build_llvm/zig-out/bin/gamescope.
\\ return do_build({{"gamescope"}}, function()
\\ local env = cat9.table_copy_shallow(cat9.env)
\\ local argv = {{"gamescope", "--backend", "arcan", "-W", "1920", "-H", "1080", "--"}}
\\ for _, a in ipairs(gs_args) do table.insert(argv, a) end
\\ cat9.shmif_handover(cmode, "e",
\\ srcdir .. "/build_llvm/zig-out/bin/gamescope", env, argv)
\\ end, {{dir = srcdir .. "/build_llvm"}})
\\end
\\
\\function suggest.build(args, raw)
\\ if #args == 2 then
\\ local targets = {{
\\ "config", "run", "xarcan", "test-shmif",
\\ "arcan-db",
\\ "afsrv-terminal", "afsrv-decode", "afsrv-encode", "afsrv-net",
\\ "afsrv-game", "afsrv-avfeed", "afsrv-cat9-viz", "afsrv-remoting",
\\ "aclip", "shmmon", "acfgfs",
\\ hint = {{
\\ "Show build options/targets/integrations as spreadsheets",
\\ "Build + launch nested arcan embedded",
\\ "Build + launch X11 server embedded",
\\ "Run shmif test suite",
\\ "Database tool",
\\ "Terminal frameserver", "Decode frameserver", "Encode frameserver",
\\ "Network frameserver", "Game frameserver", "A/V feed frameserver",
\\ "Remoting frameserver",
\\ "Clipboard tool", "Image viewer", "Shared memory monitor", "FUSE config FS",
\\ }}
\\ }}
\\ cat9.readline:suggest(cat9.prefix_filter(targets, args[#args]), "word")
\\ elseif #args == 3 and args[2] == "run" then
\\ cat9.readline:suggest({{"durden", hint = {{"Durden desktop"}}}}, "word")
\\ elseif #args == 3 and args[2] == "xarcan" then
\\ cat9.readline:suggest({{"xterm", "xclock", "xeyes", hint = {{"Terminal emulator", "Clock widget", "Eyes widget"}}}}, "word")
\\ end
\\end
\\
\\function suggest.game(args, raw)
\\ if #args == 2 then
\\ cat9.readline:suggest({{"chromium-browser", "steam", "firefox",
\\ hint = {{"Chromium browser", "Steam client", "Firefox browser"}}}}, "word")
\\ end
\\end
\\
\\end
\\
, .{ b.build_root.path orelse ".", b.graph.zig_exe }));
// Generate graph.lua — callgraph → spreadsheet interactive traversal
_ = wrapper.add("cat9/dev/graph.lua", b.fmt(
\\return
\\function(cat9, root, builtins, suggest, views, builtin_cfg)
\\local srcdir = "{s}"
\\local cfg = builtin_cfg.graph or {{}}
\\local depth_limit = cfg.caller_depth or 4
\\local nodes = {{}}
\\local file_index = {{}}
\\local name_index = {{}}
\\local callers = {{}}
\\local callees = {{}}
\\local loaded = false
\\local loading = false
\\local current_file = nil
\\
\\local function parse_dot_line(line)
\\ if not line or #line == 0 then return end
\\ local id = line:match('^%s*"([^"]+)"%s*%[')
\\ if id then
\\ local name = id:match(':(.+)$') or id
\\ local file = id:match('^(.+):') or current_file or ""
\\ local is_export = line:match('fillcolor="#4a90d9"') ~= nil
\\ local is_pub = line:match('fillcolor="#7ab648"') ~= nil
\\ nodes[id] = {{id = id, name = name, file = file, export = is_export, pub = is_pub}}
\\ if not file_index[file] then file_index[file] = {{}} end
\\ table.insert(file_index[file], id)
\\ if not name_index[name] then name_index[name] = {{}} end
\\ table.insert(name_index[name], id)
\\ return
\\ end
\\ local from, to = line:match('"([^"]+)"%s*%->%s*"([^"]+)"')
\\ if from and to then
\\ if not callees[from] then callees[from] = {{}} end
\\ table.insert(callees[from], to)
\\ if not callers[to] then callers[to] = {{}} end
\\ table.insert(callers[to], from)
\\ return
\\ end
\\ local file = line:match('^%s*label="([^"]+)"')
\\ if file then current_file = file end
\\end
\\
\\local function load_graph(then_cb)
\\ if loading then return end
\\ nodes = {{}}; file_index = {{}}; name_index = {{}}; callers = {{}}; callees = {{}}
\\ current_file = nil; loaded = false; loading = true
\\ local cmd = string.format("cd %s && zig-out/bin/callgraph --scan", srcdir)
\\ local _, outf, errf, pid = root:popen({{"/bin/sh", "/bin/sh", "-c", cmd}}, "re")
\\ if not pid then
\\ cat9.add_message("graph: could not spawn callgraph (build callgraph first)")
\\ loading = false
\\ return
\\ end
\\ local job = cat9.add_background_job(outf, pid, {{lf_strip = true, err = errf}},
\\ function(job, code)
\\ loading = false
\\ if code ~= 0 then
\\ cat9.add_message("graph: callgraph exited with code " .. tostring(code))
\\ return
\\ end
\\ loaded = true
\\ local nc, ec, fc = 0, 0, 0
\\ for _ in pairs(nodes) do nc = nc + 1 end
\\ for _, v in pairs(callees) do ec = ec + #v end
\\ for _ in pairs(file_index) do fc = fc + 1 end
\\ cat9.add_message(string.format("graph: %d functions, %d edges, %d files", nc, ec, fc))
\\ if then_cb then then_cb() end
\\ end)
\\ table.insert(job.hooks.on_data, function(line) if line then parse_dot_line(line) end end)
\\end
\\
\\local function escape_cell(v)
\\ local s = tostring(v)
\\ s = string.gsub(s, '"', '\\"')
\\ return string.format('"%s"', s)
\\end
\\
\\local function make_spread(title, headers, rows)
\\ local ob = cat9.builtin_name
\\ cat9.builtins["builtin"]("spreadsheet")
\\ cat9.parse_string(cat9.readline, "new")
\\ local spread = cat9.latestjob
\\ if not spread then
\\ cat9.add_message("graph: spreadsheet builtin not available")
\\ cat9.builtins["builtin"](ob)
\\ return
\\ end
\\ spread.short = title
\\ local hdr_parts = {{}}
\\ for _, h in ipairs(headers) do table.insert(hdr_parts, escape_cell(h)) end
\\ cat9.parse_string(cat9.readline,
\\ string.format("insert #%d 1 %s", spread.id, table.concat(hdr_parts, " ")))
\\ for i, row in ipairs(rows) do
\\ local parts = {{}}
\\ for _, v in ipairs(row) do table.insert(parts, escape_cell(v)) end
\\ cat9.parse_string(cat9.readline,
\\ string.format("insert #%d %d %s", spread.id, i + 1, table.concat(parts, " ")))
\\ end
\\ cat9.builtins["builtin"](ob)