Packaging
Building, packaging, and distributing desktop applications for Windows, macOS, and Linux
You are an expert in building, packaging, code-signing, and distributing desktop applications across Windows, macOS, and Linux. ## Key Points - **Windows**: NSIS (.exe installer), MSI, MSIX, portable .exe - **macOS**: DMG, PKG, .app bundle - **Linux**: AppImage, .deb, .rpm, Snap, Flatpak - Always code-sign your application. Unsigned apps trigger scary OS warnings and may be blocked entirely. - Use ASAR packaging in Electron to bundle source files into a single archive, improving load time and preventing casual code inspection. - Build platform-specific installers on the target OS when possible. Cross-compilation works but can be fragile. - Use CI/CD (GitHub Actions, etc.) with a build matrix to automate builds for all platforms on every release. - Generate icons from a single high-resolution source (1024x1024 PNG) using automated tools. - Test your packaged app on a clean machine (or VM) to catch missing dependencies. - Include an uninstaller and handle upgrades cleanly — don't leave orphaned files. - Set `"asar": true` but unpack native modules (`.node` files) that need direct filesystem access. - **Missing native dependencies on Linux**: List all required shared libraries in the `depends` field for .deb packages. ## Quick Example ```bash npm install --save-dev electron-builder ``` ```bash npm install --save-dev @electron-forge/cli npx electron-forge import ```
skilldb get desktop-app-skills/PackagingFull skill: 440 linesPackaging & Distribution — Desktop Apps
You are an expert in building, packaging, code-signing, and distributing desktop applications across Windows, macOS, and Linux.
Overview
Packaging transforms a development project into installable artifacts for each operating system. This includes compiling native code, bundling frontend assets, generating platform-specific installers, code-signing for trust and security, and notarizing for macOS Gatekeeper. The two dominant toolchains are electron-builder / electron-forge for Electron apps and Tauri's built-in CLI for Tauri apps.
Installer formats by platform:
- Windows: NSIS (.exe installer), MSI, MSIX, portable .exe
- macOS: DMG, PKG, .app bundle
- Linux: AppImage, .deb, .rpm, Snap, Flatpak
Setup & Configuration
Electron: electron-builder
npm install --save-dev electron-builder
// package.json
{
"scripts": {
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux"
},
"build": {
"appId": "com.yourcompany.yourapp",
"productName": "Your App",
"copyright": "Copyright © 2025 Your Company",
"directories": {
"output": "dist",
"buildResources": "build"
},
"files": [
"src/**/*",
"node_modules/**/*",
"package.json"
],
"extraResources": [
{
"from": "assets/",
"to": "assets/",
"filter": ["**/*"]
}
]
}
}
Electron: electron-forge
npm install --save-dev @electron-forge/cli
npx electron-forge import
// forge.config.js
module.exports = {
packagerConfig: {
asar: true,
icon: './build/icon',
},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: { name: 'your_app' },
},
{
name: '@electron-forge/maker-dmg',
config: { format: 'ULFO' },
},
{
name: '@electron-forge/maker-deb',
config: {
options: {
maintainer: 'Your Name',
homepage: 'https://yourapp.com',
},
},
},
],
};
Tauri build
# Build for current platform
cargo tauri build
# Build specific targets
cargo tauri build --target x86_64-pc-windows-msvc
cargo tauri build --bundles nsis,msi
Core Patterns
electron-builder: Windows configuration
{
"build": {
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64", "arm64"]
}
],
"icon": "build/icon.ico",
"publisherName": "Your Company",
"signingHashAlgorithms": ["sha256"],
"certificateFile": null,
"certificatePassword": null
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"installerIcon": "build/installer-icon.ico",
"uninstallerIcon": "build/uninstaller-icon.ico",
"installerHeaderIcon": "build/installer-icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Your App",
"include": "build/installer.nsh"
}
}
}
electron-builder: macOS configuration
{
"build": {
"mac": {
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
},
{
"target": "zip",
"arch": ["x64", "arm64"]
}
],
"icon": "build/icon.icns",
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"notarize": {
"teamId": "YOUR_TEAM_ID"
}
},
"dmg": {
"contents": [
{ "x": 130, "y": 220 },
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
],
"window": {
"width": 540,
"height": 380
}
}
}
}
macOS entitlements file
<!-- build/entitlements.mac.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.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
electron-builder: Linux configuration
{
"build": {
"linux": {
"target": [
"AppImage",
"deb",
"rpm"
],
"icon": "build/icons",
"category": "Utility",
"maintainer": "your-email@example.com",
"vendor": "Your Company",
"synopsis": "Short description of your app",
"description": "Longer description of your application."
},
"deb": {
"depends": ["libnotify4", "libxtst6", "libnss3"],
"fpm": ["--after-install=build/after-install.sh"]
},
"appImage": {
"artifactName": "${productName}-${version}-${arch}.AppImage"
}
}
}
Code signing: Windows
# Using Azure Trusted Signing (recommended for CI)
# Set environment variables:
export AZURE_TENANT_ID="your-tenant-id"
export AZURE_CLIENT_ID="your-client-id"
export AZURE_CLIENT_SECRET="your-client-secret"
# electron-builder reads CSC_LINK and CSC_KEY_PASSWORD for PFX signing
export CSC_LINK="path/to/certificate.pfx"
export CSC_KEY_PASSWORD="your-password"
npm run build:win
Code signing: macOS
# Requires Apple Developer account and Xcode command-line tools
# electron-builder auto-detects signing identity from Keychain
# Set environment variables for notarization
export APPLE_ID="your-email@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="YOUR_TEAM_ID"
npm run build:mac
Tauri: Platform-specific build configuration
// tauri.conf.json
{
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "http://timestamp.digicert.com",
"wix": null,
"nsis": {
"installerIcon": "icons/icon.ico",
"headerImage": "icons/nsis-header.bmp",
"sidebarImage": "icons/nsis-sidebar.bmp"
}
},
"macOS": {
"minimumSystemVersion": "10.15",
"signingIdentity": null,
"providerShortName": null,
"entitlements": null,
"frameworks": []
},
"linux": {
"deb": {
"depends": []
},
"appimage": {
"bundleMediaFramework": false
}
}
}
}
ASAR packaging (Electron)
{
"build": {
"asar": true,
"asarUnpack": [
"node_modules/sharp/**/*",
"node_modules/sqlite3/**/*",
"**/*.node"
]
}
}
CI/CD: GitHub Actions build matrix
# .github/workflows/build.yml
name: Build Desktop App
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
include:
- os: windows-latest
target: win
- os: macos-latest
target: mac
- os: ubuntu-latest
target: linux
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Build (Windows)
if: matrix.target == 'win'
env:
CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
run: npm run build:win
- name: Build (macOS)
if: matrix.target == 'mac'
env:
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run build:mac
- name: Build (Linux)
if: matrix.target == 'linux'
run: npm run build:linux
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.target }}
path: dist/*
Icon generation
# Required icon sizes
# Windows: icon.ico (256x256, 128x128, 64x64, 48x48, 32x32, 16x16)
# macOS: icon.icns (512x512@2x, 512x512, 256x256, 128x128, 64x64, 32x32, 16x16)
# Linux: PNG files at various sizes (512x512, 256x256, 128x128, etc.)
# Tauri has a built-in icon generator
cargo tauri icon path/to/source-icon-1024x1024.png
# For Electron, use electron-icon-builder
npx electron-icon-builder --input=source-icon.png --output=build
Best Practices
- Always code-sign your application. Unsigned apps trigger scary OS warnings and may be blocked entirely.
- Use ASAR packaging in Electron to bundle source files into a single archive, improving load time and preventing casual code inspection.
- Build platform-specific installers on the target OS when possible. Cross-compilation works but can be fragile.
- Use CI/CD (GitHub Actions, etc.) with a build matrix to automate builds for all platforms on every release.
- Generate icons from a single high-resolution source (1024x1024 PNG) using automated tools.
- Test your packaged app on a clean machine (or VM) to catch missing dependencies.
- Include an uninstaller and handle upgrades cleanly — don't leave orphaned files.
- Set
"asar": truebut unpack native modules (.nodefiles) that need direct filesystem access.
Common Pitfalls
- Missing native dependencies on Linux: List all required shared libraries in the
dependsfield for .deb packages. - macOS notarization failures: Hardened runtime must be enabled, and all binaries (including native modules) must be signed. Use
codesign --verify --deep --strictto check. - Large bundle sizes in Electron: The full Chromium runtime is bundled (~150 MB+). Audit
node_modulesand usefilespatterns to exclude unnecessary files. - Path issues in packaged apps:
__dirnameresolves differently in development vs. production (especially inside ASAR). Useapp.getAppPath()andapp.isPackagedto construct correct paths. - Windows SmartScreen: New signing certificates trigger SmartScreen warnings until they build reputation. EV certificates bypass this immediately.
- Forgetting to test the installer: Always test the full install, run, update, and uninstall cycle on each platform before releasing.
- Architecture mismatches: Build ARM64 variants for Apple Silicon Macs and Windows on ARM. Universal binaries (macOS) cover both architectures in one download.
Core Philosophy
Packaging is the bridge between your development environment and the user's machine. A perfectly working app in development is worthless if it crashes, fails to install, or triggers security warnings on the user's system. Invest in packaging early and automate it completely — the build matrix, code signing, notarization, and installer generation should all happen in CI without human intervention.
Code signing is not optional. On macOS, unsigned apps are blocked by Gatekeeper. On Windows, unsigned installers trigger SmartScreen warnings that tell users the app "might put your PC at risk." Users who see these warnings will not install your app. Budget for signing certificates and integrate them into your CI pipeline from the first release.
Test the full lifecycle on clean machines. Development machines have dependencies, PATH entries, and registry keys that mask packaging bugs. Every release should be tested on a clean VM or container: install, launch, use, update, and uninstall. The packaging process should leave no orphaned files, no broken registry entries, and no confused users.
Anti-Patterns
-
Testing only the development build — running
npm startin the source tree does not exercise the packaging pipeline; always test the packaged installer on a clean machine before releasing. -
Skipping code signing to save time or money — unsigned apps trigger OS security warnings that prevent installation; this is not a cosmetic issue but a functional blocker for most users.
-
Bundling unnecessary files — including
node_modulestest fixtures,.gitdirectories, or development configs in the packaged app bloats the installer and may leak sensitive information. -
Using
__dirnamewithout accounting for ASAR packaging — paths resolve differently inside an ASAR archive than in development; useapp.getAppPath()andapp.isPackagedfor correct path construction. -
Not building for ARM architectures — Apple Silicon Macs and Windows on ARM are increasingly common; shipping only x64 binaries forces users to run under Rosetta or x86 emulation with degraded performance.
Install this skill directly: skilldb add desktop-app-skills
Related Skills
Auto Update
Auto-update mechanisms for desktop applications using electron-updater and Tauri's built-in updater
Electron Ipc
IPC communication patterns for Electron main and renderer process messaging
Electron
Electron fundamentals for building cross-platform desktop applications with web technologies
File System Access
File system access patterns, native dialogs, and drag-and-drop for desktop applications
Native Menus
System tray icons, native application menus, and desktop notifications for Electron and Tauri apps
Security
Desktop application security including context isolation, preload scripts, CSP, and sandboxing for Electron and Tauri