cross-compilation

cross-compilation

building for platforms other than the one you're on. zig makes this unusually easy, but there are still patterns to know.

basic cross-compilation

zig can target any platform from any platform:

const target = b.standardTargetOptions(.{});  // from -Dtarget=...

user runs: zig build -Dtarget=aarch64-linux-gnu

that's it for pure zig. c dependencies complicate things.

cpu targeting

bun explicitly sets cpu models per platform:

pub fn getCpuModel(os: OperatingSystem, arch: Arch) ?Target.Query.CpuModel {
    if (os == .linux and arch == .aarch64) {
        return .{ .explicit = &Target.aarch64.cpu.cortex_a35 };
    }
    if (os == .mac and arch == .aarch64) {
        return .{ .explicit = &Target.aarch64.cpu.apple_m1 };
    }
    // x86_64 defaults to haswell for avx2
    return null;
}

and offers a "baseline" mode for maximum compatibility:

if (opts.baseline) {
    // target nehalem (~2008) instead of haswell (~2013)
    return .{ .explicit = &Target.x86_64.cpu.nehalem };
}

baseline builds run on older hardware but miss avx2 optimizations.

glibc version

linux binaries link against glibc. if you build against glibc 2.34, it won't run on systems with glibc 2.17.

pub fn getOSGlibCVersion(os: OperatingSystem) ?Version {
    return switch (os) {
        .linux => .{ .major = 2, .minor = 26, .patch = 0 },
        else => null,
    };
}

bun targets glibc 2.26 (from ~2017) for broad compatibility.

macos universal binaries

ghostty builds for both x86_64 and aarch64, then combines with lipo:

// build for both architectures
const x86_lib = try buildLib(b, deps.retarget(b, x86_64_macos));
const arm_lib = try buildLib(b, deps.retarget(b, aarch64_macos));

// combine into universal binary
const lipo_step = LipoStep.create(b, .{
    .input_a = x86_lib.getEmittedBin(),
    .input_b = arm_lib.getEmittedBin(),
    .out_name = "libghostty.a",
});

the deps.retarget() pattern creates a copy of SharedDeps pointing at a different target.

xcframework for apple platforms

for ios apps, you need an xcframework containing:

  • macos universal (x86_64 + arm64)
  • ios arm64
  • ios simulator (arm64 + x86_64)
const macos = try buildMacOSUniversal(b, deps);
const ios = try buildLib(b, deps.retarget(b, .{ .os_tag = .ios, .cpu_arch = .aarch64 }));
const ios_sim = try buildLib(b, deps.retarget(b, .{ .os_tag = .ios, .abi = .simulator }));

// xcodebuild -create-xcframework ...

minimum os versions

centralize version requirements:

pub fn osVersionMin(os: std.Target.Os.Tag) std.Target.Os.SemVer {
    return switch (os) {
        .macos => .{ .major = 13, .minor = 0, .patch = 0 },
        .ios => .{ .major = 17, .minor = 0, .patch = 0 },
        else => .{ .major = 0, .minor = 0, .patch = 0 },
    };
}

apply when creating targets:

const target = b.resolveTargetQuery(.{
    .os_tag = .macos,
    .os_version_min = Config.osVersionMin(.macos),
});

environment detection

helpful warnings for common mistakes:

fn checkNixShell(exe: *std.Build.Step.Compile, cfg: *const Config) !void {
    std.fs.accessAbsolute("/etc/NIXOS", .{}) catch return;  // not nixos
    if (cfg.env.get("IN_NIX_SHELL") != null) return;  // in nix shell, good

    try exe.step.addError(
        "Building on NixOS outside nix shell. " ++
        "Use: nix develop -c zig build",
        .{},
    );
}

sources: