Skip to content

Linux Support

Linux is one of the most natural platforms for Zig. Many Zig programs are built, tested, and deployed on Linux because Linux is common in servers, containers, embedded...

Linux is one of the most natural platforms for Zig. Many Zig programs are built, tested, and deployed on Linux because Linux is common in servers, containers, embedded systems, command-line tools, and systems programming.

For a beginner, the main idea is this: Zig can write ordinary Linux programs using the standard library, and it can also go lower when you need direct access to Linux system calls, files, sockets, terminals, and memory.

A simple Zig program works normally on Linux:

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello from Linux!\n", .{});
}

Build it:

zig build-exe main.zig

Run it:

./main

On Linux, ./main means “run the file named main in the current directory.” Unlike Windows, executable files usually do not need .exe.

Linux Uses Unix-Style Paths

Linux paths use forward slashes:

/home/alice/projects/app/main.zig

The root directory is:

/

A path starting with / is absolute:

/etc/hosts

A path without / at the beginning is relative to the current directory:

src/main.zig

In Zig, you can often avoid hardcoding paths by using std.fs:

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("hello.txt", .{});
    defer file.close();

    var buffer: [128]u8 = undefined;
    const n = try file.read(&buffer);

    std.debug.print("{s}\n", .{buffer[0..n]});
}

This code opens hello.txt from the current working directory.

File Permissions Matter

Linux has executable permissions. A file may exist but still fail to run if it does not have permission to execute.

You may see:

Permission denied

Fix it with:

chmod +x main

Then run:

./main

When Zig builds an executable, it normally creates a runnable file. But permissions still matter when copying files, extracting archives, or deploying binaries.

Linux permissions also affect reading and writing files. A program may fail because the user does not have access to a path.

That is normal. In Zig, these failures appear as errors that your program must handle.

Environment Variables

Linux programs often use environment variables for configuration.

Examples:

HOME=/home/alice
PATH=/usr/local/bin:/usr/bin:/bin
USER=alice

In Zig, you can read environment variables through the standard library:

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const home = try std.process.getEnvVarOwned(allocator, "HOME");
    defer allocator.free(home);

    std.debug.print("HOME = {s}\n", .{home});
}

This allocates memory for the variable value, so the program frees it with defer.

A missing environment variable is not a crash. It is an error case your program can handle.

Standard Input, Output, and Error

Linux command-line programs commonly use three streams:

stdin for input.

stdout for normal output.

stderr for error messages.

Here is a program that writes to stdout:

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("normal output\n", .{});
}

And stderr:

const std = @import("std");

pub fn main() !void {
    const stderr = std.io.getStdErr().writer();
    try stderr.print("error output\n", .{});
}

This distinction is important because Linux users often combine programs with pipes and redirects.

Example:

./tool > output.txt

This sends stdout to output.txt.

./tool 2> errors.txt

This sends stderr to errors.txt.

A good command-line program respects this convention.

Pipes and Redirection

Linux programs are often designed to work together.

Example:

cat names.txt | ./filter | sort > result.txt

Each program does one job. The output of one program becomes the input of the next.

A Zig program can read from stdin:

const std = @import("std");

pub fn main() !void {
    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut().writer();

    var buffer: [1024]u8 = undefined;

    while (true) {
        const n = try stdin.read(&buffer);
        if (n == 0) break;

        try stdout.writeAll(buffer[0..n]);
    }
}

This program copies stdin to stdout. It behaves like a tiny version of cat.

Build it:

zig build-exe copy.zig

Use it:

cat input.txt | ./copy > output.txt

This is a core Linux idea: programs become more useful when they cooperate through streams.

Linux System Calls

At the lowest level, Linux programs interact with the kernel through system calls.

Examples include:

open

read

write

close

mmap

socket

accept

execve

Most beginner Zig programs should use std instead of calling system calls directly.

For example, prefer:

std.fs.cwd().openFile("hello.txt", .{})

over manually calling low-level Linux APIs.

But Zig gives you access to lower-level OS interfaces when needed. That is useful for operating systems work, runtimes, databases, networking tools, and performance-sensitive software.

Detecting Linux at Compile Time

You can check whether the target OS is Linux:

const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    if (builtin.os.tag == .linux) {
        std.debug.print("Target OS: Linux\n", .{});
    } else {
        std.debug.print("Target OS: not Linux\n", .{});
    }
}

This uses target information known at compile time.

You can use it to select Linux-specific code:

if (builtin.os.tag == .linux) {
    // Linux implementation
} else {
    // Other implementation
}

Use this only when the behavior really differs. If the standard library already gives you a portable API, use that first.

Building for Linux

On Linux, the simplest command is:

zig build-exe main.zig

You can also specify a target:

zig build-exe main.zig -target x86_64-linux

For ARM Linux:

zig build-exe main.zig -target aarch64-linux

This is useful when building for servers, Raspberry Pi devices, containers, and embedded boards.

Zig’s cross-compilation support makes it normal to build Linux binaries from other operating systems too.

For example, from macOS or Windows:

zig build-exe main.zig -target x86_64-linux

The result is a Linux executable.

glibc and musl

On Linux, C programs usually depend on a C standard library. The two common choices are glibc and musl.

glibc is common on mainstream Linux distributions such as Ubuntu, Debian, Fedora, and Arch.

musl is common in lightweight environments such as Alpine Linux.

When building Zig programs, the C library choice matters if your program links with libc or C libraries.

You may see targets that include the ABI:

x86_64-linux-gnu
x86_64-linux-musl

gnu usually means glibc.

musl means musl libc.

For many simple Zig programs, you do not need to think about this immediately. But when distributing Linux binaries, it becomes important. A binary built for one libc environment may not run correctly in another environment.

A common practical choice for portable command-line tools is to build with musl when possible, because static linking is often easier.

Static and Dynamic Linking

A dynamically linked program depends on shared libraries at runtime.

A statically linked program includes more of what it needs inside the executable.

Dynamic linking can produce smaller binaries and use system libraries.

Static linking can make deployment simpler.

On Linux, deployment often raises questions like:

Will this binary run on older distributions?

Does the target machine have the required shared libraries?

Do I need glibc or musl?

Should I ship one binary or several?

Zig helps by making cross-target builds easier, but you still need to understand your deployment environment.

Signals

Linux uses signals to notify processes about events.

Common signals include:

SIGINT, often sent by pressing Ctrl+C.

SIGTERM, commonly used to ask a process to exit.

SIGKILL, used to force a process to stop.

Beginners do not need to handle signals immediately. But for long-running programs such as servers, workers, and daemons, signal handling matters.

A production server should usually respond cleanly to termination requests. That means closing files, flushing logs, finishing in-progress work when possible, and releasing resources.

Zig can work with these OS-level mechanisms, but signal programming requires care.

Linux Networking

Linux is a common platform for network servers.

With Zig, you can write TCP clients, TCP servers, HTTP tools, and protocol implementations.

At the portable level, use standard library networking APIs when they fit.

At the lower level, Linux networking uses sockets.

A server usually follows this pattern:

Create a socket.

Bind it to an address and port.

Listen for connections.

Accept a connection.

Read and write bytes.

Close the connection.

Zig is well suited for this kind of code because it makes errors explicit. Network programming has many possible failures: ports may be busy, connections may close, reads may time out, and writes may be partial.

Linux Containers

Many Zig programs are deployed in Linux containers.

A container image often includes only the files needed to run the program.

A small Zig binary can be useful here because it may reduce image size and simplify deployment.

For example, a container might contain:

/app/server
/etc/ssl/certs

and little else.

When using containers, remember that the program still runs on a Linux kernel. File paths, environment variables, signals, stdout, stderr, and process exit codes all matter.

Exit Codes

Linux programs return an exit code.

By convention:

0 means success.

Non-zero means failure.

In Zig, returning an error from main usually causes a failure exit.

Example:

const std = @import("std");

pub fn main() !void {
    return error.SomethingFailed;
}

For command-line tools, exit codes are important because shell scripts use them.

Example:

./tool
echo $?

This prints the exit code of the previous command.

A useful CLI tool should return meaningful success or failure status.

Complete Example

Here is a small Linux-friendly program that reads from stdin and writes to stdout:

const std = @import("std");
const builtin = @import("builtin");

pub fn main() !void {
    const stderr = std.io.getStdErr().writer();

    if (builtin.os.tag != .linux) {
        try stderr.print("warning: this example is written for Linux-style usage\n", .{});
    }

    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut().writer();

    var buffer: [4096]u8 = undefined;

    while (true) {
        const n = try stdin.read(&buffer);
        if (n == 0) break;

        try stdout.writeAll(buffer[0..n]);
    }
}

Build it:

zig build-exe copy.zig

Run it:

echo "hello linux" | ./copy

Output:

hello linux

Use it with files:

./copy < input.txt > output.txt

This example follows normal Linux conventions. It reads from stdin, writes normal data to stdout, writes warnings to stderr, and works with pipes and redirects.

Linux support in Zig is practical because Zig lets you start with portable standard library code, then move down to operating system details when needed. For beginners, the best path is clear: learn std.fs, std.process, stdin, stdout, stderr, paths, permissions, and exit codes first. Then study system calls, sockets, signals, and linking when your programs need them.