How to Package Native SDK Apps for Distribution on macOS, Linux, and Windows

TLDR: Native SDK apps are packaged by the native CLI through zig build package -Dpackage-target=<os>, which assembles a ReleaseFast binary, app.zon manifest, and icon assets into a platform-specific bundle under zig-out/package/.

The vercel-labs/native repository provides a Zig-based toolkit for building cross-platform desktop applications with the Native SDK. Packaging these applications for end users is handled by the native CLI, which orchestrates the zig build package command to produce platform-specific distributables. The workflow is implemented primarily in src/tooling/templates.zig and src/tooling/package.zig, where build options are parsed, the executable is compiled, and the final archive is assembled.

Packaging Workflow Overview

The packaging pipeline runs in three phases defined in src/tooling/templates.zig and src/tooling/package.zig.

Select a Package Target

The CLI injects a PackageTarget option into the build graph. In src/tooling/templates.zig, the package_target option is parsed around line 1416 and mapped to a target tag near line 1536.

Build the Executable

zig build package first compiles the app using the standard native build flow. By default, it emits a ReleaseFast binary to zig-out/bin/<app>, then passes that binary as a file argument to the packaging step. This behavior is implemented in src/tooling/templates.zig at lines 1554–1557.

Generate the Package Artifact

The packaging step collects the binary, the app icon, and the app.zon manifest into a folder under zig-out/package/. The directory name follows the pattern:


zig-out/package/<app>-0.1.0-<target>-<opt>-<suffix>

The final suffix is appended by the packageSuffix helper function defined at line 1917 of src/tooling/templates.zig. The resulting folder is then converted into a DMG for macOS, an AppImage for Linux, or an MSI for Windows.

Step-by-Step Packaging Guide

You can generate distributable artifacts for each supported operating system using the same underlying command with different target flags.

Step Command Description
1 native init my-app Scaffold a new project with app.zon, source files, and assets/icon.png.
2 zig build -Doptimize=ReleaseFast Compile the app for the host platform. Output goes to zig-out/bin/.
3 zig build package -Dpackage-target=macos Generate zig-out/package/my-app-0.1.0-macos-ReleaseFast.dmg or a .app bundle.
4 zig build package -Dpackage-target=linux Generate zig-out/package/my-app-0.1.0-linux-ReleaseFast.AppImage.
5 zig build package -Dpackage-target=windows Generate zig-out/package/my-app-0.1.0-windows-ReleaseFast.msi.

All three packaging commands share the same implementation in src/tooling/templates.zig; only the package_target flag changes the platform-specific logic.

Important Build Flags

  • -Dpackage-target=<os> — Sets the distribution platform to macos, linux, or windows.
  • -Doptimize=<mode> — Controls the build mode; ReleaseFast is the default when packaging.
  • --web-engine <engine> — (Optional) Chooses the WebView engine for web-based frontends.
  • --cef-dir <path> — (Optional) Points to a pre-downloaded CEF binary for macOS or Windows.

Packaging Internals in src/tooling/templates.zig

The CLI constructs the packaging step by invoking a system command wired directly into the Zig build graph. As implemented in src/tooling/templates.zig (lines 1554–1567), the build script configures the command like this:

const package = b.addSystemCommand(&.{
    "package",
    @tagName(package_target),
    package_optimize_name,
    b.fmt("zig-out/package/{s}-0.1.0-{s}-{s}{s}",
          .{ app_exe_name, @tagName(package_target), package_optimize_name,
             packageSuffix(package_target) })
});
package.setEnvironmentVariable("NATIVE_SDK_PATH", b.pathFromRoot(native_sdk_path));
package.addFileArg(package_exe.getEmittedBin());
package.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });

This snippet demonstrates how the binary emitted by package_exe.getEmittedBin() is injected into the packaging pipeline alongside metadata flags.

Required app.zon Manifest

Every packaged application must include a valid app.zon manifest that declares its identity, supported platforms, and security policy. A minimal example is defined in src/tooling/templates.zig (around lines 2450–2470):

{
  "id": "dev.native_sdk.my-app",
  "name": "my-app",
  "display_name": "My App",
  "description": "A counter that lives in one native window.",
  "version": "0.1.0",
  "icons": ["assets/icon.png"],
  "platforms": ["macos", "linux", "windows"],
  "permissions": ["view", "command"],
  "capabilities": ["native_views", "gpu_surfaces"],
  "shell": {
    "windows": [{
      "label": "main",
      "title": "My App",
      "width": 480,
      "height": 320,
      "restore_state": false,
      "views": [{ "label": "main-canvas", "kind": "gpu_surface", "fill": true }]
    }]
  },
  "security": { "navigation": { "allowed_origins": ["zero://app", "zero://inline"] } },
  "web_engine": "system",
  "cef": { "dir": "third_party/cef/<os>", "auto_install": false }
}

The platforms array must include the target OS, and the icons entry is required because the CLI automatically converts assets/icon.png into the appropriate platform-specific format (.icns, .ico, etc.).

Customizing the Package Build

For advanced use cases, you can modify the low-level packaging behavior in src/tooling/package.zig, which serves as the entry point for the package command. If you need to embed additional resources, pass extra arguments after package_exe is added:

package.addArgs(&.{ "--extra-file", "extra/config.json" });

The packageSuffix helper at line 1917 of src/tooling/templates.zig determines the final archive name, while the archive creation logic itself resides in src/tooling/package.zig.

Debugging a Package Build

To inspect the exact commands executed during packaging, enable verbose logging:

zig build package -Dpackage-target=windows -Dverbose=true

With verbose mode enabled, the build system prints the generated package command and surfaces any errors from the underlying native_sdk.addApp call.

Summary

  • Use zig build package -Dpackage-target=<os> to create release-ready bundles for macOS, Linux, and Windows.
  • The packaging logic is centralized in src/tooling/templates.zig, with platform-specific assembly handled by src/tooling/package.zig.
  • Ensure your app.zon manifest lists the target platform and includes an icon asset for automatic conversion.
  • Packaged artifacts are written to zig-out/package/ as DMGs for macOS, AppImages for Linux, and MSIs for Windows.

Frequently Asked Questions

What command do I use to package a Native SDK app for macOS?

Run zig build package -Dpackage-target=macos from your project root. According to the vercel-labs/native source code, this invokes the packaging step in src/tooling/templates.zig and outputs a .dmg or .app bundle under zig-out/package/ that includes the ReleaseFast binary and converted icon assets.

Where does the Native SDK store packaged binaries?

The build system emits the compiled binary to zig-out/bin/<app> and then assembles the final distributable folder at zig-out/package/<app>-0.1.0-<target>-<opt>-<suffix>. The exact suffix is generated by the packageSuffix function in src/tooling/templates.zig at line 1917.

Can I bundle additional files with my Native SDK app?

Yes. You can customize the packaging step by editing src/tooling/package.zig or passing extra arguments in your build script, such as package.addArgs(&.{ "--extra-file", "extra/config.json" });. The packaging command defined in src/tooling/templates.zig accepts these arguments and includes them in the platform-specific archive.

Does the Native SDK automatically convert my app icon for each platform?

Yes. As long as your app.zon manifest references an assets/icon.png file, the native CLI handles conversion to the required platform formats—.icns for macOS, .ico for Windows, and PNG/XPM assets for Linux—during the zig build package step.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →