Skip to main content
Technology & EngineeringDesktop App440 lines

Packaging

Building, packaging, and distributing desktop applications for Windows, macOS, and Linux

Quick Summary29 lines
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 lines
Paste into your CLAUDE.md or agent config

Packaging & 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": true but unpack native modules (.node files) that need direct filesystem access.

Common Pitfalls

  • Missing native dependencies on Linux: List all required shared libraries in the depends field for .deb packages.
  • macOS notarization failures: Hardened runtime must be enabled, and all binaries (including native modules) must be signed. Use codesign --verify --deep --strict to check.
  • Large bundle sizes in Electron: The full Chromium runtime is bundled (~150 MB+). Audit node_modules and use files patterns to exclude unnecessary files.
  • Path issues in packaged apps: __dirname resolves differently in development vs. production (especially inside ASAR). Use app.getAppPath() and app.isPackaged to 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 start in 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_modules test fixtures, .git directories, or development configs in the packaged app bloats the installer and may leak sensitive information.

  • Using __dirname without accounting for ASAR packaging — paths resolve differently inside an ASAR archive than in development; use app.getAppPath() and app.isPackaged for 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

Get CLI access →