macOS is one of Zig’s main desktop targets. You can use Zig on macOS to write command-line tools, development utilities, servers, libraries, and cross-platform applications.
macOS is one of Zig’s main desktop targets. You can use Zig on macOS to write command-line tools, development utilities, servers, libraries, and cross-platform applications.
For beginners, the main idea is simple: macOS is Unix-like, so many habits from Linux also apply. It uses / paths, terminals, permissions, stdin, stdout, stderr, pipes, and exit codes. But macOS also has its own system libraries, application model, security rules, and CPU architecture details.
A simple Zig program works normally on macOS:
const std = @import("std");
pub fn main() void {
std.debug.print("Hello from macOS!\n", .{});
}Build it:
zig build-exe main.zigRun it:
./mainThe ./ means “run this program from the current directory.”
macOS Is Unix-Like
macOS has a Unix-style command line. Paths look like this:
/Users/alice/projects/app/main.zigThe root directory is:
/The user’s home directory is usually:
/Users/aliceSo file code that works on Linux often looks similar on macOS:
const std = @import("std");
pub fn main() !void {
const file = try std.fs.cwd().openFile("hello.txt", .{});
defer file.close();
var buffer: [256]u8 = undefined;
const n = try file.read(&buffer);
std.debug.print("{s}\n", .{buffer[0..n]});
}This opens hello.txt in the current working directory, reads some bytes, and prints them.
Use std.fs first. It gives you portable file handling across macOS, Linux, and Windows.
Shells on macOS
The default shell on modern macOS is usually zsh.
You will commonly run commands like:
zig build-exe main.zig
./mainYou can also use Bash, Fish, or another shell, but shell syntax can vary.
For Zig programs, this matters mostly when reading command-line arguments. The shell parses quotes before your program receives the arguments.
Example:
./main hello "two words"Your Zig program receives three arguments:
./main
hello
two wordsA simple argument-printing program:
const std = @import("std");
pub fn main() !void {
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
var index: usize = 0;
while (args.next()) |arg| {
std.debug.print("arg[{d}] = {s}\n", .{ index, arg });
index += 1;
}
}Build and run:
zig build-exe main.zig
./main hello "two words"Possible output:
arg[0] = ./main
arg[1] = hello
arg[2] = two wordsmacOS CPU Targets
Modern macOS machines commonly use Apple Silicon CPUs, such as M1, M2, M3, and later. These use the aarch64 architecture.
Older Intel Macs use x86_64.
That gives two common Zig targets:
aarch64-macos
x86_64-macosOn an Apple Silicon Mac, a normal local build usually targets aarch64-macos:
zig build-exe main.zigTo build explicitly for Apple Silicon:
zig build-exe main.zig -target aarch64-macosTo build for Intel Macs:
zig build-exe main.zig -target x86_64-macosThis is useful when distributing binaries. Some users may still have Intel Macs, while newer machines use Apple Silicon.
Universal Binaries
A macOS universal binary contains code for more than one CPU architecture, usually x86_64 and aarch64.
Conceptually, it is one file containing two builds:
mytool
x86_64 code
aarch64 codeZig can build each architecture separately. Then macOS tools such as lipo can combine them.
Example idea:
zig build-exe main.zig -target x86_64-macos -femit-bin=mytool-x86_64
zig build-exe main.zig -target aarch64-macos -femit-bin=mytool-aarch64
lipo -create -output mytool mytool-x86_64 mytool-aarch64Now mytool can run natively on both Intel and Apple Silicon Macs.
For beginner projects, you do not need universal binaries immediately. But for distributing public macOS tools, they are useful.
Detecting macOS at Compile Time
Zig lets you check the target operating system:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() void {
if (builtin.os.tag == .macos) {
std.debug.print("Target OS: macOS\n", .{});
} else {
std.debug.print("Target OS: not macOS\n", .{});
}
}This is a compile-time target check.
Use it when behavior must differ by platform:
if (builtin.os.tag == .macos) {
// macOS-specific implementation
} else {
// other implementation
}But prefer portable standard library APIs when they are enough.
Standard Output and Standard Error
Like Linux, macOS programs use:
stdin for input.
stdout for normal output.
stderr for error output.
Write to stdout:
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("normal output\n", .{});
}Write to stderr:
const std = @import("std");
pub fn main() !void {
const stderr = std.io.getStdErr().writer();
try stderr.print("error output\n", .{});
}This matters because shell users redirect streams:
./tool > output.txt
./tool 2> errors.txtWell-behaved command-line tools keep normal output and error output separate.
Permissions
macOS uses Unix-style permissions.
A file may need execute permission before it can run:
chmod +x mytool
./mytoolYou may also run into macOS security checks when downloading binaries from the internet. macOS may block unsigned or quarantined executables.
For local learning, this usually does not matter. For distributing software, you may need to understand signing, notarization, Gatekeeper, and quarantine attributes.
Those are macOS deployment topics, not core Zig topics. But they affect real applications.
Frameworks and System Libraries
macOS has system frameworks such as:
Foundation
CoreFoundation
AppKit
Security
CoreGraphics
MetalA framework is a bundle of code and resources provided by the operating system.
Command-line Zig programs often do not need these. But GUI programs, graphics programs, system tools, and native macOS integrations may need them.
Zig can link system libraries and frameworks through the build system.
A simplified build idea:
exe.linkFramework("Foundation");For larger macOS programs, you will often combine Zig with C APIs, Objective-C APIs, or system frameworks.
Objective-C and macOS APIs
Many native macOS APIs are written for Objective-C or Swift.
Zig can interoperate with C directly. Objective-C interop is more complex because Objective-C has its own runtime and message-passing model.
For beginners, do not start with AppKit or Cocoa. Start with command-line programs. Then learn C interop. After that, approach Objective-C runtime calls or wrapper libraries if you need native macOS GUI work.
A practical order is:
Learn Zig basics.
Learn Zig’s standard library.
Learn C interop.
Learn macOS frameworks.
Learn app packaging, signing, and notarization.
That order avoids mixing too many hard topics at once.
Dynamic Libraries on macOS
macOS uses dynamic libraries with names like:
libsomething.dylibApplications may also link against frameworks.
When distributing a macOS binary, you must consider where its dynamic libraries come from. A program that runs on your machine may fail on another machine if it depends on a library that is not installed there.
This is similar to Linux dynamic linking, but macOS has its own tooling and rules.
For small command-line tools, prefer minimal dependencies when possible.
Cross-Compiling to macOS
Zig supports targeting macOS, but macOS builds can involve Apple SDK details, system frameworks, and platform rules.
A simple target command looks like:
zig build-exe main.zig -target aarch64-macosor:
zig build-exe main.zig -target x86_64-macosFor plain Zig code with no special platform dependencies, this is straightforward.
For programs that link macOS frameworks or system libraries, cross-compilation can require the correct SDK and build configuration.
The beginner rule is simple: cross-compiling basic Zig code is easy; cross-compiling native macOS apps requires more platform knowledge.
Homebrew and Zig
Many macOS users install developer tools through Homebrew.
You may see commands like:
brew install zigBut for learning a specific Zig version, downloading from Zig’s official site is often clearer. Package managers may lag behind or may install a version different from the one used in this book.
Always check:
zig versionThat tells you which Zig compiler you are actually using.
Complete Example
Here is a small macOS-friendly command-line program:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
if (builtin.os.tag == .macos) {
try stdout.print("Target OS: macOS\n", .{});
} else {
try stderr.print("warning: this program was built for another OS target\n", .{});
}
var args = try std.process.argsWithAllocator(std.heap.page_allocator);
defer args.deinit();
var index: usize = 0;
while (args.next()) |arg| {
try stdout.print("arg[{d}] = {s}\n", .{ index, arg });
index += 1;
}
}Build it:
zig build-exe main.zigRun it:
./main hello "from macOS"Possible output:
Target OS: macOS
arg[0] = ./main
arg[1] = hello
arg[2] = from macOSBuild explicitly for Apple Silicon:
zig build-exe main.zig -target aarch64-macosBuild explicitly for Intel macOS:
zig build-exe main.zig -target x86_64-macosThe Practical View
For ordinary command-line programs, macOS support feels close to Linux support. You use Unix-style paths, terminal commands, permissions, pipes, stdout, stderr, and exit codes.
For native macOS applications, the platform becomes more specific. You need to understand frameworks, Objective-C or Swift APIs, app bundles, signing, notarization, and Apple’s deployment rules.
Start with the portable layer. Use std.fs, std.process, stdin, stdout, stderr, and compile-time target checks. Then learn macOS-specific APIs only when your program needs them.