Packaging for macOS

Package your Tauri v2 application for macOS as an app bundle and DMG disk image, configure Info.plist, entitlements, custom files, and handle Intel and Apple Silicon builds

When you build a Tauri v2 application on a Mac, the output is a folder that looks and behaves like a single file — a .app bundle. macOS treats that folder as an executable application. Tauri wraps the bundle in a DMG disk image by default, which is the format users expect for downloading and installing Mac apps. Before you ship it, complete macOS Code Signing and notarization.

Under the hood, the .app bundle is a directory with a rigid internal structure, a metadata file called Info.plist, and your compiled Rust binary plus all web resources. This page walks through how to produce that bundle, what goes inside it, how to configure it, and what macOS requires before you can distribute it to other people.

How to Build a macOS App Bundle

Tauri creates the bundle when you run tauri build on a Mac. By default, the bundler produces both a .app bundle and a DMG disk image. If you only want the raw .app directory — for testing or for embedding inside your own installer — you can restrict the output to just the app bundle.

npm run tauri build -- --bundles app

Run the command without --bundles app to get the full DMG output as well. The resulting bundle lands in src-tauri/target/release/bundle/macos/. You will see a directory like YourApp.app. Double‑click it to launch your application on the same machine.

All Good:

If you can launch the .app from Finder and it opens without an immediate crash, the core bundle structure is correct.

What’s Inside the .app Bundle

macOS expects a very specific directory layout. Tauri generates it for you, but understanding the pieces helps when you debug missing resources or signing failures.

YourApp.app/
├── Contents/
│   ├── Info.plist            # Application metadata
│   ├── MacOS/
│   │   └── your-app          # Compiled Rust binary
│   ├── Resources/
│   │   ├── icon.icns         # App icon
│   │   └── ...               # Your bundled web assets
│   ├── _CodeSignature/       # Created by codesign tool
│   ├── Frameworks/           # Embedded macOS frameworks
│   ├── PlugIns/              # Plugins if used
│   └── SharedSupport/        # Shared files you add

The MacOS/ directory holds the single executable that starts your app. Resources/ contains the icon and all the web frontend files that Tauri copied from your Vite build. Frameworks/ is empty by default — it is where you put extra .framework or .dylib libraries your Rust code links against. PlugIns/ can hold app extensions. SharedSupport/ is a conventional place for read‑only data shared across all users of the app.

Info.plist is the heart of the bundle’s identity. macOS reads this XML file to learn the app’s name, version, bundle identifier, minimum system version, and which system capabilities it requests (like camera or microphone access). Tauri fills in the essential keys automatically from your tauri.conf.json, but you often need to extend it.

Customizing Info.plist

To add custom keys — for example, to request permission descriptions for the camera and microphone — create a file named Info.plist directly in the src-tauri directory. Tauri merges your file with the generated defaults.

src-tauri/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSCameraUsageDescription</key>
    <string>Camera access is needed for video calls</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>Microphone access is needed for audio input</string>
</dict>
</plist>

Any key you set here ends up in the final YourApp.app/Contents/Info.plist. If you accidentally overwrite a key that Tauri already manages — such as CFBundleVersion or CFBundleShortVersionString — you can break the version information or cause macOS to reject the bundle during notarization.

Do Not Overwrite Tauri-Managed Keys:

Keys like CFBundleExecutable, CFBundleIdentifier, CFBundleVersion, CFBundleShortVersionString, and LSMinimumSystemVersion are controlled by Tauri’s configuration. Adding them manually can create conflicting values that cause the app to fail to launch or notarize.

Localizing Info.plist Strings

A single Info.plist file supports only one language. If your app asks for camera permission in English but the user’s system language is German, macOS will still show the English description. To provide localized permission prompts, you create InfoPlist.strings files inside language‑specific .lproj directories and tell Tauri to bundle them as resources.

Create a folder structure like this in your project:

src-tauri/
├── tauri.conf.json
├── infoplist/
│   ├── de.lproj/
│   │   └── InfoPlist.strings
│   └── fr.lproj/
│       └── InfoPlist.strings

The lproj directories must follow the pattern <language-code>.lproj using a two‑letter BCP 47 language code. The string catalogue files must be named exactly InfoPlist.strings.

Inside each file, map the keys to localized values:

src-tauri/infoplist/de.lproj/InfoPlist.strings
NSCameraUsageDescription = "Kamerazugriff für Videoanrufe erforderlich";
NSMicrophoneUsageDescription = "Mikrofonzugriff für Audioeingabe erforderlich";

Then add these files as resources in your Tauri configuration so they land in the correct place inside the bundle:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": {
      "infoplist/**": "./"
    }
  }
}

Tauri copies the infoplist/ directory tree into the .app/Contents/Resources folder. At runtime macOS picks the appropriate .lproj folder based on the user’s language preferences.

The DMG Disk Image

A DMG (Disk Image) is a mountable file that contains the .app bundle and optionally a shortcut to the Applications folder. Users open the DMG, drag the app icon to Applications, and then eject the disk image. This is the standard macOS distribution format and the one Tauri produces by default when you run tauri build without restricting the bundle types.

The generated DMG is bare‑bones: it shows your app icon and an Applications folder alias. For a polished first impression — with a custom background image, icon positions, and a license agreement — you need to run a post‑build script. Tauri does not include a built‑in DMG customizer for v2, but you can use command‑line tools like create-dmg (Node.js) or dmgbuild (Python) after the build completes.

# Example: customize the DMG with create-dmg after tauri build
npm install -g create-dmg
create-dmg \
  --background background.png \
  --window-size 600 400 \
  --icon-size 100 \
  --app-drop-link 400 200 \
  "YourApp.dmg" \
  "src-tauri/target/release/bundle/macos/YourApp.app"

Even without customization, the DMG produced by Tauri works for distribution as long as the app is correctly signed and notarized.

Entitlements and the App Sandbox

Entitlements are a set of capabilities that your app requests from macOS. They are baked into the app’s signature and enforced by the operating system. The most important entitlement for a Tauri app that will be distributed through the Mac App Store (or that wants to adopt hardening) is the App Sandbox.

Without the sandbox, your app can read and write files anywhere the user has access. With the sandbox, the system restricts file access, network connections, and hardware access to only what you explicitly declare. Entitlements are defined in a separate .plist file and referenced during code signing.

Create an Entitlements.plist file in src-tauri:

src-tauri/Entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>
</plist>

Then point Tauri to this file in the bundle configuration:

src-tauri/tauri.conf.json
{
  "bundle": {
    "macOS": {
      "entitlements": "./Entitlements.plist"
    }
  }
}

When you sign the app for distribution, the entitlements file is embedded into the signature. If you request a sandbox entitlement but the app tries to access a path outside the allowed scope, macOS terminates the process. Entitlements are also required for features like push notifications, iCloud, and USB device access.

Missing Entitlements Cause Silent Failures:

If your app uses the camera or microphone at runtime but the corresponding entitlements are absent, the operation fails with a generic permission error. Always add the appropriate usage description keys to Info.plist alongside the entitlements — the description key tells the user why you need access, and the entitlement tells the system you are allowed to ask.

Setting a Minimum macOS Version

By default, Tauri v2 sets the deployment target to macOS 10.13 (High Sierra). If your app uses APIs that require a newer version — Metal rendering in WebKit, for instance — or if you want to drop support for older systems, you can raise the minimum.

src-tauri/tauri.conf.json
{
  "bundle": {
    "macOS": {
      "minimumSystemVersion": "12.0"
    }
  }
}

This value gets written to the LSMinimumSystemVersion key in Info.plist. macOS will refuse to launch the app on versions below the specified threshold and display an alert to the user.

Embedding macOS Frameworks

If your Rust code links against a system framework like CoreAudio or a third‑party .dylib, you need to embed those libraries inside the bundle so they travel with the app. Tauri can copy them automatically.

src-tauri/tauri.conf.json
{
  "bundle": {
    "macOS": {
      "frameworks": [
        "CoreAudio",
        "./libs/libmycustom.dylib",
        "./frameworks/MyLibrary.framework"
      ]
    }
  }
}

System frameworks that are part of macOS (like CoreAudio) are referenced by name and do not need to be physically copied — the linker already knows where to find them. Custom frameworks and dynamic libraries listed with relative paths are copied into the Frameworks/ directory of the bundle. Ensure the relative paths start from the location of tauri.conf.json.

Adding Arbitrary Files to the Bundle

You may need to place files in specific locations inside the bundle — for example, a provisioning profile for certain entitlements, or documentation that ships inside the app. Use the macOS.files map in tauri.conf.json.

src-tauri/tauri.conf.json
{
  "bundle": {
    "macOS": {
      "files": {
        "embedded.provisionprofile": "./profile-name.provisionprofile",
        "SharedSupport/readme.md": "./docs/readme.md"
      }
    }
  }
}

The keys are destination paths relative to YourApp.app/Contents/. In the example above, profile-name.provisionprofile ends up at Contents/embedded.provisionprofile, and readme.md lands in Contents/SharedSupport/readme.md.

Intel, Apple Silicon, and Universal Binaries

When you run tauri build on a Mac, the resulting binary matches the architecture of the machine you are building on. On an Apple Silicon Mac (M1, M2, M3), you get an arm64 binary. On an Intel Mac, you get x86_64.

If you want to distribute a single bundle that runs natively on both architectures, you need a universal binary. Tauri does not produce universal binaries automatically — the Rust toolchain can cross‑compile, but the bundler does not orchestrate the lipo step for you. The manual process looks like this:

  1. Build for the host architecture as usual.
  2. Build for the other architecture by specifying a target:
    # On an Apple Silicon Mac, build the Intel binary
    rustup target add x86_64-apple-darwin
    npm run tauri build -- --target x86_64-apple-darwin --bundles app
    
  3. Use lipo to combine the two binaries:
    lipo -create \
      target/aarch64-apple-darwin/release/your-app \
      target/x86_64-apple-darwin/release/your-app \
      -output universal-binary
    
  4. Replace the single‑arch binary inside the .app bundle with the universal one and then sign and notarize.

For most independent developers, distributing separate Intel and Apple Silicon builds is simpler. Users with automatic update mechanisms (like the Tauri updater) can fetch the correct architecture automatically.

Code Signing and Notarization

A .app bundle that is not signed with a valid Apple Developer ID will not open on another person’s Mac unless they manually override Gatekeeper through System Preferences. For any kind of public distribution, signing is mandatory. Notarization — Apple’s automated malware scan — is required for apps distributed outside the Mac App Store.

Tauri’s build process can sign the bundle automatically if you configure signing identities in tauri.conf.json or supply environment variables. The details are covered in the macOS Code Signing section. For packaging purposes, know that:

  • You need an Apple Developer Program membership ($99/year).
  • The tauri.conf.json bundle > macOS > signingIdentity field or the APPLE_SIGNING_IDENTITY environment variable tells Tauri which certificate to use.
  • Entitlements must be provided and match the capabilities you use.
  • After building, you can notarize the DMG (or the .app inside it) using xcrun notarytool and staple the ticket.

If you skip signing, the .app still works on your own development machine — but it is not distributable.

Platform Requirements for Building

You can only build a macOS .app bundle on a Mac. Cross‑compilation from Linux or Windows is not supported because macOS‑specific tools (codesign, productbuild, and the Apple SDKs) are required.

Your Mac must have:

  • Xcode (install from the App Store) — provides the command‑line tools, SDKs, and the codesign utility.
  • Xcode Command Line Tools — usually included, but you can run xcode-select --install to be sure.
  • Rust and the appropriate targets — if you need to build for the opposite architecture, add the target with rustup target add.
  • Node.js and npm (or your chosen package manager) — the frontend build still runs through Vite.

CI Builds for macOS:

If you use GitHub Actions, the macos-latest runner provides Xcode and all necessary tools. Set up your workflow to run tauri build, sign the output using secrets‑stored certificates, and upload the DMG as an artifact. This is the recommended way to produce distributable builds consistently.

Verifying the Final Bundle

After building, you can inspect what your users will see:

# List the DMG and .app bundle
ls -la src-tauri/target/release/bundle/macos/
# Check the binary architecture
file src-tauri/target/release/bundle/macos/YourApp.app/Contents/MacOS/your-app
# Examine the Info.plist that Tauri generated
plutil -p src-tauri/target/release/bundle/macos/YourApp.app/Contents/Info.plist
# Confirm entitlements (if signed)
codesign -d --entitlements - src-tauri/target/release/bundle/macos/YourApp.app

If the file command shows arm64 or x86_64, the binary matches your expectations. The plutil output lets you verify that your custom Info.plist keys merged correctly. The codesign command reveals the embedded entitlements — useful to catch missing sandbox declarations early.

Gatekeeper Will Reject Unsigned Apps:

Even on your own Mac, if you download the .app from the internet (simulating a user), macOS may quarantine it and refuse to open it unless it is signed. Test distribution flow by archiving the .app, downloading it, and trying to launch it.


A macOS .app bundle is both a directory structure and a set of promises to the operating system — about what the app is named, which versions of macOS it supports, and what it is allowed to do. Getting the packaging right means the difference between a double‑click that launches your app and a cryptic Gatekeeper error.