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: