Packaging Cross-Platform Apps
Packaging means preparing your program so other people can download it, install it, run it, and trust what they are running.
Packaging Cross-Platform Apps
Packaging means preparing your program so other people can download it, install it, run it, and trust what they are running.
For Zig programs, packaging often starts with one simple advantage: Zig can build small native binaries for many targets.
A cross-platform release may include files like this:
mytool-linux-x86_64
mytool-linux-aarch64
mytool-macos-x86_64
mytool-macos-aarch64
mytool-windows-x86_64.exe
Each file is built for a different target.
Start with Clear Targets
Do not say “all platforms” unless you test all platforms.
Start with a small release matrix:
x86_64-linux
aarch64-linux
x86_64-macos
aarch64-macos
x86_64-windows
These cover many common desktop, server, and laptop users.
Build commands may look like:
zig build-exe src/main.zig -target x86_64-linux -O ReleaseFast -femit-bin=dist/mytool-linux-x86_64
zig build-exe src/main.zig -target aarch64-linux -O ReleaseFast -femit-bin=dist/mytool-linux-aarch64
zig build-exe src/main.zig -target x86_64-macos -O ReleaseFast -femit-bin=dist/mytool-macos-x86_64
zig build-exe src/main.zig -target aarch64-macos -O ReleaseFast -femit-bin=dist/mytool-macos-aarch64
zig build-exe src/main.zig -target x86_64-windows -O ReleaseFast -femit-bin=dist/mytool-windows-x86_64.exe
This produces one binary per target.
Use Release Modes
Debug builds are for development. Release builds are for users.
Common optimization modes include:
ReleaseFast
ReleaseSmall
ReleaseSafe
ReleaseFast focuses on speed.
ReleaseSmall focuses on smaller binaries.
ReleaseSafe keeps more runtime safety checks while still optimizing.
For command-line tools, ReleaseFast is common. For small utilities, ReleaseSmall can be attractive. For software where safety checks are worth the overhead, consider ReleaseSafe.
Example:
zig build-exe src/main.zig -O ReleaseSmall
Name Files Clearly
A release file should make its target obvious.
Good names:
mytool-0.1.0-linux-x86_64.tar.gz
mytool-0.1.0-linux-aarch64.tar.gz
mytool-0.1.0-macos-aarch64.tar.gz
mytool-0.1.0-windows-x86_64.zip
Poor names:
mytool
release
final
binary
Users should know which file to download without reading source code.
Include More Than the Binary
A package should usually include:
binary
README
LICENSE
CHANGELOG
shell completion files if available
example config if needed
For Linux and macOS, a .tar.gz package is common:
mytool
README.md
LICENSE
CHANGELOG.md
For Windows, a .zip package is common:
mytool.exe
README.md
LICENSE
CHANGELOG.md
The binary alone may run, but a package should help users understand what they downloaded.
Linux Packaging
The simplest Linux release is a tarball:
tar -czf mytool-0.1.0-linux-x86_64.tar.gz -C dist/linux-x86_64 .
A user can extract it:
tar -xzf mytool-0.1.0-linux-x86_64.tar.gz
./mytool
For deeper Linux integration, you may later provide:
.deb
.rpm
AppImage
Arch package
Flatpak
Snap
But do not start there. First make a correct binary release.
Linux packaging complexity often comes from libc and dynamic linking. A binary built for one Linux environment may fail on another if it depends on incompatible shared libraries.
If possible, test your Linux binary on multiple distributions.
macOS Packaging
For command-line tools, macOS can use a tarball:
mytool-0.1.0-macos-aarch64.tar.gz
mytool-0.1.0-macos-x86_64.tar.gz
For wider distribution, you may need to think about:
universal binaries
code signing
notarization
Homebrew formula
app bundles for GUI apps
A universal binary can support both Intel and Apple Silicon Macs in one file.
Conceptually:
zig build-exe src/main.zig -target x86_64-macos -O ReleaseFast -femit-bin=mytool-x86_64
zig build-exe src/main.zig -target aarch64-macos -O ReleaseFast -femit-bin=mytool-aarch64
lipo -create -output mytool mytool-x86_64 mytool-aarch64
For a small developer tool, separate architecture builds are often acceptable. For a polished macOS release, a universal binary is more convenient.
Windows Packaging
For Windows, ship an .exe.
A simple package can be:
mytool-0.1.0-windows-x86_64.zip
mytool.exe
README.md
LICENSE
CHANGELOG.md
Users may run it from PowerShell:
.\mytool.exe
For more formal distribution, you may later provide:
MSI installer
winget package
Scoop manifest
Chocolatey package
signed executable
As with macOS, signing matters when distributing to many users. Unsigned binaries may trigger security warnings.
Checksums
A checksum lets users verify that a downloaded file was not corrupted or replaced.
Common checksum files look like:
mytool-0.1.0-linux-x86_64.tar.gz
mytool-0.1.0-linux-x86_64.tar.gz.sha256
Generate a SHA-256 checksum:
sha256sum mytool-0.1.0-linux-x86_64.tar.gz > mytool-0.1.0-linux-x86_64.tar.gz.sha256
On macOS:
shasum -a 256 mytool-0.1.0-macos-aarch64.tar.gz > mytool-0.1.0-macos-aarch64.tar.gz.sha256
Checksums are simple and useful. They are not a full substitute for signatures, but they are a good baseline.
Version Numbers
Use version numbers consistently.
A simple format is semantic versioning:
0.1.0
0.2.0
1.0.0
1.1.0
Put the version in:
Git tag
binary output
package file name
README
changelog
Your program can expose its version:
const std = @import("std");
pub fn main() !void {
try std.io.getStdOut().writer().print("mytool 0.1.0\n", .{});
}
Later, you can pass version information through the build system instead of hardcoding it.
Build Script Packaging
For a real project, you usually do not want to type long build commands manually.
A build.zig file can define targets, build modes, install steps, and tests.
A package command may eventually do this:
zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast
Then CI can call the same build logic.
The principle is important: put repeatable release logic in scripts. Do not rely on memory.
Continuous Integration
A cross-platform project should build in CI.
A typical CI release job:
check out source
install Zig
run tests
build each target
package artifacts
generate checksums
upload release files
CI helps prevent mistakes such as forgetting one target, shipping a stale binary, or releasing a build that no longer passes tests.
Cross-compilation helps here because one CI runner may build several targets. But some targets still need real runtime testing.
Runtime Testing
A binary that builds may still fail at runtime.
Test at least:
program starts
--help works
--version works
basic command works
error path works
file paths work
non-ASCII paths if relevant
For platform-specific behavior, test on the actual platform.
Examples:
Windows path with spaces
macOS Apple Silicon execution
Linux glibc compatibility
Linux musl build if provided
terminal output behavior
config directory behavior
Packaging without runtime testing is risky.
Directory Layout for Releases
A clean project may use this layout:
project/
src/
main.zig
build.zig
README.md
LICENSE
CHANGELOG.md
dist/
mytool-0.1.0-linux-x86_64/
mytool
README.md
LICENSE
CHANGELOG.md
mytool-0.1.0-windows-x86_64/
mytool.exe
README.md
LICENSE
CHANGELOG.md
Then archive each directory.
For Linux and macOS:
tar -czf mytool-0.1.0-linux-x86_64.tar.gz mytool-0.1.0-linux-x86_64
For Windows:
zip -r mytool-0.1.0-windows-x86_64.zip mytool-0.1.0-windows-x86_64
Avoid Hidden Dependencies
A cross-platform package should not secretly depend on tools or files from your development machine.
Common hidden dependencies:
shared libraries
config files
certificate files
plugins
dynamic assets
relative paths
shell scripts
environment variables
Test your package in a clean environment.
For Linux, a container can help.
For Windows, test on a clean VM if possible.
For macOS, test on a machine that did not build the binary.
Complete Example Release Script
A simple Unix shell script for building several targets:
#!/usr/bin/env sh
set -eu
name="mytool"
version="0.1.0"
rm -rf dist
mkdir -p dist
build_one() {
target="$1"
out="$2"
zig build-exe src/main.zig \
-target "$target" \
-O ReleaseFast \
-femit-bin="dist/$out"
}
build_one x86_64-linux "$name-$version-linux-x86_64"
build_one aarch64-linux "$name-$version-linux-aarch64"
build_one x86_64-macos "$name-$version-macos-x86_64"
build_one aarch64-macos "$name-$version-macos-aarch64"
build_one x86_64-windows "$name-$version-windows-x86_64.exe"
cd dist
for file in *; do
case "$file" in
*.exe)
zip "$file.zip" "$file"
;;
*)
tar -czf "$file.tar.gz" "$file"
;;
esac
done
sha256sum *.tar.gz *.zip > SHA256SUMS
This is not a complete professional release system, but it shows the core idea: build repeatably, name outputs clearly, archive them, and generate checksums.
The Practical View
Packaging cross-platform Zig apps is mostly about discipline.
Choose clear targets. Build release binaries. Name files clearly. Include license and documentation. Generate checksums. Test on real platforms. Keep release commands repeatable.
Zig makes the compilation part unusually convenient. The remaining work is release engineering: naming, packaging, signing, testing, documentation, and trust.