Skip to content

Packaging Applications

Packaging means preparing your program so another person can download it, install it, and run it.

Packaging means preparing your program so another person can download it, install it, and run it.

Building creates the program. Packaging prepares the program for distribution.

For a small Zig command-line tool, packaging may be as simple as:

zig build -Doptimize=ReleaseFast --prefix dist

This creates an output directory such as:

dist/
  bin/
    myapp

You can then compress dist/ and publish it.

What Should Go Into a Package

A package should include everything the user needs.

For a simple command-line application, that usually means:

myapp
README.md
LICENSE

For a larger application, it may include:

bin/
  myapp
share/
  myapp/
    config.toml
    templates/
    assets/
README.md
LICENSE

The binary alone may not be enough if your program needs data files, default configuration, documentation, or runtime assets.

Build a Release Binary

Most packages should use a release mode.

For speed:

zig build -Doptimize=ReleaseFast

For smaller size:

zig build -Doptimize=ReleaseSmall

For safer optimized builds:

zig build -Doptimize=ReleaseSafe

A practical default for many beginner projects is:

zig build -Doptimize=ReleaseSafe --prefix dist

This gives an optimized binary while keeping safety checks.

Package for a Specific Target

A release package should usually name its target.

For Linux x86_64:

zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast --prefix dist/linux-x86_64

For Linux ARM64:

zig build -Dtarget=aarch64-linux -Doptimize=ReleaseFast --prefix dist/linux-aarch64

For Windows x86_64:

zig build -Dtarget=x86_64-windows -Doptimize=ReleaseFast --prefix dist/windows-x86_64

For macOS ARM64:

zig build -Dtarget=aarch64-macos -Doptimize=ReleaseFast --prefix dist/macos-aarch64

This creates separate output directories for separate platforms.

That avoids overwriting files and makes distribution clearer.

Use Clear Package Names

A package name should tell the user what it contains.

For example:

myapp-0.1.0-linux-x86_64.tar.gz
myapp-0.1.0-linux-aarch64.tar.gz
myapp-0.1.0-windows-x86_64.zip
myapp-0.1.0-macos-aarch64.tar.gz

This name includes:

program name
version
operating system
CPU architecture
archive format

That is better than:

release.zip

A clear package name prevents mistakes.

Add a Version String

It is useful for a program to know its own version.

In build.zig, define a version option:

const version = b.option(
    []const u8,
    "version",
    "Application version string",
) orelse "dev";

Pass it into the program:

const options = b.addOptions();
options.addOption([]const u8, "version", version);

exe.root_module.addOptions("build_options", options);

Then in src/main.zig:

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

pub fn main() void {
    std.debug.print("version: {s}\n", .{build_options.version});
}

Now build with:

zig build -Dversion=0.1.0 -Doptimize=ReleaseFast

The binary can print its version.

Include License and README

A package should include a license file.

Even if the source code is public, the release archive should contain the license. Users should not need to search the repository to know what they are allowed to do.

A package should also include basic usage instructions.

For example:

README.md
LICENSE

At minimum, the README should explain:

what the program does
how to run it
basic examples
where to report issues

A small README is better than none.

Static vs Dynamic Dependencies

Before publishing a package, check whether the binary depends on shared libraries.

On Linux:

ldd dist/linux-x86_64/bin/myapp

On macOS:

otool -L dist/macos-aarch64/bin/myapp

A dynamically linked program may need extra libraries installed on the user’s machine.

A statically linked or mostly self-contained program is usually easier to distribute.

This matters most for Linux releases, because different Linux distributions may have different library versions.

Packaging Data Files

If your program needs data files, install them into the package.

A common layout is:

dist/
  bin/
    myapp
  share/
    myapp/
      default.conf
      templates/
      assets/

Your program then needs a way to find those files.

For simple tools, you may let the user pass a path:

myapp --config share/myapp/default.conf

For installed applications, you may search relative to the executable or use platform-specific data directories.

Do not assume the current working directory is the project root. Once packaged, the program may be run from anywhere.

Archive the Package

After building into dist/, create an archive.

On Linux or macOS:

tar -czf myapp-0.1.0-linux-x86_64.tar.gz -C dist/linux-x86_64 .

For Windows releases, .zip is common:

zip -r myapp-0.1.0-windows-x86_64.zip dist/windows-x86_64

The archive should unpack into a clean directory. Avoid archives that scatter files into the current folder.

Better:

myapp-0.1.0-linux-x86_64/
  bin/
    myapp
  README.md
  LICENSE

Worse:

bin/
README.md
LICENSE

The first form is safer for users.

Add a Package Step

You can add a package step to build.zig.

For example:

const package_step = b.step("package", "Build release package");
package_step.dependOn(b.getInstallStep());

Now this works:

zig build package -Doptimize=ReleaseFast --prefix dist

This simple version only depends on installation. A more advanced package step might run tar, copy license files, or create several target-specific packages.

For beginners, start with a clear install directory. Add archive creation later.

Build Several Release Outputs

A project can build several release directories:

zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast --prefix dist/linux-x86_64
zig build -Dtarget=aarch64-linux -Doptimize=ReleaseFast --prefix dist/linux-aarch64
zig build -Dtarget=x86_64-windows -Doptimize=ReleaseFast --prefix dist/windows-x86_64

Then archive each one separately:

tar -czf myapp-0.1.0-linux-x86_64.tar.gz -C dist/linux-x86_64 .
tar -czf myapp-0.1.0-linux-aarch64.tar.gz -C dist/linux-aarch64 .
zip -r myapp-0.1.0-windows-x86_64.zip dist/windows-x86_64

This is enough for many small open-source tools.

Test the Package

A package should be tested after packaging.

Do not test only the binary in the build cache.

Test the installed output:

./dist/linux-x86_64/bin/myapp --help

Then test the archive:

mkdir /tmp/test-myapp
tar -xzf myapp-0.1.0-linux-x86_64.tar.gz -C /tmp/test-myapp
/tmp/test-myapp/bin/myapp --help

This catches missing data files, bad paths, missing shared libraries, and wrong executable names.

Common Mistakes

A common mistake is packaging from the build cache instead of the install directory.

Use zig-out or your --prefix directory. Do not copy random files from .zig-cache.

Another mistake is forgetting runtime files. If your program needs templates, schemas, config files, or assets, include them.

Another mistake is using one dist/ directory for several targets and overwriting binaries.

Another mistake is publishing a dynamically linked binary without checking its runtime library dependencies.

Another mistake is shipping a binary with no version information. Users and bug reports become harder to manage.

The Important Idea

Packaging turns build output into something another person can use.

A good beginner workflow is:

zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast -Dversion=0.1.0 --prefix dist/linux-x86_64

Then include basic files:

README.md
LICENSE

Then archive the directory with a clear name:

myapp-0.1.0-linux-x86_64.tar.gz

A package should be predictable, complete, and easy to test.