| name | macos-app-bundle |
| description | macOS application bundling with cargo-bundle and entitlements |
| tags | macos, bundling, codesign, notarization, distribution |
macos-app-bundle
Create distributable macOS .app bundles for Rust applications using cargo-bundle, with proper code signing, entitlements, and notarization for Gatekeeper approval.
Quick Reference
# Install cargo-bundle (Zed's fork)
cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy
# Build and bundle
cargo bundle --release
# Sign and notarize
codesign --deep --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Name (TEAM_ID)" \
"target/release/bundle/osx/App.app"
xcrun notarytool submit App.dmg --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$TEAM_ID" --wait
xcrun stapler staple App.dmg
cargo-bundle
Use Zed's fork for GPUI apps - it's battle-tested and actively maintained.
Installation
cargo install cargo-bundle --git https://github.com/zed-industries/cargo-bundle.git --branch zed-deploy
Usage
# Development build
cargo bundle
# Release build
cargo bundle --release
# Specific target (universal binary)
cargo bundle --release --target aarch64-apple-darwin
cargo bundle --release --target x86_64-apple-darwin
Output: target/release/bundle/osx/App Name.app
Bundle Metadata
Configure in Cargo.toml under [package.metadata.bundle]:
[package.metadata.bundle]
name = "Script Kit" # Display name (can include spaces)
identifier = "com.scriptkit.app" # Reverse-DNS bundle ID
icon = ["assets/icon.png", "assets/icon@2x.png", "assets/icon.icns"]
version = "0.1.0" # Must match package version
copyright = "Copyright (c) 2024 Script Kit. All rights reserved."
category = "public.app-category.developer-tools"
short_description = "Automation made simple"
osx_minimum_system_version = "10.15" # Catalina minimum
osx_url_schemes = ["scriptkit"] # Custom URL handler
resources = ["assets/*"] # Bundle these files
osx_info_plist_exts = ["assets/Info.plist.ext"] # Custom Info.plist additions
Common Categories
| Category | Use Case |
|---|---|
public.app-category.developer-tools |
IDEs, terminals, automation |
public.app-category.productivity |
Task managers, note-taking |
public.app-category.utilities |
System tools, menu bar apps |
public.app-category.business |
Enterprise applications |
Info.plist
cargo-bundle generates Contents/Info.plist from metadata. Extend it with osx_info_plist_exts:
assets/Info.plist.ext
<key>LSUIElement</key>
<true/>
<key>LSBackgroundOnly</key>
<false/>
Key Info.plist Fields
| Key | Description | Example |
|---|---|---|
CFBundleIdentifier |
Unique app ID | com.scriptkit.app |
CFBundleName |
Display name | Script Kit |
CFBundleVersion |
Build number | 1 |
CFBundleShortVersionString |
Version string | 1.0.0 |
LSMinimumSystemVersion |
Minimum macOS | 10.15 |
CFBundleURLTypes |
URL schemes | See below |
LSUIElement |
Agent app (no Dock) | true/false |
LSBackgroundOnly |
Background-only | true/false |
URL Scheme Registration
Registered via osx_url_schemes in Cargo.toml. Results in:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.scriptkit.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>scriptkit</string>
</array>
</dict>
</array>
Handle in Rust via gpui::AppContext::on_open_urls().
LSUIElement
Makes app an agent app - runs without Dock icon or menu bar. Perfect for:
- Launcher apps (like Raycast, Alfred)
- Menu bar utilities
- System tray applications
- Background daemons with UI
<!-- assets/Info.plist.ext -->
<key>LSUIElement</key>
<true/>
LSUIElement vs LSBackgroundOnly
| Property | Dock Icon | Menu Bar | Windows |
|---|---|---|---|
| Neither set | Yes | Yes | Yes |
LSUIElement=true |
No | No | Yes |
LSBackgroundOnly=true |
No | No | No |
Script Kit uses LSUIElement=true with LSBackgroundOnly=false - no Dock/menu but can show windows.
Entitlements
Entitlements enable specific capabilities and are required for notarization.
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>
<!-- HARDENED RUNTIME EXCEPTIONS -->
<!-- Required for JIT (bun/Node.js) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Required for generated code execution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Load non-Apple signed libraries -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Child process spawning -->
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<!-- AUTOMATION -->
<key>com.apple.security.automation.apple-events</key>
<true/>
<!-- HARDWARE -->
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<!-- NETWORK -->
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- FILE ACCESS -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
</dict>
</plist>
Entitlements vs Runtime Permissions
| Type | When Applied | User Interaction |
|---|---|---|
| Entitlements | Baked into code signature | None - enables capability |
| Runtime Permissions | First use | User grants in System Preferences |
Example: Accessibility requires BOTH:
- No special entitlement needed
- User grants permission in System Preferences > Security & Privacy > Accessibility
Common Entitlements
| Entitlement | Purpose |
|---|---|
com.apple.security.cs.allow-jit |
JavaScript/JIT compilation |
com.apple.security.cs.allow-unsigned-executable-memory |
Runtime code generation |
com.apple.security.cs.disable-library-validation |
Load third-party dylibs |
com.apple.security.automation.apple-events |
AppleScript/osascript |
com.apple.security.network.client |
Outbound network |
com.apple.security.network.server |
Listen on ports |
com.apple.security.device.camera |
Camera access |
com.apple.security.device.audio-input |
Microphone access |
URL Schemes
Register custom URL handlers to open your app via yourapp:// links.
Configuration
# Cargo.toml
[package.metadata.bundle]
osx_url_schemes = ["scriptkit"]
Handling in Code
// In your GPUI app
cx.on_open_urls(|urls, cx| {
for url in urls {
if url.scheme() == "scriptkit" {
// Handle: scriptkit://action?param=value
handle_url(url, cx);
}
}
});
Testing
open "scriptkit://run?script=hello"
Code Signing
Prerequisites
- Apple Developer Account ($99/year)
- Developer ID Application certificate (for distribution outside App Store)
Find Your Identity
security find-identity -v -p codesigning
Sign the Bundle
# Sign with hardened runtime (required for notarization)
codesign --deep --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Your Name (TEAM_ID)" \
"target/release/bundle/osx/Script Kit.app"
# Verify
codesign -vvv --deep --strict "Script Kit.app"
spctl -a -vvv "Script Kit.app"
Ad-Hoc Signing (Development Only)
codesign --force --deep --sign - "Script Kit.app"
Warning: Ad-hoc signed apps trigger Gatekeeper warnings.
Environment Variables for CI
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAM_ID)"
export APPLE_ID="your@email.com"
export APPLE_APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" # App-specific password
export APPLE_TEAM_ID="XXXXXXXXXX"
Notarization
Apple requires notarization for apps distributed outside the App Store (macOS 10.15+).
Submit for Notarization
# Create DMG first
hdiutil create -volname "Script Kit" \
-srcfolder "target/release/bundle/osx/Script Kit.app" \
-ov -format UDZO \
"ScriptKit.dmg"
# Sign DMG
codesign --force --sign "$APPLE_SIGNING_IDENTITY" "ScriptKit.dmg"
# Submit and wait
xcrun notarytool submit "ScriptKit.dmg" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
# Staple ticket to DMG
xcrun stapler staple "ScriptKit.dmg"
Check Notarization Status
xcrun notarytool history --apple-id "$APPLE_ID" --password "$APPLE_APP_PASSWORD" --team-id "$APPLE_TEAM_ID"
Common Notarization Errors
| Error | Solution |
|---|---|
| "Invalid credentials" | Check APPLE_ID and app-specific password |
| "The signature is invalid" | Ensure entitlements.plist exists and is valid |
| "Hardened runtime not enabled" | Add --options runtime to codesign |
| "Contains unsigned code" | Sign all nested binaries with --deep |
Icon Generation
Required Sizes for .icns
| Size | Filename |
|---|---|
| 16x16 | icon_16x16.png |
| 32x32 | icon_16x16@2x.png, icon_32x32.png |
| 64x64 | icon_32x32@2x.png |
| 128x128 | icon_128x128.png |
| 256x256 | icon_128x128@2x.png, icon_256x256.png |
| 512x512 | icon_256x256@2x.png, icon_512x512.png |
| 1024x1024 | icon_512x512@2x.png |
Generate from 1024x1024 Source
mkdir MyIcon.iconset
sips -z 16 16 icon-1024.png --out MyIcon.iconset/icon_16x16.png
sips -z 32 32 icon-1024.png --out MyIcon.iconset/icon_16x16@2x.png
sips -z 32 32 icon-1024.png --out MyIcon.iconset/icon_32x32.png
sips -z 64 64 icon-1024.png --out MyIcon.iconset/icon_32x32@2x.png
sips -z 128 128 icon-1024.png --out MyIcon.iconset/icon_128x128.png
sips -z 256 256 icon-1024.png --out MyIcon.iconset/icon_128x128@2x.png
sips -z 256 256 icon-1024.png --out MyIcon.iconset/icon_256x256.png
sips -z 512 512 icon-1024.png --out MyIcon.iconset/icon_256x256@2x.png
sips -z 512 512 icon-1024.png --out MyIcon.iconset/icon_512x512.png
cp icon-1024.png MyIcon.iconset/icon_512x512@2x.png
iconutil -c icns MyIcon.iconset
Bundle Structure
Script Kit.app/
├── Contents/
│ ├── Info.plist # App metadata (generated + extensions)
│ ├── MacOS/
│ │ └── script-kit-gpui # Main executable
│ ├── Resources/
│ │ ├── AppIcon.icns # App icon
│ │ ├── assets/ # Bundled resources
│ │ └── ...
│ ├── Frameworks/ # Bundled frameworks (if any)
│ └── _CodeSignature/ # Code signature
Anti-patterns
Don't: Skip Hardened Runtime
# WRONG - won't notarize
codesign --force --sign "$IDENTITY" App.app
# CORRECT
codesign --force --options runtime --sign "$IDENTITY" App.app
Don't: Forget Entitlements
# WRONG - JIT apps will crash
codesign --options runtime --sign "$IDENTITY" App.app
# CORRECT
codesign --options runtime --entitlements entitlements.plist --sign "$IDENTITY" App.app
Don't: Sign Before Building
# WRONG - signature invalidated
codesign ... App.app
cargo bundle --release # Overwrites!
# CORRECT
cargo bundle --release
codesign ... App.app
Don't: Use Overly Broad Entitlements
<!-- WRONG - App Store rejection -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
Only include entitlements you actually need.
Don't: Hardcode Signing Identity
# WRONG - breaks on other machines
codesign --sign "Developer ID Application: John (ABC123)" App.app
# CORRECT - use environment variable
codesign --sign "$APPLE_SIGNING_IDENTITY" App.app
Don't: Forget to Staple
# WRONG - users still see Gatekeeper warning
xcrun notarytool submit App.dmg --wait
# Done!
# CORRECT - staple the ticket
xcrun notarytool submit App.dmg --wait
xcrun stapler staple App.dmg
CI/CD Integration
GitHub Actions Secrets
| Secret | Description |
|---|---|
APPLE_CERTIFICATE_BASE64 |
Base64-encoded .p12 certificate |
APPLE_CERTIFICATE_PASSWORD |
Password for .p12 |
APPLE_ID |
Apple Developer email |
APPLE_APP_PASSWORD |
App-specific password |
APPLE_TEAM_ID |
10-character Team ID |
Workflow Steps
- Build release binary
- Create .app bundle with cargo-bundle
- Import certificate to temporary keychain
- Sign with hardened runtime + entitlements
- Create DMG
- Notarize and staple
- Upload to release
See BUNDLING.md for full GitHub Actions workflow example.