🌐 English | 한국어 | 日本語 | 简体中文 | 繁體中文
Simplify your macOS App deployment
zapp is a CLI and Go library for packaging and distributing macOS applications. Bundle dependencies, create DMG/PKG installers, sign, and notarize with one .zapp.yaml, or run individual commands as needed.
Installation · Quick start · Configuration · Usage · Go library
- Create DMG files
- Create PKG files
- Code signing
- Notarization / Stapling
- Modify plist (version)
- Auto binary dependencies bundling
- Declarative project configuration and Go library API
macOS / Linux
curl -fsSL https://raw.githubusercontent.com/ironpark/zapp/refs/heads/main/scripts/install.sh | shWindows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/ironpark/zapp/refs/heads/main/scripts/install.ps1 | iex"Installs the latest published release after SHA-256 verification. macOS/Linux: ~/.local/bin (add it to your PATH if prompted). Windows: %LOCALAPPDATA%\zapp\bin, added to your user PATH. No administrator access is required.
Set ZAPP_VERSION to a release tag to pin a version, or ZAPP_INSTALL_DIR to an absolute installation directory.
brew tap ironpark/zapp
brew install --cask zappgo install github.com/ironpark/zapp/cmd/zapp@latestSource builds require Go 1.27.0 or later. Linux and Windows (amd64/arm64) include prebuilt signing libraries, so no separate download or Rust build is needed. Enable cgo and install a C compiler: GCC on Linux, LLVM MinGW on Windows. See the platform build instructions.
- uses: ironpark/zapp@v1
with:
certificate: ${{ secrets.CERTIFICATE_P12_BASE64 }}
certificate-password: ${{ secrets.CERTIFICATE_PASSWORD }}
api-key: ${{ secrets.ASC_API_KEY }}Builds the project's .zapp.yaml on macOS, Linux or Windows runners and reports the DMG and PKG paths as outputs. ironpark/zapp/setup@v1 only installs zapp. See the GitHub Action guide for every input.
Start in a directory containing an already-built dist/MyApp.app. Zapp does not compile your application. init creates DMG and PKG sections; build writes both installers to dist by default.
Use one .zapp.yaml for dependency bundling, DMG/PKG packaging, signing, and notarization.
zapp init --app dist/MyApp.app
zapp config show
zapp build # Run configured steps
zapp build dmg pkg # Skip dep; configured signing/notarization still run
zapp build --dry-run # List what build would do, without doing it
zapp verify dist/MyApp.dmg # Check it is signed, stapled and accepted by Gatekeeper
zapp dmg --title "MyApp" # overrides project title
zapp pkg --no-sign --no-notarizeThe generated configuration has this shape. Add dep, sign, or notarize to enable those steps.
version: 1
app: dist/MyApp.app
out: dist
# dep: {libs: [/opt/homebrew/lib]}
dmg: {}
pkg: {}Define shared paths and build steps in .zapp.yaml. See the annotated example for the full set of options.
You can also edit the project in the ggui desktop GUI:
zapp gui
zapp gui --config release/.zapp.yamlTabs cover project, DMG, PKG, dependencies, signing and notarization settings. The DMG tab previews backgrounds and app icons and lets you drag icons into position. Save the configuration, then build with the existing CLI commands. See the GUI guide for shortcuts, JSON fields and desktop requirements. Saving preserves relative paths and variable expressions but rewrites formatting and comments.
dmg, pkg, and dep search for .zapp.yaml from the current directory upward.
zapp dmg # Discover .zapp.yaml
zapp dmg --config release/.zapp.yaml # Choose a configuration file
zapp dmg --no-config --app MyApp.app # Run without a configuration file
zapp config show # Inspect the effective configurationzapp init creates a starter configuration. Overwriting an existing file requires --force.
When an option is set in more than one place, the leftmost source wins:
CLI flags → ZAPP_* environment variables → configuration file → defaults
To derive an environment variable name, uppercase the flag, replace hyphens with underscores, and add ZAPP_.
| CLI flag | Environment variable | Example value |
|---|---|---|
--app |
ZAPP_APP |
dist/MyApp.app |
--title |
ZAPP_TITLE |
MyApp |
--out |
ZAPP_OUT |
dist/MyApp.dmg (for dmg) |
--window-width |
ZAPP_WINDOW_WIDTH |
720 |
--libs |
ZAPP_LIBS |
/usr/local/lib,/opt/homebrew/lib |
| Where a path is set | Relative to |
|---|---|
| Configuration file | The configuration file's directory |
| CLI flag or environment variable | The current working directory |
Top-level out is an output directory; dmg.out and pkg.out are output file paths.
version: 1
app: dist/MyApp.app
out: dist
dmg:
out: dist/MyApp-installer.dmg
pkg:
out: dist/MyApp-installer.pkgThe following variables work in string values and contents keys:
| Variable | Value |
|---|---|
${app} |
App path |
${app.name} |
App name |
${app.version} |
App version |
${env:NAME} |
Environment variable NAME; fails if unset |
dmg:
title: ${app.name}
out: dist/${app.name}-${app.version}.dmgAdding a sign: or notarize: section enables that step. This example uses a macOS keychain profile:
sign:
identity: ${env:ZAPP_IDENTITY}
notarize:
profile: my-profile
staple: true- Skip a step for an individual run with
--no-signor--no-notarize. - Existing
--sign --notarize --profile ... --stapleflags also work. - Supply secrets through
ZAPP_P12_PASSWORD,ZAPP_P12_BASE64(a PKCS#12 certificate as base64, e.g. from a CI secret),ZAPP_PASSWORD, or their CLI flags. Configuration files cannot containsign.p12Passwordornotarize.password. zapp config showomits passwords.- Signing keeps the entitlements the app was built with. Set
sign.entitlements(or--entitlements) to sign it with an entitlements plist instead; see signing.
See signing and notarization for platform-specific certificates and authentication.
zip: archives the app once it is signed, notarized and stapled, before the DMG and PKG are built, so they carry the stapled app too. checksums: writes a SHA256SUMS file for the ZIP, DMG and PKG, and appcast: adds the release to a Sparkle update feed, signed with ZAPP_SPARKLE_KEY. upload: sends the artifacts to HTTP endpoints when the build ends.
zip:
checksums:
upload:
- url: https://releases.example.com/${app.version}/${file.name}
headers:
Authorization: Bearer ${env:RELEASE_TOKEN}
artifacts: [zip, dmg, checksums]An upload with github: {tag: v${app.version}} in place of url adds the artifacts to a GitHub release instead, using GITHUB_TOKEN. Credential headers must come from ${env:…}. Skip uploading for one run with --no-upload. See distributing and uploading.
verify: true stops the build before anything is published unless the app and every artifact are signed with a Developer ID, timestamped, hardened and stapled; zapp verify checks existing files the same way (verifying). homebrew: writes a Homebrew cask for the release and commits it to your tap (Homebrew casks).
PKG configuration forms
| Form | Purpose and fields |
|---|---|
| Short | Package one app with identifier, version, installLocation, scripts, minOS, and license |
| Full | Define multiple components and a distribution, with selectable choices |
Short and full fields cannot be mixed.
- Identifier and version default to the app's Info.plist values.
licenseaccepts a file path or a language map such as{default: license.txt, en: license-en.txt}.type: componentcreates a single component without product installer UI.
See the PKG engine documentation for details.
Migrate a legacy DMG configuration
Flat DMG configurations still work, with a deprecation warning:
zapp dmg --config examples/dmg/layout.yaml --out release.dmgTheir output paths remain relative to the working directory. To migrate to a project configuration, move layout fields under dmg: and keep shared app at the root.
dep, dmg, and pkg accept --sign --notarize --profile "profile" --staple. With signing enabled, Zapp signs the app before packaging and signs the generated installers. Notarization and stapling apply to the resulting artifacts. The keychain examples below are for macOS; see signing and notarization for Linux/Windows certificates and API keys.
Tip
If the --identity flag is not used to select a certificate, Zapp will automatically select an available certificate from the current keychain.
zapp sign --target="path/to/MyApp.app"zapp sign --identity="Developer ID Application" --target="path/to/MyApp.app"A PKCS#12 (.p12) certificate needs no manual keychain import: on macOS Zapp imports it into a temporary keychain for the run and deletes it afterwards. In CI, pass it as base64 through ZAPP_P12_BASE64 (or --p12-base64). See signing and notarization for a GitHub Actions example.
zapp sign --p12-file="developer-id.p12" --p12-password-file="password.txt" --target="path/to/MyApp.app"# CI, e.g. GitHub Actions secrets
ZAPP_P12_BASE64="$CERTIFICATE" ZAPP_P12_PASSWORD="$CERTIFICATE_PASSWORD" \
zapp sign --identity="TEAMID1234" --target="path/to/MyApp.app"Note
When executing the notarize command, if Zapp receives an app bundle path, it automatically compresses the app bundle and attempts to notarize it.
zapp notarize --profile="key-chain-profile" --target="path/to/MyApp.app" --staplezapp notarize --apple-id="your@email.com" --password="pswd" --team-id="XXXXX" --target="path/to/MyApp.app" --stapleNote
This process inspects the dependencies of the application executable, includes the necessary libraries within /Contents/Frameworks and modifies the link paths to enable standalone execution.
zapp dep --app="path/to/target.app"zapp dep --app="path/to/target.app" --libs="/usr/local/lib" --libs="/opt/homebrew/Cellar/ffmpeg/7.0.2/lib"zapp dep --app="path/to/target.app" --sign --notarize --profile "profile" --stapleZapp can be used to create DMG files, a common format used for distributing macOS apps. It greatly simplifies the DMG creation process by automatically extracting icons from the app bundle, compositing disk icons, and providing an interface for drag-and-drop installation of the app.
DMG generation is pure Go, built on the macfs, udif
and lzfse packages, with no hdiutil dependency. The image is
assembled whole rather than created and then mounted to be decorated, so no
volume is ever mounted during a build. Signing and notarization use the existing
backends.
HFS+ remains the default filesystem. Use --fs apfs for APFS, or
--fs apfs-case-sensitive to distinguish names such as App and app.
Both APFS modes require macOS 10.13 or later to open and can be generated on
Linux or macOS. They create a single unencrypted volume; snapshots and existing
image editing are not supported.
FinderInfo and resource forks are preserved from input files. On Linux these
attributes use the user.com.apple.* namespace. The mounted volume always
contains its icon; attaching an icon to the host .dmg file is skipped when
Linux cannot store it because of extended-attribute size or support limits.
zapp dmg --app="MyApp.app" --fs apfs
zapp dmg --app="MyApp.app" --fs apfs-case-sensitive --format ulfoPass --format ulfo to compress with LZFSE instead of zlib, which produces a
smaller image that only macOS 10.11 and later can read. The default, udzo, is
read by every version of macOS when used with HFS+. Selecting APFS still requires
macOS 10.13 regardless of compression.
zapp dmg --app="path/to/target.app"zapp dmg --title="My App" \
--app="path/to/target.app" \
--icon="path/to/icon.icns" \
--bg="path/to/background.png" \
--out="MyApp.dmg"--icon accepts ICNS or PNG. PNG artwork is converted to ICNS with its aspect
ratio and transparency preserved. Omitting the extension in --out MyApp
creates MyApp.dmg; signing and notarization use that same path.
Define files and icon positions in dmg.contents in .zapp.yaml. This example places the app, an Applications link, and a guide in the image.
version: 1
app: dist/MyApp.app
dmg:
title: MyApp
window:
width: 720
height: 460
iconSize: 96
labelSize: 14
contents:
dist/MyApp.app:
pos: [180, 200]
/Applications:
link: true
pos: [540, 200]
docs/README.pdf:
name: Guide.pdf
pos: [360, 350]zapp dmg --config .zapp.yaml --out dist/MyApp.dmgItem fields
Each contents key is a source path. Explicit contents replaces the default app and Applications layout, so include every item you need.
| Field | Description |
|---|---|
pos: [x, y] |
Nonnegative icon-center coordinates from the top-left of the Finder content area |
icon |
Optional PNG/JPEG/ICNS custom icon for copied items; unsupported for links or signed code |
link |
true creates a symbolic link; omit to copy a file or directory |
name |
Name inside the image; defaults to the source name |
File and directory paths are relative to the configuration file. Link targets are preserved literally. The CLI --out path is relative to the working directory.
Move only the default icons
To reposition the app and Applications link, run without contents:
zapp dmg --app MyApp.app \
--app-position 180,200 \
--applications-position 540,200Additional options and constraints
--app-positionand--applications-positioncannot be combined with explicitcontents.- Without
contents,appis required. Withcontents,appsupplies the default title and disk icon. Setdmg.titlewhen omittingapp. - Set
dmg.iconto ICNS or PNG artwork anddmg.backgroundto a background image. Filesystem, compression, signing, and notarization options also work with custom layouts. - Set
version: 1. Unknown fields and duplicate keys are rejected. - Image names must be unique ignoring case and Unicode normalization, and cannot use reserved DMG metadata names.
- If the layout exceeds
.DS_Storecapacity, reduce the item count or shorten names.
See the project example for a complete configuration, or the migration notes for legacy flat layouts.
zapp dmg --app="path/to/target.app" --sign --notarize --profile "profile" --staplePKG generation uses the pure Go macpkg package, with no
pkgbuild or productbuild dependency. Signing and notarization use the existing backends.
Tip
If the --version and --identifier flags are not set, these values will be automatically retrieved from the Info.plist file of the provided app bundle
zapp pkg --app="path/to/target.app"zapp pkg --out="MyApp.pkg" --version="1.2.3" --identifier="com.example.myapp" --app="path/to/target.app"Include End User License Agreement (EULA) files in multiple languages:
zapp pkg --eula=en:eula_en.txt,es:eula_es.txt,fr:eula_fr.txt --app="path/to/target.app"zapp pkg --app="path/to/target.app" --sign --notarize --profile "profile" --staplezapp plist reads and edits a .plist file, or the Info.plist inside a .app
bundle. The file is rewritten in the format it was read in, so a binary
Info.plist stays binary. Editing signed bundle contents requires signing the
app again; make plist changes before signing.
zapp plist get "path/to/target.app" CFBundleVersion
zapp plist set "path/to/target.app" CFBundleVersion 1.2.3
zapp plist delete "path/to/target.app" LSUIElementA key keeps the type it already has, so a boolean stays a boolean:
# writes <false/>, not the string "false", which macOS would read as true
zapp plist set "path/to/target.app" LSUIElement falseA key that does not exist yet becomes a boolean for true or false, an
integer for a whole number, and a string otherwise, so a version like 1.0
stays a string.
Nested values are addressed with a dotted path. A key that itself contains dots, as entitlements keys do, is matched before the name is read as a path:
zapp plist get "path/to/target.app" NSAppTransportSecurity.NSAllowsArbitraryLoads
zapp plist get "entitlements.plist" com.apple.security.app-sandboxzapp plist bump "path/to/target.app" # 1.4.2 -> 1.4.3
zapp plist bump "path/to/target.app" --minor # 1.4.2 -> 1.5.0
zapp plist bump "path/to/target.app" --major # 1.4.2 -> 2.0.0
zapp plist bump "path/to/target.app" --key CFBundleShortVersionStringWithout a flag the last component is raised, which is what a build number
wants. --major, --minor and --patch raise that component and reset the
ones after it.
The following is a complete example showing how to use zapp to dependency bundling, codesign, packaging, notarize, and staple MyApp.app:
# Dependency bundling
zapp dep --app="MyApp.app"
# Codesign / notarize / staple
zapp sign --target="MyApp.app"
zapp notarize --profile="key-chain-profile" --target="MyApp.app" --staple
# Create pkg/dmg file
zapp pkg --app="MyApp.app" --out="MyApp.pkg"
zapp dmg --app="MyApp.app" --out="MyApp.dmg"
# Codesign / notarize / staple for pkg/dmg
zapp sign --target="MyApp.dmg"
zapp sign --target="MyApp.pkg"
zapp notarize --profile="key-chain-profile" --target="MyApp.pkg" --staple
zapp notarize --profile="key-chain-profile" --target="MyApp.dmg" --stapleor just use the shorthand command
zapp dep --app="MyApp.app" --sign --notarize --profile="key-chain-profile" --staple
zapp pkg --out="MyApp.pkg" --app="MyApp.app" \
--sign --notarize --profile="key-chain-profile" --staple
zapp dmg --out="MyApp.dmg" --app="MyApp.app" \
--sign --notarize --profile="key-chain-profile" --stapleImport github.com/ironpark/zapp to load a project and build DMG/PKG installers.
project, err := zapp.Load(".zapp.yaml")
if err != nil {
return err
}
plan, err := project.Resolve()
if err != nil {
return err
}
artifacts, err := plan.Build(ctx, zapp.StepDMG, zapp.StepPKG)
if err != nil {
return err
}You can also construct zapp.Project directly or run individual operations with BuildDMG, BuildPKG, and other methods. Output paths are available in artifacts.DMG and artifacts.PKG.
Zapp is released under the MIT License.
If you encounter any issues or have questions, please file an issue on the GitHub issue tracker.