Repository navigation
Expand file tree
/
Copy pathbuild.zig
More file actions
1105 lines (1019 loc) · 58.1 KB
/
Copy pathbuild.zig
File metadata and controls
1105 lines (1019 loc) · 58.1 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 builtin = @import("builtin");
comptime {
// 0.16.0's bundled libc++ fails to compile against the macOS 27 SDK
// (`use of undeclared identifier 'INFINITY'` in its vendored <random>);
// fixed in 0.17.0, which is why the floor moved.
if (builtin.zig_version.major == 0 and builtin.zig_version.minor < 17) {
@compileError(std.fmt.comptimePrint(
"mlx-serve requires Zig 0.17 (have {d}.{d}.{d}). Run ./scripts/fetch-zig.sh or grab it from https://ziglang.org/download/.",
.{ builtin.zig_version.major, builtin.zig_version.minor, builtin.zig_version.patch },
));
}
}
/// stb_image_write's JPEG bit-packer relies on WRAPPING left shifts of a signed
/// int (`bitBuf <<= 8`), which is UB by the letter of C and which Zig's C
/// frontend TRAPS under UBSan — so a Debug build of any graph that encodes a
/// JPEG aborts inside libc. One flag list, used at every site that compiles it,
/// because the flag is about the C source and not about which graph it is in
/// (`zig build test` is Debug and ran 6 crashed tests without it).
const stb_write_flags: []const []const u8 = &.{ "-O2", "-fno-sanitize=undefined" };
pub fn build(b: *std.Build) void {
// Pin LC_BUILD_VERSION minos to macOS 26.2 — the honest floor: the linked
// libmlx is built at deployment target 26.2 (NAX kernels, scripts/
// build-mlx.sh), so on older macOS the binary can't run anyway; failing at
// the binary with a clear dyld version error beats "loading" and dying on
// the dylib. Matches app LSMinimumSystemVersion + Package.swift. Guard:
// tests/test_mlx_staged_nax.sh (binary minos check).
// Host-gated: on a Linux box a semver 26.2 min would be read as a glibc
// floor (glibc has no 26.2) and break the native Linux graph.
const target = b.standardTargetOptions(.{
.default_target = if (builtin.os.tag == .macos) .{
.os_version_min = .{ .semver = .{ .major = 26, .minor = 2, .patch = 0 } },
} else .{},
});
const optimize = b.standardOptimizeOption(.{});
// Setting any non-default target field disables Zig's native macOS SDK detection,
// so we resolve the SDK path ourselves and surface its frameworks dir.
const macos_sdk_frameworks: ?[]const u8 = blk: {
if (target.result.os.tag != .macos) break :blk null;
var code: u8 = undefined;
const stdout = b.runAllowFail(
&.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" },
&code,
.inherit,
) catch break :blk null;
const sdk = std.mem.trim(u8, stdout, " \n\r\t");
if (sdk.len == 0) break :blk null;
break :blk b.fmt("{s}/System/Library/Frameworks", .{sdk});
};
if (target.result.os.tag == .macos) {
verifyBrewDeps(b);
verifyMlxStage(b);
}
// Hermetic Latent2RGB/JPEG tests: the COMPILED artifact links no MLX and
// no Homebrew webp, which is what lets a Linux Cloud Agent build it — on
// Linux this is the only graph registered. On a Mac the step builds the
// same hermetic artifact, but `verifyBrewDeps`/`verifyMlxStage` above run
// at CONFIGURE time for every step, so it is not a way to build without a
// staged mlx.
// Native query — do not inherit the macOS 26.2 minos default_target.
addPreviewTest(b, b.resolveTargetQuery(.{}));
// Linux serve graph: same engine sources, macOS-only engines (ds4 Metal,
// embedded llama.cpp, ANE objc) replaced by compile-time stubs, mlx +
// mlx-c staged from the Linux Vulkan fork by scripts/build-mlx-linux.sh.
// Gated on the TARGET so the Linux exe can be built on any host zig can
// target; the host gate below still guards the macOS graph.
if (target.result.os.tag == .linux) {
addLinuxServe(b, target, optimize);
return;
}
if (builtin.os.tag != .macos) return;
// App version. Release builds pass it explicitly (app/build.sh computes the
// next CalVer from the GitHub releases and stamps it into app/Info.plist;
// the release workflow passes the tag). A plain `zig build` used to fall
// back to a literal "0.1.0-dev", which then showed up as the version in
// `--version` AND on the console page — so a dev build reported a version
// that exists nowhere. Default to the checked-in Info.plist stamp instead:
// one source of truth, already in the repo, and the same string the last
// real build shipped. Same pattern as the engine pins below.
const version = b.option([]const u8, "version", "Version string") orelse readAppVersion(b) orelse "0.0.0-dev";
const mas = b.option(bool, "mas", "MAS build (no curl/model-pull subprocess)") orelse false;
// Engine-version pins surfaced by `mlx-serve --version` (the macOS app spawns
// it and parses the output — see src/version.zig). These are the versions
// that have NO runtime query API (MLX + ggml report themselves at runtime):
// --mlx-c-version pinned mlx-c submodule version; defaults from the
// lib/mlx/.version stamp (written by scripts/build-mlx.sh)
// --ds4-commit pinned ds4 submodule short commit (build.sh: `git rev-parse`)
// --llama-tag llama.cpp release tag; defaults from lib/llama/.version
// (written by scripts/fetch-llama.sh) so a plain dev build
// still reports it. app/build.sh passes all three.
const mlx_c_version = b.option([]const u8, "mlx-c-version", "Pinned mlx-c version") orelse readMlxcPin(b) orelse "unknown";
const ds4_commit = b.option([]const u8, "ds4-commit", "Pinned ds4 submodule short commit") orelse "unknown";
const llama_tag = b.option([]const u8, "llama-tag", "llama.cpp release tag (bNNNN)") orelse readLlamaTag(b) orelse "unknown";
const build_options = b.addOptions();
build_options.addOption([]const u8, "version", version);
build_options.addOption(bool, "mas", mas);
build_options.addOption([]const u8, "mlx_c_version", mlx_c_version);
build_options.addOption([]const u8, "ds4_commit", ds4_commit);
build_options.addOption([]const u8, "llama_tag", llama_tag);
const git_sha = b.option([]const u8, "git-sha", "Engine build id for the round-cost table: a release sha stands for the executable bytes, which are then not hashed; the MLX dylib and metallib fingerprints are always mixed in") orelse "";
build_options.addOption([]const u8, "git_sha", git_sha);
// false for the macOS exe/tests; the iOS static-lib step (`zig build ios-lib`)
// builds its own options with ios=true so the engine swaps the embedded
// ds4 + llama.cpp engines for no-op stubs (iOS serves MLX safetensors only).
build_options.addOption(bool, "ios", false);
// True when the embedded engines (ds4, libllama) are actually linked:
// macOS (Metal) and Linux (CUDA) exe = yes; the iOS static lib = no, it
// gets compile-time stubs (src/arch/*_stub.zig, src/ds4_ffi_stub.zig).
// The stub selection reads this option, NOT `ios` — `ios` keeps its own
// meaning (low-mem policy, sandboxing assumptions).
build_options.addOption(bool, "embedded_engines", true);
// DeepSeek-V4.1's EXL3 repack through mlx-stream (macOS only; Linux and iOS build the stub).
build_options.addOption(bool, "mlx_stream", true);
// The corpus replay and benchmark tests run tens of seconds in Debug (#639), so
// `zig build test` skips them and `zig build test -Dslow-tests` runs them.
build_options.addOption(bool, "slow_tests", b.option(bool, "slow-tests", "Also run the slow corpus-replay and benchmark tests") orelse false);
// ds4 Metal kernel sources embedded via @embedFile and exposed as a
// named module so src/arch/ds4.zig can import them with `@import("ds4_metal_sources")`
// without traversing the project root.
const ds4_metal_sources = b.createModule(.{
.root_source_file = b.path("lib/ds4_metal_sources.zig"),
.target = target,
.optimize = optimize,
});
const mlx_steel_sources = b.createModule(.{
.root_source_file = b.path("lib/mlx_steel_sources.zig"),
.target = target,
.optimize = optimize,
});
const opencode2_plugin = b.createModule(.{
.root_source_file = b.path("lib/opencode2_plugin.zig"),
.target = target,
.optimize = optimize,
});
const agent_skills = b.createModule(.{
.root_source_file = b.path("skills/agent_skills.zig"),
.target = target,
.optimize = optimize,
});
const mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libcpp = true,
.imports = &.{
.{ .name = "build_options", .module = build_options.createModule() },
.{ .name = "ds4_metal_sources", .module = ds4_metal_sources },
.{ .name = "mlx_steel_sources", .module = mlx_steel_sources },
.{ .name = "opencode2_plugin", .module = opencode2_plugin },
.{ .name = "agent_skills", .module = agent_skills },
.{ .name = "jinja_c", .module = addCHeaderModule(b, b.path("lib/jinja_cpp/jinja_wrapper.h"), b.path("lib/jinja_cpp"), target, optimize, "") },
.{ .name = "stb", .module = addCHeaderModule(b, b.path("lib/stb_image.h"), b.path("lib"), target, optimize, "") },
.{ .name = "webp", .module = addCHeaderModule(b, .{ .cwd_relative = "/opt/homebrew/include/webp/decode.h" }, .{ .cwd_relative = "/opt/homebrew/include" }, target, optimize, "") },
},
});
// Jinja2 template engine (from llama.cpp's common/jinja + nlohmann/json).
// Pre-compiled as a static library with system clang++ (C++17 requires system libc++).
// Rebuild with: cd lib/jinja_cpp && for f in jinja_wrapper caps lexer parser runtime jinja_string value; do clang++ -std=c++17 -O2 -DNDEBUG -I . -c $f.cpp -o obj/$f.o; done && ar rcs libjinja.a obj/*.o
mod.addObjectFile(b.path("lib/jinja_cpp/libjinja.a"));
mod.addIncludePath(b.path("lib/jinja_cpp"));
// stb_image for JPEG/PNG decoding in the vision pipeline
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_impl.c"), .flags = &.{"-O2"} });
// stb_image_write for PNG encoding (native image-generation endpoint)
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_write_impl.c"), .flags = stb_write_flags });
mod.addIncludePath(b.path("lib"));
// xatlas UV unwrapping + FQMS decimation (MIT, vendored) with C shims for the
// Hunyuan3D texture paint stage. See lib/{xatlas,fqms} + src/{uvwrap,mesh_simplify}.zig.
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/xatlas"));
mod.addCSourceFile(.{ .file = b.path("lib/fqms/fqms_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/fqms"));
// ds4 inference engine for DSV4-Flash (Metal backend, macOS only). See
// `lib/ds4/` submodule pinned at 9139e2a and `src/arch/ds4.zig`. Kernel
// sources are embedded via `lib/ds4_metal_sources.zig` and extracted at
// runtime to ~/.mlx-serve/ds4-metal/<hash>/.
addDs4Sources(b, mod);
mod.addIncludePath(b.path("lib/ds4"));
// ANE prefill-MLP offload (perf-plan-aug-17 P5): objc bridge to the
// private AppleNeuralEngine framework (dlopen'd at runtime — the probe
// returns unavailable on machines/OSes without it) + the per-layer MLP
// MIL program builder. See lib/ane/ + src/ane.zig; provenance in NOTICE.
addAneSources(b, mod);
// llama.cpp libllama for generic GGUF models (Metal backend, macOS only).
// Staged by `scripts/fetch-llama.sh` into lib/llama/ (a single self-contained
// dylib + headers extracted from the pinned XCFramework). See src/arch/llama.zig.
addLlamaLib(b, mod);
addGgufModule(b, mod, target, optimize);
addExl3Module(b, mod, target, optimize);
addMlxStreamModule(b, mod, target, optimize);
// mlx + mlx-c: self-built from the pinned submodules (lib/mlx-src,
// lib/mlxc-src) into lib/mlx by scripts/build-mlx.sh, with NAX kernels
// enabled (the Homebrew bottle ships without them). MUST come before the
// /opt/homebrew lib path so a leftover brew mlx-c can never win the link.
addMlxLib(b, mod);
// webp include/lib paths (homebrew)
mod.addIncludePath(.{ .cwd_relative = "/opt/homebrew/include" });
mod.addLibraryPath(.{ .cwd_relative = "/opt/homebrew/lib" });
mod.linkSystemLibrary("webp", .{});
if (macos_sdk_frameworks) |fw_path| {
mod.addFrameworkPath(.{ .cwd_relative = fw_path });
}
mod.linkFramework("IOKit", .{});
mod.linkFramework("CoreFoundation", .{});
mod.linkFramework("Foundation", .{});
mod.linkFramework("Metal", .{});
mod.linkFramework("IOSurface", .{});
const exe = b.addExecutable(.{
.name = "mlx-serve",
.root_module = mod,
});
// Ensure Mach-O header has room for install_name_tool path changes (app bundling)
exe.headerpad_max_install_names = true;
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_cmd.addPassthruArgs();
const run_step = b.step("run", "Run mlx-serve");
run_step.dependOn(&run_cmd.step);
// Unit tests — reuses the same module config (mlx-c, jinja_cpp, etc.)
const test_mod = b.createModule(.{
.root_source_file = b.path("src/tests.zig"),
.target = target,
.optimize = optimize,
.link_libcpp = true,
.imports = &.{
.{ .name = "build_options", .module = build_options.createModule() },
.{ .name = "ds4_metal_sources", .module = ds4_metal_sources },
.{ .name = "mlx_steel_sources", .module = mlx_steel_sources },
.{ .name = "opencode2_plugin", .module = opencode2_plugin },
.{ .name = "agent_skills", .module = agent_skills },
.{ .name = "jinja_c", .module = addCHeaderModule(b, b.path("lib/jinja_cpp/jinja_wrapper.h"), b.path("lib/jinja_cpp"), target, optimize, "") },
.{ .name = "stb", .module = addCHeaderModule(b, b.path("lib/stb_image.h"), b.path("lib"), target, optimize, "") },
.{ .name = "webp", .module = addCHeaderModule(b, .{ .cwd_relative = "/opt/homebrew/include/webp/decode.h" }, .{ .cwd_relative = "/opt/homebrew/include" }, target, optimize, "") },
},
});
test_mod.addObjectFile(b.path("lib/jinja_cpp/libjinja.a"));
test_mod.addIncludePath(b.path("lib/jinja_cpp"));
test_mod.addCSourceFile(.{ .file = b.path("lib/stb_image_impl.c"), .flags = &.{"-O2"} });
test_mod.addCSourceFile(.{ .file = b.path("lib/stb_image_write_impl.c"), .flags = stb_write_flags });
test_mod.addIncludePath(b.path("lib"));
test_mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
test_mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
test_mod.addIncludePath(b.path("lib/xatlas"));
test_mod.addCSourceFile(.{ .file = b.path("lib/fqms/fqms_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
test_mod.addIncludePath(b.path("lib/fqms"));
addDs4Sources(b, test_mod);
test_mod.addIncludePath(b.path("lib/ds4"));
addAneSources(b, test_mod);
addLlamaLib(b, test_mod);
addGgufModule(b, test_mod, target, optimize);
addExl3Module(b, test_mod, target, optimize);
addMlxStreamModule(b, test_mod, target, optimize);
test_mod.linkSystemLibrary("c++", .{});
addMlxLib(b, test_mod);
test_mod.addIncludePath(.{ .cwd_relative = "/opt/homebrew/include" });
test_mod.addLibraryPath(.{ .cwd_relative = "/opt/homebrew/lib" });
test_mod.linkSystemLibrary("webp", .{});
if (macos_sdk_frameworks) |fw_path| {
test_mod.addFrameworkPath(.{ .cwd_relative = fw_path });
}
test_mod.linkFramework("IOKit", .{});
test_mod.linkFramework("CoreFoundation", .{});
test_mod.linkFramework("Foundation", .{});
test_mod.linkFramework("Metal", .{});
test_mod.linkFramework("IOSurface", .{});
const test_filter = b.option([]const u8, "test-filter", "Only run tests whose name contains this substring");
const qwen_preprocess_fixture = b.option(
[]const u8,
"qwen-preprocess-fixture",
"CPU reference fixture for the gated Qwen preprocessing parity test",
);
const unit_tests = b.addTest(.{
.root_module = test_mod,
.filters = if (test_filter) |f| &.{f} else &.{},
});
const test_build = b.step("test-build", "Compile unit tests without running them");
test_build.dependOn(&b.addInstallArtifact(unit_tests, .{ .dest_dir = .{ .override = .{ .custom = "tests" } } }).step);
const run_unit_tests = b.addRunArtifact(unit_tests);
if (qwen_preprocess_fixture) |fixture| {
run_unit_tests.setEnvironmentVariable("QWEN_PREPROCESS_FIXTURE", fixture);
run_unit_tests.addFileInput(.{ .cwd_relative = b.fmt("{s}/manifest.json", .{fixture}) });
run_unit_tests.addFileInput(.{ .cwd_relative = b.fmt("{s}/source_rgb.bin", .{fixture}) });
run_unit_tests.addFileInput(.{ .cwd_relative = b.fmt("{s}/pixel_values.bin", .{fixture}) });
}
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
{
const t = addMlxStreamTests(b, target, optimize, test_filter, macos_sdk_frameworks, "src/tests.zig", "mlx-stream-test");
test_build.dependOn(&b.addInstallArtifact(t, .{ .dest_dir = .{ .override = .{ .custom = "tests" } } }).step);
const run = b.addRunArtifact(t);
test_step.dependOn(&run.step);
b.step("mlx-stream-test", "Run the mlx-stream plugin's tests").dependOn(&run.step);
// Its own binary (the last test checks that nothing in it created a Metal device), outside `zig build test`:
// it recompiles every plugin file's tests.
const conf = b.addRunArtifact(addMlxStreamTests(b, target, optimize, null, macos_sdk_frameworks, "src/conformance.zig", "mlx-stream-conformance"));
conf.setEnvironmentVariable("MLX_DEFAULT_DEVICE", "cpu");
b.step("mlx-stream-conformance", "Run the mlx-stream plugin's conformance suite (CPU lane, no device)").dependOn(&conf.step);
}
// ── vz-agent: the Agent Sandbox's guest-side binary.
//
// A standalone static aarch64-linux-musl ELF (~200 KB) that the app injects
// into the guest rootfs before boot, exactly like `/.vz-init`. It serves the
// vsock exec protocol (`src/vz_agent.zig`), replacing the hvc1 console shell.
//
// It is NOT imported by main.zig — it links nothing but libc and never runs
// on macOS. Its tests do, though: `serveConnection` is OS-agnostic, so the
// whole request → spawn → stream → exit path is exercised over a socketpair
// here on the host. Wire them into `zig build test` explicitly, since the
// main test module's root never reaches this file.
addVzAgent(b, target, optimize, test_step);
// ── iOS on-device engine: a static library (libmlxserve.a) linking the
// MLX-only decode path. ds4 + llama.cpp are stubbed (build_options.ios =
// true). Two slices: `zig build ios-lib` (device, arm64-iphoneos) and
// `zig build ios-lib-sim` (arm64 iphonesimulator). Driven by the iPhone
// app project's build scripts (../mlx-iphone/scripts/build-zig-ios.sh),
// which supply the matching --sysroot and copy the artifact out of
// zig-out/ios/<sdk>/lib. `-Dios-include=<dir>` points at the iOS dist's
// include dir for third-party headers (webp); defaults to Homebrew's,
// whose versions are pinned identical by verifyBrewDeps.
const ios_include = b.option([]const u8, "ios-include", "Include dir for webp/stb headers when cross-compiling the iOS lib") orelse "/opt/homebrew/include";
addIosLib(b, version, ios_include, .{ .step = "ios-lib", .abi = .none, .sdk = "iphoneos" });
addIosLib(b, version, ios_include, .{ .step = "ios-lib-sim", .abi = .simulator, .sdk = "iphonesimulator" });
}
/// Hermetic Latent2RGB + JPEG tests. No MLX, no Homebrew — the only `zig build`
/// graph that is valid on Linux (issue #208). The ARTIFACT is hermetic, the
/// STEP is not a way around a missing mlx: `verifyBrewDeps` + `verifyMlxStage`
/// run at configure time for every step, so on a Mac `lib/mlx/` must be staged
/// before this builds. UBSan is off for stb (its bit shifts trip the sanitizer).
fn addPreviewTest(b: *std.Build, target: std.Build.ResolvedTarget) void {
const mod = b.createModule(.{
.root_source_file = b.path("src/preview.zig"),
.target = target,
.optimize = .ReleaseFast,
.link_libc = true,
});
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_write_impl.c"), .flags = stb_write_flags });
mod.addIncludePath(b.path("lib"));
const tests = b.addTest(.{
.name = "preview-test",
.root_module = mod,
});
const run = b.addRunArtifact(tests);
const step = b.step("preview-test", "Hermetic JPEG / Latent2RGB preview tests (no MLX)");
step.dependOn(&run.step);
}
/// `zig build` on/for Linux: the full mlx-serve HTTP server against the
/// Linux MLX (Vulkan backend) + mlx-c pair staged into lib/mlx by
/// scripts/build-mlx-linux.sh. Mirrors the macOS graph minus everything
/// Apple-specific:
/// - ds4 on its CUDA backend (scripts/build-ds4-linux.sh) and llama.cpp's
/// CUDA release (scripts/fetch-llama.sh) instead of Metal; no ANE;
/// - no IOKit/Metal frameworks, no metallib fingerprint (round_cost mixes
/// exe bytes only when git_sha is empty, and mixMlxArtifacts no-ops
/// without dyld);
/// - system libwebp instead of Homebrew;
/// - libjinja built for the host by the same staging script (the committed
/// libjinja.a holds Mach-O objects).
fn addLinuxServe(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void {
verifyMlxStageLinux(b);
const version = b.option([]const u8, "version", "Version string") orelse readAppVersion(b) orelse "0.0.0-dev";
const mlx_c_version = b.option([]const u8, "mlx-c-version", "Pinned mlx-c version") orelse readMlxcPin(b) orelse "unknown";
const build_options = b.addOptions();
build_options.addOption([]const u8, "version", version);
build_options.addOption(bool, "mas", false);
build_options.addOption([]const u8, "mlx_c_version", mlx_c_version);
build_options.addOption([]const u8, "ds4_commit", b.option([]const u8, "ds4-commit", "Pinned ds4 submodule short commit") orelse "unknown");
build_options.addOption([]const u8, "llama_tag", readLlamaTag(b) orelse "unknown");
build_options.addOption([]const u8, "git_sha", "");
build_options.addOption(bool, "ios", false);
build_options.addOption(bool, "embedded_engines", true);
build_options.addOption(bool, "mlx_stream", false);
// The corpus replay and benchmark tests run tens of seconds in Debug (#639), so
// `zig build test` skips them and `zig build test -Dslow-tests` runs them.
build_options.addOption(bool, "slow_tests", b.option(bool, "slow-tests", "Also run the slow corpus-replay and benchmark tests") orelse false);
const opencode2_plugin = b.createModule(.{
.root_source_file = b.path("lib/opencode2_plugin.zig"),
.target = target,
.optimize = optimize,
});
const agent_skills = b.createModule(.{
.root_source_file = b.path("skills/agent_skills.zig"),
.target = target,
.optimize = optimize,
});
const mlx_steel_sources = b.createModule(.{
.root_source_file = b.path("lib/mlx_steel_sources.zig"),
.target = target,
.optimize = optimize,
});
// The server and its unit tests (`zig build test`, root src/tests.zig) share one module setup.
var mods: [2]*std.Build.Module = undefined;
for (&mods, [_][]const u8{ "src/main.zig", "src/tests.zig" }) |*slot, root| {
const mod = b.createModule(.{
.root_source_file = b.path(root),
.target = target,
.optimize = optimize,
.link_libcpp = true,
.imports = &.{
.{ .name = "build_options", .module = build_options.createModule() },
.{ .name = "mlx_steel_sources", .module = mlx_steel_sources },
.{ .name = "opencode2_plugin", .module = opencode2_plugin },
.{ .name = "agent_skills", .module = agent_skills },
.{ .name = "jinja_c", .module = addCHeaderModule(b, b.path("lib/jinja_cpp/jinja_wrapper.h"), b.path("lib/jinja_cpp"), target, optimize, "") },
.{ .name = "stb", .module = addCHeaderModule(b, b.path("lib/stb_image.h"), b.path("lib"), target, optimize, "") },
.{ .name = "webp", .module = addCHeaderModule(b, .{ .cwd_relative = "/usr/include/webp/decode.h" }, .{ .cwd_relative = "/usr/include" }, target, optimize, "") },
},
});
// Jinja2 template engine — same vendored sources as the macOS graph, built
// as an ELF static lib by scripts/build-mlx-linux.sh (zig c++).
addGgufModule(b, mod, target, optimize);
addExl3Module(b, mod, target, optimize);
mod.addObjectFile(b.path("lib/jinja_cpp/libjinja-linux.a"));
mod.addIncludePath(b.path("lib/jinja_cpp"));
// stb_image (JPEG/PNG decode) + stb_image_write (PNG encode), xatlas
// (UV unwrap for Hunyuan3D texture paint) — portable C/C++.
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_impl.c"), .flags = &.{"-O2"} });
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_write_impl.c"), .flags = stb_write_flags });
mod.addIncludePath(b.path("lib"));
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/xatlas"));
mod.addCSourceFile(.{ .file = b.path("lib/fqms/fqms_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/fqms"));
// ANE offload C ABI → unavailable stubs on Linux (src/ane_stub.c); ane.zig
// compiles unchanged and gates itself off via available() == false.
mod.addCSourceFile(.{ .file = b.path("src/ane_stub.c"), .flags = &.{"-O2"} });
// mlx (Vulkan fork) + mlx-c, staged in lib/mlx — same link shape as macOS.
addMlxLib(b, mod);
addDs4LinuxLib(b, mod);
addLlamaLib(b, mod);
// ELF has no @loader_path: the Mach-O rpaths emitted above are inert here,
// so the loader never finds libmlxc.so. Mirror them in $ORIGIN form.
for ([_][]const u8{ "mlx/lib", "llama/lib", "ds4-linux" }) |dir| {
mod.addRPath(.{ .cwd_relative = b.fmt("$ORIGIN/../../lib/{s}", .{dir}) });
mod.addRPath(.{ .cwd_relative = b.fmt("$ORIGIN/../../../lib/{s}", .{dir}) });
}
// System libwebp for the vision pipeline (pkg-config resolves -lwebp).
mod.linkSystemLibrary("webp", .{});
// Bonjour/mDNS peer discovery (src/lan.zig) via Avahi's dns_sd compat lib
// (Arch: avahi ships /usr/lib/libdns_sd.so; Debian: libavahi-compat-libdnssd-dev).
mod.linkSystemLibrary("dns_sd", .{ .use_pkg_config = .no });
slot.* = mod;
}
const exe = b.addExecutable(.{
.name = "mlx-serve",
.root_module = mods[0],
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_cmd.addPassthruArgs();
const run_step = b.step("run", "Run mlx-serve");
run_step.dependOn(&run_cmd.step);
const test_filter = b.option([]const u8, "test-filter", "Only run tests whose name contains this substring");
const unit_tests = b.addTest(.{
.root_module = mods[1],
.filters = if (test_filter) |f| &.{f} else &.{},
});
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
}
/// Linux counterpart of verifyMlxStage: fail loudly when scripts/
/// build-mlx-linux.sh has not staged the Linux mlx/mlx-c pair.
fn verifyMlxStageLinux(b: *std.Build) void {
const stage_ok = blk: {
buildRootHandle(b).access(b.graph.io, "lib/mlx/lib/libmlxc.so", .{}) catch break :blk false;
buildRootHandle(b).access(b.graph.io, "lib/mlx/lib/libmlx.so", .{}) catch break :blk false;
buildRootHandle(b).access(b.graph.io, "lib/mlx/.version", .{}) catch break :blk false;
break :blk true;
};
if (!stage_ok) {
std.debug.print(
"\n[mlx-serve] lib/mlx is not staged for Linux. Build it with:\n" ++
" git submodule update --init lib/mlxc-src && MLX_SOURCE=<staged Linux mlx tree> ./scripts/build-mlx-linux.sh\n\n",
.{},
);
std.process.exit(1);
}
// The server embeds files from the opencode2 submodule
// (lib/opencode2_plugin.zig @embedFile). A missing checkout surfaces as a
// cryptic FileNotFound mid-compile, so check it at configure time.
buildRootHandle(b).access(b.graph.io, "lib/opencode2-mlx-serve/LICENSE", .{}) catch {
std.debug.print(
"\n[mlx-serve] lib/opencode2-mlx-serve is not checked out. Run:\n" ++
" git submodule update --init lib/opencode2-mlx-serve\n\n",
.{},
);
std.process.exit(1);
};
// The embedded engines (ds4 on CUDA, llama.cpp's CUDA release).
for ([_][2][]const u8{
.{ "lib/ds4-linux/libds4.so", "git submodule update --init lib/ds4 && ./scripts/build-ds4-linux.sh" },
.{ "lib/llama/lib/libllama.so", "./scripts/fetch-llama.sh" },
}) |stage| {
buildRootHandle(b).access(b.graph.io, stage[0], .{}) catch {
std.debug.print("\n[mlx-serve] {s} is not staged. Run:\n {s}\n\n", .{ stage[0], stage[1] });
std.process.exit(1);
};
}
}
/// `zig build vz-agent` → `zig-out/guest/vz-agent` (static aarch64 Linux ELF),
/// plus the host-side unit tests wired into `zig build test`.
fn addVzAgent(
b: *std.Build,
host_target: std.Build.ResolvedTarget,
host_optimize: std.builtin.OptimizeMode,
test_step: *std.Build.Step,
) void {
// Guest binary. musl + static so it runs on ANY base image — the bundled
// Debian rootfs for the App Store build, or a user-chosen alpine/slim image.
const guest_target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .linux,
.abi = .musl,
});
const guest = b.addExecutable(.{
.name = "vz-agent",
.root_module = b.createModule(.{
.root_source_file = b.path("src/vz_agent.zig"),
.target = guest_target,
// Size, not speed: it shuttles bytes between a socket and a pipe.
.optimize = .ReleaseSmall,
.link_libc = true,
}),
});
const install = b.addInstallArtifact(guest, .{
.dest_dir = .{ .override = .{ .custom = "guest" } },
});
const step = b.step("vz-agent", "Build the Agent Sandbox guest binary (static aarch64-linux)");
step.dependOn(&install.step);
// Host-side tests of the same source.
const tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/vz_agent.zig"),
.target = host_target,
.optimize = host_optimize,
.link_libc = true,
}),
});
test_step.dependOn(&b.addRunArtifact(tests).step);
// A macOS-native build of the SAME source, which listens on a unix socket
// instead of vsock. `GuestExecInteropTests` (Swift) drives it, so the host
// frame driver and the guest agent are proven against each other without a
// VM — the golden-byte tests alone can't catch a streaming bug.
const host_agent = b.addExecutable(.{
.name = "vz-agent-host",
.root_module = b.createModule(.{
.root_source_file = b.path("src/vz_agent.zig"),
.target = host_target,
.optimize = host_optimize,
.link_libc = true,
}),
});
const host_step = b.step("vz-agent-host", "Build vz-agent natively (unix-socket mode, for interop tests)");
host_step.dependOn(&b.addInstallArtifact(host_agent, .{}).step);
}
const IosSlice = struct { step: []const u8, abi: std.Target.Abi, sdk: []const u8 };
fn addIosLib(b: *std.Build, version: []const u8, ios_include: []const u8, slice: IosSlice) void {
// Min 18.0 to match the MLX metallib (Metal 3.2). abi=.none → device,
// abi=.simulator → iOS Simulator slice.
const ios_target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
.os_version_min = .{ .semver = .{ .major = 18, .minor = 0, .patch = 0 } },
.abi = slice.abi,
});
const ios_options = b.addOptions();
ios_options.addOption([]const u8, "version", version);
ios_options.addOption(bool, "ios", true);
// Mirror the build options the shared engine sources read (server.zig,
// scheduler.zig). iOS is sandboxed (no curl/model-pull subprocess), so
// mas=true; ds4 + llama.cpp are stubbed here, so their version pins are
// unreported. Without these the iOS lib fails to compile ("options has no
// member named 'mas'/...").
ios_options.addOption(bool, "mas", true);
ios_options.addOption(bool, "embedded_engines", false);
ios_options.addOption(bool, "mlx_stream", false);
ios_options.addOption([]const u8, "mlx_c_version", "unknown");
ios_options.addOption([]const u8, "ds4_commit", "unknown");
ios_options.addOption([]const u8, "llama_tag", "unknown");
ios_options.addOption([]const u8, "git_sha", "");
const mod = b.createModule(.{
.root_source_file = b.path("src/ios_lib.zig"),
.target = ios_target,
.optimize = .ReleaseFast,
.link_libc = true,
.link_libcpp = true,
.imports = &.{
.{ .name = "build_options", .module = ios_options.createModule() },
},
});
addGgufModule(b, mod, ios_target, .ReleaseFast);
addExl3Module(b, mod, ios_target, .ReleaseFast);
// Apple cross-compiles don't auto-resolve the SDK's libc/frameworks from
// --sysroot alone, so wire them explicitly (resolved per slice via xcrun).
//
// NO iOS SDK → register NOTHING (the `ios-lib` steps just don't exist in
// this environment) instead of failing the whole configure: app/build.sh
// pins DEVELOPER_DIR to the CommandLineTools for the macOS link, and CLT
// ships no iOS SDKs — a @panic here aborted every macOS app build even
// though nobody asked for an iOS step.
var code: u8 = undefined;
const sdk_path = b.runAllowFail(
&.{ "xcrun", "--sdk", slice.sdk, "--show-sdk-path" },
&code,
.ignore, // silent when absent — CLT environments hit this on purpose
) catch return;
const ios_sdk = std.mem.trim(u8, sdk_path, " \n\r\t");
if (ios_sdk.len == 0) return;
mod.addSystemIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include", .{ios_sdk}) });
mod.addFrameworkPath(.{ .cwd_relative = b.fmt("{s}/System/Library/Frameworks", .{ios_sdk}) });
// Headers for the @import("jinja_c")/@import("stb") sites (jinja_wrapper.h,
// stb_image.h, webp/decode.h). The matching static archives are linked by
// Xcode at final app-link time.
mod.addIncludePath(b.path("lib/jinja_cpp"));
mod.addIncludePath(b.path("lib"));
mod.addIncludePath(.{ .cwd_relative = ios_include });
mod.addImport("jinja_c", addCHeaderModule(b, b.path("lib/jinja_cpp/jinja_wrapper.h"), b.path("lib/jinja_cpp"), ios_target, .ReleaseFast, ios_sdk));
mod.addImport("stb", addCHeaderModule(b, b.path("lib/stb_image.h"), b.path("lib"), ios_target, .ReleaseFast, ios_sdk));
mod.addImport("webp", addCHeaderModule(b, .{ .cwd_relative = b.fmt("{s}/webp/decode.h", .{ios_include}) }, .{ .cwd_relative = ios_include }, ios_target, .ReleaseFast, ios_sdk));
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_impl.c"), .flags = &.{"-O2"} });
mod.addCSourceFile(.{ .file = b.path("lib/stb_image_write_impl.c"), .flags = stb_write_flags });
// xatlas UV unwrapping (C++), used by the Hunyuan3D texture paint stage via
// src/uvwrap.zig extern decls — compiled into the lib like the macOS exe.
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addCSourceFile(.{ .file = b.path("lib/xatlas/xatlas_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/xatlas"));
mod.addCSourceFile(.{ .file = b.path("lib/fqms/fqms_shim.cpp"), .flags = &.{ "-std=c++17", "-O2", "-DNDEBUG" } });
mod.addIncludePath(b.path("lib/fqms"));
const lib = b.addLibrary(.{
.name = "mlxserve",
.root_module = mod,
.linkage = .static,
});
lib.bundle_compiler_rt = true;
const install = b.addInstallArtifact(lib, .{
.dest_dir = .{ .override = .{ .custom = b.fmt("ios/{s}/lib", .{slice.sdk}) } },
});
const step = b.step(slice.step, b.fmt("Build the iOS engine static lib ({s})", .{slice.sdk}));
step.dependOn(&install.step);
}
/// Translates a single C header into an importable module (`@import("name")`
/// at the call site) via `addTranslateC`, replacing an inline `@cImport` —
/// removed as a language builtin in 0.17.0-dev.
fn addCHeaderModule(
b: *std.Build,
header_path: std.Build.LazyPath,
include_dir: std.Build.LazyPath,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
ios_sdk: []const u8,
) *std.Build.Module {
const translate = b.addTranslateC(.{
.root_source_file = header_path,
.target = target,
.optimize = optimize,
});
translate.addIncludePath(include_dir);
// iOS cross-compile: addTranslateC (0.17's @cImport replacement) does NOT
// inherit the parent module's SDK include search the way inline @cImport
// used to, so a header that pulls in <stdio.h>/<inttypes.h> can't find the
// Apple libc and translation fails. Wire the SDK's system include here.
// Host builds pass "" (their toolchain resolves the system headers).
if (ios_sdk.len > 0)
translate.addSystemIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include", .{ios_sdk}) });
return translate.createModule();
}
fn addDs4Sources(b: *std.Build, module: *std.Build.Module) void {
// Match ds4's Makefile flags (lib/ds4/Makefile lines 10–11). We drop
// `-mcpu=native` so the produced binary stays portable across Apple
// Silicon generations — ds4 itself ships portable IR for its Metal
// kernels, and the C host code is not perf-critical compared to the GPU
// path. `-Wno-unused-parameter` + `-Wno-unused-variable` keep upstream's
// warnings from breaking our build without patching the submodule.
const c_flags = &[_][]const u8{
"-O3",
"-ffast-math",
"-std=c99",
"-Wno-unused-parameter",
"-Wno-unused-variable",
"-Wno-unused-but-set-variable",
"-Wno-unused-function",
"-Wno-deprecated-declarations",
};
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4.c"), .flags = c_flags });
// ds4.c #includes ds4_distributed.h; the engine/session path links its impl.
// ds4_gpu.h is implemented in ds4_metal.m; ds4_kvstore/web/help/agent.c and
// ds4_gpu_args.c are CLI/server-only and not part of the library path
// mlx-serve embeds (upstream Makefile CORE_OBJS is the authority).
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_distributed.c"), .flags = c_flags });
// SSD weight-streaming (issue #39): ds4_ssd.c is a standalone TU (#includes
// only ds4_ssd.h) implementing the streaming expert cache the engine_options
// ssd_streaming_* fields drive. Added upstream after the previous pin.
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_ssd.c"), .flags = c_flags });
// Two-machine tensor parallelism + multi-GPU layer placement (pin 9139e2a):
// ds4.c references ds4_tp_* and ds4_compute_layer_placement/ds4_layer_pack_print
// unconditionally, so both TUs must link even though we never enable TP.
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_tp.c"), .flags = c_flags });
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_layer_pack.c"), .flags = c_flags });
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_image.c"), .flags = c_flags });
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_engram.c"), .flags = c_flags });
// Our own shim: exports sizeof/offsetof of the real C structs so the
// ds4_ffi.zig layout test catches mirror drift (mid-struct-insert class).
module.addCSourceFile(.{ .file = b.path("src/ds4_layout_check.c"), .flags = c_flags });
const objc_flags = &[_][]const u8{
"-O3",
"-ffast-math",
"-fobjc-arc",
"-Wno-unused-parameter",
"-Wno-unused-variable",
"-Wno-unused-but-set-variable",
"-Wno-unused-function",
"-Wno-deprecated-declarations",
};
module.addCSourceFile(.{ .file = b.path("lib/ds4/ds4_metal.m"), .flags = objc_flags });
}
/// ds4 on its CUDA backend: scripts/build-ds4-linux.sh links upstream's library objects
/// (nvcc) into lib/ds4-linux/libds4.so, with its GNU C++ runtime and CUDA libraries inside.
fn addDs4LinuxLib(b: *std.Build, module: *std.Build.Module) void {
module.addIncludePath(b.path("lib/ds4"));
module.addLibraryPath(b.path("lib/ds4-linux"));
module.linkSystemLibrary("ds4", .{ .use_pkg_config = .no });
module.addCSourceFile(.{ .file = b.path("src/ds4_layout_check.c"), .flags = &.{"-std=c99"} });
}
/// ANE prefill offload sources (lib/ane): the private-framework bridge and
/// the per-layer MLP program builder, both ARC objc. Runtime-probed —
/// compiling them in costs nothing on machines without the framework.
fn addAneSources(b: *std.Build, module: *std.Build.Module) void {
const objc_flags = &[_][]const u8{
"-O3",
"-fobjc-arc",
"-Wno-deprecated-declarations",
};
module.addCSourceFile(.{ .file = b.path("lib/ane/ane_bridge.m"), .flags = objc_flags });
module.addCSourceFile(.{ .file = b.path("lib/ane/ane_mlp.m"), .flags = objc_flags });
module.addIncludePath(b.path("lib/ane"));
}
fn buildRootHandle(b: *std.Build) std.Io.Dir {
return b.root.root_dir.handle;
}
/// The llama.cpp tag staged by scripts/fetch-llama.sh (it writes LLAMA_TAG to
/// `lib/llama/.version`). Read at configure time so a plain `zig build` reports
/// the real tag without app/build.sh having to pass `--llama-tag`. Returns null
/// (→ "unknown") when llama hasn't been fetched yet.
fn readLlamaTag(b: *std.Build) ?[]const u8 {
const bytes = buildRootHandle(b).readFileAlloc(
b.graph.io,
"lib/llama/.version",
b.allocator,
.limited(256),
) catch return null;
const trimmed = std.mem.trim(u8, bytes, " \t\r\n");
return if (trimmed.len == 0) null else b.dupe(trimmed);
}
/// lib/mlx-serve-gguf: GGUF files served on MLX (no llama.cpp). It reaches
/// MLX through `host.mlx`, so the host root file must expose `pub const mlx`.
/// `-Dgguf-dir=/abs/path` builds against a checkout instead of the submodule.
fn addGgufModule(b: *std.Build, host: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void {
const gguf = b.createModule(.{
.root_source_file = ggufRoot(b),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "mlx_host", .module = host }},
});
host.addImport("mlx_serve_gguf", gguf);
}
/// `b.option` may be declared once; the graphs share this answer.
var gguf_root: ?std.Build.LazyPath = null;
fn ggufRoot(b: *std.Build) std.Build.LazyPath {
if (gguf_root) |r| return r;
const dir = b.option([]const u8, "gguf-dir", "mlx-serve-gguf checkout to build against (default: lib/mlx-serve-gguf)");
gguf_root = if (dir) |d| .{ .cwd_relative = b.pathJoin(&.{ d, "src/root.zig" }) } else b.path("lib/mlx-serve-gguf/src/root.zig");
return gguf_root.?;
}
/// mlx-stream (its own repo, `-Dmlx-stream-dir=/abs/path`): DeepSeek-V4.1's EXL3
/// streaming repack on the plugin's own arch and kernels (`src/arch/mlx_stream.zig`).
/// Its contract types are its own `sdk` module; both reach the host through `mlx_host`.
fn addMlxStreamModule(b: *std.Build, host: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void {
host.addImport("mlx_stream", mlxStreamPkg(b, host, mlxStreamDir(b), "src/root.zig", target, optimize, false));
}
fn mlxStreamPkg(b: *std.Build, host: *std.Build.Module, dir: []const u8, root: []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, inject: bool) *std.Build.Module {
const sdk = b.createModule(.{
.root_source_file = .{ .cwd_relative = b.pathJoin(&.{ dir, "sdk/root.zig" }) },
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "mlx_host", .module = host }},
});
const m = b.createModule(.{
.root_source_file = .{ .cwd_relative = b.pathJoin(&.{ dir, root }) },
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{ .{ .name = "mlx_host", .module = host }, .{ .name = "sdk", .module = sdk } },
});
// The read pool (pthreads, pread into slot rows), its MTLSharedEvent signal (non-ARC objc), and the MLX
// event / alloc shims, which include the staged MLX's private headers and link libmlx.
const csrc = b.pathJoin(&.{ dir, "csrc" });
const c: []const []const u8 = if (inject) &.{ "-O2", "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread", "-DQ3LD_INJECT" } else &.{ "-O2", "-std=c11", "-Wall", "-Wextra", "-Werror", "-pthread" };
const cxx: []const []const u8 = &.{ "-std=c++20", "-O2", "-D_METAL_", "-DACCELERATE_NEW_LAPACK", "-fno-sanitize=all", "-Wall", "-Wno-unused-parameter", "-Wno-deprecated-declarations" };
m.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ csrc, "q3_lookahead4_exl3.c" }) }, .flags = c });
m.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ csrc, "q3_event_shim.mm" }) }, .flags = &.{ "-O2", "-Wall", "-Wextra", "-Werror", "-fno-objc-arc" } });
m.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ csrc, "mlx_event_shim.cpp" }) }, .flags = cxx });
m.addCSourceFile(.{ .file = .{ .cwd_relative = b.pathJoin(&.{ csrc, "mlx_alloc_shim.cpp" }) }, .flags = cxx });
m.addIncludePath(.{ .cwd_relative = csrc });
m.addIncludePath(b.path("lib/mlx/include"));
m.addIncludePath(b.path("lib/mlx/include/metal_cpp"));
m.addIncludePath(b.path("lib/mlxc-src"));
m.linkSystemLibrary("mlx", .{ .use_pkg_config = .no });
// The shims make the server load @rpath/libmlx.dylib itself: the app bundle keeps it in Contents/Frameworks/, the
// CLI zip in lib/ beside the binary.
m.addRPath(.{ .cwd_relative = "@executable_path/../Frameworks" });
m.addRPath(.{ .cwd_relative = "@executable_path/lib" });
return m;
}
/// One of the plugin's own test roots (src/tests.zig, src/conformance.zig) over the host surface plugins import
/// (`src/plugin_host.zig`), with the read pool's scripted faults.
fn addMlxStreamTests(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, filter: ?[]const u8, frameworks: ?[]const u8, root: []const u8, name: []const u8) *std.Build.Step.Compile {
const host = b.createModule(.{ .root_source_file = b.path("src/plugin_host.zig"), .target = target, .optimize = optimize, .link_libc = true, .link_libcpp = true });
addMlxLib(b, host);
if (frameworks) |fw| host.addFrameworkPath(.{ .cwd_relative = fw });
for ([_][]const u8{ "IOKit", "CoreFoundation", "Foundation", "Metal", "IOSurface" }) |f| host.linkFramework(f, .{});
const m = mlxStreamPkg(b, host, mlxStreamDir(b), root, target, optimize, true);
return b.addTest(.{ .name = name, .root_module = m, .filters = if (filter) |f| &.{f} else &.{} });
}
/// The plugin checkout: `-Dmlx-stream-dir`, else the `lib/mlx-stream` submodule. `b.option` may be declared
/// once; the graphs share this answer.
var mlx_stream_dir: ?[]const u8 = null;
fn mlxStreamDir(b: *std.Build) []const u8 {
if (mlx_stream_dir == null) mlx_stream_dir = b.option([]const u8, "mlx-stream-dir", "mlx-stream checkout to build DeepSeek-V4.1's EXL3 repack from (default: lib/mlx-stream)") orelse
b.pathJoin(&.{ b.root.root_dir.path orelse ".", "lib/mlx-stream" });
return mlx_stream_dir.?;
}
/// lib/sushi: EXL3 routed experts, Sushi's `sushi_exl3` module. It reaches
/// mlx, log and io_util through `mlx_host`, so the host root exposes them.
/// `-Dsushi-dir=/abs/path` builds against a checkout instead of the submodule.
fn addExl3Module(b: *std.Build, host: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void {
const exl3 = b.createModule(.{
.root_source_file = exl3Root(b),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "mlx_host", .module = host }},
});
host.addImport("sushi_exl3", exl3);
}
var exl3_root: ?std.Build.LazyPath = null;
fn exl3Root(b: *std.Build) std.Build.LazyPath {
if (exl3_root) |r| return r;
const dir = b.option([]const u8, "sushi-dir", "sushi checkout to build against (default: lib/sushi)");
exl3_root = if (dir) |d| .{ .cwd_relative = b.pathJoin(&.{ d, "src/exl3/root.zig" }) } else b.path("lib/sushi/src/exl3/root.zig");
return exl3_root.?;
}
fn addLlamaLib(b: *std.Build, module: *std.Build.Module) void {
// Link the prebuilt libllama staged by scripts/fetch-llama.sh. The dylib's
// install-name is @rpath/libllama.dylib; we add an rpath to its build-tree
// location so `zig build run` / unit tests resolve it in dev. The app bundle
// and CLI tarball rewrite that reference to @executable_path/... and re-sign
// with the Developer ID (see release.yml / app/build.sh).
module.addIncludePath(b.path("lib/llama/include"));
module.addLibraryPath(b.path("lib/llama/lib"));
// use_pkg_config = .no: a Homebrew `llama.cpp` install ships a llama.pc that
// would otherwise hijack this link (pulling in /opt/homebrew's version + its
// separate libggml). We want exactly the pinned dylib staged in lib/llama/lib.
module.linkSystemLibrary("llama", .{ .use_pkg_config = .no });
// The macOS dylib merges ggml in; the Linux release keeps it separate.
if (module.resolved_target.?.result.os.tag == .linux) {
module.linkSystemLibrary("ggml", .{ .use_pkg_config = .no });
module.linkSystemLibrary("ggml-base", .{ .use_pkg_config = .no });
}
// @loader_path resolves against the BINARY's own location at launch,
// not the launching process's cwd. A bare relative string here (e.g.
// "lib/llama/lib") gets baked verbatim into LC_RPATH when `zig build`
// runs with cwd == build root (b.build_root.path is null in that case,
// so b.path() can't make it absolute) and dyld then resolves that
// relative string against argv[0]'s cwd, breaking any launch from
// outside the repo root. An absolute path avoids that but bakes in a
// machine-specific location, so the build isn't relocatable. This is
// relative to the binary itself, so it stays correct from any launch
// cwd and survives copying the whole zig-out + lib tree elsewhere.
//
// This module backs two different binaries at two different depths
// under the build root, so one entry can't serve both: the installed
// exe lands at zig-out/bin/mlx-serve (@loader_path = zig-out/bin/, 2
// levels up to root), while `zig build test` runs straight out of
// .zig-cache/o/<hash>/test (@loader_path = that dir, 3 levels up to
// root). dyld tries every LC_RPATH entry in order and silently skips
// ones that don't resolve, so listing both depths here is safe — each
// binary finds its own and ignores the other.
module.addRPath(.{ .cwd_relative = "@loader_path/../../lib/llama/lib" });
module.addRPath(.{ .cwd_relative = "@loader_path/../../../lib/llama/lib" });
// Our clean C shim over llama.h (src/llama_ffi.zig mirrors lib/llama_shim/llama_shim.h).
// C++ for llama.cpp's C++-linked NextN staging calls (MTP).
module.addIncludePath(b.path("lib/llama_shim"));
module.addCSourceFile(.{
.file = b.path("lib/llama_shim/llama_shim.cpp"),
.flags = &.{ "-O2", "-std=c++17", "-Wno-unused-parameter" },
});
}
/// Link the self-built mlx + mlx-c staged in lib/mlx by scripts/build-mlx.sh
/// (pinned submodules lib/mlx-src + lib/mlxc-src, deployment target 26.2 so
/// MLX's NAX kernels are compiled in — the Homebrew bottle ships without them
/// and hard-wires is_nax_available() false even on M5). Install names are
/// @rpath/...; the build-tree rpath resolves them in dev, release.yml /
/// app/build.sh rewrite to @executable_path and re-sign for bundles.
/// Guard test: tests/test_mlx_staged_nax.sh.
fn addMlxLib(b: *std.Build, module: *std.Build.Module) void {
module.addIncludePath(b.path("lib/mlx/include"));
module.addLibraryPath(b.path("lib/mlx/lib"));
// use_pkg_config = .no: a leftover Homebrew mlx-c must never hijack this
// link — we want exactly the staged NAX-enabled pair (same class as the
// llama.pc hijack above).
module.linkSystemLibrary("mlxc", .{ .use_pkg_config = .no });
// See addLlamaLib above: @loader_path is relative to the binary itself,
// so this stays correct regardless of the launching process's cwd and
// stays relocatable across machines. Two entries for the same reason —
// the installed exe and the `zig build test` binary sit at different
// depths under the build root.