about things

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: