DMG

DMG is the standard disk image format for distributing macOS applications. AppBundler builds .dmg images with a fully open-source toolchain, so they can be created and signed on any platform without Xcode.

DMG installation

Build pipeline

The standard Apple workflow signs the app bundle with codesign, packs it into a disk image with hdiutil, signs the image, submits it for notarization with notarytool, and attaches the notarization ticket with stapler:

MyApp.app → codesign → hdiutil → codesign → notarytool → stapler

These tools are available only on macOS. AppBundler uses open-source equivalents instead:

MyApp.app → rcodesign → xorriso → dmg → rcodesign notarize

Here dmg comes from libdmg-hfsplus, and rcodesign handles both code signing and notarization. All tools are redistributable and cross-compiled with BinaryBuilder, so DMG images can be built and signed from Linux and Windows as well as macOS.

Package contents

Before creating the image, AppBundler assembles the MyApp.app bundle with the following layout:

PathPurpose
Contents/MacOS/MyAppApplication launcher
Contents/Libraries/mainBinary stub reference
Contents/Info.plistApplication metadata
Contents/Resources/icon.icnsApplication icon
../DS_StoreControls the installer's appearance

Sandboxing and permissions are configured through Entitlements.plist, which is embedded in the launcher's signature during code signing.

Native launcher required

The launcher must be a native binary, since macOS code signing and entitlements cannot be applied to scripts.

Code signing

Apple issues code signing certificates only to members of the Apple Developer Program, which costs €99/year as of September 2026. Distribution outside the Mac App Store requires a Developer ID Application certificate, and only the account holder can create one. A standalone certificate.pfx can be produced on any platform with OpenSSL:

  1. Generate a private key and a certificate signing request:
   openssl genrsa -out developer_id.key 2048
   openssl req -new -key developer_id.key -out developer_id.csr \
       -subj "/emailAddress=you@example.com/CN=Your Name/C=LV"
  1. In the Apple Developer portal, create a new Developer ID Application certificate, upload developer_id.csr, and download the resulting .cer file.

  2. Combine the certificate and key into a password-protected .pfx:

   openssl x509 -inform DER -in developerID_application.cer -out developer_id.pem
   openssl pkcs12 -export -inkey developer_id.key -in developer_id.pem -out dmg/certificate.pfx

Notarization additionally requires an App Store Connect API key, created in App Store Connect under Users and Access → Integrations.

Notarization

macOS codesigning has an additional requirement beyond certificate signing: Apple requires all distributed applications to be notarized. Notarization means submitting the bundle to Apple's servers, where it is checked for proper structure and the absence of malware. Two settings must be enabled in LocalPreferences.toml for a bundle to pass notarization:

dmg.shallow_signing = false
dmg.hardened_runtime = true

Note on shallow vs. deep signing: Deep signing is disabled by default because it takes considerable time for Julia applications and currently tends to fail with rcodesign deep signing. Unfortunately, codesign --verify --deep --verbose=4 myapp.app passes even with shallow signing, so the only reliable way to verify that deep signing is correct is to submit the bundle to Apple's notary service and inspect the response. Budget some time for this when setting up notarization for the first time.

Self-signing

For testing, AppBundler can generate a self-signed certificate at dmg/certificate.pfx:

AppBundler.install_dmg_certificate("dmg/certificate.pfx")

The generated password is printed to stdout. Pass it to the build with --password=mypassword. For a quick local build, use --selfsign instead: it signs with a throwaway certificate and ignores dmg/certificate.pfx.

Apple notarizes only packages signed with a Developer ID certificate, so self-signed builds cannot be notarized. Gatekeeper blocks them by default, and they must be installed as described below.

Installing self-signed packages

Gatekeeper blocks applications that are not signed with a Developer ID certificate and notarized. Users must explicitly allow them, as described in Apple's guide to opening apps from an unknown developer. To automate this, AppBundler provides recipes/dmg/bootstrap.sh. It mounts the DMG, copies the app to /Applications, and removes the quarantine attribute:

bootstrap.sh myapp.dmg

This also enables one-line web installs: a hosted install.sh can download the DMG and bootstrap.sh and run bootstrap.sh myapp.dmg, invoked with e.g.:

curl -fsSL https://example.com/install.sh | sh

API

dmg_config = DMG(project; selfsign = true)

bundle(dmg_config, dmg_archive) do app_stage
    # install files into app_stage
end
AppBundler.DMG — Type
DMG([project]; arch, compress, windowed, kwargs...)

Create a DMG configuration object for macOS application packaging.

When project is provided, configuration files are searched in project, then project/meta, then the built-in recipes directory. Application parameters (APP_NAME, APP_VERSION, etc.) are read from project/Project.toml, and packaging defaults (selfsign, compression, etc.) are read from project/LocalPreferences.toml. Without project, only the built-in recipes and the active project's LocalPreferences.toml are used.

Arguments

  • project: Path to a project directory containing Project.toml, optional LocalPreferences.toml, and optional meta/dmg/ overrides

Keyword Arguments

  • prefix = joinpath(dirname(@__DIR__), "recipes"): Base directory or array of directories to search for configuration files in sequential order
  • icon = get_path(prefix, ["dmg/icon.icns", "dmg/icon.png", "icon.icns"]): Path to application icon (.icns or .png)
  • info_config = get_path(prefix, "dmg/Info.plist"): Path to Info.plist template with app metadata
  • command::Cmd: Command launching the application; defaults to dmg.command preference. Its executable and escaped arguments are exposed to the templates as COMMAND
  • entitlements = get_path(prefix, "dmg/Entitlements.plist"): Path to entitlements file for code signing
  • dsstore = get_path(prefix, ["dmg/DS_Store.toml", "dmg/DS_Store"]): Path to DS_Store file or TOML template for Finder window appearance
  • selfsign: If true, generate a temporary self-signed certificate instead of using pfx_cert; defaults to selfsign preference
  • pfx_cert = get_path(prefix, "dmg/certificate.pfx"): Path to code signing certificate
  • shallow_signing: If true, sign only the top-level bundle rather than all nested binaries; defaults to dmg.shallow_signing preference
  • hardened_runtime: If true, enable hardened runtime during signing (required for notarization); defaults to dmg.hardened_runtime preference
  • sandboxed_runtime: If true, enable the App Sandbox entitlement; defaults to dmg.sandboxed_runtime preference
  • main_launcher: Path to the Julia entry-point script. When set, a native redirect launcher is installed at Contents/MacOS/<app_name> and the script itself at Contents/Libraries/main; resolved from prefix using the bundler predicate; omitted if not found
  • hfsplus = false: If true, use HFS+ filesystem when building the disk image otherwise uses ISO
  • windowed: If true, the application runs without a console window; defaults to windowed preference
  • compress: If true, pack the staging directory into a .dmg disk image; defaults to compress preference
  • compression: Compression algorithm for the disk image (:lzma, :bzip2, :zlib, or :lzfse); defaults to dmg.compression preference
  • arch = Sys.ARCH: Target CPU architecture
  • predicate: Bundler predicate used for hook selection; defaults to bundler preference
  • parameters: Dictionary of parameters for Mustache template rendering. When project is provided, pre-populated from Project.toml and preferences: APP_NAME, APP_DISPLAY_NAME, APP_VERSION, BUILD_NUMBER, APP_SUMMARY, APP_DESCRIPTION, BUNDLE_IDENTIFIER, PUBLISHER_DISPLAY_NAME, MODULE_NAME (Julia-based bundles only), WINDOWED, and SANDBOXED_RUNTIME

Examples

DMG()                                    # default recipes only
DMG(app_dir)                             # project with Project.toml parameters
DMG(app_dir; hardened_runtime = false)   # project with keyword overrides
DMG(; prefix = ["custom/", "recipes/"]) # explicit search path
source