Packages
mob_new
0.3.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_device.zig.eex
// build_device.zig — Native build orchestration for <%= display_name %> (iOS device).
//
// Phase 2 iter 12 of the build-system migration. Sister to ios/build.zig
// (which targets iOS sim). Handles the seven standard native sources for
// the iphoneos build:
//
// Plain C (zig cc):
// - driver_tab_ios.zig (or .c — extension auto-detected at compile)
// - enif_keepalive.c
//
// ObjC (xcrun cc via system command — Apple's clang for -fmodules):
// - $MOB_DIR/ios/MobNode.m
// - $MOB_DIR/ios/mob_nif.m (-DSTATIC_ERLANG_NIF + swift hdr)
// - $MOB_DIR/ios/mob_beam.m (-DMOB_BUNDLE_OTP, -DERTS_VSN, -DOTP_RELEASE)
// - ios/AppDelegate.m (in-project, 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
//
// Differences vs build.zig (iOS sim):
// - SDK: iphoneos vs iphonesimulator
// - Target: arm64-apple-ios17.0 vs aarch64-ios-simulator
// - mob_beam.m needs -DMOB_BUNDLE_OTP + ERTS_VSN/OTP_RELEASE strings
//
// EPMD compile, erl_errno_id_compat stub, the link, bundle, codesign,
// and devicectl install all stay in build_device.sh for this iter — they
// move into Mix as iter 12b/c.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
});
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 otp_release = required(b, "otp_release", "OTP release number (e.g. 29)");
const sdkroot = required(b, "sdkroot", "iOS device 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 project's ios/ dir");
const module_name = required(b, "module_name", "Swift module name (display_name)");
const sqlite_static = b.option(bool, "sqlite_static", "exqlite NIF is statically linked (-DMOB_STATIC_SQLITE_NIF on driver_tab)") orelse false;
const epmd_build_src = required(b, "epmd_build_src", "OTP tree exposing erts/epmd/src + erts/include (the iOS-device OTP cache by default)");
const errno_compat = required(b, "errno_compat", "Absolute path to the generated erl_errno_id_compat.c shim");
const sqlite_static_lib = b.option([]const u8, "sqlite_static_lib", "Absolute path to sqlite3_nif.a (when exqlite is statically linked)") orelse "";
// MLX + EMLX (Apple's MLX numerics + the EMLX Nx backend). Enabled by
// `mix mob.enable mlx`. mob_dev fetches a pre-cross-compiled bundle via
// MobDev.MLXDownloader and passes its root here.
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 "";
// Project-side NIFs declared in `mob.exs :static_nifs` (see issue #18).
// mob_dev passes these comma-separated lists after resolving each entry
// against `c_src/<name>.c` and `native/<name>/Cargo.toml`. Empty by
// default — old projects without project-side NIFs are unaffected.
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 "";
const objects_step = b.step("objects", "Compile C, ObjC, and Swift objects for iOS device");
const binary_step = b.step("binary", "Compile + link the iOS device 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 ------------------------------------------------
const swift_run = b.addSystemCommand(&.{
"xcrun",
"-sdk",
"iphoneos",
"swiftc",
"-target",
"arm64-apple-ios17.0",
"-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" });
swift_run.addFileArg(.{ .cwd_relative = b.fmt("{s}/ios/MobViewModel.swift", .{mob_dir}) });
swift_run.addFileArg(.{ .cwd_relative = b.fmt("{s}/ios/MobRootView.swift", .{mob_dir}) });
swift_run.addFileArg(.{ .cwd_relative = b.fmt("{s}/ios/MobGpuView.swift", .{mob_dir}) });
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 });
}
}
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_base = &[_][]const u8{
"-Os",
"-ffunction-sections",
"-fdata-sections",
"-miphoneos-version-min=17.0",
};
// Build the C-path driver_tab compile flags, appending one -D<guard> per
// enabled NIF. The Zig-path driver_tab gets the same set via
// b.addOptions() below.
var driver_tab_flags_buf = std.ArrayList([]const u8).empty;
defer driver_tab_flags_buf.deinit(b.allocator);
for (c_flags_base) |f| driver_tab_flags_buf.append(b.allocator, f) catch unreachable;
if (sqlite_static) driver_tab_flags_buf.append(b.allocator, "-DMOB_STATIC_SQLITE_NIF") 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;
const driver_tab_flags: []const []const u8 = driver_tab_flags_buf.items;
// Auto-detect .zig vs .c on driver_tab. The .zig path threads
// sqlite_static via b.addOptions() — a comptime `if (sqlite_static)`
// inside the .zig file replaces the C preprocessor `#ifdef
// MOB_STATIC_SQLITE_NIF`. The .c path keeps the legacy
// -DMOB_STATIC_SQLITE_NIF flag through driver_tab_flags.
const driver_tab_lp = if (std.mem.endsWith(u8, driver_tab, ".zig")) blk: {
const opts = b.addOptions();
opts.addOption(bool, "sqlite_static", sqlite_static);
opts.addOption(bool, "emlx_static", mlx_static);
opts.addOption(bool, "nx_eigen_static", nxeigen_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");
installAndCollect(b, objects_step, &objs, addCObject(b, .{
.name = "enif_keepalive",
.source = enif_keepalive,
.target = target,
.optimize = optimize,
.c_flags = c_flags_base,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
}), "enif_keepalive.o");
// --- ObjC via xcrun cc -----------------------------------------------------
const mob_beam_flags = &[_][]const u8{
"-DMOB_BUNDLE_OTP",
b.fmt("-DERTS_VSN=\"{s}\"", .{erts_vsn}),
b.fmt("-DOTP_RELEASE=\"{s}\"", .{otp_release}),
};
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}),
.extra_flags = &.{"-DSTATIC_ERLANG_NIF"},
.swift_header = swift_header,
},
.{
.name = "mob_beam",
.source = b.fmt("{s}/ios/mob_beam.m", .{mob_dir}),
.extra_flags = mob_beam_flags,
},
.{
.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| {
installAndCollect(b, objects_step, &objs, 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,
}), b.fmt("{s}.o", .{spec.name}));
}
// --- EPMD (in-process, NO_DAEMON) ──────────────────────────────────────────
// EPMD's main() is renamed to epmd_ios_main() and run on a background
// thread inside the BEAM. -DNO_DAEMON elides the run_daemon() body so
// fork() never gets pulled into the link (iOS sandbox would deny it).
// The epmd.c source is patched separately (idempotent inline edit in
// build_device.sh) to wrap the run_daemon call site in #ifndef NO_DAEMON
// — pre-Phase-2 OTP tarballs ship without that guard.
const epmd_src = b.fmt("{s}/erts/epmd/src", .{epmd_build_src});
const epmd_flags = &[_][]const u8{
"-DHAVE_CONFIG_H",
"-DEPMD_PORT_NO=4369",
"-Dmain=epmd_ios_main",
"-DNO_DAEMON",
"-Os",
"-ffunction-sections",
"-fdata-sections",
"-miphoneos-version-min=17.0",
};
const epmd_specs = [_]CObjectSpec{
.{ .name = "epmd_main", .source = b.fmt("{s}/epmd.c", .{epmd_src}) },
.{ .name = "epmd_srv", .source = b.fmt("{s}/epmd_srv.c", .{epmd_src}) },
.{ .name = "epmd_cli", .source = b.fmt("{s}/epmd_cli.c", .{epmd_src}) },
};
for (epmd_specs) |spec| {
installAndCollect(b, objects_step, &objs, addEpmdObject(b, .{
.name = spec.name,
.source = spec.source,
.target = target,
.optimize = optimize,
.c_flags = epmd_flags,
.epmd_build_src = epmd_build_src,
.epmd_src = epmd_src,
.sdkroot = sdkroot,
}), b.fmt("{s}.o", .{spec.name}));
}
// --- erl_errno_id_compat ──────────────────────────────────────────────────
// Weak shim for erl_errno_id_unknown. The iOS-device OTP tarball
// includes the legacy erl_posix_str.o (sim/Android tarballs don't),
// which the linker pulls in to satisfy _erl_errno_id and which leaves
// _erl_errno_id_unknown undefined. The weak shim resolves it; if a
// future tarball drops erl_posix_str.o this becomes dead code (loses
// to the real symbol). See native_build.ex's
// generate_erl_errno_compat_stub/1 for the full diagnosis.
installAndCollect(b, objects_step, &objs, addCObject(b, .{
.name = "erl_errno_id_compat",
.source = errno_compat,
.target = target,
.optimize = optimize,
.c_flags = c_flags_base,
.mob_dir = mob_dir,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sdkroot = sdkroot,
}), "erl_errno_id_compat.o");
// --- Project-side C NIFs (auto-wired from mob.exs :static_nifs) ───────────
// Each entry name maps to `c_src/<name>.c`. mob_dev passes the
// comma-separated list of names, plus the project root path for
// resolving the sources. -DSTATIC_ERLANG_NIF selects the static-link
// dispatch path in erl_nif.h; -DSTATIC_ERLANG_NIF_LIBNAME=<name>
// overrides the init function's symbol name to `<name>_nif_init`
// (matching driver_tab's declaration). Without the latter, the
// macro tries to mangle the BEAM module name (e.g.
// `Elixir.App.Nifs.Foo`) into a C identifier, which doesn't compile.
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_base.len + 2) catch unreachable;
@memcpy(flags[0..c_flags_base.len], c_flags_base);
flags[c_flags_base.len] = "-DSTATIC_ERLANG_NIF";
flags[c_flags_base.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}));
}
}
// --- Link via xcrun swiftc -------------------------------------------------
addLink(b, binary_step, .{
.module_name = module_name,
.otp_root = otp_root,
.erts_vsn = erts_vsn,
.sqlite_static_lib = sqlite_static_lib,
.mlx_static = mlx_static,
.mlx_dir = mlx_dir,
.nxeigen_static = nxeigen_static,
.nxeigen_dir = nxeigen_dir,
.project_rust_libs = project_rust_libs,
.objects = objs.items,
});
}
const CObjectSpec = struct {
name: []const u8,
source: []const u8,
};
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` so the comptime gate around the guarded NIF row
// fires correctly. nil means "no build_options module needed."
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/internal", .{ 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",
"iphoneos",
"cc",
"-arch",
"arm64",
"-miphoneos-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/internal", .{ 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| {
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}));
}
const EpmdObjectOptions = struct {
name: []const u8,
source: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
c_flags: []const []const u8,
epmd_build_src: []const u8,
epmd_src: []const u8,
sdkroot: []const u8,
};
fn addEpmdObject(b: *std.Build, opts: EpmdObjectOptions) std.Build.LazyPath {
const mod = b.createModule(.{
.target = opts.target,
.optimize = opts.optimize,
});
mod.addCSourceFile(.{
.file = .{ .cwd_relative = opts.source },
.flags = opts.c_flags,
});
// EPMD's includes are scattered across the OTP tree:
// erts/aarch64-apple-ios/ architecture-specific config.h etc.
// erts/epmd/src/ epmd's own headers
// erts/include/ public BEAM headers
// erts/include/internal/ internal BEAM headers
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/erts/aarch64-apple-ios", .{opts.epmd_build_src}),
});
mod.addIncludePath(.{ .cwd_relative = opts.epmd_src });
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/erts/include", .{opts.epmd_build_src}),
});
mod.addIncludePath(.{
.cwd_relative = b.fmt("{s}/erts/include/internal", .{opts.epmd_build_src}),
});
mod.addSystemIncludePath(.{
.cwd_relative = b.fmt("{s}/usr/include", .{opts.sdkroot}),
});
const obj = b.addObject(.{
.name = opts.name,
.root_module = mod,
});
return obj.getEmittedBin();
}
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,
sqlite_static_lib: []const u8,
// MLX bundle (libmlx.a + libemlx.a) — empty mlx_dir means MLX is off.
mlx_static: bool = false,
mlx_dir: []const u8 = "",
// NxEigen. Empty nxeigen_dir / false nxeigen_static = NxEigen is off.
nxeigen_static: bool = false,
nxeigen_dir: []const u8 = "",
// Comma-separated absolute paths to project-side Rust NIF .a files.
// Empty if the project has no Rust NIFs.
project_rust_libs: []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",
"iphoneos",
"swiftc",
"-target",
"arm64-apple-ios17.0",
});
// 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. Same order as the iOS sim link (and the
// prior CMake-based device link). 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.
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 }));
}
// Optional static-link of the exqlite NIF (passed by build_device.sh
// when the project depends on exqlite).
if (opts.sqlite_static_lib.len > 0) {
run.addArg(opts.sqlite_static_lib);
}
// MLX + EMLX static archives, wired in when `mix mob.enable mlx` set
// mlx_static=true and mob_dev passed the cached bundle path through
// mlx_dir. libemlx.a holds the NIF (emlx_nif_nif_init); libmlx.a is
// the upstream Apple 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. Eigen is header-only,
// so there's no second library to link — just libnx_eigen.a.
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
// --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);
}
}
run.addArgs(&.{ "-lz", "-lc++", "-lpthread" });
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 });
}
// MLX-CPU uses Apple's Accelerate framework (vectorized BLAS/LAPACK
// tuned for Apple silicon; available on iOS device + sim). When we
// later ship the Metal variant, add Metal + MetalPerformanceShaders
// here behind a separate option.
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);
};
}