Packages
mob_new
0.4.13
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.45
0.1.44
0.1.43
0.1.42
0.1.40
0.1.33
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Project generator for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
priv/templates/mob.new/ios/build.zig.eex
// build.zig — Native build orchestration for <%= display_name %> (iOS sim).
//
// Phase 2 of the build-system migration. Owns the full native compile + link
// of the iOS sim binary:
//
// Plain C (zig cc):
// - driver_tab_ios.zig — the static-NIF table (per-app generated;
// legacy .c also supported — extension auto-detected at the
// compile step below)
// - enif_keepalive.c — auto-generated by build.sh from libbeam.a's
// erl_nif.o, then handed to this build for
// compilation
//
// ObjC (xcrun cc via system command — Apple's clang is required for
// -fmodules to find Foundation/WebKit/AVFoundation; zig's bundled clang
// can't build Apple framework module maps):
// - $MOB_DIR/ios/MobNode.m
// - $MOB_DIR/ios/mob_nif.m (-DSTATIC_ERLANG_NIF, needs swift hdr)
// - $MOB_DIR/ios/mob_beam.m
// - ios/AppDelegate.m (in-project, needs swift hdr)
// - ios/beam_main.m (in-project)
//
// Swift (xcrun swiftc via system command):
// - $MOB_DIR/ios/MobViewModel.swift
// - $MOB_DIR/ios/MobRootView.swift
// - optional project Swift sources from -Dproject_swift_sources
// produces swift_mob.o + MobApp-Swift.h.
//
// Link (xcrun swiftc via system command):
// all 8 .o files + OTP/crypto static libs + UIKit/Foundation/CoreGraphics
// /QuartzCore/SwiftUI frameworks + -dead_strip → final binary.
//
// Build steps:
//
// `zig build objects` — compile only, leave outputs in zig-out/
// `zig build binary` — full pipeline through the link, binary at
// zig-out/<%= display_name %>
//
// Subsequent Phase 2 commits will move the bundle/install glue out of
// build.sh into a Mix task.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
.abi = .simulator,
});
const optimize: std.builtin.OptimizeMode = .ReleaseSmall;
const mob_dir = required(b, "mob_dir", "Path to mob library");
const otp_root = required(b, "otp_root", "Path to iOS OTP runtime");
const erts_vsn = required(b, "erts_vsn", "ERTS version dir name (e.g. erts-17.0)");
const sdkroot = required(b, "sdkroot", "iOS simulator SDK path");
const driver_tab = required(b, "driver_tab", "Absolute path to driver_tab_ios.{zig,c}");
const enif_keepalive = required(b, "enif_keepalive", "Absolute path to generated enif_keepalive.c");
const project_ios_dir = required(b, "project_ios_dir", "Absolute path to the project's ios/ dir");
const module_name = required(b, "module_name", "Swift module name (the project's display name)");
// Project-side NIFs declared in `mob.exs :static_nifs` (see issue #18).
// Same shape as the device build — see `build_device.zig` for the
// companion. Empty if the project has no project-side NIFs.
const project_root = b.option([]const u8, "project_root", "Absolute path to the project root (for c_src/<name>.c lookups)") orelse "";
const project_c_nifs = b.option([]const u8, "project_c_nifs", "Comma-separated C NIF names (each at c_src/<name>.c); empty if none") orelse "";
const project_rust_libs = b.option([]const u8, "project_rust_libs", "Comma-separated absolute paths to Rust NIF .a files (pre-built by mob_dev)") orelse "";
const project_swift_sources = b.option([]const u8, "project_swift_sources", "Comma-separated absolute paths to extra project Swift sources; empty if none") orelse "";
// Plugin contributions gathered by mob_dev from activated plugin manifests.
// swift_files: absolute paths to .swift compiled into the app's Swift module
// (same step as project_swift_sources). frameworks: extra iOS frameworks
// added to the link step alongside the hardcoded base set.
const plugin_swift_files = b.option([]const u8, "plugin_swift_files", "Comma-separated absolute paths to plugin Swift sources; empty if none") orelse "";
const plugin_frameworks = b.option([]const u8, "plugin_frameworks", "Comma-separated iOS framework names contributed by plugins; empty if none") orelse "";
const plugin_c_nifs = b.option([]const u8, "plugin_c_nifs", "Comma-separated absolute paths to plugin C NIF sources; basename = NIF module name; empty if none") orelse "";
const plugin_static_libs = b.option([]const u8, "plugin_static_libs", "Comma-separated absolute paths to plugin cpp_archive .a files (pre-built by mob_dev); empty if none") orelse "";
// MLX + EMLX. See build_device.zig.eex for full rationale.
const mlx_static = b.option(bool, "mlx_static", "EMLX NIF statically linked + libmlx.a included (-DMOB_STATIC_EMLX_NIF on driver_tab)") orelse false;
const mlx_dir = b.option([]const u8, "mlx_dir", "Absolute path to extracted MLX bundle (libmlx.a + libemlx.a + include/)") orelse "";
// NxEigen (Eigen-backed Nx backend, C++ NIF). mob_dev cross-compiles
// libnx_eigen.a per arch and threads the output dir through nxeigen_dir.
const nxeigen_static = b.option(bool, "nxeigen_static", "NxEigen NIF statically linked (-DMOB_STATIC_NX_EIGEN_NIF on driver_tab)") orelse false;
const nxeigen_dir = b.option([]const u8, "nxeigen_dir", "Absolute path to dir containing libnx_eigen.a") orelse "";
// TFLite (TensorFlow Lite via nx_tflite_mob). mob_dev cross-compiles
// libtflite_nif.a per arch and threads the path through tflite_dir; the
// TensorFlowLiteC.framework comes via tflite_framework_dir. The static
// flag flips the comptime gate in driver_tab_ios.zig — without it the
// generated driver_tab (which always references build_options.tflite_static
// because MobDev.StaticNifs.default_nifs/0 includes :tflite_nif on :all)
// fails to compile with "struct 'options' has no member named 'tflite_static'".
const tflite_static = b.option(bool, "tflite_static", "TFLite NIF statically linked (-DMOB_STATIC_TFLITE_NIF on driver_tab)") orelse false;
// mob_dev's MobDev.NativeBuild.tflite_zig_args_ios/1 emits these
// alongside -Dtflite_static. Declared as accepted b.option values so
// the zig invocation doesn't reject them as unknown. Currently a
// placeholder for iOS TFLite link-side wiring (mirrors how the Android
// template declares tflite_static without yet using a per-arch tflite_lib).
const tflite_dir = b.option([]const u8, "tflite_dir", "Absolute path to dir containing libtflite_nif.a (iOS, when TFLite enabled)") orelse "";
const tflite_framework_dir = b.option([]const u8, "tflite_framework_dir", "Absolute path to TensorFlowLiteC.framework Frameworks/ dir (iOS, when TFLite enabled)") orelse "";
_ = tflite_dir;
_ = tflite_framework_dir;
const objects_step = b.step(
"objects",
"Compile C, ObjC, and Swift objects (no link)",
);
const binary_step = b.step(
"binary",
"Compile + link the iOS sim binary (default)",
);
b.default_step = binary_step;
// Collect every produced .o LazyPath so the link step can consume them.
var objs = std.ArrayList(std.Build.LazyPath).empty;
defer objs.deinit(b.allocator);
// --- Swift via xcrun swiftc ------------------------------------------------
// Produces swift_mob.o + MobApp-Swift.h. The .h is read by the ObjC steps
// for mob_nif.m and AppDelegate.m, so they declare a build-graph dependency
// on this run + use swift_h.dirname() as their -I include path.
const swift_run = b.addSystemCommand(&.{
"xcrun",
"-sdk",
"iphonesimulator",
"swiftc",
"-target",
"arm64-apple-ios17.0-simulator",
"-module-name",
module_name,
"-import-objc-header",
});
swift_run.addArg(b.fmt("{s}/ios/MobDemo-Bridging-Header.h", .{mob_dir}));
swift_run.addArg("-I");
swift_run.addArg(b.fmt("{s}/ios", .{mob_dir}));
swift_run.addArgs(&.{ "-parse-as-library", "-wmo" });
// Glob all mob Swift sources so a newly-added file (e.g. MobGpuView.swift,
// referenced by MobRootView) compiles without editing this template.
{
const glob_io = b.graph.io;
var mob_ios = b.build_root.handle.openDir(glob_io, b.fmt("{s}/ios", .{mob_dir}), .{ .iterate = true }) catch @panic("mob ios dir not found");
defer mob_ios.close(glob_io);
var swift_entries = mob_ios.iterate();
while (swift_entries.next(glob_io) catch @panic("mob ios dir iterate failed")) |entry| {
if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".swift")) {
swift_run.addFileArg(.{ .cwd_relative = b.fmt("{s}/ios/{s}", .{ mob_dir, entry.name }) });
}
}
}
if (project_swift_sources.len > 0) {
var swift_it = std.mem.splitScalar(u8, project_swift_sources, ',');
while (swift_it.next()) |source| {
if (source.len == 0) continue;
swift_run.addFileArg(.{ .cwd_relative = source });
}
}
// Plugin-contributed Swift sources compile in the same step so the
// -emit-objc-header pass sees everything that needs to bridge to ObjC.
if (plugin_swift_files.len > 0) {
var plugin_swift_it = std.mem.splitScalar(u8, plugin_swift_files, ',');
while (plugin_swift_it.next()) |source| {
if (source.len == 0) continue;
swift_run.addFileArg(.{ .cwd_relative = source });
}
}
swift_run.addArg("-c");
swift_run.addArg("-emit-objc-header");
swift_run.addArg("-emit-objc-header-path");
const swift_header = swift_run.addOutputFileArg("MobApp-Swift.h");
swift_run.addArg("-o");
const swift_obj = swift_run.addOutputFileArg("swift_mob.o");
installAndCollect(b, objects_step, &objs, swift_obj, "swift_mob.o");
// --- Plain C via zig cc ----------------------------------------------------
const c_flags = &[_][]const u8{
"-Os",
"-ffunction-sections",
"-fdata-sections",
"-mios-simulator-version-min=17.0",
};
// driver_tab is driver_tab_ios.zig by default (Phase 6a iter 4);
// .c is supported for projects that prefer hand-editable C dispatch
// tables (`mix mob.regen_driver_tab --format c`). The Zig path
// declares the C-ABI types it needs directly via `extern struct`,
// so it doesn't need any of the iOS SDK include paths the C path
// requires. Auto-detect by extension.
//
// The Zig file imports `build_options` for the sqlite_static
// comptime gate. Sim builds always set sqlite_static = false
// (exqlite loads dynamically as a .so on sim).
// Build the C-path driver_tab compile flags, threading any enabled
// -DMOB_STATIC_<X>_NIF guards through. The Zig path uses b.addOptions()
// instead and consumes the same booleans at comptime.
var driver_tab_flags_buf = std.ArrayList([]const u8).empty;
defer driver_tab_flags_buf.deinit(b.allocator);
for (c_flags) |f| driver_tab_flags_buf.append(b.allocator, f) catch unreachable;
if (mlx_static) driver_tab_flags_buf.append(b.allocator, "-DMOB_STATIC_EMLX_NIF") catch unreachable;
if (nxeigen_static) driver_tab_flags_buf.append(b.allocator, "-DMOB_STATIC_NX_EIGEN_NIF") catch unreachable;
if (tflite_static) driver_tab_flags_buf.append(b.allocator, "-DMOB_STATIC_TFLITE_NIF") catch unreachable;
const driver_tab_flags: []const []const u8 = driver_tab_flags_buf.items;
const driver_tab_lp = if (std.mem.endsWith(u8, driver_tab, ".zig")) blk: {
const opts = b.addOptions();
// Sim builds: exqlite loads dynamically as a .so, so sqlite_static
// is always false here. EMLX has no sim/device distinction — same
// static-link story regardless.
opts.addOption(bool, "sqlite_static", false);
opts.addOption(bool, "emlx_static", mlx_static);
opts.addOption(bool, "nx_eigen_static", nxeigen_static);
opts.addOption(bool, "tflite_static", tflite_static);
break :blk addZigObject(b, .{
.name = "driver_tab_ios",
.source = driver_tab,
.target = target,
.optimize = optimize,
.build_options = opts,
});
} else addCObject(b, .{
.name = "driver_tab_ios",
.source = driver_tab,
.target = target,
.optimize = optimize,
.c_flags = driver_tab_flags,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
});
installAndCollect(b, objects_step, &objs, driver_tab_lp, "driver_tab_ios.o");
const enif_lp = addCObject(b, .{
.name = "enif_keepalive",
.source = enif_keepalive,
.target = target,
.optimize = optimize,
.c_flags = c_flags,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
});
installAndCollect(b, objects_step, &objs, enif_lp, "enif_keepalive.o");
// --- ObjC via xcrun cc -----------------------------------------------------
const objc_specs = [_]ObjcSpec{
.{ .name = "MobNode", .source = b.fmt("{s}/ios/MobNode.m", .{mob_dir}) },
.{
.name = "mob_nif",
.source = b.fmt("{s}/ios/mob_nif.m", .{mob_dir}),
// -DSTATIC_ERLANG_NIF prevents fall-through to dlopen (which iOS
// rejects). swift_header dependency makes Swift run first so
// MobApp-Swift.h is available on the include path.
.extra_flags = &.{"-DSTATIC_ERLANG_NIF"},
.swift_header = swift_header,
},
.{ .name = "mob_beam", .source = b.fmt("{s}/ios/mob_beam.m", .{mob_dir}) },
.{
.name = "AppDelegate",
.source = b.fmt("{s}/AppDelegate.m", .{project_ios_dir}),
.swift_header = swift_header,
},
.{ .name = "beam_main", .source = b.fmt("{s}/beam_main.m", .{project_ios_dir}) },
};
for (objc_specs) |spec| {
const obj_lp = addObjcObject(b, .{
.name = spec.name,
.source = spec.source,
.extra_flags = spec.extra_flags,
.swift_header = spec.swift_header,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
});
installAndCollect(b, objects_step, &objs, obj_lp, b.fmt("{s}.o", .{spec.name}));
}
// --- Project-side C NIFs (auto-wired from mob.exs :static_nifs) ───────────
// Same logic as build_device.zig. See that file for the full rationale.
if (project_c_nifs.len > 0) {
var c_it = std.mem.splitScalar(u8, project_c_nifs, ',');
while (c_it.next()) |nif_name| {
if (nif_name.len == 0) continue;
const flags = b.allocator.alloc([]const u8, c_flags.len + 2) catch unreachable;
@memcpy(flags[0..c_flags.len], c_flags);
flags[c_flags.len] = "-DSTATIC_ERLANG_NIF";
flags[c_flags.len + 1] = b.fmt("-DSTATIC_ERLANG_NIF_LIBNAME={s}", .{nif_name});
installAndCollect(b, objects_step, &objs, addCObject(b, .{
.name = nif_name,
.source = b.fmt("{s}/c_src/{s}.c", .{ project_root, nif_name }),
.target = target,
.optimize = optimize,
.c_flags = flags,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
}), b.fmt("{s}.o", .{nif_name}));
}
}
// --- Plugin-side C NIFs (tier-1 plugins; absolute paths from mob_dev) ─────
// Mirrors the project_c_nifs block above and the Android build.zig
// plugin_c_nifs path. basename minus ".c" = the NIF module name = the
// STATIC_ERLANG_NIF_LIBNAME, which must match the <module>_nif_init symbol
// the generated driver_tab_ios references (populated by resolved_nifs/0).
if (plugin_c_nifs.len > 0) {
var p_it = std.mem.splitScalar(u8, plugin_c_nifs, ',');
while (p_it.next()) |path| {
if (path.len == 0) continue;
const base = std.fs.path.basename(path);
// .m sources (manifest lang: :objc) are Objective-C — an iOS plugin
// NIF driving an Apple framework (CoreLocation, etc.). Compiled as
// ObjC by extension; add -fobjc-arc + -fmodules. ".c"/".m" are both 2.
const is_objc = std.mem.endsWith(u8, base, ".m");
const name = if (std.mem.endsWith(u8, base, ".c") or is_objc) base[0 .. base.len - 2] else base;
if (is_objc) {
// ObjC plugin NIFs that import heavy framework modules (UIKit,
// Accelerate/vImage — e.g. mob_camera) must be built by Apple's
// clang via xcrun, NOT zig's bundled clang: zig clang fails to
// build those system framework modules against current Xcode SDKs
// ("umbrella header for module 'Accelerate.vecLib' does not include
// 'lapack.h'", UIKit missing 'UIUtilities/UIDefines.h'). This is
// exactly why core's own mob_nif.m goes through addObjcObject. The
// STATIC_ERLANG_NIF defines ride along as extra_flags.
installAndCollect(b, objects_step, &objs, addObjcObject(b, .{
.name = name,
.source = path,
.extra_flags = &.{
"-DSTATIC_ERLANG_NIF",
b.fmt("-DSTATIC_ERLANG_NIF_LIBNAME={s}", .{name}),
},
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
}), b.fmt("{s}.o", .{name}));
continue;
}
const flags = b.allocator.alloc([]const u8, c_flags.len + 2) catch unreachable;
@memcpy(flags[0..c_flags.len], c_flags);
flags[c_flags.len] = "-DSTATIC_ERLANG_NIF";
flags[c_flags.len + 1] = b.fmt("-DSTATIC_ERLANG_NIF_LIBNAME={s}", .{name});
installAndCollect(b, objects_step, &objs, addCObject(b, .{
.name = name,
.source = path,
.target = target,
.optimize = optimize,
.c_flags = flags,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
}), b.fmt("{s}.o", .{name}));
}
}
// --- Link via xcrun swiftc -------------------------------------------------
addLink(b, binary_step, .{
.module_name = module_name,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.mlx_static = mlx_static,
.mlx_dir = mlx_dir,
.nxeigen_static = nxeigen_static,
.nxeigen_dir = nxeigen_dir,
.project_rust_libs = project_rust_libs,
.plugin_static_libs = plugin_static_libs,
.plugin_frameworks = plugin_frameworks,
.objects = objs.items,
});
}
const ObjcSpec = struct {
name: []const u8,
source: []const u8,
extra_flags: []const []const u8 = &.{},
swift_header: ?std.Build.LazyPath = null,
};
const CObjectOptions = struct {
name: []const u8,
source: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
c_flags: []const []const u8,
mob_dir: []const u8,
otp_root: []const u8,
erts_vsn: []const u8,
sdkroot: []const u8,
};
const ZigObjectOptions = struct {
name: []const u8,
source: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
// build_options is imported by the .zig module via
// `@import("build_options")`. driver_tab_ios.zig uses it to read
// `sqlite_static`. nil means no module is registered (driver_tab.zig
// imports it unconditionally so callers should pass an Options step
// — sim builds set sqlite_static=false, device builds derive from
// the real flag).
build_options: ?*std.Build.Step.Options = null,
};
// addZigObject compiles a single .zig source file into a relocatable
// object whose exports use the C ABI. Used by Phase 6a's driver_tab.zig
// path; matches addCObject's interface so the call site can pick one or
// the other based on the source file's extension.
fn addZigObject(b: *std.Build, opts: ZigObjectOptions) std.Build.LazyPath {
const mod = b.createModule(.{
.root_source_file = .{ .cwd_relative = opts.source },
.target = opts.target,
.optimize = opts.optimize,
});
if (opts.build_options) |build_opts| {
mod.addOptions("build_options", build_opts);
}
const obj = b.addObject(.{
.name = opts.name,
.root_module = mod,
});
return obj.getEmittedBin();
}
fn addCObject(b: *std.Build, opts: CObjectOptions) std.Build.LazyPath {
const mod = b.createModule(.{
.target = opts.target,
.optimize = opts.optimize,
});
mod.addCSourceFile(.{
.file = .{ .cwd_relative = opts.source },
.flags = opts.c_flags,
});
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/{s}/include", .{ opts.otp_root, opts.erts_vsn }),
});
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/{s}/include/aarch64-apple-iossimulator", .{ opts.otp_root, opts.erts_vsn }),
});
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/ios", .{opts.mob_dir}),
});
mod.addSystemIncludePath(.{
.cwd_relative = b.fmt("{s}/usr/include", .{opts.sdkroot}),
});
mod.addFrameworkPath(.{
.cwd_relative = b.fmt("{s}/System/Library/Frameworks", .{opts.sdkroot}),
});
const obj = b.addObject(.{
.name = opts.name,
.root_module = mod,
});
return obj.getEmittedBin();
}
const ObjcObjectOptions = struct {
name: []const u8,
source: []const u8,
extra_flags: []const []const u8 = &.{},
swift_header: ?std.Build.LazyPath = null,
mob_dir: []const u8,
otp_root: []const u8,
erts_vsn: []const u8,
sdkroot: []const u8,
};
fn addObjcObject(b: *std.Build, opts: ObjcObjectOptions) std.Build.LazyPath {
const run = b.addSystemCommand(&.{
"xcrun",
"-sdk",
"iphonesimulator",
"cc",
"-arch",
"arm64",
"-mios-simulator-version-min=17.0",
"-Os",
"-ffunction-sections",
"-fdata-sections",
"-fobjc-arc",
"-fmodules",
});
run.addArg(b.fmt("-I{s}/{s}/include", .{ opts.otp_root, opts.erts_vsn }));
run.addArg(b.fmt("-I{s}/{s}/include/aarch64-apple-iossimulator", .{ opts.otp_root, opts.erts_vsn }));
run.addArg(b.fmt("-I{s}/ios", .{opts.mob_dir}));
run.addArg(b.fmt("-isysroot{s}", .{opts.sdkroot}));
if (opts.swift_header) |sh| {
// addPrefixedDirectoryArg both passes -I<dir> as one arg AND wires
// the build-graph dependency so the swift step runs before this objc
// step.
run.addPrefixedDirectoryArg("-I", sh.dirname());
}
for (opts.extra_flags) |flag| run.addArg(flag);
run.addArg("-c");
run.addFileArg(.{ .cwd_relative = opts.source });
run.addArg("-o");
return run.addOutputFileArg(b.fmt("{s}.o", .{opts.name}));
}
fn installAndCollect(
b: *std.Build,
step: *std.Build.Step,
objs: *std.ArrayList(std.Build.LazyPath),
lp: std.Build.LazyPath,
install_name: []const u8,
) void {
const install = b.addInstallFile(lp, install_name);
step.dependOn(&install.step);
objs.append(b.allocator, lp) catch @panic("OOM");
}
const LinkOptions = struct {
module_name: []const u8,
otp_root: []const u8,
erts_vsn: []const u8,
// MLX bundle. Empty mlx_dir / false mlx_static = MLX not in this build.
mlx_static: bool = false,
mlx_dir: []const u8 = "",
// NxEigen. Empty nxeigen_dir / false nxeigen_static = NxEigen not in this build.
nxeigen_static: bool = false,
nxeigen_dir: []const u8 = "",
// Comma-separated absolute paths to project-side Rust NIF .a files
// (pre-built by mob_dev with `cargo rustc --target
// aarch64-apple-ios-sim --crate-type staticlib`). Empty if none.
project_rust_libs: []const u8,
// Plugin cpp_archive `.a` archives (comma-separated abs paths). Same link
// shape as project_rust_libs. Empty if no cpp_archive plugin is active.
plugin_static_libs: []const u8 = "",
// Plugin-contributed extra iOS frameworks (comma-separated).
plugin_frameworks: []const u8 = "",
objects: []const std.Build.LazyPath,
};
fn addLink(b: *std.Build, step: *std.Build.Step, opts: LinkOptions) void {
const run = b.addSystemCommand(&.{
"xcrun",
"-sdk",
"iphonesimulator",
"swiftc",
"-target",
"arm64-apple-ios17.0-simulator",
});
// All compiled object files. addFileArg propagates the dependency so
// each .o's compile step is implicitly required by the link step.
for (opts.objects) |obj| run.addFileArg(obj);
// OTP + crypto static libs. Order matters — libbeam.a first so its
// erts_static_nif_tab[] override resolves against driver_tab_ios.o
// before the BEAM's own empty default. liberts_internal_r and libethread
// satisfy BEAM internals; libcrypto.a ships OpenSSL; libzstd / libepcre /
// libryu / asn1rt_nif / crypto.a are statically-linked NIF dependencies.
const lib_names = [_][]const u8{
"lib/libbeam.a",
"lib/internal/liberts_internal_r.a",
"lib/internal/libethread.a",
"lib/libzstd.a",
"lib/libepcre.a",
"lib/libryu.a",
"lib/asn1rt_nif.a",
"lib/crypto.a",
"lib/libcrypto.a",
};
for (lib_names) |name| {
run.addArg(b.fmt("{s}/{s}/{s}", .{ opts.otp_root, opts.erts_vsn, name }));
}
// MLX + EMLX (Apple's MLX numerics + the EMLX Nx backend), wired in
// when `mix mob.enable mlx` set mlx_static=true. libemlx.a holds the
// NIF (emlx_nif_nif_init); libmlx.a is the underlying MLX library.
if (opts.mlx_static and opts.mlx_dir.len > 0) {
run.addArg(b.fmt("{s}/lib/libemlx.a", .{opts.mlx_dir}));
run.addArg(b.fmt("{s}/lib/libmlx.a", .{opts.mlx_dir}));
}
// NxEigen (Eigen-backed Nx backend, C++ NIF). Wired in when
// `mix mob.enable nxeigen` set nxeigen_static=true and mob_dev passed
// the per-arch build dir through nxeigen_dir. libnx_eigen.a holds
// nx_eigen_nif_init (the static-NIF symbol); no separate runtime
// library (Eigen is header-only).
if (opts.nxeigen_static and opts.nxeigen_dir.len > 0) {
run.addArg(b.fmt("{s}/libnx_eigen.a", .{opts.nxeigen_dir}));
}
// Project-side Rust NIF static archives (auto-wired from mob.exs
// :static_nifs entries that have a native/<name>/Cargo.toml). mob_dev
// cross-compiles each with `cargo rustc --target aarch64-apple-ios-sim
// --crate-type staticlib` before invoking zig.
if (opts.project_rust_libs.len > 0) {
var rust_it = std.mem.splitScalar(u8, opts.project_rust_libs, ',');
while (rust_it.next()) |lib_path| {
if (lib_path.len == 0) continue;
// `addFileArg` (not `addArg`) so the cache key picks up the
// contents of the .a, not just its path. Otherwise rebuilding
// a project-side rust crate produces a new .a that the link
// step ignores as UP-TO-DATE.
const lp: std.Build.LazyPath = .{ .cwd_relative = lib_path };
run.addFileArg(lp);
}
}
// Plugin cpp_archive NIFs (e.g. an Nx CPU backend). Each lib<mod>.a exports
// <mod>_nif_init, referenced by driver_tab_ios, so the linker pulls it in —
// same mechanism as project_rust_libs / libnx_eigen.a above. addFileArg so
// the cache key tracks archive contents.
if (opts.plugin_static_libs.len > 0) {
var ps_it = std.mem.splitScalar(u8, opts.plugin_static_libs, ',');
while (ps_it.next()) |lib_path| {
if (lib_path.len == 0) continue;
const lp: std.Build.LazyPath = .{ .cwd_relative = lib_path };
run.addFileArg(lp);
}
}
run.addArgs(&.{ "-lz", "-lc++", "-lpthread" });
// -dead_strip + the per-function/data sections compile flags drop every
// function and data object that no live symbol references — the C-side
// analog of beam_lib:strip_release/1. Single biggest C-side win on
// bundle size per the GRiSP nano writeup.
run.addArgs(&.{ "-Xlinker", "-dead_strip" });
const frameworks_base = [_][]const u8{
"UIKit",
"Foundation",
"CoreGraphics",
"QuartzCore",
"SwiftUI",
};
for (frameworks_base) |fw| {
run.addArgs(&.{ "-Xlinker", "-framework", "-Xlinker", fw });
}
// Plugin-contributed frameworks (gathered by mob_dev from activated
// manifests). Same shape as the base list — duplicates against the base
// are harmless; ld dedups -framework args.
if (opts.plugin_frameworks.len > 0) {
var fw_it = std.mem.splitScalar(u8, opts.plugin_frameworks, ',');
while (fw_it.next()) |fw| {
if (fw.len == 0) continue;
run.addArgs(&.{ "-Xlinker", "-framework", "-Xlinker", fw });
}
}
// Apple's Accelerate framework provides vectorized BLAS/LAPACK used by
// MLX-CPU. Available on iOS sim + device — only linked when MLX is on.
if (opts.mlx_static) {
run.addArgs(&.{ "-Xlinker", "-framework", "-Xlinker", "Accelerate" });
}
run.addArg("-o");
const binary = run.addOutputFileArg(opts.module_name);
const install = b.addInstallFile(binary, opts.module_name);
step.dependOn(&install.step);
}
fn required(b: *std.Build, name: []const u8, desc: []const u8) []const u8 {
return b.option([]const u8, name, desc) orelse {
std.debug.print(
"ERROR: -D{s}=... is required ({s})\n",
.{ name, desc },
);
std.process.exit(1);
};
}