# Technical Documentation: electron-userland/electron-builder

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/electron-userland/electron-builder)

### Overview (`README.md`)

# electron-builder [](https://www.npmjs.com/package/electron-builder) [](https://yarn.pm/electron-builder)
A complete solution to package and build a ready for distribution [Electron](https://electronjs.org) app for macOS, Windows and Linux with “auto update” support out of the box. 📦

Always looking for community contributions! 👀 Setting up a [dev environment](https://github.com/electron-userland/electron-builder/blob/master/CONTRIBUTING.md) is easy to do 🪩

## Sponsors

<table>
   <tr align="center">
      <td>
         <a href="https://workflowy.com">
            <div>
               <img src="website/static/sponsor-logos/Workflowy.png" alt="WorkFlowy" title="WorkFlowy" style="height: 75px;" height="75"/>
            </div>
            Notes, Tasks, Projects.<br>All in a Single Place.
         </a>
         <br>
      </td>
   </tr>
   <tr align="center">
      <td>
         <br>
         <a href="https://tidepool.org">
            <div>
               <img src="website/static/sponsor-logos/Tidepool_Logo_Light.svg" alt="Tidepool" title="Tidepool" style="height: 75px;" height="75" />
            </div>
            Your gateway to understanding your diabetes data
         </a>
         <br>
      </td>
      <td>
         <br>
         <a href="https://keygen.sh/?via=electron-builder">
            <div>
               <img src="https://keygen.sh/images/logo-pill.png" alt="Keygen" title="Keygen" style="height: 75px;" height="75" />
            </div>
            An open, source-available software licensing and distribution API
         </a>
         <br>
      </td>
   </tr>
   <tr align="center">
      <td>
         <br>
         <a href="https://www.todesktop.com/electron?utm_source=electron-builder">
            <div>
               <img src="https://www.todesktop.com/new-logo/todesktop-logo.png" alt="ToDesktop" title="ToDesktop" style="height: 75px;" height="75" />
            </div>
            ToDesktop: An all-in-one platform for building and releasing Electron apps
         </a>
         <br>
      </td>
      <td>
         <br>
         <a href="https://www.dashcam.io/?ref=electron_builder">
            <div>
               <img src="https://user-images.githubusercontent.com/318295/226675216-ab6aad0c-526c-4a45-a0a8-3906ac614b8b.png" alt="Dashcam" title="Dashcam" style="height: 75px;" height="75" />
            </div>
            Dashcam: Capture the steps to reproduce any bug with video crash reports for Electron.
         </a>
         <br>
      </td>
   </tr>
</table>


## Documentation

See the full documentation on [electron.build](https://www.electron.build).

* NPM packages management:
    * [Native application dependencies](https://electron.atom.io/docs/tutorial/using-native-node-modules/) compilation (including [Yarn](http://yarnpkg.com/) support).
    * Development dependencies are never included. You don't need to ignore them explicitly.
    * [Two package.json structure](https://www.electron.build/docs/tutorials/two-package-structure) is supported, but you are not forced to use it even if you have native production dependencies.
* [Code Signing](https://www.electron.build/docs/features/code-signing) on a CI server or development machine.
* [Auto Update](https://www.electron.build/docs/features/auto-update) ready application packaging.
* Numerous target formats:
    * All platforms: `7z`, `zip`, `tar.xz`, `tar.7z`, `tar.lz`, `tar.gz`, `tar.bz2`, `dir` (unpacked directory).
    * [macOS](https://www.electron.build/docs/mac): `dmg`, `pkg`, `mas`.
    * [Linux](https://www.electron.build/docs/linux): [AppImage](http://appimage.org), [snap](http://snapcraft.io), debian package (`deb`), `rpm`, `freebsd`, `pacman`, `p5p`, `apk`.
    * [Windows](https://www.electron.build/docs/win): `nsis` (Installer), `nsis-web` (Web installer), `portable` (portable app without installation), AppX (Windows Store), MSI, Squirrel.Windows.
* [Publishing artifacts](https://www.electron.build/docs/publish) to GitHub Releases, Amazon S3, DigitalOcean Spaces and Bintray.
* Advanced building:
    * Pack in a distributable format (already packaged app).
    * Separate [build steps](https://github.com/electron-userland/electron-builder/issues/1102#issuecomment-271845854).
    * Build and publish in parallel, using hard links on CI server to reduce IO and disk space usage.
* [Docker](https://www.electron.build/docs/features/multi-platform-build#docker) images to build Electron app for Linux or Windows on any platform.
* Downloads all required tool files on demand automatically (e.g., to code sign a Windows application, to make AppX), no setup required.

| Question                               | Answer                                                                            |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| “I want to configure electron-builder” | [See options](https://www.electron.build/docs/configuration)                 |
| “I found a bug or I have a question”   | [Open an issue](https://github.com/electron-userland/electron-builder/issues/new) |
| “I want to support development”        | [Donate](https://www.electron.build/docs/donate)                                       |

## Requirements

**Node.js >=22.12.0** is required for v27.

> **⚠️ Upgrading from v26?** v27 is a major release with breaking changes (native ESM, Node >=22.12, and removed deprecated APIs). Start with **[What's New in v27](https://www.electron.build/docs/migration/whats-new-v27)** for the highlights and the silent default changes, then **read the full [breaking changes](https://www.electron.build/docs/migration/v27-breaking-changes)** before upgrading.
>
> Then run the automated migration command — it rewrites your config in place:
> ```bash
> electron-builder migrate-schema
> ```
> Step-by-step walkthrough: **[v26 → v27 migration guide](https://github.com/electron-userland/electron-builder/blob/master/MIGRATION.md)** · [electron.build/docs/migration/v26-to-v27](https://www.electron.build/docs/migration/v26-to-v27)

## Installation
```
yarn add electron-builder --dev
// or npm, pnpm, bun
```

### Note for Yarn 3

Yarn 3 uses PnP by default, but electron-builder still needs node-modules (ref: [yarnpkg/berry#4804](https://github.com/yarnpkg/berry/issues/4804#issuecomment-1234407305)). Add configuration in the `.yarnrc.yaml` as follows:
```
nodeLinker: "node-modules"
```
This instructs Yarn to use node-modules instead of PnP.

## Quick Setup Guide

[electron-quick-start](https://github.com/electron/electron-quick-start) is the official minimal starter for a new Electron application.

1. Clone the starter and add electron-builder:
    ```bash
    git clone https://github.com/electron/electron-quick-start
    cd electron-quick-start
    npm install
    npm install electron-builder --save-dev
    ```

2. Specify the standard fields in the application `package.json` — `name`, `description`, `version` and [author](https://docs.npmjs.com/files/package.json#people-fields-author-contributors).

3. Specify the [build](https://www.electron.build/docs/configuration) configuration in the `package.json` as follows:
    ```json
    "build": {
      "appId": "your.id",
      "mac": {
        "category": "your.app.category.type"
      }
    }
    ```
   See [all options](https://www.electron.build/docs/configuration). Option [files](https://www.electron.build/docs/contents#files) to indicate which files should be packed in the final application, including the entry file, maybe required.
   You can also use separate configuration files, such as `js`, `ts`, `yml`, and `json`/`json5`. See [read-config-file](https://www.npmjs.com/package/read-config-file) for supported extensions. [JS Example for programmatic API](https://www.electron.build/docs/programmatic-usage)

4. Add [icons](https://www.electron.build/docs/features/icons-and-images).

5. Add the [scripts](https://docs.npmjs.com/cli/using-npm/scripts) key to the development `package.json`:
    ```json
    "scripts": {
      "app:dir": "electron-builder --dir",
      "app:dist": "electron-builder"
    }
    ```
    Then you can run `yarn app:dist` (to package in a distributable format (e.g. dmg, windows installer, deb package)) or `yarn app:dir` (only generates the package directory without really packaging it. This is useful for testing purposes).

    To ensure your native dependencies are always matched electron version, simply add script `"postinstall": "electron-builder install-app-deps"` to your `package.json`.

6. If you have native addons of your own that are part of the application (not as a dependency), set [nativeModules.nodeGypRebuild](https://www.electron.build/docs/configuration) to `true`.

Please note that everything is packaged into an asar archive [by default](https://www.electron.build/docs/configuration).

For an app that will be shipped to production, you should sign your application. See [Where to buy code signing certificates](https://www.electron.build/docs/features/code-signing#certificate-types).

## Programmatic Usage
TypeScript types are provided and can be found [here](https://www.electron.build/docs/api/index). See the full [programmatic usage guide](https://www.electron.build/docs/programmatic-usage).

```js
// ESM (recommended)
import { build, Platform } from "electron-builder"

await build({
  targets: Platform.MAC.createTarget(),
  config: {
    // build options — see https://www.electron.build/
  }
})
```

```js
// CommonJS — also works on Node >=22.12.0
const { build, Platform } = require("electron-builder")

build({ targets: Platform.MAC.createTarget(), config: {} })
  .then(() => { /* done */ })
  .catch(console.error)
```

## Community Boilerplates

* [electron-react-boilerplate](https://github.com/chentsulin/electron-react-boilerplate) A boilerplate for scalable cross-platform desktop apps.
* [electron-vue-vite](https://github.com/caoxiemeihao/electron-vue-vite) A real simple Electron + Vue3 + Vite5 boilerplate.
* [vite-electron-builder](https://github.com/cawa-93/vite-electron-builder) Secure boilerplate for Electron app based on Vite. Supports multiple frameworks.
* [electronjs-with-nextjs](https://github.com/saulotarsobc/electronjs-with-nextjs) ElectronJS application with NextJS and TypeScript.

## Debug

Set the `DEBUG` environment variable to debug what electron-builder is doing:
```bash
DEBUG=electron-builder
```

`FPM_DEBUG` env to add more details about building linux targets (except snap and appimage).

`DEBUG_DMG=true` env var to add more debugging/verbosity from `hdiutil` (macOS).

:::tip[Windows cmd]
On [Windows](https://github.com/visionmedia/debug#windows-command-prompt-notes) the environment variable is set using the set command.
```bash
set DEBUG=electron-builder
```
:::

:::tip[PowerShell]
PowerShell uses different syntax to set environment variables.
```bash
$env:DEBUG = "electron-builder"
```
:::

If you've identified a bug and want to report it, use [electron/minimal-repro](https://github.com/electron/minimal-repro) to create a minimal reproduction case. Attaching one to your issue significantly speeds up diagnosis.

## Donate

This open source work is accomplished during free time. If you'd like to have more time invested in it, please consider [donating](https://www.electron.build/docs/donate).

---

### Overview (`.changeset/README.md`)

# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

---

### Overview (`docker/readme.md`)

It is developer documentation. See [user documentation](https://www.electron.build/docs/features/multi-platform-build#docker).

# Build

`pnpm docker-images`

# Notes

* We use [named data volume](https://madcoda.com/2016/03/docker-named-volume-explained/) instead of mounted host directory to store `node_modules` because NPM is unreliable and NPM team [doesn't want to fix it](https://github.com/npm/npm/issues/3565).

  `${PWD##*/}-node-modules` is used as name of data volume — it is your current directory name (e. g. `foo`) and suffix `-node-modules`.

  As downside, you have to manually clear outdated data volumes, use `docker volume rm your-name`.

---

### Overview (`test/README.md`)

# Test file classes: `*Test.ts` vs `*.e2e.ts`
Packaging tests that need the built installer/archive (mount the dmg, list the nupkg, read `latest.yml`, run the
installer under wine, …) live in `*.e2e.ts` files, next to the `*Test.ts` file of the same area (e.g.
`windows/oneClickInstallerTest.ts` + `windows/oneClickInstaller.e2e.ts`). Everything in a `*Test.ts` file stops once the
app directory is assembled (a `dir` target, `afterPackTestHook`, `effectiveOptionComputed`, or a thrown configuration
error). Generated toolset suites opt in with `e2e: true` in their `SuiteConfig` and are emitted as `<suite>__<dims>__e2e.ts`
(or `<suite>__<dims>.win.e2e.ts` when platform-gated).

`TEST_MODE` selects the class: `all` (default; CI runs both on every PR and master commit), `unit` or `e2e`. `TEST_FILES`
is a substring override and always wins over the mode. Snapshots follow the file name: `foo.e2e.ts` →
`test/snapshots/.../foo.e2e.js.snap`.

# Inspect output if test uses temporary directory
Set environment variable `TEST_APP_TMP_DIR` (e.g. `/tmp/electron-builder-test`).
Specified directory will be used instead of random temporary directory and *cleared* on each run.

## Test Code Signing Ceritificates
If test installer certificate is expired: http://security.stackexchange.com/questions/17909/how-to-create-an-apple-installer-package-signing-certificate

---

### Overview (`packages/app-builder-lib/README.md`)

# app-builder-lib

Various utilities. Used by [electron-builder](https://github.com/electron-userland/electron-builder).

---

### Overview (`packages/builder-util-runtime/readme.md`)

# builder-util-runtime

HTTP utilities. Used by [electron-builder](https://github.com/electron-userland/electron-builder).

---

### Overview (`packages/builder-util/readme.md`)

# builder-util

Various utilities. Used by [electron-builder](https://github.com/electron-userland/electron-builder).

---

### Overview (`packages/dmg-builder/readme.md`)

# dmg-builder

Utilities to build DMG. Used by [electron-builder](https://github.com/electron-userland/electron-builder).

---

### Overview (`packages/electron-builder-squirrel-windows/readme.md`)

# electron-builder-squirrel-windows

Plugin for [electron-builder](https://github.com/electron-userland/electron-builder) to build Squirrel.Windows installer.

---

### Overview (`packages/electron-forge-maker-appimage/readme.md`)

# electron-installer-appimage

[electron-builder](https://github.com/electron-userland/electron-builder) [appimage](http://appimage.org) target for electron-forge.

Recommended to build electron-forge project using electron-builder directly.
[Publishing](https://www.electron.build/publish),
[Auto Update](https://electron.build/docs/features/auto-update)
and [Code Signing](https://electron.build/docs/features/code-signing) supported only in this case
(including all other useful [packaging options](https://electron.build/docs/configuration) like [files](https://electron.build/docs/file-patterns)).

---

### Overview (`packages/electron-forge-maker-nsis-web/readme.md`)

# electron-forge-maker-nsis-web

[electron-builder](https://github.com/electron-userland/electron-builder) [nsis-web](https://electron.build/nsis) target for electron-forge.

Recommended to build electron-forge project using electron-builder directly.
[Publishing](https://www.electron.build/publish),
[Auto Update](https://electron.build/docs/features/auto-update)
and [Code Signing](https://electron.build/docs/features/code-signing) supported only in this case
(including all other useful [packaging options](https://electron.build/docs/configuration) like [files](https://electron.build/docs/file-patterns)).

---

### Overview (`packages/electron-forge-maker-nsis/readme.md`)

# electron-forge-maker-nsis

[electron-builder](https://github.com/electron-userland/electron-builder) [nsis](https://electron.build/nsis) target for electron-forge.

Recommended to build electron-forge project using electron-builder directly.
[Publishing](https://www.electron.build/publish),
[Auto Update](https://electron.build/docs/features/auto-update)
and [Code Signing](https://electron.build/docs/features/code-signing) supported only in this case
(including all other useful [packaging options](https://electron.build/docs/configuration) like [files](https://electron.build/docs/file-patterns)).

---

### Overview (`packages/electron-forge-maker-snap/readme.md`)

# electron-installer-snap

[electron-builder](https://github.com/electron-userland/electron-builder) [snap](https://snapcraft.io) target for electron-forge.

Recommended to build electron-forge project using electron-builder directly.
[Publishing](https://www.electron.build/publish),
[Auto Update](https://electron.build/docs/features/auto-update)
and [Code Signing](https://electron.build/docs/features/code-signing) supported only in this case
(including all other useful [packaging options](https://electron.build/docs/configuration) like [files](https://electron.build/docs/file-patterns)).

---

### Overview (`packages/electron-publish/readme.md`)

# electron-publish

Part of [electron-builder](https://github.com/electron-userland/electron-builder).

See the [Publishing Artifacts](https://www.electron.build/publish) for more information.

Can be used standalone.

---

### Overview (`packages/electron-updater/README.md`)

# electron-updater

This module allows you to automatically update your application. You only need to install this module and write two lines of code!
To publish your updates you just need simple file hosting, it does not require a dedicated server.

See [Auto Update](https://electron.build/docs/features/auto-update) for more information.

Supported OS:
 - macOS ([Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac)).
 - Windows (NSIS).
 - Linux (AppImage, rpm, deb).

## Credits

Thanks to Evolve Labs for donating the npm package name.

---

### Overview (`packages/app-builder-lib/templates/nsis/README.md`)

# NSIS

⚠️ **It is developer documentation.** If your are looking for usage guide, see [user documentation](https://electron.build/nsis).

NSIS stands for Nullsoft Scriptable Install System. electron-builder utilizes a [customized version](https://github.com/electron-userland/electron-builder-binaries) of it and uses `NsisMultiUser` plugin to handle installation for single user or all users on the computer.

## Some links
http://www.mathiaswestin.net/2012/09/how-to-make-per-user-installation-with.html

https://msdn.microsoft.com/en-us/library/windows/desktop/dd378457(v=vs.85).aspx#FOLDERID_UserProgramFiles

https://github.com/Drizin/NsisMultiUser

NSIS vs Inno Setup — it is not easy to choose because both are far from ideal, e.g. inno also doesn't have built-in per-user installation implementation — http://stackoverflow.com/questions/34330668/inno-setup-custom-dialog-with-per-user-or-per-machine-installation.

http://stackoverflow.com/questions/2565215/checking-if-the-application-is-running-in-nsis-before-uninstalling

One-click installer: http://forums.winamp.com/showthread.php?t=300479

## Localization (l10n)

For translators, the strings to be displayed are included in [`assistedMessages.yml`](https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/nsis/assistedMessages.yml) and [`messages.yml`]https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/nsis/messages.yml).

As for other strings in NSIS, head to [electron-userland/electron-builder-binaries](https://github.com/electron-userland/electron-builder-binaries) or the upstream repository on [Sorceforge](https://sourceforge.net/p/nsis/code/HEAD/tree/).

## GUID
See [docs](https://electron.build/nsis).

We use UUID v5 to generate sha-1 name-based UUID.

http://stackoverflow.com/questions/3029994/convert-uri-to-guid
https://alexandrebrisebois.wordpress.com/2013/11/14/create-predictable-guids-for-your-windows-azure-table-storage-entities/
https://github.com/Squirrel/Squirrel.Windows/pull/658

## Compression

NSIS LZMA compression is slower and worse compared to external `7za` compression. Slower because `7za` is multi-threaded, worse because LZMA codec implementation is outdated and BCJ2 filter is not enabled.
Difference for test app — 4 MB (before: 36.3 after: 32.8).

And compression time is also greatly reduced.

Since NSIS is awesome, no disadvantages in our approach — [compression is disabled](http://nsis.sourceforge.net/Reference/SetCompress) before `File /oname=app.7z "${APP_ARCHIVE}"` and enabled after (it is the reasons why `SOLID` compression is not used).
So, opposite to Squirrel.Windows, archive is not twice compressed.

So, in your custom NSIS scripts you should not use any compression instructions. Only `SetCompress` if you need to disable compression for already archived file.

---

### Appimage (`website/docs/appimage.md`)

The top-level [appImage](configuration.md#appimage) key contains a set of options instructing electron-builder on how it should build [AppImage](https://appimage.org/) files.

[AppImage](https://appimage.org/) is a self-contained, portable Linux application format that packages everything an application needs to run into a single executable file — no installation, no root access required. Under the hood, the application files are stored in a [squashfs](https://en.wikipedia.org/wiki/SquashFS) filesystem that is mounted on launch.

AppImage is one of the two default Linux targets for electron-builder (along with Snap).

## AppImage vs. Snap vs. Flatpak vs. DEB

| Aspect | AppImage | Snap | Flatpak | DEB/RPM |
|---|---|---|---|---|
| Installation | None | `snap install` | `flatpak install` | `apt`/`dnf` |
| Sandboxed | No | Yes (strict) | Yes | No |
| Auto-update | AppImageUpdate / electron-updater | Snap Store | Flathub | Package manager |
| Cross-distro | Yes | Yes (snapd needed) | Yes | No |
| Root required | No | No | No | Yes |
| electron-builder default | **Yes** | **Yes** | No | No |

## How AppImage Works

An AppImage is a single executable file that bundles:

- A **runtime binary** prepended at the start of the file (handles mounting and launching)
- A **squashfs filesystem** containing the application and all its dependencies

## Running an AppImage

Make the file executable, then run it directly — no installation or root privileges needed:

```bash
chmod +x MyApp-1.0.0-x86_64.AppImage
./MyApp-1.0.0-x86_64.AppImage
```

### Extracting an AppImage

You can extract the contents of any AppImage without running it:

```bash
./MyApp-1.0.0-x86_64.AppImage --appimage-extract
# Contents are extracted to ./squashfs-root/
```

This is useful for inspection, manual desktop integration, or debugging.

## Desktop Integration

:::info[Desktop Integration]
Since electron-builder 21, desktop integration (creating `.desktop` files, associating file types, registering with application menus) is NOT handled by the AppImage itself.
:::

AppImages do not self-integrate with your desktop by default. There are two approaches:

### Automatic Integration via AppImageLauncher (recommended)

[AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) integrates AppImages into your application menu and manages updates. Install it from your distribution's package manager or from its [releases page](https://github.com/TheAssassin/AppImageLauncher/releases).

Once installed, double-clicking an AppImage will prompt you to integrate it. AppImageLauncher handles:

- Creating a `.desktop` entry in `~/.local/share/applications/`
- Placing icons in `~/.local/share/icons/`
- Registering MIME type associations

### Manual Integration

Extract the AppImage (see above) and copy the relevant files:

```bash
# Extract
./MyApp-1.0.0-x86_64.AppImage --appimage-extract

# Copy desktop entry and icon
cp squashfs-root/myapp.desktop ~/.local/share/applications/
cp squashfs-root/myapp.png ~/.local/share/icons/hicolor/256x256/apps/

# Update the Exec= line in the .desktop file to point to the AppImage
sed -i "s|Exec=.*|Exec=/path/to/MyApp-1.0.0-x86_64.AppImage|" \
  ~/.local/share/applications/myapp.desktop

# Refresh the desktop database
update-desktop-database ~/.local/share/applications/
```

## Desktop Entry Customization

Customize the `.desktop` file metadata embedded in the AppImage via the `linux.desktop` option:

```yaml
linux:
  desktop:
    entry:
      Name: My Application
      Comment: A great application
      Categories: Office;Productivity
      Keywords: productivity;tool
      StartupWMClass: myapp
    desktopActions:
      NewWindow:
        Name: New Window
        Exec: myapp --new-window
```

See [Linux Configuration](linux.md#desktop-file-customization) for the full desktop entry reference.

## MIME Type Associations

Associate your AppImage with file types via `linux.mimeTypes`:

```yaml
linux:
  mimeTypes:
    - application/x-myapp-document
```

## Auto-Update Support

AppImages support differential (delta) updates via the `electron-updater` package. electron-builder embeds a blockmap directly into the AppImage binary at build time — no separate file needs to be published alongside it. electron-updater reads the embedded blockmap to determine which blocks have changed and downloads only those blocks. Configure a publish provider and electron-updater will handle updates:

```yaml
publish:
  provider: github   # or s3, generic, etc.
```

See [Auto Update](features/auto-update.md) for the full setup guide.

## Architecture Support

| Architecture | CLI Flag | Notes |
|---|---|---|
| `x64` | `--x64` | Standard 64-bit x86 Linux |
| `arm64` | `--arm64` | 64-bit ARM (Raspberry Pi 4+, server ARM) |
| `armv7l` | `--armv7l` | 32-bit ARM (Raspberry Pi 3 and older) |

Build requirements: Linux host (or [Docker](features/multi-platform-build.md#docker)) for all architectures.

## Toolsets

The AppImage build tools are configured via the top-level [`toolsets.appimage`](./toolsets.md) option. As of v27 the default resolves to the **static, FUSE3-compatible runtime**; the legacy FUSE2 runtime is available by pinning `"0.0.0"`.

| `toolsets.appimage` | Runtime | Notes |
|---|---|---|
| unset / `"latest"` → `1.1.0` | Static runtime (no host FUSE needed) | **Default in v27** |
| `"0.0.0"` | Legacy FUSE2 | Opt-in only — requires `libfuse.so.2` on the host |

### Default — static runtime

The default toolset bundles a **static AppImage runtime** that does not depend on FUSE2. The runtime is prepended directly to the squashfs filesystem and handles mounting internally, so AppImages run on modern distributions (Arch, Fedora, Ubuntu 24.04+) without installing FUSE. Its `AppRun` entry point also performs smart sandbox detection — it passes `--no-sandbox` to Electron only when unprivileged user namespaces are unavailable, rather than always.

:::note[v27: default --no-sandbox change]
With the legacy FUSE2 runtime (`toolsets.appimage: "0.0.0"`), electron-builder injects `--no-sandbox` by default. With the default static runtime it does **not** — `AppRun` adds it only when user namespaces are unavailable. Set `executableArgs: ["--no-sandbox"]` if you need to force it.
:::

### Legacy runtime (`0.0.0`) — FUSE2

Pin `toolsets: { appimage: "0.0.0" }` only if you specifically need the original FUSE2 runtime. FUSE2 has been unmaintained since 2017 and is increasingly unavailable on modern distributions, where an AppImage built with it fails to launch:

```
dlopen(): error loading libfuse.so.2
AppImages require FUSE to run.
```

```yaml
toolsets:
  appimage: "0.0.0"   # legacy FUSE2 runtime — only if you need it
```

## Compression

AppImage files use [squashfs](https://en.wikipedia.org/wiki/SquashFS) internally. The compression algorithm affects file size and launch speed.

:::info[Compression support by toolset]
- **Legacy FUSE2 (`0.0.0`)**: an explicit `appImage.compression` of `xz` or `gzip` is forwarded to mksquashfs; otherwise the root `compression: "maximum"` selects `xz`. All other values use mksquashfs's default (gzip).
- **Static runtime (default)**: supports `gzip` and `zstd`. The `appImage.compression` option selects the algorithm directly; `"xz"` is mapped to `"zstd"` (xz is not compiled into the static runtime binary).
:::

| Algorithm | File size | Decompression speed | Notes |
|---|---|---|---|
| `zstd` | Moderate | Fast | Default for static runtime |
| `gzip` | Moderate | Moderate | Legacy mksquashfs default |
| `xz` | Smallest | Slowest | Mapped to `zstd` on static runtime |

Direct algorithm selection (the legacy `0.0.0` toolset forwards only `gzip` and `xz`):

```yaml
appImage:
  compression: zstd   # gzip | zstd | xz (mapped to zstd)
```

## License Agreement (EULA)

If your application requires users to accept a license before first launch, set the `license` option to the path of a `.txt` or `.html` file:

```yaml
appImage:
  license: build/EULA.txt
```

The license dialog is shown on first launch using `zenity`, `kdialog`, or `Xdialog` (whichever is available). Acceptance is recorded in `$XDG_CONFIG_HOME/<ProductFilename>/eulaAccepted`.

## Build Requirements

AppImages must be built on Linux (or via Docker). They cannot be cross-compiled from macOS or Windows.

To build on a non-Linux host:

```bash
docker run --rm -ti \
  --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
  --env ELECTRON_CACHE="/root/.cache/electron" \
  --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
  -v ${PWD}:/project \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  electronuserland/builder:wine \
  /bin/bash -c "npm install && npx electron-builder --linux"
```

See [Multi Platform Build](features/multi-platform-build.md#docker) for full Docker documentation.

## Debugging

Enable verbose output:

```bash
DEBUG=electron-builder electron-builder --linux AppImage
```

Test the built AppImage:

```bash
chmod +x dist/MyApp-1.0.0.AppImage
./dist/MyApp-1.0.0.AppImage --no-sandbox
```

The `--no-sandbox` flag may be required on some distributions where user namespaces are restricted.

## Configuration

{!./app-builder-lib.Interface.AppImageOptions.md!}

---

### Appx (`website/docs/appx.md`)

The top-level [appx](configuration.md) key contains a set of options instructing electron-builder on how it should build AppX packages for the Windows Store or enterprise sideloading.

All options are optional. All required AppX configuration is inferred and computed automatically.

## When to Use AppX

AppX is the packaging format for the Microsoft Store (also called MSIX). Choose AppX when:

- **Windows Store distribution** — your app will be listed in the Microsoft Store
- **Enterprise sideloading** — IT manages app distribution via MDM/Intune without the Store
- **Windows 10S / Windows SE** — these locked-down Windows editions only run Store apps

For standard consumer distribution, [NSIS](nsis.md) is simpler. For enterprise deployment without the Store, [MSI](msi.md) is often preferred.

## AppX vs. NSIS vs. MSI

| Aspect | AppX | NSIS | MSI |
|---|---|---|---|
| Windows Store | Yes | No | No |
| Sideloading | Yes (signed) | N/A | N/A |
| Enterprise MDM | Limited | No | Yes |
| Sandboxed | Partial | No | No |
| Auto-update | Store only (not electron-updater) | electron-updater | Not supported via electron-updater |
| Code signing | Required (or Store) | Recommended | Recommended |

## Code Signing {#appx-package-code-signing}

- **Store distribution** — no manual signing needed. The Windows Store signs the package with a Microsoft certificate during the submission process.
- **Sideloading / enterprise** — the AppX must be signed with a trusted certificate. See [Windows Code Signing](features/code-signing/code-signing-win.md).

For self-signed certificates during development, see [Microsoft's self-signed certificate guide](https://learn.microsoft.com/en-us/windows/msix/package/create-certificate-package-signing).

## AppX Assets

AppX requires specific logo/icon assets. Place them in the `appx` folder inside your [build resources](configuration.md) directory (default: `build/appx/`):

| Asset | File Name | Required | Size |
|---|---|---|---|
| Store Logo | `StoreLogo.png` | Yes | 50×50 |
| Square 150×150 | `Square150x150Logo.png` | Yes | 150×150 |
| Square 44×44 | `Square44x44Logo.png` | Yes | 44×44 |
| Wide 310×150 | `Wide310x150Logo.png` | Yes | 310×150 |
| Badge Logo | `BadgeLogo.png` | Optional | 24×24 |
| Large Tile | `LargeTile.png` | Optional | 310×310 |
| Small Tile | `SmallTile.png` | Optional | 71×71 |
| Splash Screen | `SplashScreen.png` | Optional | 620×300 |

Default assets are used for the required logos if you don't provide your own. Scale variants (e.g., `Square44x44Logo.scale-200.png`) are supported — see [Microsoft's tile asset guidelines](https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets).

## Identity and Publisher

```yaml
appx:
  applicationId: "MyApp"                       # must be alphanumeric, no spaces
  identityName: "MyCompany.MyApp"              # unique within the Store
  publisher: "CN=My Company, O=My Company, C=US"  # must match certificate Subject
  publisherDisplayName: "My Company"
  displayName: "My Application"               # displayed in Start menu
```

:::warning[Publisher Must Match Certificate]
The `publisher` value must exactly match the Subject field of your code signing certificate. Mismatches cause installation failures.
:::

## Capabilities

AppX apps declare the system capabilities they use. Specify capabilities in `appx.capabilities`:

```yaml
appx:
  capabilities:
    - runFullTrust                  # default — allows unrestricted execution
    - internetClient               # outbound internet access
    - privateNetworkClientServer   # local network
    - webcam                       # camera access
    - microphone                   # microphone access
```

`runFullTrust` is required for most Electron apps (included by default). The full list of capabilities is in [Microsoft's documentation](https://docs.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations).

## Tile Customization

```yaml
appx:
  backgroundColor: "#2c2c2c"    # tile background color (CSS color)
  showNameOnTiles: false        # show app name on tile (default: false)
```

## Language Support

```yaml
appx:
  languages:
    - en-US
    - de-DE
    - fr-FR
    - ja-JP
```

Declare which languages your app supports. This affects Store listings and localized resources.

## Version Targeting

```yaml
appx:
  minVersion: "10.0.17763.0"       # Windows 10 1809 minimum
  maxVersionTested: "10.0.22621.0" # Windows 11 22H2 tested maximum
```

## Auto-Launch Extension

```yaml
appx:
  addAutoLaunchExtension: false    # Default: false
```

When `true`, adds a startup task extension that runs the app on user login. Requires the `runFullTrust` capability.

## Custom Manifest

For advanced scenarios, provide a completely custom AppX manifest:

```yaml
appx:
  customManifestPath: build/AppxManifest.xml
```

Or inject additional XML extensions:

```yaml
appx:
  customExtensionsPath: build/appx-extensions.xml
```

## Publishing to the Windows Store

1. Create a [Microsoft Developer account](https://developer.microsoft.com/en-us/store/register/) ($19 one-time fee)
2. Reserve your app name in [Partner Center](https://partner.microsoft.com/en-us/dashboard/windows/overview)
3. Build your AppX: `electron-builder --win appx`
4. Test the package locally (see Testing below)
5. Upload the `.appx` file via Partner Center submission
6. Complete the submission (age rating, pricing, screenshots, etc.)
7. Wait for Store certification (typically 1-3 business days)

## Sideloading Without the Store

For enterprise distribution without the Store:

```powershell
# Enable sideloading (Windows 10 1803+, this is the default)
# On Windows 10 S/SE, you must use the Store

# Install the AppX
Add-AppxPackage -Path ".\MyApp-1.0.0.appx"
```

If using a self-signed certificate, the user must trust it first (see "Common Questions" below).

## Testing Locally

```powershell
# Install locally (Developer Mode must be enabled for unsigned packages)
Add-AppxPackage -Path ".\MyApp-1.0.0.appx"

# Uninstall
Get-AppxPackage | Where-Object {$_.Name -like "*MyApp*"} | Remove-AppxPackage
```

## Common Questions

**How do I install an AppX with a self-signed certificate?**

Import the certificate into "Trusted People" on the target machine — see [Microsoft's guide to signing MSIX packages](https://learn.microsoft.com/en-us/windows/msix/package/sign-app-package-using-signtool). Then install the AppX normally with `Add-AppxPackage`.

**Does AppX support auto-updates without the Store?**

No — AppX auto-updates are handled by the Windows Store only. electron-updater does not support AppX packages. For auto-updates outside the Store, use NSIS instead.

## Configuration

{!./app-builder-lib.Interface.AppXOptions.md!}

---

### Architecture (`website/docs/architecture.md`)

# Architecture & Multi-Arch Builds

electron-builder can produce builds for multiple CPU architectures. This page covers architecture support by platform, how to build universal macOS binaries, and cross-compilation considerations.

## Supported Architectures by Platform

| Platform | Architecture | CLI Flag | Notes |
|---|---|---|---|
| macOS | x64 | `--x64` | Intel Macs |
| macOS | arm64 | `--arm64` | Apple Silicon (M1/M2/M3/M4) |
| macOS | universal | `--universal` | Runs natively on both Intel and Apple Silicon |
| Windows | x64 | `--x64` | Most common desktop target |
| Windows | ia32 | `--ia32` | 32-bit; legacy support. Requires `electronVersion` <= 43.x — [Electron 44 removed Windows ia32 builds](https://github.com/electron/electron/pull/51816) |
| Windows | arm64 | `--arm64` | Windows on ARM (Surface Pro X, etc.) |
| Linux | x64 | `--x64` | Standard 64-bit x86 Linux |
| Linux | arm64 | `--arm64` | Raspberry Pi 4+, ARM servers, cloud instances |
| Linux | armv7l | `--armv7l` | Raspberry Pi 3 and older, 32-bit ARM. Requires `electronVersion` <= 43.x — [Electron 44 removed Linux armv7l builds](https://github.com/electron/electron/pull/51816) |

:::note[32-bit support ends with Electron 43]
Electron 44 removed Windows ia32 and Linux armv7l builds ([electron/electron#51816](https://github.com/electron/electron/pull/51816)). To keep shipping those architectures, stay on Electron <= 43.x — the v43 series is supported until its end-of-life in January 2027. electron-builder fails fast with a configuration error when such a build is requested against Electron 44+ (downgraded to a warning when a custom `electronDist` or Electron mirror is configured).
:::

## Specifying Architecture

### CLI

```bash
# Build for a specific architecture
electron-builder --mac --x64
electron-builder --mac --arm64
electron-builder --mac --universal

# Build multiple architectures in sequence
electron-builder --win --x64 --arm64

# Build for all configured targets and architectures
electron-builder --mac --win --linux
```

### Configuration

```yaml
mac:
  target:
    - target: dmg
      arch:
        - x64
        - arm64
        - universal
    - target: zip
      arch: universal
```

:::note[v27: arch: "all" now expands to x64 + arm64]
On Windows and Linux, `arch: "all"` (and the default multi-arch build) expands to **x64 + arm64** in v27 — it no longer includes `ia32`. Request `ia32` (or `armv7l`) explicitly if you still build 32-bit. See [What's New in v27](./migration/whats-new-v27.md).
:::

## macOS: Universal Binaries

A **universal binary** contains both x64 and arm64 slices in a single executable. It runs natively on Intel Macs and Apple Silicon without Rosetta 2 translation.

### Building Universal

```bash
electron-builder --mac --universal
```

Or set in configuration:

```yaml
mac:
  target:
    - target: dmg
      arch: universal
```

electron-builder downloads both x64 and arm64 Electron binaries, then merges them using `lipo`.

### How ASAR Merging Works

When building a universal binary, electron-builder must merge two ASAR archives (one per architecture). This works automatically for pure JavaScript. For native modules:

```yaml
mac:
  universal:                # v27: universal-build options live under mac.universal
    mergeASARs: true        # Default: true — merge the two per-arch ASARs into one
    singleArchFiles: ""     # Glob of files that must NOT be merged (arch-specific binaries)
    x64ArchFiles: ""        # Glob of files that exist only in the x64 build
```

### Arch-Specific Native Modules

If you have native modules that differ between x64 and arm64, you must configure which files should remain architecture-specific:

```yaml
mac:
  universal:
    mergeASARs: true
    singleArchFiles: "node_modules/canvas/**"   # only exists in x64
```

For modules that exist in both architectures but have different binaries:

```yaml
mac:
  universal:
    mergeASARs: true
    x64ArchFiles: "node_modules/my-module/build/Release/my_module.node"
```

### Verifying a Universal Binary

```bash
# Check that both slices are present
lipo -info dist/mac-universal/MyApp.app/Contents/MacOS/MyApp
# Should output: Architectures in the fat file: x64 arm64

# Check a framework
lipo -info "dist/mac-universal/MyApp.app/Contents/Frameworks/Electron Framework.framework/Electron Framework"
```

### Universal vs. Separate Builds

| Approach | Pros | Cons |
|---|---|---|
| Universal (`universal`) | Single download for all users | ~2x file size |
| Separate (`x64` + `arm64`) | Smaller download per user | Two separate release artifacts |

Most consumer apps ship universal binaries for the best user experience. If binary size is critical, ship separate architectures.

## Windows ARM64

Windows ARM64 builds run natively on ARM-based Windows devices (Surface Pro X, Snapdragon-based laptops, etc.).

```bash
electron-builder --win --arm64
```

:::note[Cross-compilation from x64]
Windows ARM64 builds can be cross-compiled from an x64 Windows machine. The Electron binary for ARM64 is downloaded automatically.
:::

## Linux ARM

Linux ARM builds are commonly used for:
- **Raspberry Pi** — `armv7l` (Pi 3 and older) or `arm64` (Pi 4+)
- **ARM servers** — `arm64` (AWS Graviton, Ampere Altra, etc.)
- **Embedded Linux** — `armv7l`

```bash
# For Raspberry Pi 4+ and ARM servers
electron-builder --linux --arm64

# For older Raspberry Pi (32-bit OS)
electron-builder --linux --armv7l
```

### Building Linux ARM on x64

Linux ARM can be built on an x64 machine using QEMU emulation via Docker:

```bash
# Install QEMU support (one-time setup)
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm

# Build ARM64 AppImage
docker run --rm \
  -v ${PWD}:/project \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  --platform linux/arm64 \
  electronuserland/builder \
  /bin/bash -c "npm ci && npx electron-builder --linux --arm64"
```

## Cross-Platform Builds

### Building Windows on Linux/macOS

Use the Wine-based Docker image:

```bash
docker run --rm \
  -v ${PWD}:/project \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  electronuserland/builder:wine \
  /bin/bash -c "npm ci && npx electron-builder --win"
```

Wine is used to run NSIS (which generates the `.exe` installer). The Electron binary itself is compiled natively — Wine is only needed for the installer tool.

### Building Linux on macOS

Linux builds require either a native Linux machine or Docker:

```bash
docker run --rm \
  -v ${PWD}:/project \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  electronuserland/builder \
  /bin/bash -c "npm ci && npx electron-builder --linux"
```

### Building AppX on macOS (via Parallels VM)

For AppX (Windows Store), `MakeAppx.exe` requires Windows. When building on macOS with Parallels Desktop (Pro Edition), electron-builder automatically detects and uses the running Windows VM.

See [Multi Platform Build](features/multi-platform-build.md) for full Docker documentation.

## Native Modules and Architecture

Native modules (`.node` files) are compiled for a specific architecture and cannot be used in a different architecture without recompilation.

In practice, electron-builder handles this automatically during the build process when `nativeModules.npmRebuild: true` (the default; in v27 the native-module options `npmRebuild`, `nodeGypRebuild`, `buildDependenciesFromSource`, and `rebuildMode` live under `nativeModules`).

:::note[v27: node_modules are arch/os-filtered on every build]
v27 filters `node_modules` by each package's `package.json` `cpu` / `os` fields against the **target** arch and platform on every build (previously this effectively only mattered for `universal` macOS builds). A dependency that declares an incompatible `cpu`/`os` for the target is excluded from the packaged app. This is usually what you want; if you *intentionally* bundle a cross-arch/cross-os optional binary, include it explicitly via `extraResources` / `files`.
:::

### Universal Binary with Native Modules

Building a universal macOS binary with native modules requires that each native module has both x64 and arm64 variants. If a module only has one architecture, you'll need to use `singleArchFiles` to keep it arch-specific:

```yaml
mac:
  universal:
    mergeASARs: true
    singleArchFiles: "node_modules/serialport/**/*.node"
```

Or if you can compile the module for both architectures, the universal build will merge them automatically via `lipo`.

## Architecture in GitHub Actions

Build multiple architectures in parallel using a matrix:

```yaml
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: macos-latest
            args: --mac --universal
          - os: windows-latest
            args: --win --x64 --arm64
          - os: ubuntu-latest
            args: --linux --x64

    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx electron-builder ${{ matrix.args }} --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Related Pages

- [GitHub Actions](features/github-actions.md) — full CI/CD workflows
- [Multi Platform Build](features/multi-platform-build.md) — Docker images and cross-compilation
- [macOS Configuration](mac.md) — `mac.universal.mergeASARs`, `mac.universal.singleArchFiles`, `mac.universal.x64ArchFiles`
- [Target Selection](targets.md) — choosing the right target format per platform

---

### Cli (`website/docs/cli.md`)

---
title: CLI
---


```
/* Detailed source-code truncated for AI context efficiency. */
```


For other commands please see help using `--help` arg, e.g. `./node_modules/.bin/electron-builder install-app-deps --help`

:::tip
[npx](https://docs.npmjs.com/cli/v10/commands/npx) is bundled with Node.js, so you can simply use `npx electron-builder`.
:::

:::note[Requirements]
electron-builder v27 requires **Node.js >=22.12.0**. Upgrading from v26? Review the [v27 breaking changes](./migration/v27-breaking-changes), then follow the [migration walkthrough](./migration/v26-to-v27).
:::

## `migrate-schema`

New in v27. Rewrites your build configuration from the v26 shape to the v27 shape **in place**, applying every config-level breaking change (see the [walkthrough](./migration/v26-to-v27#step-1-run-the-automated-migrator)).

```bash
electron-builder migrate-schema            # apply changes in place
electron-builder migrate-schema --dry-run  # preview without writing (alias: -n)
```

- Auto-detects your config: the `package.json` `build` key, or `electron-builder.{yml,yaml,json,json5,toml,js,cjs,mjs,ts}`. Pass `--config <path>` for a non-default file and `--project-dir <dir>` for the project root.
- Rewrites **static** configs (`json`/`json5`/`yaml`/`package.json`) and **programmatic** ones (`.js`/`.ts`/`.cjs`/`.mjs`, via an AST codemod that preserves comments and formatting when the config reduces to a single object literal).
- **TOML** is detected but not rewritten (the `toml` library is read-only) — it prints the required changes for you to apply. **JSON5** is re-serialized as JSON (comments are not preserved).
- Prints a **warning** for anything it cannot rewrite mechanically (e.g. `squirrelWindows.customSquirrelVendorDir`) and an **advisory** for runtime defaults and behavior changes worth checking (an `nsis-web` target, a per-machine `nsis` / `nsisWeb` installer, a custom `win.sign.sign` hook without a publisher name, a `generic` publish `url` with a query string, macOS configs relying on the default entitlements). Advisories never change the file. See [what it does and does not do](./migration/v27-breaking-changes.md#new-command-migrate-schema).

:::note[Removed CLI flags]
The v22-era `--em.build` / `--em.directories` flags were removed — pass build config inline with `-c` (e.g. `-c.directories.output=dist`). Implicit publishing was also removed: pass [`--publish`](./publish) explicitly.
:::

Prepend `npx` to sample commands below if you run them from Terminal and not from `package.json` scripts.

:::note[build for macOS, Windows and Linux]
`electron-builder -mwl`
:::

:::note[build deb and tar.xz for Linux]
`electron-builder --linux deb tar.xz`
:::

:::note[build NSIS 32-bit installer for Windows (Electron <= 43 only)]
`electron-builder --windows nsis:ia32`

[Electron 44 removed Windows ia32 builds](https://github.com/electron/electron/pull/51816) — ia32 requires `electronVersion` <= 43.x (supported until the v43 series reaches end-of-life in January 2027).
:::

:::note[set package.json property foo to bar]
`electron-builder -c.extraMetadata.foo=bar`
:::

:::note[configure unicode options for NSIS]
`electron-builder -c.nsis.unicode=false`
:::

## Target

Without target configuration, electron-builder builds Electron app for current platform and current architecture using default target.

* macOS - DMG and ZIP for Squirrel.Mac.
* Windows - [NSIS](./nsis.md).
* Linux:
    - if you build on Windows or macOS: [Snap](./snap.md) and [AppImage](./appimage.md) for x64.
    - if you build on Linux: [Snap](./snap.md) and [AppImage](./appimage.md) for current architecture.

Platforms and archs can be configured using [CLI args](https://github.com/electron-userland/electron-builder#cli-usage), or in the configuration.

For example, if you don't want to pass `--x64` and `--arm64` flags each time, but instead build by default NSIS target for all archs for Windows (add `ia32` only when staying on Electron <= 43):

:::note[Configuration]

package.json
```json
"build": {
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64",
          "arm64"
        ]
      }
    ]
  },
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": [
          "universal"
        ]
      }
    ]
  }
}
```

electron-builder.yml
```yaml
win:
  target:
    - target: nsis
      arch:
        - x64
        - arm64
mac:
  target:
    - target: dmg
      arch: universal
```

electron-builder.config.js
```js
module.exports = {
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64",
          "arm64"
        ]
      }
    ]
  },
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": [
          "universal"
        ]
      }
    ]
  }
}
```
:::

and use
```
build -wl
```

### TargetConfiguration
* **<code id="TargetConfiguration-target">target</code>** String - The target name. e.g. `snap`.
* <code id="TargetConfiguration-arch">arch</code> "x64" | "ia32" | "armv7l" | "arm64" | "universal" - The arch or list of archs.

---

### Configuration (`website/docs/configuration.md`)

---
title: "Configuration"
---

{!./partials/_upgrading-from-v26.md!}

electron-builder configuration can be defined

* in the `package.json` file of your project using the `build` key on the top level:
   ```json
   "build": {
     "appId": "com.example.app"
   }
   ```
* or through the `--config <path/to/yml-or-json5-or-toml-or-js>` option. Defaults to `electron-builder.yml`.
   ```yaml
   appId: "com.example.app"
   ```

    `json`, [json5](http://json5.org), [toml](https://github.com/toml-lang/toml) or `js`/`ts` (exported configuration or function that produces configuration) formats are also supported.

    :::tip
    If you want to use a `js` file, do not name it `electron-builder.js`. It will [conflict](https://github.com/electron-userland/electron-builder/issues/6227) with the `electron-builder` package name.
    :::

    :::tip
    If you want to use [toml](https://en.wikipedia.org/wiki/TOML), install it with `npm install toml --save-dev`.
    :::

Most of the options accept `null` — for example, to explicitly set that DMG icon must be default volume icon from the OS and default rules must be not applied (i.e. use application icon as DMG icon), set `dmg.icon` to `null`.

## Artifact File Name Template

`${ext}` macro is supported in addition to [file macros](./file-patterns.md#file-macros).

## Environment Variables from File

Env file `electron-builder.env` in the current dir ([example](https://github.com/motdotla/dotenv-expand/blob/1cc80d02e1f8aa749253a04a2061c0fecb9bdb69/tests/.env)). Supported only for CLI usage.

## Effective Configuration

Before packaging, electron-builder writes the resolved configuration to `builder-effective-config.yaml` in the output directory (`directories.output`, `dist` by default). It is the merged result of `package.json`, the config file, presets and CLI overrides, plus values detected during the build such as `electronVersion`. Sensitive fields (passwords, tokens, `cscLink`) are masked.

By default the file is written only for local interactive builds, not on CI or when stdout is piped. Set `writeEffectiveConfig` to `true` to always write it (e.g. `electron-builder -c.writeEffectiveConfig=true` in a CI step), or to `false` to never write it.

## How to Read Docs

* Name of optional property is normal, **required** is bold.
* Type is specified after property name: `Array<String> | String`. Union like this means that you can specify or string (`**/*`), or array of strings (`["**/*", "!foo.js"]`).

### Common Configuration

  {!./app-builder-lib.Interface.CommonConfiguration.md!}

---

## Overridable per Platform Options

Following options can be set also per platform (top-level keys [mac](mac.md), [linux](linux.md) and [win](win.md)) if need.

## Base Configuration

  {!./app-builder-lib.Interface.PlatformSpecificBuildOptions.md!}

## Metadata
Some standard fields should be defined in the `package.json`.

  {!./app-builder-lib.Interface.Metadata.md!}

## Electron Download & Mirror

electron-builder downloads the Electron distribution to package via [`@electron/get`](https://github.com/electron/get). Point it at a mirror (e.g. a corporate proxy or a China mirror) with the `electronGet` option:

```json5
{
  "build": {
    "electronGet": {
      "mirrorOptions": { "mirror": "https://npmmirror.com/mirrors/electron/" }
    }
  }
}
```

:::note[v27: renamed from electronDownload]
This key was `electronDownload` in v26. In v27 it is `electronGet`, reshaped to `@electron/get` v5 options: the flat `mirror` moved under `mirrorOptions.mirror`, `isVerifyChecksum: false` became `unsafelyDisableChecksums: true`, and `cache` / `customDir` / `customFilename` / `strictSSL` / `force` were dropped. `electron-builder migrate-schema` performs the rename. See [v27 Breaking Changes → electronGet](./migration/v27-breaking-changes.md#electrondownload-electronget).
:::

## Build Version Management
`CFBundleVersion` (macOS) and `FileVersion` (Windows) will be set automatically to `version.build_number` on CI server (Travis, AppVeyor, CircleCI and Bamboo supported).

## Build Hooks

  {!./features/hooks.md!}

---

### Contents (`website/docs/contents.md`)

# Application Contents

This page covers how electron-builder determines which files are included in the packaged app and how to customize file inclusion.

## How electron-builder Collects Files

electron-builder uses a two-phase approach to build the app bundle:

1. **App files** — your application source code and assets, placed inside the ASAR archive (or directly in the app folder if ASAR is disabled)
2. **Extra files/resources** — additional files copied into specific locations in the output package, outside the ASAR archive

## Default File Inclusion

By default, electron-builder includes all files matching `**/*` from the app directory, minus a comprehensive set of automatically-excluded files:

**Always excluded:**
- `node_modules` dev dependencies (only production dependencies are included)
- `*.iml`, `*.o`, `*.hprof`, `*.pyc`, `*.pyo`, `*.rbc`, `*.swp`, `.csproj`, `.sln`, `.slnx`, `.xproj`
- `.editorconfig`, `._*`, `.DS_Store`, `.git`, `.hg`, `.svn`, `.gitignore`, `.gitattributes`
- `__pycache__`, `.flowconfig`, `.idea`, `.vs`, `.nyc_output`
- `appveyor.yml`, `.travis.yml`, `circle.yml`
- `npm-debug.log`, `yarn.lock`, `.yarn-integrity`, `.yarn-metadata.json`
- Test directories: `{test,__tests__,tests,powered-test,example,examples}` inside node_modules
- `*.d.ts` files inside node_modules
- `node_modules/.bin`

These exclusions are always applied even when you define custom patterns.

## The `files` Option

Override which files are included using the `files` option. Patterns are glob expressions relative to the app directory:

```yaml
files:
  - "**/*"
  - "!src/**"           # exclude source files
  - "!tests/**"         # exclude test directory
  - "!**/*.map"         # exclude source maps
  - "dist/**"           # include dist directory
```

**Important behavior:**
- If any pattern does NOT start with `!` (i.e., it's an include pattern), the default `**/*` pattern is NOT automatically prepended. You must include it explicitly if you want to start from all files.
- `package.json` and `node_modules/**/*` (production only) are always included regardless of patterns.
- All default ignores (listed above) are always applied.

### Simple Include Example

```yaml
files:
  - "**/*"
  - "!src/**"
  - "!*.ts"
  - "!tsconfig*.json"
```

### Only Include Specific Directories

```yaml
files:
  - dist/**
  - assets/**
  - node_modules/**     # production node_modules (already automatic)
  - package.json        # always included, but can be explicit
```

### Exclude Large Unused Files

```yaml
files:
  - "**/*"
  - "!node_modules/**/README*"
  - "!node_modules/**/*.md"
  - "!node_modules/**/*.map"
  - "!**/test/**"
  - "!**/__tests__/**"
```

## FileSet Objects

For advanced scenarios, use FileSet objects instead of (or alongside) glob strings. FileSets let you copy files from a different source directory:

```yaml
files:
  - from: dist
    to: .
    filter:
      - "**/*"
      - "!**/*.map"
  - from: assets
    to: assets
```

| Field | Description |
|---|---|
| `from` | Source path. Relative to the app directory for `files`; relative to project dir for `extraResources`/`extraFiles`. Defaults to the app directory. |
| `to` | Destination path. Relative to the ASAR root for `files`; relative to content dir for `extraFiles`; relative to resources dir for `extraResources`. |
| `filter` | Glob patterns relative to `from`. Defaults to `**/*`. |

### Renaming a File

```yaml
files:
  - from: dist/main.js
    to: app.js
```

### Copying from Outside the App Directory

```yaml
files:
  - "**/*"
  - from: ../shared/assets
    to: shared-assets
```

## Extra Files and Resources

Copy files into the packaged app outside the ASAR archive:

### `extraResources`

Copies into the `resources/` directory:
- macOS: `MyApp.app/Contents/Resources/`
- Windows/Linux: `resources/`

```yaml
extraResources:
  - from: native-binaries/
    to: bin
    filter:
      - "**/*"
  - "assets/icon.png"     # string shorthand — copies to resources/assets/icon.png
```

### `extraFiles`

Copies into the app content directory:
- macOS: `MyApp.app/Contents/`
- Windows/Linux: the install directory root

```yaml
extraFiles:
  - from: config/
    to: config
  - "LICENSE"
```

:::tip[Native Modules and Extra Binaries]
Use `extraResources` for native binaries, CLI tools, or data files that need to be accessible at runtime. Access them in your app via `process.resourcesPath`.
:::

## ASAR Packaging

ASAR is Electron's archive format — it bundles all app files into a single `app.asar` file for faster loading and to prevent casual file inspection.

```yaml
asar: {}    # ASAR is enabled by default — pass an object to configure, or set asar: false to disable
```

Set `asar: false` to disable ASAR packaging (all files are placed in the app directory directly). Useful for debugging or if you have unusual file access patterns.

### Unpacking Files from ASAR

Some files cannot be inside an ASAR archive:
- Native modules (`.node` files)
- Large binary assets that need random-access reads
- Files that need to be executed directly

Use `asar.unpack` to unpack specific files into `app.asar.unpacked/`:

```yaml
asar:
  unpack:
    - "**/*.node"                   # all native modules
    - "resources/ffmpeg"            # large binary
    - "**/**/node_modules/sharp/**" # specific module
```

Files in `app.asar.unpacked/` are accessible via the same paths as if they were in the ASAR — Electron transparently redirects reads.

### Smart Unpack

```yaml
asar:
  smartUnpack: true    # Default: true
```

When enabled, electron-builder automatically detects executables and native modules and unpacks them from ASAR. You generally don't need to configure `asar.unpack` manually unless you have unusual cases.

### ASAR Ordering

Optimize app startup time by specifying file ordering within the ASAR archive. Files listed first are packed first, enabling faster sequential reads at startup:

```yaml
asar:
  ordering: build/asar-ordering.txt
```

```
# build/asar-ordering.txt — list files loaded at startup first
index.js
renderer.js
node_modules/electron/index.js
```

## Practical Examples

### Electron App with Native Modules

```yaml
files:
  - "**/*"
  - "!src/**"
  - "!**/*.ts"
  - "!**/*.map"
asar:
  unpack:
    - "**/*.node"
```

### App with Large Video Assets

```yaml
files:
  - "**/*"
  - "!src/**"
  - "!videos/**"      # exclude from ASAR
extraResources:
  - from: videos/
    to: videos         # place outside ASAR, accessible via process.resourcesPath
```

### Monorepo Setup (Two-Package Structure)

```yaml
directories:
  app: packages/app   # app directory is not the project root

files:
  - "**/*"
  - "!src/**"
```

See [Two package.json Structure](tutorials/two-package-structure.md) for the full monorepo setup.

## Including Default-Excluded Files

electron-builder excludes a set of development-only file extensions and names by default — for example
build artifacts such as `.obj`, `.o`, `.a`, and `.d.ts`, lockfiles such as `yarn.lock` and
`package-lock.json`, and VCS/tooling metadata such as `.git`, `.idea`, and `.github`.

To ship a file that would otherwise be excluded, add an explicit `files` glob that targets it. An
explicit include overrides the matching default exclusion:

```yaml
files:
  - "**/*"
  - "**/*.obj"        # keep Wavefront .obj 3D models that are excluded by default
```

The same applies to default-excluded names — reference the name in a pattern segment, e.g.:

```yaml
files:
  - "**/*"
  - "**/.github/**"   # keep the .github directory that is excluded by default
```

Only patterns that name an extension or directory concretely opt it back in; broad patterns such as
`**/*` continue to honor the defaults.

## Troubleshooting

**File is missing from packaged app:** Check if the file is being excluded by a pattern. Run `DEBUG=electron-builder electron-builder build` to see detailed file collection output.

**Native module crashes at runtime:** The module likely needs to be outside the ASAR archive. Add it to `asar.unpack`:
```yaml
asar:
  unpack:
    - "node_modules/better-sqlite3/**"
```

**App directory is too large:** Profile what's included by examining the app bundle after building. Common culprits: source maps (`*.map`), TypeScript source files (`*.ts`), and large test fixtures.

**FileSet `from` path not found:** Remember that `from` in `files` is relative to the app directory. For `extraResources`/`extraFiles`, it's relative to the project root.

## File Contents Configuration

{!./app-builder-lib.Interface.FilesBuildOptions.md!}

## FileSet Configuration

{!./app-builder-lib.Interface.FileSet.md!}

---

### Dmg (`website/docs/dmg.md`)

---
title: "DMG"
---

The top-level [dmg](configuration.md) key contains a set of options instructing electron-builder on how it should build [DMG](https://en.wikipedia.org/wiki/Apple_Disk_Image) (macOS disk image) files.

## When to Use DMG

DMG is the standard distribution format for direct-download macOS apps. Users mount the disk image, drag the app to their Applications folder, and eject the image. Use DMG when:

- Distributing directly from your website or GitHub Releases
- Using [electron-updater](features/auto-update.md) (the `zip` target also runs alongside DMG)
- You want the most familiar macOS install experience

For Mac App Store distribution, see [MAS](mas.md). For system-level installs (kernel extensions, launch daemons), see [PKG](pkg.md).

## Window Customization

The `window` option controls the Finder window that appears when the DMG is mounted:

```yaml
dmg:
  window:
    position:
      x: 130
      y: 220
    size:
      width: 540
      height: 380
```

Standard DMG windows are 540×380 pixels. The position coordinates place the window on screen — `x: 130, y: 220` is a common convention.

## Background Image

Set a background image to make your DMG installer look polished:

```yaml
dmg:
  background: build/background.png
```

**Requirements:**
- Format: PNG (recommended for quality and transparency support)
- Size: match your `window.size` — typically 540×380 pixels
- For Retina displays: provide a `@2x` version at 1080×760 and name it `background@2x.png`. electron-builder will automatically use the high-resolution version on Retina screens.

If no background image is set, use `backgroundColor` to set a solid color:

```yaml
dmg:
  backgroundColor: "#2c2c2c"
```

## Icon Layout

The `contents` array defines the position and type of icons inside the DMG window. A typical layout has the app icon on the left and a `/Applications` shortcut on the right:

```yaml
dmg:
  contents:
    - x: 130
      y: 220
      type: file
    - x: 410
      y: 220
      type: link
      path: /Applications
```

| Field | Description |
|---|---|
| `x`, `y` | Position in the DMG window (pixels from top-left) |
| `type` | `file` — the app itself; `link` — a symlink (e.g., to `/Applications`) |
| `path` | Only for `link` type — the symlink destination |
| `name` | Optional display name override |

:::tip[Centering Icons]
For a 540×380 window with two icons, the standard horizontal positions are x=130 (app) and x=410 (Applications link), both at y=220 (vertically centered).
:::

## Volume Icon and Title

The DMG volume appears in the macOS Finder sidebar when mounted. Customize its appearance:

```yaml
dmg:
  # Badge icon shown in Finder when the DMG is mounted
  badgeIcon: build/icon.icns

  # Volume icon (what appears as the disk image itself in Finder)
  icon: build/volume-icon.icns

  # Title shown in the Finder window title bar when mounted
  title: "${productName} ${version}"

  # Icon size inside the DMG window
  iconSize: 80

  # Font size for icon labels
  iconTextSize: 12
```

The default `title` is `${productName} ${version}`. The default `iconSize` is 80 pixels.

## Disk Image Format

The `format` option controls the compression algorithm:

| Format | Description | Use Case |
|---|---|---|
| `UDZO` | zlib-compressed (default) | Good balance of size and compatibility |
| `ULFO` | LZFSE-compressed | Faster decompression, macOS 10.11+ only |
| `ULMO` | LZMA-compressed | Best compression, slower to mount, macOS 10.15+ only |
| `UDBZ` | bzip2-compressed | Better compression than UDZO, slower |
| `UDRO` | Read-only, uncompressed | Largest size, fastest to open |
| `UDRW` | Read-write | Only for development/testing |

For most cases, leave `format` at the default `UDZO`. Use `ULFO` if you are targeting macOS 10.11+ and want faster mount times for large apps. Use `ULMO` if you are targeting macOS 10.15+ and want the smallest download — it typically compresses an Electron app's DMG ~30% smaller than `UDZO`, at the cost of a few extra seconds to mount and copy at install time.

## Volume Filesystem

The `filesystem` option controls the filesystem of the DMG volume:

| Value | Notes |
|---|---|
| `APFS` | **Default as of v27.** The modern macOS filesystem — smaller, faster-to-mount images on current macOS. Cannot be mounted on macOS **before 10.13** (High Sierra). |
| `HFS+` | The pre-v27 default. Set this **only** if you must support pre-10.13 macOS. |

```yaml
dmg:
  filesystem: "HFS+"   # only for pre-10.13 (High Sierra) compatibility
```

:::note[v27 default change]
The default changed from `HFS+` to `APFS` in v27. This is a runtime default, not a config-key rename, so `migrate-schema` does not change it — set `filesystem: "HFS+"` explicitly if you need the old behavior. See [v27 Breaking Changes → DMG filesystem](./migration/v27-breaking-changes.md#dmg-filesystem-defaults-to-apfs).
:::

## DMG Size

The `size` option sets the initial filesystem size. Normally this is computed automatically — only set it if you encounter errors about insufficient space during DMG creation:

```yaml
dmg:
  size: "500m"
```

Set `shrink: false` to disable automatic shrinking of the DMG to the minimum size after creation (not recommended for production).

## Signing the DMG

The DMG itself can be signed separately from the app bundle inside it:

```yaml
dmg:
  sign: true   # Default: false
```

:::note
The app bundle inside the DMG is always signed (when signing is configured). The `sign` option here refers to signing the DMG container file itself. Signing the DMG is not required for Gatekeeper but may be requested in some enterprise environments.
:::

## Internet-Enabled DMGs

```yaml
dmg:
  internetEnabled: true   # Default: false
```

Internet-enabled DMGs automatically extract their contents and eject after first open. This is an older pattern and is no longer recommended for modern apps.

## DMG License

To display a license agreement when users open the DMG, set the `license` option in your DMG configuration or use the file-naming convention.

### Configuration-based (recommended)

Specify the license file path directly in `electron-builder.yml`:

```yaml
dmg:
  license: "build/license.rtf"
```

For multi-language builds, provide a language-code → file-path map:

```yaml
dmg:
  license:
    en_US: "build/license.rtf"
    de_DE: "build/license_de.txt"
    fr_FR: "build/license_fr.txt"
    ja_JP: "build/license_ja.txt"
    zh_CN: "build/license_zh.txt"
```

Language codes use the `ll_CC` format (ISO 639-1 language code + ISO 3166-1 country code, underscore-separated). The first entry in the map is the fallback shown when the user's language is not present.

Supported file formats: `.rtf` (recommended for rich text), `.txt` (plain text), `.html`.

:::tip[RTF recommended]
Use `.rtf` format for the best cross-language compatibility. Plain `.txt` files for Japanese, Korean, and Simplified/Traditional Chinese require macOS's CJK codec support in the bundled Python runtime.
:::

### File-naming convention (automatic discovery)

As an alternative, place license files in your [build resources](configuration.md) directory using this naming pattern — electron-builder will pick them up automatically:

| Filename | Language |
|---|---|
| `license_en.txt` or `eula_en.txt` | English |
| `license_de.rtf` | German |
| `license_fr.html` | French |
| `license_ja.txt` | Japanese |
| `license_zh_CN.txt` | Simplified Chinese |

The language code is derived from the `_LANG` suffix. The user's macOS language preference determines which license is shown; `en_US` is the fallback.

:::note[One file per language]
Having both `license_en.txt` **and** `eula_en.txt` in the same build resources directory is an error — both map to `en_US` and electron-builder will throw an `InvalidConfigurationError`. Use the config-based `license` map if you need to control which file is used.
:::

### Customizing License Button Labels

Override the default button text (Agree / Disagree / Print / Save) for each language by creating `licenseButtons_LANG.json` or `licenseButtons_LANG.yml` files in your build resources directory:

```json
{
  "languageName": "English",
  "agree": "Accept",
  "disagree": "Decline",
  "print": "Print",
  "save": "Save",
  "message": "Please read the license agreement below."
}
```

Name the file `licenseButtons_en.json` for English, `licenseButtons_de.json` for German, etc. The `message` field sets the prompt text at the top of the dialog. The legacy `description` field is also accepted as an alias for `message`.

## Complete Example

```yaml
dmg:
  background: build/dmg-background.png
  window:
    position:
      x: 130
      y: 220
    size:
      width: 540
      height: 380
  contents:
    - x: 130
      y: 220
      type: file
    - x: 410
      y: 220
      type: link
      path: /Applications
  iconSize: 80
  iconTextSize: 12
  title: "${productName} ${version}"
  format: UDZO
  # Single-language license
  license: "build/license.rtf"
```

Multi-language example:

```yaml
dmg:
  background: build/dmg-background.png
  license:
    en_US: "build/license.rtf"
    de_DE: "build/license_de.txt"
    fr_FR: "build/license_fr.txt"
  window:
    size:
      width: 540
      height: 380
  format: UDZO
```

## Troubleshooting

**Icons appear in wrong positions:** The `x` and `y` values in `contents` are absolute pixels from the top-left of the DMG window. Double-check that your coordinates are within the `window.size` bounds.

**Background not appearing:** Ensure the background image exactly matches the `window.size` dimensions. For Retina support, provide a `@2x` version at double the resolution.

**DMG too large:** The default `UDZO` format compresses well. If size is critical, try `UDBZ` for better (slower) compression.

**License not appearing:** If using the config option, verify the `license` path resolves relative to your build resources directory or project root. If using the file-naming convention, confirm the file is in the build resources directory with the correct pattern (`license_en.txt`, `eula_en.txt`, etc.) and a supported extension (`.rtf`, `.txt`, `.html`).

**"Multiple license files found" error:** You have both `license_en.txt` and `eula_en.txt` (or two files that resolve to the same language code) in your build resources. Remove the duplicate or use the config-based `license` map to point to the exact file you want.

**CJK license not appearing:** Japanese, Korean, and Chinese plain-text licenses require CJK codec support in the dmgbuild Python runtime. Use `.rtf` format for these languages, or ensure your dmgbuild bundle version is `1.2.5` or later.

## Configuration

{!./app-builder-lib.Interface.DmgOptions.md!}

---

### Donate (`website/docs/donate.md`)

---
title: Donate
---

electron-builder is free, open-source software driven through personal development time and community contributions.

This open-source work is done during free time. If you find it valuable, please consider donating!

<iframe src="https://github.com/sponsors/mmaietta/button" title="Sponsor mmaietta" height="32" width="114" style="border: 0; border-radius: 6px;"></iframe>

---

### Environment Variables (`website/docs/environment-variables.md`)

---
title: "Environment Variables"
---

# Environment Variables

electron-builder reads a handful of environment variables — almost all of them **secrets** (signing certificates, publish tokens) or **CI-provided** values (the release tag, repository slug). Everything else is configured through your [configuration file](./configuration.md), not the environment.

This page is a grouped reference for every environment variable electron-builder itself reads. For the deeper "how", each group links to the relevant guide.

:::warning[Upgrading from v26?]
Several environment variables were **removed in v27** — most notably `CI_BUILD_TAG` (use `CI_COMMIT_TAG`), `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY`, and every toolset-override variable (`USE_SYSTEM_WINE`, `ELECTRON_BUILDER_NSIS_DIR`, …). See [Removed in v27](#removed-in-v27) below and the [v27 breaking changes](./migration/v27-breaking-changes.md).
:::

---

## Code signing

Signing credentials are read from the environment so certificates and passwords never have to live in your config. **Never commit them to source control** — inject them via CI secrets. Each value accepts an HTTPS URL, a `file://` path, a local path, or a base64-encoded certificate string.

### Shared / macOS

| Variable | Description |
|---|---|
| `CSC_LINK` | The certificate (`.p12`/`.pfx`) — HTTPS URL, `file://` path, local path, or base64 string. |
| `CSC_KEY_PASSWORD` | Password that decrypts the certificate at `CSC_LINK`. |

### macOS installer (PKG)

| Variable | Description |
|---|---|
| `CSC_INSTALLER_LINK` | Certificate for signing PKG installers (a Developer ID **Installer** certificate). |
| `CSC_INSTALLER_KEY_PASSWORD` | Password for the installer certificate. |

### Windows

| Variable | Description |
|---|---|
| `WIN_CSC_LINK` | Windows certificate. Falls back to `CSC_LINK` if unset. |
| `WIN_CSC_KEY_PASSWORD` | Password for the Windows certificate. Falls back to `CSC_KEY_PASSWORD` if unset. |

:::tip[Cross-compiling Windows on macOS]
Use `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` to supply a separate Windows certificate while keeping your macOS certificate in `CSC_LINK` / `CSC_KEY_PASSWORD`.
:::

:::note[More signing variables]
The macOS keychain/identity variables (`CSC_NAME`, `CSC_IDENTITY_AUTO_DISCOVERY`, `CSC_KEYCHAIN`) and Azure Trusted Signing variables (`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`) are documented in the code-signing guides: [Code Signing](./features/code-signing/code-signing.md), [macOS](./features/code-signing/code-signing-mac.md), and [Windows](./features/code-signing/code-signing-win.md). In v27, `WIN_CSC_LINK` fallback applies only to `signtool` mode; `hsm`, `pkcs11`, and `azure` are configured through [`win.sign`](./migration/v27-breaking-changes.md#windows-signing-winsign).
:::

## Notarization (macOS)

For notarizing macOS apps distributed outside the Mac App Store. Provide **one** of the three credential sets. Full setup is in the [Notarization guide](./features/code-signing/notarization.md).

| Variable | Description |
|---|---|
| `APPLE_ID` | Apple ID email (Apple-ID auth). |
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password for that Apple ID. |
| `APPLE_TEAM_ID` | Apple Developer Team ID. |
| `APPLE_API_KEY` | Path to the App Store Connect API key (`.p8`). |
| `APPLE_API_KEY_ID` | Key ID for the App Store Connect API key. |
| `APPLE_API_ISSUER` | Issuer ID for the App Store Connect API key. |
| `APPLE_KEYCHAIN` | Keychain to search for a stored notarization profile. |
| `APPLE_KEYCHAIN_PROFILE` | Name of a `notarytool` keychain profile to use. |

---

## Publishing

Publish providers read their credentials from the environment. Publishing is **never** automatic — you must pass `--publish` explicitly (see [Publish](./publish.md)).

### GitHub

| Variable | Description |
|---|---|
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token. If either is defined, publishing defaults to `[{ provider: "github" }]`. |
| `GITHUB_RELEASE_TOKEN` | If defined, used **instead of** `GH_TOKEN`/`GITHUB_TOKEN` to publish the release (so you can keep a read-only token for update checks and a read-write token for releasing). |

### GitLab

| Variable | Description |
|---|---|
| `GITLAB_TOKEN` | GitLab token. Required to publish to the `gitlab` provider. |

### Bitbucket

| Variable | Description |
|---|---|
| `BITBUCKET_TOKEN` | Bitbucket token. Required to publish to the `bitbucket` provider. |
| `BITBUCKET_USERNAME` | Bitbucket username / Atlassian account email — selects the auth scheme (see below). |

:::warning[v27 auth change]
In v27 the Bitbucket publisher selects its auth scheme by whether a username is present:

- **`BITBUCKET_USERNAME` set** → HTTP **Basic** auth. Use this for an **app password** or an Atlassian **API token**.
- **No username** → the token is sent as `Authorization: Bearer <token>` (a repository / project / workspace **access token**).

If `BITBUCKET_TOKEN` holds an app password or API token, you **must** also set `BITBUCKET_USERNAME`. See [the breaking-change note](./migration/v27-breaking-changes.md#bitbucket-cloud-publishing-token-without-username-uses-bearer-auth).
:::

### Keygen

| Variable | Description |
|---|---|
| `KEYGEN_TOKEN` | Keygen token. If defined (and no GitHub token is), publishing defaults to `[{ provider: "keygen" }]`. |

### Cloudflare R2

| Variable | Description |
|---|---|
| `CF_R2_ACCESS_KEY_ID` | R2 access key ID (an R2 API token with Object Read & Write). |
| `CF_R2_SECRET_ACCESS_KEY` | R2 secret access key. |

### Amazon S3

| Variable | Description |
|---|---|
| `AWS_ACCESS_KEY_ID` | AWS access key ID. |
| `AWS_SECRET_ACCESS_KEY` | AWS secret access key. |
| `AWS_SESSION_TOKEN` | Optional session token for temporary credentials. |
| `AWS_PROFILE` | Named profile in `~/.aws/credentials` to use when the keys above are not set (default: `default`). |

### DigitalOcean Spaces

| Variable | Description |
|---|---|
| `DO_KEY_ID` | Spaces access key ID. |
| `DO_SECRET_KEY` | Spaces secret access key. |

### Snap Store

| Variable | Description |
|---|---|
| `SNAP_CSC_LINK` | Base64-encoded Snap Store credentials or a file path (the CI-friendly equivalent of `snapcraft.cscLink`). Used for Snap Store publishing and Launchpad remote builds. |
| `SNAPCRAFT_STORE_CREDENTIALS` | Plain-text macaroon, read directly by `snapcraft`. |

See [Snap → Authentication](./snap.md#authentication) for the credential resolution order.

---

## CI and release tags

electron-builder detects the release tag and repository from standard CI variables. These are normally **set by your CI provider**, not by you — the exception is `CI_COMMIT_TAG`, which you can set manually (e.g. on GitLab or self-hosted CI) to provide the tag.

| Variable | Description |
|---|---|
| `CI_COMMIT_TAG` | The release tag (GitLab's standard variable). Set this manually where no other tag variable is present. |
| `TRAVIS_TAG` | Tag on Travis CI. |
| `APPVEYOR_REPO_TAG_NAME` | Tag on AppVeyor. |
| `CIRCLE_TAG` | Tag on CircleCI. |
| `BITRISE_GIT_TAG` | Tag on Bitrise. |
| `BITBUCKET_TAG` | Tag on Bitbucket Pipelines. |
| `GITHUB_REF_TYPE` / `GITHUB_REF_NAME` | On GitHub Actions, the tag is taken from `GITHUB_REF_NAME` when `GITHUB_REF_TYPE` is `tag`. |

The GitHub repository is auto-detected from `TRAVIS_REPO_SLUG`, `APPVEYOR_REPO_NAME`, or `CIRCLE_PROJECT_USERNAME`/`CIRCLE_PROJECT_REPONAME` (falling back to your `package.json` `repository` field or `.git/config`).

:::warning[CI_BUILD_TAG removed in v27]
`CI_BUILD_TAG` was **removed** — use `CI_COMMIT_TAG` instead. See [the breaking-change note](./migration/v27-breaking-changes.md#ci_build_tag-environment-variable).
:::

---

## Cache & download

| Variable | Description |
|---|---|
| `ELECTRON_BUILDER_CACHE` | Overrides the cache directory used for downloaded Electron binaries and toolsets. Must be an **absolute** path. Defaults to `~/Library/Caches/electron-builder` (macOS), `%LOCALAPPDATA%` (Windows), or `$XDG_CACHE_HOME` / `~/.cache` (Linux). |
| `USE_HARD_LINKS` | Set to `"true"` to copy files via hard links where possible (faster, less disk use). Ignored on Windows. |

:::note[Electron mirror is config, not an env var]
To download Electron from a mirror, configure [`electronGet.mirrorOptions`](./migration/v27-breaking-changes.md#electrondownload-electronget) in your build config — e.g. `electronGet: { mirrorOptions: { mirror: "https://my-mirror/" } }`. (In v26 this was `electronDownload.mirror`.) See [Configuration](./configuration.md).
:::

---

## Debugging

| Variable | Description |
|---|---|
| `DEBUG=electron-builder` | Enables verbose debug logging via the [`debug`](https://www.npmjs.com/package/debug) package. |

```bash
DEBUG=electron-builder electron-builder --linux snap
```

Namespaced channels are also available — e.g. `DEBUG=electron-builder:7z` for archive operations, or `DEBUG=electron-builder*` for everything.

---

## Removed in v27

These variables were removed in v27. `electron-builder migrate-schema` does **not** rewrite them — update your scripts and config manually.

| Removed variable | Replacement |
|---|---|
| `CI_BUILD_TAG` | [`CI_COMMIT_TAG`](#ci-and-release-tags) |
| `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY` | The [`ignoredProductionDependencies`](./configuration.md) build option — see the [breaking-change note](./migration/v27-breaking-changes.md#redundant-production-dependencies-are-excluded-not-rejected). |

### Toolset overrides

The environment variables that pointed electron-builder at custom build-tool bundles are **all removed**. Supply a custom bundle through a [`ToolsetCustom`](./migration/v27-breaking-changes.md#toolset-env-var-overrides-removed) object on the relevant `toolsets` key instead (`toolsets.<name>: { url, checksum }`).

| Removed variable | What it controlled |
|---|---|
| `USE_SYSTEM_WINE` | Forced host-installed Wine instead of the bundle. Replaced by `toolsets.wine: "system"`. |
| `USE_SYSTEM_SIGNCODE` | Forced host `signtool`/`signcode`. |
| `USE_SYSTEM_OSSLSIGNCODE` | Forced host `osslsigncode`. |
| `USE_SYSTEM_FPM` | Forced host-installed `fpm`. |
| `APPIMAGE_TOOLS_PATH` | AppImage build tools (`mksquashfs`, runtime). |
| `LINUX_TOOLS_MAC_PATH` | Linux-tools-mac bundle (`ar`, `lzip`, `gtar`). |
| `CUSTOM_FPM_PATH` | FPM executable. |
| `ELECTRON_BUILDER_NSIS_DIR` | NSIS compiler bundle directory. |
| `ELECTRON_BUILDER_NSIS_RESOURCES_DIR` | NSIS resources/plugins directory. |
| `CUSTOM_NSIS_RESOURCES` | Alternate NSIS resources bundle. |
| `ELECTRON_BUILDER_WINE_TOOLSET_DIR` | Wine bundle directory. |

The two signing `USE_SYSTEM_*` variables (`USE_SYSTEM_SIGNCODE`, `USE_SYSTEM_OSSLSIGNCODE`) have **no env-var replacement** — configure signing through [`win.sign`](./migration/v27-breaking-changes.md#windows-signing-winsign) and the `winCodeSign` toolset. For the full replacement mechanics (archive formats, checksums, `file://` directories), see [Toolset env-var overrides removed](./migration/v27-breaking-changes.md#toolset-env-var-overrides-removed).

---

### File Patterns (`website/docs/file-patterns.md`)

---
title: "File Patterns"
---

* `*` Matches 0 or more characters in a single path portion
* `?` Matches 1 character
* `[...]` Matches a range of characters, similar to a RegExp range.
  If the first character of the range is `!` or `^` then it matches
  any character not in the range.
* `!(pattern|pattern|pattern)` Matches anything that does not match
  any of the patterns provided.
* `?(pattern|pattern|pattern)` Matches zero or one occurrence of the
  patterns provided.
* `+(pattern|pattern|pattern)` Matches one or more occurrences of the
  patterns provided.
* `*(a|b|c)` Matches zero or more occurrences of the patterns provided
* `@(pattern|pat*|pat?erN)` Matches exactly one of the patterns
  provided
* `**` If a "globstar" is alone in a path portion, then it matches
  zero or more directories and subdirectories searching for matches.
  It does not crawl symlinked directories.

If directory matched, all contents are copied. So, you can just specify `foo` to copy `foo` directory.

:::tip[Excluding directories]
Remember that `!doNotCopyMe/**/*` would match the files *in* the `doNotCopyMe` directory, but not the directory itself, so the [empty directory](https://github.com/gulpjs/gulp/issues/165#issuecomment-32613179) would be created.
Solution — use macro `${/*}`, e.g. `!doNotCopyMe${/*}`.
:::

## Multiple Glob Patterns
 ```js
 [
   // match all files
   "**/*",

   // except for js files in the foo/ directory
   "!foo/*.js",

   // unless it's foo/bar.js
   "foo/bar.js",
 ]
 ```

## Default Ignored Files

electron-builder excludes a set of files from the packaged app by default (development files, and certain extensions/names such as Wavefront `.obj` 3D models). In **v27** the `disableDefaultIgnoredFiles` option was removed. To keep a file that is excluded by default, add an explicit `files` glob that **names it concretely** — an explicit include now overrides the matching default exclusion:

```json5
{
  "build": {
    "files": [
      "**/*",
      "**/*.obj"  // opt a default-excluded extension back in
    ]
  }
}
```

Broad patterns such as `**/*` still honor the defaults; only a pattern that names the extension or directory concretely opts it back in. See [v27 Breaking Changes → `disableDefaultIgnoredFiles`](./migration/v27-breaking-changes.md#disabledefaultignoredfiles).

## File Macros

You can use macros in the file patterns, artifact file name patterns and publish configuration url:

* `${arch}` — expanded to `ia32`, `x64`. If no `arch`, macro will be removed from your pattern with leading space, `-` and `_` (so, you don't need to worry and can reuse pattern).
* `${os}` — expanded to `mac`, `linux` or `win` according to target platform.
* `${platform}` — expanded to `darwin`, `linux` or `win32` according to Node.js `process.platform` property.
* `${name}` – `package.json` `name`.
* `${productName}` — [Sanitized](https://www.npmjs.com/package/sanitize-filename) product name.
* `${version}`
* `${channel}` — detected prerelease component from version (e.g. `beta`).
* `${env.ENV_NAME}` — any environment variable.
* Any property of [AppInfo](./api/index.md) (e.g. `buildVersion`, `buildNumber`).

---

### Flatpak (`website/docs/flatpak.md`)

:::warning[Single-file Flatpak bundles]
Currently `electron-builder` does **not** support publishing apps to Flatpak repositories like [Flathub](https://flathub.org/). Flatpak support in `electron-builder` is limited to generating [single-file bundles](https://docs.flatpak.org/en/latest/single-file-bundles.html) which have limitations compared to apps installed from a repository.
:::

The top-level [flatpak](configuration.md) key contains a set of options instructing electron-builder on how it should build a [Flatpak](https://flatpak.org/) bundle.

## What is Flatpak?

Flatpak is a universal Linux packaging system that runs apps in a sandbox with controlled access to system resources. Unlike AppImage (no sandbox) and Snap (Ubuntu-primary), Flatpak is designed to be distro-agnostic and runs on any Linux distribution with `flatpak` installed.

Key characteristics:
- **Sandboxed** — apps run in an isolated environment with portal-based access to system resources
- **Cross-distro** — works on Ubuntu, Fedora, Arch, Debian, and more
- **Runtime-based** — apps are built against a specific runtime (freedesktop, GNOME, KDE)
- **Distributed via Flathub** (for repository-based distribution — note: currently not supported by electron-builder)

## Flatpak vs. AppImage vs. Snap

| Aspect | Flatpak | AppImage | Snap |
|---|---|---|---|
| Sandboxed | Yes (portals) | No | Yes (interfaces) |
| Distro support | Any (flatpak required) | Universal | Any (snapd required) |
| Store/repo | Flathub (not via eb) | N/A | Snap Store |
| Auto-update | Flathub (not via eb) | AppImageUpdate | Snap Store |
| electron-builder output | Single-file bundle | Single file | Package |

## Build Requirements

Install `flatpak` and `flatpak-builder` on your Linux build machine — see [Flatpak setup](https://flatpak.org/setup/).

## Runtime and SDK

Flatpak apps run against a runtime that provides base libraries. Choose the runtime that matches your app's dependencies:

```yaml
flatpak:
  runtime: org.freedesktop.Platform         # freedesktop (most common for Electron)
  sdk: org.freedesktop.Sdk
  runtimeVersion: "24.08"                   # runtime version
```

Common runtimes for Electron apps:

| Runtime | SDK | Use Case |
|---|---|---|
| `org.freedesktop.Platform` | `org.freedesktop.Sdk` | Standard Electron apps (recommended) |
| `org.gnome.Platform` | `org.gnome.Sdk` | GNOME-integrated apps |
| `org.kde.Platform` | `org.kde.Sdk` | KDE-integrated apps |

Install the runtime on your build machine before building — see [available Flatpak runtimes](https://docs.flatpak.org/en/latest/available-runtimes.html).

## Finish Args (Permissions)

The `finishArgs` array defines what the sandboxed app is allowed to access. Electron apps need several permissions:

```yaml
flatpak:
  finishArgs:
    # Display
    - "--socket=x11"
    - "--socket=wayland"
    - "--share=ipc"              # shared memory with X11 compositor
    # Network
    - "--share=network"
    # Filesystem
    - "--filesystem=home"        # access user home directory
    # Desktop integration
    - "--talk-name=org.freedesktop.Notifications"
    - "--talk-name=org.kde.StatusNotifierWatcher"
    - "--talk-name=com.canonical.AppMenu.Registrar"
    # Electron-specific
    - "--device=dri"             # GPU access
    - "--socket=pulseaudio"      # audio
```

**Common finish args for Electron:**

| Arg | Purpose |
|---|---|
| `--socket=x11` | X11 display |
| `--socket=wayland` | Wayland display |
| `--share=ipc` | Shared memory (required for X11) |
| `--share=network` | Network access |
| `--filesystem=home` | Home directory access |
| `--device=dri` | GPU/DRM device (for WebGL, video decode) |
| `--socket=pulseaudio` | Audio output |
| `--talk-name=org.freedesktop.Notifications` | Desktop notifications |
| `--filesystem=xdg-download` | Downloads folder only (less than `--filesystem=home`) |

## Branch

```yaml
flatpak:
  branch: stable    # Default: master/main
```

The branch name is embedded in the Flatpak bundle and visible to users.

## Extra Modules

Include additional system libraries not in the runtime:

```yaml
flatpak:
  modules:
    - name: zlib
      buildsystem: autotools
      sources:
        - type: archive
          url: https://www.zlib.net/zlib-1.3.tar.gz
          sha256: "..."
```

## Custom Files and Symlinks

Copy additional files into the app bundle:

```yaml
flatpak:
  files:
    - - build/extra-binary        # source
      - /app/bin/extra-binary     # destination (absolute within Flatpak)
  symlinks:
    - - /app/bin/myapp            # target
      - /app/bin/myapp-alias      # symlink location
```

## Wayland Support

```yaml
flatpak:
  useWaylandFlags: true   # Appends --enable-features=UseOzonePlatform --ozone-platform=wayland when running under a Wayland session
```

## Local Testing

Build and install the Flatpak locally:

```bash
electron-builder --linux flatpak
# This produces a .flatpak bundle file

# Install it (no root required)
flatpak install --user dist/myapp-1.0.0.flatpak

# Run it
flatpak run com.mycompany.myapp
```

## Troubleshooting

If the build fails, enable debug logging to see the underlying `flatpak-builder` output:

```bash
env DEBUG="@malept/flatpak-bundler" electron-builder build --linux flatpak
```

**Missing runtime:** Install the runtime before building — see the Runtime section above.

**Sandbox permission denied:** Check the `finishArgs` — the app is likely trying to access something not in its allowed permissions. Run `flatpak run --log-session-bus com.mycompany.myapp` to see D-Bus access denials.

**GPU rendering issues:** Ensure `--device=dri` is in `finishArgs` for WebGL support.

## Configuration

{!./app-builder-lib.Interface.FlatpakOptions.md!}

---

### Github Actions (`website/docs/github-actions.md`)

# GitHub Actions CI/CD

This guide shows how to build, sign, and publish Electron apps using GitHub Actions for all three platforms (macOS, Windows, Linux).

## Basic Concepts

- **Secrets** — store certificates, passwords, and API tokens in [GitHub repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) (Settings → Secrets and variables → Actions)
- **Matrix builds** — build for multiple platforms in parallel using a matrix strategy
- **`GH_TOKEN`** — the GitHub token electron-builder uses to upload release assets. Use `GITHUB_TOKEN` (provided automatically) or a PAT with `repo` scope

## Minimal Multi-Platform Workflow

This workflow builds for macOS, Windows, and Linux on every push to `main` and on version tags:

```yaml
name: Build and Release

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npx electron-builder --publish never
        env:
          # macOS signing
          CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
          CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
          # Windows signing
          WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist-${{ matrix.os }}
          path: dist/
```

## Publish on Tag Push

To publish GitHub releases automatically when you push a version tag (`v1.2.3`):

```yaml
name: Release

on:
  push:
    tags: ['v*']

jobs:
  release:
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Publish
        run: npx electron-builder --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # macOS signing + notarization
          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 }}
          # Windows signing
          WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
```

The `--publish always` flag uploads artifacts to the GitHub release for the pushed tag. If no release exists, electron-builder creates a draft release.

## Draft Release Workflow

A common pattern is to publish a **draft** release on every tagged push, then manually promote it to a full release after reviewing:

```yaml
# In electron-builder.yml
publish:
  provider: github
  releaseType: draft
```

Then in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".

## macOS Code Signing Setup

### Preparing the Certificate

```bash
# Export your Developer ID Application certificate from Keychain Access
# File → Export Items → save as .p12 with a password

# Encode to base64
base64 -i certificate.p12 | pbcopy   # copies to clipboard
```

Add to GitHub Secrets:
- `MAC_CSC_LINK` — the base64-encoded `.p12` content
- `MAC_CSC_KEY_PASSWORD` — the certificate password

### macOS Signing

Pass `CSC_LINK` and `CSC_KEY_PASSWORD` directly — electron-builder creates and manages a temporary keychain automatically:

```yaml
  build-mac:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build and notarize
        run: npx electron-builder --mac --publish always
        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 }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### macOS Secrets Reference

| Secret | How to Get |
|---|---|
| `MAC_CSC_LINK` | `base64 -i certificate.p12` |
| `MAC_CSC_KEY_PASSWORD` | The password you set when exporting the `.p12` |
| `APPLE_ID` | Your Apple ID email (developer account) |
| `APPLE_APP_SPECIFIC_PASSWORD` | Create at [appleid.apple.com](https://appleid.apple.com) → App-Specific Passwords |
| `APPLE_TEAM_ID` | 10-character team ID from [developer.apple.com/account](https://developer.apple.com/account) |

## Windows Code Signing Setup

```bash
# Encode your Windows certificate (.pfx) to base64
# PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Out-File cert_encoded.txt

# bash (Linux/macOS):
base64 certificate.pfx > cert_encoded.txt
```

Add to GitHub Secrets:
- `WIN_CSC_LINK` — base64-encoded `.pfx` content
- `WIN_CSC_KEY_PASSWORD` — certificate password

### Using Azure Trusted Signing

```yaml
      - name: Build and sign (Azure)
        run: npx electron-builder --win --publish always
        env:
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Configure `win.sign` with `type: "azure"` in your electron-builder config. See [Windows Code Signing](features/code-signing/code-signing-win.md#azure-trusted-signing-type-azure-beta).

## Caching for Faster Builds

Cache npm packages and Electron binaries to avoid redundant downloads:

```yaml
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm    # caches ~/.npm

      - name: Cache Electron
        uses: actions/cache@v4
        with:
          path: |
            ~/.cache/electron
            ~/.cache/electron-builder
          key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-electron-
```

## Linux Snapcraft Publishing

To publish to the Snap Store, add a `SNAPCRAFT_STORE_CREDENTIALS` secret (obtained by running `snapcraft export-login -`):

```yaml
      - name: Build and publish Snap
        run: npx electron-builder --linux snap --publish always
        env:
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Complete Production Workflow

A production-grade workflow with separate jobs per platform, caching, and GitHub release publishing:

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Troubleshooting

**"Resource not accessible by integration"**
: The workflow doesn't have permission to create releases. Add `permissions: contents: write` to the job or workflow. Also ensure `GH_TOKEN` is set.

**macOS: "No identity found"**
: `CSC_LINK` or `CSC_KEY_PASSWORD` is wrong or missing. Verify the base64-encoded certificate decodes to a valid `.p12` and that the password is correct. Run `echo "$CSC_LINK" | base64 --decode | openssl pkcs12 -info -passin pass:"$CSC_KEY_PASSWORD"` locally to verify.

**Windows: certificate base64 too long**
: Windows CI environments may truncate environment variables over 8192 characters. Re-export the `.pfx` without the full certificate chain included. See [Code Signing](features/code-signing/code-signing.md#encoding-a-certificate-for-ci).

**Build fails with "Cannot find module"**
: Native modules need to be rebuilt for the target Electron version. Ensure `npm ci` (not `npm install`) is used, and check that `electron-rebuild` or `@electron/rebuild` is configured if needed.

## Related Pages

- [Code Signing](features/code-signing/code-signing.md) — environment variables, certificate types
- [macOS Notarization](notarization.md) — notarization setup and troubleshooting
- [Auto Update](features/auto-update.md) — configuring electron-updater with GitHub releases
- [Multi Platform Build](features/multi-platform-build.md) — building for other platforms using Docker

---

### Glossary (`website/docs/glossary.md`)

# Glossary

Quick reference for terms used throughout the electron-builder documentation.

---

## A

**Ad-hoc signing**
: A macOS code signature that identifies the binary but isn't tied to an Apple developer identity. Used for local development on Apple Silicon. Set `mac.sign.identity` to `"-"` to request ad-hoc signing. Unlike a proper Developer ID signature, ad-hoc signatures are not trusted by Gatekeeper for distribution.

**AppImage**
: A self-contained Linux application format. A single executable file that runs on any Linux distribution without installation. Users download the file, mark it executable (`chmod +x`), and run it. One of the two default Linux targets for electron-builder (along with Snap). See [AppImage](appimage.md).

**AppX / MSIX**
: Microsoft's modern app packaging format for the Windows Store and enterprise MDM deployment. electron-builder's `appx` target produces `.appx` files. Microsoft's newer `.msix` format is functionally similar. See [AppX](appx.md).

**ASAR**
: **A**tom **S**hell **AR**chive — Electron's archive format for bundling application source files. Similar to a `tar` file but optimized for random access. Files inside ASAR are read through Electron's virtual filesystem. Native modules (`.node` files) must be excluded via `asar.unpack`.

**asar.unpack**
: A configuration option listing glob patterns for files that should be placed in `app.asar.unpacked/` rather than inside the ASAR archive. Required for native modules and large binaries that need direct filesystem access.

---

## C

**CA (Certificate Authority)**
: An organization that issues and vouches for digital certificates. For Windows code signing, any major CA (DigiCert, Sectigo, SSL.com) works. For macOS, only Apple-issued certificates are recognized by Gatekeeper.

**CFBundleIdentifier**
: The unique reverse-DNS identifier for a macOS application bundle (e.g., `com.company.myapp`). Set via `appId` in electron-builder config. Must be unique across all apps on a user's machine and within the App Store.

**CSC (Code Signing Certificate)**
: The prefix for environment variables that provide code signing credentials to electron-builder (`CSC_LINK`, `CSC_KEY_PASSWORD`, etc.). See [Code Signing](features/code-signing/code-signing.md).

---

## D

**DMG**
: **D**isk i**M**a**G**e — the standard macOS distribution format. A virtual disk that mounts when double-clicked, showing the app icon and an Applications shortcut for drag-and-drop installation. See [DMG](dmg.md).

**Developer ID**
: An Apple code signing certificate type for apps distributed outside the Mac App Store. Comes in two forms: "Developer ID Application" (for signing app bundles) and "Developer ID Installer" (for signing PKG installers). Both require Apple Developer Program membership.

---

## E

**Entitlements**
: macOS security declarations that grant your app specific capabilities. Required when Hardened Runtime is enabled. Stored in `.plist` XML files referenced by `mac.sign.entitlements` and `mac.sign.entitlementsInherit`. Examples: `com.apple.security.cs.allow-jit` (required by Electron), `com.apple.security.network.client` (outbound networking).

**EV Certificate (Extended Validation)**
: A Windows code signing certificate with higher trust than standard OV certificates. EV certificates are physically bound to a USB security key and cannot be exported — which makes them incompatible with most CI/CD systems. Since 2024, SmartScreen no longer treats EV-signed installers differently from OV-signed ones; both build reputation over time.

---

## F

**Flatpak**
: A Linux packaging system that runs apps in a sandbox with controlled system access. Apps declare required permissions via "finish args." electron-builder produces single-file Flatpak bundles (not Flathub repository packages). See [Flatpak](flatpak.md).

**FPM (Effing Package Management)**
: A command-line tool used internally by electron-builder to create DEB, RPM, Pacman, APK, FreeBSD, and P5P packages. You generally don't interact with it directly; use `fpm` passthrough options in your config for advanced cases.

**Fuses**
: Feature flags embedded in the Electron binary that control security-sensitive behaviors at build time. Unlike runtime flags, fuses cannot be changed after the app is packaged. See [Adding Electron Fuses](tutorials/adding-electron-fuses.md).

---

## G

**Gatekeeper**
: Apple's security system that checks apps for valid code signatures and (on macOS 10.15+) notarization before allowing them to run. An app without a valid Developer ID signature prompts a security warning or is blocked entirely.

**GUID (Globally Unique Identifier)**
: A 128-bit identifier used by Windows installers to track application identity across installs and upgrades. In NSIS, this is the `guid` option. In MSI, this is the `upgradeCode`. **Critical:** changing the GUID between releases breaks silent upgrades of existing installations.

---

## H

**Hardened Runtime**
: A macOS security mode (`mac.sign.hardenedRuntime: true`) that restricts what your app can do without explicit entitlement declarations. Required for notarization. It blocks JIT compilation, unsigned library loading, and other potentially dangerous operations unless granted via entitlements.

---

## I

**IDP (Installer Data Package / P5P)**
: Solaris IPS (Image Packaging System) package format. electron-builder's `p5p` target produces packages for Solaris and illumos-based distributions.

---

## K

**Keychain**
: macOS's system for storing credentials, certificates, and keys. electron-builder reads code signing certificates from the keychain. On CI, you create a temporary keychain, import the certificate, and unlock it before building.

---

## M

**MAS (Mac App Store)**
: Apple's app distribution platform for macOS. MAS apps are sandboxed and require entitlements for any system access. electron-builder's `mas` target builds for App Store submission; `mas-dev` builds for local testing with a development provisioning profile. See [MAS](mas.md).

---

## N

**Notarization**
: Apple's service that scans your app for malware and issues a cryptographic ticket confirming the scan. Required for distributing apps outside the Mac App Store on macOS 10.15+. electron-builder handles notarization automatically when `mac.notarize: true`. See [Notarization](notarization.md).

**NSIS (Nullsoft Scriptable Install System)**
: The Windows installer framework used by electron-builder's `nsis` and `nsis-web` targets. Produces highly customizable `.exe` installers. The default Windows target. See [NSIS](nsis.md).

---

## O

**OV Certificate (Organization Validated)**
: A standard Windows code signing certificate that requires organizational identity verification. Unlike EV certificates, OV certificates can be exported as `.pfx` files and used in CI/CD. SmartScreen trust builds gradually based on how many users have downloaded and run the installer.

---

## P

**PKG**
: macOS installer package format. Used when your app needs to install files outside `/Applications`, run pre/post-install scripts, or install system components like launch daemons. Requires a separate "Developer ID Installer" certificate. See [PKG](pkg.md).

**Portable**
: A Windows build target that produces a single `.exe` file requiring no installation. The app runs directly without writing to the registry or creating Start menu entries. See the Windows targets section in [NSIS](nsis.md).

**Provisioning Profile**
: A macOS/iOS file that ties a certificate, an App ID, and a set of entitlements together. Required for Mac App Store submissions (`mas` target). Obtained from the Apple Developer portal.

---

## S

**Sandbox**
: An isolation mechanism that restricts what an application can access. Flatpak and Snap use sandboxes on Linux. Mac App Store apps are sandboxed by Apple's App Sandbox framework. Sandboxed apps must declare all required permissions.

**Signing Identity**
: The name of a code signing certificate as it appears in the macOS Keychain (e.g., `Developer ID Application: My Company (ABCDE12345)`). Referenced by `CSC_NAME` or `mac.sign.identity` in electron-builder config.

**SmartScreen**
: Windows Defender's application reputation service. It warns users when they run unsigned apps or apps from publishers with low download reputation. Since 2024, no certificate type bypasses the reputation-building period; EV and OV certificates both require time.

**Snap**
: Canonical's Linux packaging format, primarily for Ubuntu. Snap apps run in a sandbox with declared "interface plugs" controlling system access. electron-builder's `snap` target produces Snap packages for Snap Store distribution. See [Snap](snap.md).

**Squirrel**
: A Windows auto-update framework. electron-builder's `squirrel.windows` target is a legacy option — new projects should use NSIS with electron-updater instead. See [Squirrel.Windows](squirrel-windows.md).

**Stapling**
: Attaching a notarization ticket to an app bundle so that Gatekeeper can verify notarization offline (without contacting Apple's servers). electron-builder staples the ticket automatically after notarization completes.

---

## U

**Universal Binary**
: A macOS binary that contains both x64 (Intel) and arm64 (Apple Silicon) code. The OS selects the appropriate slice at launch. Build with `--universal` or `arch: universal`. Results in a ~2× larger download but runs natively on all modern Macs.

**UpgradeCode**
: A GUID in MSI packages that identifies the application across versions for upgrade and uninstall operations. Must remain constant across all versions. Changing the UpgradeCode breaks silent upgrades and leaves old versions orphaned on managed systems. See [MSI](msi.md).

---

## W

**WiX (Windows Installer XML Toolset)**
: The toolchain used by electron-builder internally to produce MSI packages. Provides advanced Windows installer features like component tracking, repair, and rollback. You can pass additional WiX arguments via `msi.additionalWixArgs`.

---

### Linux (`website/docs/linux.md`)

---
title: "Linux"
---

{!./partials/_upgrading-from-v26.md!}

The top-level [linux](configuration.md) key contains a set of options instructing electron-builder on how it should build Linux targets. These options are applicable to any Linux target.

## Linux Target Overview

electron-builder supports a wide range of Linux package formats. Choose based on your target audience and distribution method:

| Target | Format | Best For | Distro Support |
|---|---|---|---|
| `AppImage` | Single file | Broad compatibility, no install | Universal |
| `deb` | Debian package | Ubuntu, Debian, Mint | Debian-based |
| `rpm` | RPM package | Fedora, RHEL, openSUSE | Red Hat-based |
| `snap` | Snap package | Ubuntu-primary | Any (snapd required) |
| `flatpak` | Flatpak | Cross-distro sandboxed | Any (flatpak required) |
| `pacman` | Arch package | Arch Linux, Manjaro | Arch-based |
| `apk` | Alpine package | Alpine Linux, containers | Alpine |
| `freebsd` | FreeBSD pkg | FreeBSD | FreeBSD |
| `p5p` | Solaris IPS | Solaris, illumos | Solaris-based |
| `zip`, `7z`, `tar.*` | Archive | Custom CDN distribution | Universal |
| `dir` | Directory | Development/debugging | N/A |

The default targets are `AppImage` and `Snap`.

## Common Linux Configuration

Options set in the `linux` key apply to all Linux targets:

```yaml
linux:
  target:
    - AppImage
    - deb
    - rpm
  category: Utility                  # freedesktop.org category
  synopsis: "A short description"    # one-line description
  description: "Full description"    # longer description
  maintainer: "Name <email>"         # package maintainer
  vendor: "My Company"               # package vendor
  mimeTypes:
    - application/x-myapp
  executableArgs:
    - --enable-features=WebContentsFocusOnResize
```

:::note[v27: launcher entrypoint]
Every Linux target (deb/rpm, AppImage, snap, flatpak) now launches through a generated `<executableName>-launcher` shell script rather than invoking the executable directly. `executableArgs` (and `forceX11`) are injected into that launcher, and the generated `.desktop` `Exec` key points at it (e.g. `Exec=/opt/MyApp/MyApp-launcher %U`) rather than inlining the args. This makes `executableArgs` apply consistently across all Linux targets. If you ship a custom `.desktop` override, an AppArmor/snap profile, or a MIME handler that hard-codes the `Exec` command, point it at the `*-launcher` script. See [v27 Breaking Changes → Linux launcher entrypoint](./migration/v27-breaking-changes.md#linux-launcher-entrypoint).
:::

## Desktop File Customization

All Linux targets include a `.desktop` file for application menu integration. Customize it via `linux.desktop`:

```yaml
linux:
  desktop:
    entry:
      Name: My Application
      Comment: A fantastic application
      Categories: Office;Productivity
      Keywords: productivity;notes;organizer
      StartupWMClass: my-app       # helps window managers match window to icon
      Terminal: false
      Type: Application
    desktopActions:
      NewWindow:
        Name: New Window
        Exec: my-app --new-window
      NewFile:
        Name: New File
        Exec: my-app --new-file
```

Standard freedesktop.org category values include: `AudioVideo`, `Audio`, `Video`, `Development`, `Education`, `Game`, `Graphics`, `Network`, `Office`, `Science`, `Settings`, `System`, `Utility`.

## Window Association (`desktopName`)

Electron derives its [`app_id`](https://github.com/electron/electron/blob/main/lib/browser/init.ts) — which becomes the window's `WM_CLASS` — from the `desktopName` field in your root `package.json`. Desktop environments (GNOME, KDE, etc.) use `WM_CLASS` to match a running window to its `.desktop` entry for taskbar grouping, dock badges, and launcher highlighting.

**Without `desktopName` set, this association can break**: the fallback `WM_CLASS` may not match the installed `.desktop` filename, so the DE treats the running app as an unknown window.

To fix this, set `desktopName` in `package.json`:

```json title="package.json"
{
  "desktopName": "com.example.MyApp"
}
```

electron-builder installs the `.desktop` file as `${desktopName}.desktop` (here `com.example.MyApp.desktop`) and sets `StartupWMClass=com.example.MyApp`, both matching Electron's `app_id`. A trailing `.desktop` in `desktopName` is tolerated (stripped before the filename is composed), so configs written for earlier versions keep working. When `desktopName` is absent, the filename falls back to `executableName`.

:::note[Changed in v27]
The installed `.desktop` filename is now always derived from `desktopName`. The `linux.syncDesktopName` flag that previously gated this behaviour has been removed — see the [v27 breaking changes](./migration/v27-breaking-changes.md#linuxsyncdesktopname-always-synced).
:::

:::warning[desktopName is required for reliable window association]
If `desktopName` is absent, electron-builder will log a warning at build time. Without it, desktop environments on GNOME, KDE, and others may not correctly link the running application window to its launcher entry.
:::

See the [freedesktop.org specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/example.html) for all valid keys.

## Icon Configuration

Provide a directory of PNG icons named with their size:

```yaml
linux:
  icon: build/icons/
```

Files must be named `NxN.png` (e.g., `256x256.png`, `512x512.png`). If not specified, electron-builder auto-generates Linux icons from the macOS `.icns` file (if present) or the Windows `.ico` file.

Recommended sizes: 16, 24, 32, 48, 64, 96, 128, 256, 512 pixels.

## Debian Package (`deb`)

The `deb` target creates a `.deb` package installable via `apt` on Debian, Ubuntu, Mint, and other Debian-based distributions.

```yaml
deb:
  depends:
    - libgtk-3-0
    - libnotify4
    - libnss3
    - libxss1
    - libxtst6
    - xdg-utils
    - libatspi2.0-0
    - libuuid1
    - libsecret-1-0
  recommends:
    - libappindicator3-1    # system tray support
  packageCategory: utils
  priority: optional
```

**Common `depends` packages for Electron apps:**
- `libgtk-3-0` — GTK3 (required)
- `libnotify4` — desktop notifications
- `libnss3` — NSS (Chromium networking)
- `libxss1` — screen saver extension
- `libxtst6` — X test extension
- `xdg-utils` — desktop integration utilities
- `libatspi2.0-0` — accessibility
- `libuuid1` — UUID support
- `libsecret-1-0` — keychain/Secret Service

**Priority values:** `required`, `important`, `standard`, `optional` (use `optional` for most apps)

## RPM Package (`rpm`)

The `rpm` target creates a `.rpm` package for Fedora, RHEL, CentOS, openSUSE, and other RPM-based distributions.

```yaml
rpm:
  depends:
    - libXScrnSaver
    - libnotify
    - libappindicator
    - nss
    - gtk3
  compression: xz     # gz | bzip2 | xz | lzo (default: xz)
  packageCategory: Applications/Internet
```

**Common `depends` for Electron on RPM systems:**
- `gtk3` — GTK3
- `libnotify` — notifications
- `libXScrnSaver` — screen saver
- `nss` — networking

**After-install/after-remove scripts:**

```yaml
rpm:
  afterInstall: build/scripts/after-install.sh
  afterRemove: build/scripts/after-remove.sh
```

:::note[v27: maintainer-script variable syntax]
electron-builder interpolates variables into maintainer scripts (`afterInstall` / `afterRemove`) using shell-style `${var}`. The legacy EJS `<%= var %>` interpolation was **removed** in v27 — update any templated maintainer scripts to `${var}`. See [v27 Breaking Changes → Linux maintainer-script EJS syntax](./migration/v27-breaking-changes.md#linux-maintainer-script-ejs-template-syntax).
:::

## Pacman Package (`pacman`)

:::warning[Beta]
The `pacman` target is in beta. Test thoroughly before distributing.
:::

Creates a `.pkg.tar.zst` package for Arch Linux and Arch-based distributions (Manjaro, EndeavourOS, etc.):

```yaml
pacman:
  depends:
    - gtk3
    - libnotify
    - nss
    - libxss
    - libxtst
    - xdg-utils
    - at-spi2-core
    - libsecret
```

## Alpine APK Package (`apk`)

Creates an `.apk` package for Alpine Linux, commonly used in Docker containers:

```yaml
apk:
  depends:
    - gtk+3.0
    - libnotify
    - nss
```

:::note[musl libc]
Alpine uses musl libc rather than glibc. Electron bundles its own copy of glibc-dependent libraries, but some native modules may not be compatible with Alpine without recompilation.
:::

## FreeBSD Package (`freebsd`)

Creates a `.pkg` package (not to be confused with macOS PKG) for FreeBSD systems:

```yaml
freebsd:
  depends:
    - gtk3
    - libnotify
    - nss
```

## FPM Passthrough Options

The DEB, RPM, Pacman, APK, FreeBSD, and P5P targets are built using [FPM](https://github.com/jordansissel/fpm) internally. Pass additional FPM command-line arguments:

```yaml
deb:
  fpm:
    - "--deb-changelog=CHANGELOG.md"
    - "--deb-no-default-config-files"
```

## AppArmor Profile (Ubuntu 24+)

Ubuntu 24.04 introduced mandatory AppArmor profiles for application sandboxing. Specify a custom profile:

```yaml
deb:
  appArmorProfile: build/apparmor-profile
```

Without a profile, the app may be restricted by Ubuntu's default profile. See the [AppArmor documentation](https://ubuntu.com/server/docs/how-to/security/apparmor/) for profile syntax.

## Base Linux Configuration

{!./app-builder-lib.Interface.LinuxConfiguration.md!}

## Debian Package Options

The top-level [deb](configuration.md) key contains a set of options for Debian packages.

{!./app-builder-lib.Interface.DebOptions.md!}

All [LinuxTargetSpecificOptions](linux.md#linuxtargetspecificoptions-apk-freebsd-pacman-p5p-and-rpm-options) can also be specified in `deb` to customize Debian packages.

## `LinuxTargetSpecificOptions` — APK, FreeBSD, Pacman, P5P and RPM Options {#linuxtargetspecificoptions-apk-freebsd-pacman-p5p-and-rpm-options}

The top-level `apk`, `freebsd`, `pacman`, `p5p` and `rpm` keys contain options for their respective Linux targets.

{!./app-builder-lib.Interface.LinuxTargetSpecificOptions.md!}

---

### Mac (`website/docs/mac.md`)

---
title: "macOS"
---

{!./partials/_upgrading-from-v26.md!}

The top-level [mac](configuration.md) key contains a set of options instructing electron-builder on how it should build macOS targets. These options are applicable to any macOS target.

## macOS Target Overview

electron-builder supports several macOS distribution formats. Choose based on your distribution channel:

| Target | Best For | Signed? | Notarized? |
|---|---|---|---|
| `dmg` | Standard consumer distribution | Yes | Yes |
| `zip` | Update servers (electron-updater), minimal package | Yes | Yes |
| `pkg` | System-level installs, kernel extensions, launch daemons | Yes | Yes |
| `mas` | Mac App Store distribution | Yes (Mac App Distribution) | No (MAS handles it) |
| `mas-dev` | Local testing of MAS builds | Yes (Apple Development / Mac Developer) | No |
| `7z`, `tar.*` | Archive formats, custom CDN distribution | Optional | Optional |
| `dir` | Development/debugging — unpacked app | No | No |

The default targets are `zip` and `dmg` (both are required for Squirrel.Mac auto-update).

## Bundle ID

The `appId` property sets the [CFBundleIdentifier](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) for your macOS app. This is a critical identifier — set it explicitly:

```yaml
appId: "com.mycompany.myapp"
```

- Use reverse-DNS format: `com.yourcompany.appname`
- Must be unique in the Mac App Store if you intend to submit there
- Changing it after first release will break existing user data paths (NSUserDefaults, sandboxed containers, etc.)

## Architecture Support

electron-builder supports building for multiple CPU architectures:

| Architecture | CLI Flag | Description |
|---|---|---|
| `x64` | `--x64` | Intel 64-bit (traditional Mac) |
| `arm64` | `--arm64` | Apple Silicon (M1, M2, M3, M4) |
| `universal` | `--universal` | Fat binary containing both x64 and arm64 |

### Universal Binaries

A universal binary runs natively on both Intel and Apple Silicon Macs with no performance penalty:

```yaml
mac:
  target:
    - target: dmg
      arch: universal
```

**Universal binary options:**

- `mergeASARs` — merge x64 and arm64 ASAR archives into a single universal ASAR (`true` by default). Disable only if you have architecture-specific native modules that cannot be fat-binary merged.
- `singleArchFiles` — glob pattern for files that are single-arch and should NOT be merged (e.g., pre-built native binaries distributed only for one arch).
- `x64ArchFiles` — glob pattern for files that are x64-only. These are kept as x64 in the universal binary rather than being fat-binary merged.

In v27 these options live under `mac.universal` (a pass-through to [`@electron/universal`](https://github.com/electron/universal)) and only take effect when the target arch is `universal`:

```yaml
mac:
  universal:
    mergeASARs: true
    singleArchFiles: "**/*.node"   # keep native modules as separate arch files
```

### Recommended: Build Per-Arch on Correct Hardware

While cross-compilation is possible, the most reliable approach is to build `arm64` on Apple Silicon and `x64` on Intel (or use a matrix in CI). Universal builds work best when both arches are produced natively and then merged.

## Code Signing

macOS apps must be signed to avoid Gatekeeper warnings. See [Code Signing](features/code-signing/code-signing.md) for full setup.

:::note[v27: signing options moved under mac.sign]
In v27 every macOS signing option (`identity`, `entitlements`, `hardenedRuntime`, `type`, `provisioningProfile`, `binaries`, `requirements`, `timestamp`, …) lives inside a single `mac.sign` object (`sign.identity`, `sign.hardenedRuntime`, …); `signIgnore` is now `sign.ignore`. `electron-builder migrate-schema` rewrites the old flat keys automatically. The same structure applies to `mas` and `masDev`. See [v27 Breaking Changes → macOS signing](./migration/v27-breaking-changes.md#macos-signing-macsign).
:::

### Certificate Identity

Use `mac.sign.identity` to specify the signing certificate by name:

```yaml
mac:
  sign:
    identity: "Developer ID Application: My Company (TEAM1234AB)"
```

Or use environment variables — the recommended approach for CI:

```bash
export CSC_LINK=/path/to/certificate.p12
export CSC_KEY_PASSWORD=yourpassword
```

Set `mac.sign.identity: null` (or `mac.sign: null`) to skip signing entirely. Set `mac.sign.identity: "-"` to use an ad-hoc signature (app will only run on the machine that built it).

:::warning[Ad-hoc signing and Hardened Runtime]
If you disable code signing, you should also disable Hardened Runtime (`mac.sign.hardenedRuntime: false`), as the combination of no signing and enabled Hardened Runtime may prevent the app from launching.
:::

### Hardened Runtime

`mac.sign.hardenedRuntime` is required for notarization on macOS 10.15+. It restricts what the app can do — you may need entitlements to allow capabilities. The default is `true` for standard `darwin` builds and `false` for Mac App Store builds (`mas` / `mas-dev`).

## Entitlements

**You usually do not need to write an entitlements file at all.** The defaults are already minimal and correct for a stock Electron app:

| File being signed | Default entitlements |
|---|---|
| The app bundle | `com.apple.security.cs.allow-jit` |
| Renderer / GPU helpers | `com.apple.security.cs.allow-jit` |
| Plugin helper | `allow-jit`, `allow-unsigned-executable-memory`, `disable-library-validation` |
| Frameworks, native modules, unpacked binaries | `allow-jit` plus osx-sign's Chromium-derived `device.*` / `personal-information.*` entitlements (`default.darwin.plist`) |

Nested binaries do **not** inherit the app's entitlements. They are handed to [`@electron/osx-sign`](https://github.com/electron/osx-sign), which picks a per-file default modelled on Chromium's own entitlements: the looser exceptions go only to the plugin helper that actually needs them, and every other nested file (frameworks, `.node` modules, executables in `app.asar.unpacked`) receives `default.darwin.plist` — `allow-jit` plus `com.apple.security.device.{audio-input,bluetooth,camera,print,usb}` and `com.apple.security.personal-information.{location,photos-library}`. Outside the plugin helper, none of these defaults grant `disable-library-validation` or `allow-unsigned-executable-memory`.

:::warning[Changed in v27]
Earlier versions applied a single entitlements file to the app **and** every nested binary, granting `com.apple.security.cs.allow-unsigned-executable-memory` and `com.apple.security.cs.disable-library-validation` to every process. Modern Electron does not need either in the main process, and both materially weaken the Hardened Runtime, so they are no longer granted by default. If your app depends on them, add them to your own `build/entitlements.mac.plist` (and `build/entitlements.mac.inherit.plist` for nested binaries) — see [Loading third-party or unsigned binaries](#loading-third-party-or-unsigned-binaries) below.
:::

### Supplying your own entitlements

Create `build/entitlements.mac.plist` to override the app-level defaults:

```xml
<?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/>
  
  
  
</dict>
</plist>
```

Your file **replaces** the default rather than extending it, so remember to keep `com.apple.security.cs.allow-jit`.

A custom `build/entitlements.mac.plist` applies to the app bundle only — nested binaries keep the per-file defaults described above and do not pick up your custom keys. The inherit plist is for entitlements a *nested process* needs for itself — for example `disable-library-validation` on a sidecar executable in `app.asar.unpacked` that loads third-party libraries of its own, or on a helper process that loads native modules. In that case add `build/entitlements.mac.inherit.plist` (or set `mac.sign.entitlementsInherit`). Doing so applies one plist to every nested binary and gives up the per-file defaults, so prefer leaving it absent unless you need it.

### Loading third-party or unsigned binaries

`com.apple.security.cs.disable-library-validation` turns off macOS library validation for the whole process. Grant it only when your app loads a framework, plugin, or native module signed by a **different** Team ID (or not signed at all) — for example a sidecar binary downloaded at runtime, or one excluded from signing via `mac.sign.ignore`.

Library validation is enforced by the process that loads the code, based on that process's own entitlements; what the library itself was signed with does not matter. A native module loaded by the main process therefore needs the key in `build/entitlements.mac.plist`; one loaded by a helper process (a `utilityProcess`, or a renderer with `nodeIntegration`) needs it in `build/entitlements.mac.inherit.plist` as well. The post-sign check below is skipped when the app entitlements grant it, because the main process is the usual loader.

After signing, electron-builder inspects the Mach-O libraries and bundles in `app.asar.unpacked` and `Contents/PlugIns` (which electron-builder never re-signs) and warns if any of them carry a foreign or missing signature while the entitlement is absent, so you find out at build time rather than from a launch crash. Executables (such as a bundled `ffmpeg`) are spawned rather than loaded, so they are not subject to library validation and are not flagged.

Ad-hoc builds (`mac.sign.identity: "-"`) are handled automatically: an ad-hoc signature carries no Team ID, so electron-builder applies a built-in ad-hoc entitlements file that includes `disable-library-validation` for the app and its nested binaries.

electron-builder auto-detects `build/entitlements.mac.plist` and `build/entitlements.mac.inherit.plist` when present. To point at custom paths, set them under `mac.sign`:

```yaml
mac:
  sign:
    entitlements: build/entitlements.mac.plist
    entitlementsInherit: build/entitlements.mac.inherit.plist
```

Common entitlements for Electron apps:

| Entitlement | When Needed |
|---|---|
| `com.apple.security.cs.allow-jit` | Always — V8 requires JIT (granted by default) |
| `com.apple.security.cs.allow-unsigned-executable-memory` | Legacy Electron only — not needed by modern V8/Electron and weakens the Hardened Runtime |
| `com.apple.security.cs.disable-library-validation` | Loading frameworks/native modules signed by another team, or unsigned |
| `com.apple.security.network.client` | Outgoing network connections (sandboxed apps) |
| `com.apple.security.network.server` | Listening for connections (sandboxed apps) |
| `com.apple.security.files.user-selected.read-write` | Open/save panels (sandboxed apps) |
| `com.apple.security.device.camera` | Camera access |
| `com.apple.security.device.microphone` | Microphone access |
| `com.apple.security.app-sandbox` | Required for Mac App Store — see [MAS](mas.md) |

:::info[Entitlements and Notarization]
Notarization requires Hardened Runtime + appropriate entitlements. See [Notarization](features/code-signing/notarization.md) for the complete notarization workflow.
:::

## Info.plist Customization

Inject arbitrary `Info.plist` keys using `extendInfo`:

```yaml
mac:
  extendInfo:
    NSMicrophoneUsageDescription: "This app uses the microphone for..."
    NSCameraUsageDescription: "This app uses the camera for..."
    LSMultipleInstancesProhibited: true
    CFBundleURLTypes:
      - CFBundleURLSchemes:
          - myapp
        CFBundleURLName: "com.mycompany.myapp"
```

## Helper Bundle IDs

Electron spawns several helper processes, each with its own bundle ID. electron-builder sets these automatically based on your `appId`, but you can override them:

| Option | Default | Process |
|---|---|---|
| `helperBundleId` | `${appId}.helper` | Generic helper |
| `helperRendererBundleId` | `${appId}.helper.Renderer` | Renderer process |
| `helperPluginBundleId` | `${appId}.helper.Plugin` | Plugin helper |
| `helperGPUBundleId` | `${appId}.helper.GPU` | GPU process |
| `helperEHBundleId` | `${appId}.helper.EH` | Exception handler |
| `helperNPBundleId` | `${appId}.helper.NP` | NP helper |

You only need to override these if you have a specific naming requirement (e.g., for provisioning profiles that enumerate each helper ID explicitly).

## Other Common Options

**Dark mode:** Set `darkModeSupport: true` if your app supports the system dark mode. This adds the `NSRequiresAquaSystemAppearance: false` key to `Info.plist`.

**Minimum system version:** `minimumSystemVersion` sets the `LSMinimumSystemVersion` in `Info.plist`. Electron itself has a minimum macOS version requirement — don't set this lower than Electron's requirement.

**Signing additional binaries:** Use `mac.sign.binaries` to list paths to additional native binaries within your app bundle that need to be signed (e.g., embedded CLIs, helper tools).

```yaml
mac:
  sign:
    binaries:
      - Contents/MacOS/my-native-helper
      - Contents/Frameworks/MyFramework.framework/Versions/A/MyFramework
```

**Product / executable name validation:** As of v27, electron-builder rejects a `productName` or `executableName` that would require filename sanitization (so the generated `<Name> Helper.app` bundles stay consistent with `CFBundleName` and Electron's helper discovery). If a build fails with an "is not a valid macOS app bundle name" error, choose a name that needs no sanitization.

## Configuration

{!./app-builder-lib.Interface.MacConfiguration.md!}

---

### Mas (`website/docs/mas.md`)

---
title: "Mac App Store"
---

{!./partials/_upgrading-from-v26.md!}

The top-level [mas](configuration.md) key contains a set of options instructing electron-builder on how it should build the MAS (Mac Application Store) target. Inherits all [macOS options](mac.md).

Use the `mas-dev` target (configured via the top-level `masDev` key) for local testing of MAS builds with a development provisioning profile.

## MAS vs. Direct Distribution

| Aspect | MAS Distribution | Direct Distribution (DMG) |
|---|---|---|
| Certificate | Mac App Distribution | Developer ID Application |
| Notarization | Not required (Apple handles it) | Required (macOS 10.15+) |
| Sandboxing | Mandatory | Optional |
| Update mechanism | Mac App Store | electron-updater |
| Revenue | Apple takes 30% (15% for small dev program) | 100% to you |
| Discovery | App Store search and browsing | Your own marketing |
| Review | Apple review (1-7 days) | Instant |
| macOS version support | As Apple dictates | You control |

## Prerequisites

### Certificates

You need a **Mac App Distribution** certificate (for signing the app) and a **Mac Installer Distribution** certificate (for signing the PKG submitted to App Store Connect). Both are issued from your [Apple Developer account](https://developer.apple.com/account/).

:::tip
For local testing with `mas-dev`, you use a **Development** provisioning profile and a standard developer certificate, not the distribution ones.
:::

### Provisioning Profile

MAS apps require a provisioning profile that:
- Lists the specific entitlements your app uses
- Is tied to your App ID and certificate

Create provisioning profiles at [developer.apple.com](https://developer.apple.com/account/resources/profiles/list).

```yaml
mas:
  sign:
    provisioningProfile: build/MyApp_AppStore.provisionprofile
```

:::note[v27: signing options are under mas.sign]
Like `mac`, all signing options on `mas` and `masDev` moved into a `sign` object in v27 (`sign.provisioningProfile`, `sign.entitlements`, `sign.entitlementsInherit`, …). `electron-builder migrate-schema` rewrites the old flat keys. See [v27 Breaking Changes → macOS signing](./migration/v27-breaking-changes.md#macos-signing-macsign).
:::

### App Sandbox

All MAS apps must be sandboxed. Add to your entitlements (`build/entitlements.mas.plist`):

```xml
<?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.cs.allow-jit</key>
  <true/>
  
  <key>com.apple.security.network.client</key>
  <true/>
</dict>
</plist>
```

And `build/entitlements.mas.inherit.plist` for helper processes:

```xml
<?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.inherit</key>
  <true/>
</dict>
</plist>
```

Point to these in your configuration:

```yaml
mas:
  sign:
    entitlements: build/entitlements.mas.plist
    entitlementsInherit: build/entitlements.mas.inherit.plist
    provisioningProfile: build/MyApp_AppStore.provisionprofile
```

## Common MAS Entitlements

| Entitlement | Purpose |
|---|---|
| `com.apple.security.app-sandbox` | Required for MAS |
| `com.apple.security.network.client` | Outgoing network connections |
| `com.apple.security.network.server` | Incoming connections |
| `com.apple.security.files.user-selected.read-write` | Open/Save panels |
| `com.apple.security.files.downloads.read-write` | Downloads folder access |
| `com.apple.security.device.camera` | Camera |
| `com.apple.security.device.microphone` | Microphone |
| `com.apple.security.personal-information.contacts` | Contacts |
| `com.apple.security.personal-information.calendars` | Calendar |
| `com.apple.security.print` | Printing |
| `com.apple.security.automation.apple-events` | AppleScript/Apple Events |

## Testing with mas-dev

The `mas-dev` target produces a build signed with a development certificate and development provisioning profile — suitable for testing sandbox behavior on your machine without going through App Store review.

```yaml
masDev:
  sign:
    provisioningProfile: build/MyApp_Dev.provisionprofile
    entitlements: build/entitlements.mas.plist
    entitlementsInherit: build/entitlements.mas.inherit.plist
```

Build the dev target:

```bash
electron-builder --mac mas-dev
```

## Building for the App Store

```bash
electron-builder --mac mas
```

This produces:
- `MyApp-<version>.pkg` — the package to upload to App Store Connect

## Submitting to App Store Connect

1. Open [App Store Connect](https://appstoreconnect.apple.com/)
2. Create your app listing (if new) under **My Apps → +**
3. Use **Transporter** (free, from the Mac App Store) or `xcrun altool` / `xcrun notarytool` to upload the `.pkg`
4. The uploaded build appears in App Store Connect after processing (usually a few minutes)
5. Select the build for your release and submit for review

## Common Review Rejection Reasons

- **Sandbox violations** — the app attempts to access files or resources not permitted by entitlements
- **Deprecated APIs** — using APIs Apple has removed or flagged (check release notes)
- **Missing privacy strings** — if you access camera/mic/location, `NSCameraUsageDescription` etc. must be in `Info.plist`
- **UI guidelines violations** — buttons, windows, or flows that don't match Human Interface Guidelines
- **Crash on launch** — always test the MAS build on a clean machine before submitting

## Configuration

{!./app-builder-lib.Interface.MasConfiguration.md!}

---

### Msi Wrapped (`website/docs/msi-wrapped.md`)

---
title: "MSI Wrapped"
---

The top-level [msiWrapped](configuration.md) key contains a set of options instructing electron-builder on how it should build Windows MSI-Wrapped packages.

## What is MSI-Wrapped?

MSI-Wrapped is a hybrid approach that combines the customizability of NSIS with the enterprise deployment compatibility of MSI. It works by:

1. Building your normal NSIS installer
2. Wrapping the NSIS `.exe` inside a minimal MSI package
3. The MSI acts as a thin shell that executes the NSIS installer when deployed

This allows enterprise IT teams to deploy the package through MSI-compatible tools (Group Policy, SCCM, Intune) while you retain all of NSIS's powerful customization capabilities for the actual installation experience.

## MSI-Wrapped vs. Native MSI

| Aspect | MSI-Wrapped | Native MSI |
|---|---|---|
| Underlying installer | NSIS | WiX-generated MSI |
| NSIS customization | Full | None |
| Enterprise MSI deployment | Yes | Yes |
| Silent install | Via NSIS flags | Via MSI `/quiet` |
| Windows Store (MSIX) | Limited | Better |
| Complexity | Lower | Higher |

## When to Use MSI-Wrapped

Choose MSI-Wrapped when:
- You already have a well-configured NSIS installer and need to deploy it via Group Policy or MDM
- Your enterprise customers require MSI format but you don't want to maintain a separate MSI configuration
- You need NSIS-level UI customization combined with MSI deployment

Choose [native MSI](msi.md) when:
- You need full MSI capabilities (custom actions, rollback, MSI database manipulation)
- You're targeting Windows Store / MSIX conversion
- You need clean per-machine/per-user support without NSIS

## Configuration

### Wrapped Installer Arguments

Pass additional command-line arguments to the wrapped NSIS installer when it is executed:

```yaml
msiWrapped:
  wrappedInstallerArgs: "/S /D=C:\\MyApp"
```

### Impersonation

```yaml
msiWrapped:
  impersonate: false    # Default: false
```

When `impersonate: true`, the MSI wrapper executes the NSIS installer under the user's identity rather than the system/SYSTEM account. This is useful for per-user installations that need to access user-specific resources.

### Upgrade Code

```yaml
msiWrapped:
  upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
```

:::warning[Critical: Do Not Change After First Release]
See the [MSI upgrade code warning](msi.md#upgrade-code) — the same applies here.
:::

## Complete Example

```yaml
win:
  target:
    - nsis
    - msi-wrapped

nsis:
  oneClick: false
  perMachine: true
  allowToChangeInstallationDirectory: true

msiWrapped:
  upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
  impersonate: false
  wrappedInstallerArgs: "/SILENT"
```

## Configuration

{!./app-builder-lib.Interface.MsiWrappedOptions.md!}

---

### Msi (`website/docs/msi.md`)

The top-level [msi](configuration.md) key contains a set of options instructing electron-builder on how it should build Windows MSI (Microsoft Installer) packages.

## When to Use MSI

MSI packages use Windows Installer technology and are the standard format for enterprise software deployment:

- **Group Policy deployment** — IT administrators can silently push MSI packages to managed machines via Active Directory
- **SCCM / Intune / MDM** — Microsoft's enterprise management tools work natively with MSI
- **Silent installation** — standard `/quiet` and `/passive` flags work without custom configuration
- **Windows Store compatibility** — MSI can be used as a basis for MSIX packaging (see [MSI-Wrapped](msi-wrapped.md))
- **Upgrade/downgrade control** — Windows Installer handles version management via upgrade codes

## MSI vs. NSIS vs. Portable

| Aspect | MSI | NSIS | Portable |
|---|---|---|---|
| Enterprise deployment | Excellent | Limited | None |
| Silent install | `/quiet` flag | Custom scripting | N/A |
| Group Policy | Yes | No | No |
| Customizability | Moderate (WiX) | Excellent | N/A |
| UI flexibility | Limited | Highly customizable | N/A |
| File size | Typically larger | Compressed | No overhead |
| Auto-update | Not supported via electron-updater | electron-updater | Manual |
| Recommended for | Enterprise | Consumer | USB/no-install |

## Upgrade Code

The `upgradeCode` is a GUID that uniquely identifies your product across all versions. It persists through upgrades and is used by Windows Installer to find and upgrade existing installations.

:::warning[Critical: Do Not Change After First Release]
If you change the `upgradeCode` after your first release, Windows Installer will treat the new version as a completely different product — existing installations will NOT be upgraded, and users will end up with both versions installed. Set this once and never change it.
:::

```yaml
msi:
  upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
```

If not specified, a deterministic GUID is generated from the app ID (`appId`). Explicitly setting it is strongly recommended for production.

## One-Click Installation

```yaml
msi:
  oneClick: true    # Default: true
```

When `oneClick` is `true`, the installer runs without showing UI (silent install). Set to `false` for a traditional wizard-style installer.

## Per-Machine vs. Per-User Installation

```yaml
msi:
  perMachine: false   # Default: false (per-user install)
```

- `perMachine: false` — installs for the current user only, no admin required
- `perMachine: true` — installs for all users, requires administrator privileges

For enterprise deployment via Group Policy, `perMachine: true` is typically required.

## Desktop and Start Menu Shortcuts

Shortcut creation is controlled via the inherited `CommonWindowsInstallerConfiguration` options:

```yaml
msi:
  createDesktopShortcut: true         # Default: true
  createStartMenuShortcut: true       # Default: true
  menuCategory: "My Company"          # Creates a subfolder in Start Menu
  shortcutName: "My App"             # Overrides default shortcut name
  runAfterFinish: true                # Launch app after installation completes
```

## WiX Customization

The MSI target uses [WiX Toolset](https://wixtoolset.org/) internally. You can pass additional arguments to the WiX compiler (`candle.exe`) and linker (`light.exe`):

```yaml
msi:
  additionalWixArgs:
    - "-dMyVariable=MyValue"
    - "-ext WixUIExtension"
  additionalLightArgs:
    - "-sice:ICE80"
```

Use `msiProjectCreated` hook to modify the generated WiX project files before they are compiled:

```yaml
msiProjectCreated: ./msi-hook.js
```

## Warnings as Errors

```yaml
msi:
  warningsAsErrors: true    # Default: true
```

WiX compiler warnings are treated as errors by default. Set to `false` if you're using advanced WiX features that trigger known benign warnings.

## Silent Installation

MSI packages support standard Windows Installer command-line flags for silent deployment:

```cmd
# Silent install (no UI)
MyApp-Setup.msi /quiet

# Passive install (progress bar only)
MyApp-Setup.msi /passive

# Silent uninstall
msiexec /x {PRODUCT-CODE} /quiet

# Install per-machine
MyApp-Setup.msi /quiet ALLUSERS=1

# Install to custom path
MyApp-Setup.msi /quiet INSTALLDIR="C:\MyCustomPath\MyApp"
```

## Complete Example

```yaml
win:
  target:
    - msi

msi:
  oneClick: true
  perMachine: true
  upgradeCode: "{A1234567-BCDE-F012-3456-789ABCDEF012}"
  createDesktopShortcut: true
  createStartMenuShortcut: true
  menuCategory: "My Company"
  runAfterFinish: false       # Don't auto-launch after silent enterprise install
  warningsAsErrors: true
```

## Troubleshooting

**Build fails with WiX error:** Enable verbose output with `DEBUG=electron-builder` and check the WiX error codes. Common issues include missing files or invalid GUIDs.

**Upgrade installs alongside old version:** Verify the `upgradeCode` is set and has not changed between versions. Also ensure `ProductCode` (auto-generated per version) is different.

**Silent install shows UI:** Confirm `oneClick: true` is set and the `/quiet` flag is being used. Some antivirus software intercepts installer launches.

**Per-machine install fails without admin:** Per-machine installation requires elevation. Ensure the deployment mechanism (SCCM, Intune, GPO) runs the installer in a privileged context.

## Configuration

{!./app-builder-lib.Interface.MsiOptions.md!}

---

### Msix (`website/docs/msix.md`)

The top-level [msix](configuration.md) key contains a set of options instructing electron-builder on how it should build MSIX packages for the Microsoft Store, enterprise sideloading, and modern Windows deployment.

:::info[Beta]
The MSIX target is **beta**. The configuration surface and generated manifest are still evolving. Pin your electron-builder version and re-test packaging after upgrades.
:::

All options are optional. All required MSIX configuration is inferred and computed automatically from your application metadata.

## MSIX vs. AppX

MSIX is the modern successor to AppX. They share the same underlying manifest format (`AppxManifest.xml`), the same packaging tool (`makeappx.exe`), and the same signing requirements, but MSIX exposes newer manifest schemas and deployment features that AppX does not.

| Capability | AppX ([appx](appx.md)) | MSIX (this target) |
|---|---|---|
| Artifact | `.appx` | `.msix`, `.msixbundle`, `.msixupload` |
| Multi-arch bundle | No | Yes (`.msixbundle`) |
| Store upload archive | Manual | Yes (`.msixupload`) |
| Package Integrity (`uap10`) | No | Yes (`enforcePackageIntegrity`) |
| Windows Services (`desktop6`) | No | Yes (`windowsServices`) |
| Required toolset | any | `winCodeSign` ≥ `1.0.0` |

If you are starting fresh, prefer **MSIX**. AppX remains for backward compatibility.

## When to Use MSIX

Choose MSIX when:

- **Windows Store distribution** — your app will be listed in the Microsoft Store.
- **Enterprise sideloading** — IT manages app distribution via MDM/Intune without the Store.
- **Windows 10S / Windows SE** — these locked-down editions only run Store-packaged apps.
- **You need modern packaging features** — package integrity, bundled architectures, or registered Windows services.

For standard consumer distribution with self-managed auto-updates, [NSIS](nsis.md) is simpler. For enterprise deployment without the Store, [MSI](msi.md) is often preferred.

## Requirements

MSIX packaging requires a modern Windows Kits toolset that bundles `makeappx.exe`, `makepri.exe`, and the MSIX-capable Windows SDK. Set [`toolsets.winCodeSign`](configuration.md) to any modern bundle `>= 1.0.0` (e.g. `1.0.0`, `1.1.0`, or `1.3.0`):

```yaml
toolsets:
  winCodeSign: "1.1.0"
win:
  target: msix
```

:::warning
The legacy `winCodeSign-2.6.0` bundle (toolset `0.0.0`) does **not** include the SDK version required for MSIX. Building MSIX without a modern toolset throws an `InvalidConfigurationError`.
:::

MSIX can be built on:

- **Windows 10 / Windows Server 2012 R2 (version 6.3+) or later** — native.
- **macOS via [Parallels Desktop](https://www.parallels.com/)** — electron-builder drives a Windows VM through Parallels to run the Windows-only packaging tools. See [Building on macOS](#building-on-macos) below.

Linux is not supported for MSIX.

## Quick Start

```yaml
win:
  target: msix
toolsets:
  winCodeSign: "1.1.0"
msix:
  identityName: "MyCompany.MyApp"
  publisher: "CN=My Company, O=My Company, C=US"
  publisherDisplayName: "My Company"
  displayName: "My Application"
```

```powershell
electron-builder --win msix
```

## MSIX Assets

MSIX uses the same logo/icon assets as AppX. Place them in the `appx` folder inside your [build resources](configuration.md) directory (default: `build/appx/`):

| Asset | File Name | Required | Size |
|---|---|---|---|
| Store Logo | `StoreLogo.png` | Yes | 50×50 |
| Square 150×150 | `Square150x150Logo.png` | Yes | 150×150 |
| Square 44×44 | `Square44x44Logo.png` | Yes | 44×44 |
| Wide 310×150 | `Wide310x150Logo.png` | Yes | 310×150 |
| Badge Logo | `BadgeLogo.png` | Optional | 24×24 |
| Large Tile | `LargeTile.png` | Optional | 310×310 |
| Small Tile | `SmallTile.png` | Optional | 71×71 |
| Splash Screen | `SplashScreen.png` | Optional | 620×300 |

Default assets are substituted for the required logos if you don't provide your own. Scale variants (e.g., `Square44x44Logo.scale-200.png`, `BadgeLogo.targetsize-48.png`) are supported — when any scaled asset is present, electron-builder runs `makepri.exe` to generate `resources.pri` automatically. See [Microsoft's tile asset guidelines](https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets).

## Identity and Publisher

```yaml
msix:
  applicationId: "MyApp"                            # alpha-numeric segments separated by periods, each segment starting with a letter
  identityName: "MyCompany.MyApp"                   # unique within the Store; 3–50 chars, [a-zA-Z0-9.-]
  publisher: "CN=My Company, O=My Company, C=US"    # must match certificate Subject exactly
  publisherDisplayName: "My Company"                # friendly publisher name shown to users
  displayName: "My Application"                     # shown in Start menu / Store
```

Defaults when omitted:

- `applicationId` → derived from `identityName` (a leading numeric prefix such as `01234` is stripped automatically, with a warning), falling back to the application name. Validated to 1–64 characters.
- `identityName` → the application `name`. Validated to 3–50 characters of `[a-zA-Z0-9.-]`.
- `publisherDisplayName` → company name from your application metadata (`author` in `package.json`). Building fails if neither is set.

:::warning[Publisher Must Match Certificate]
The `publisher` value must exactly match the Subject field of your code signing certificate. Mismatches cause installation failures. When building for the Store (no certificate configured), electron-builder uses `CN=ms` as a placeholder — the Store re-signs on submission.
:::

## Capabilities

MSIX apps declare the system capabilities they use:

```yaml
msix:
  capabilities:
    - runFullTrust                  # required for Electron apps — always included
    - internetClient                # outbound internet access
    - privateNetworkClientServer    # local network
    - webcam                        # camera access
    - microphone                    # microphone access
```

`runFullTrust` is required for Electron apps and is **always added**, even if you specify a different set — you cannot remove it. Invalid capability names cause the build to fail with `invalid windows capabilities`. The full list is in [Microsoft's capability documentation](https://docs.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations).

## Tile Customization

```yaml
msix:
  backgroundColor: "#2c2c2c"    # tile background color (CSS color). Default: #464646
  showNameOnTiles: false        # overlay app name on the wide & square tiles. Default: false
```

Optional tile assets enable extra visual elements automatically when present in `build/appx/`: `BadgeLogo.png` (lock-screen badge), `LargeTile.png` (310×310), `SmallTile.png` (71×71), and `SplashScreen.png`.

## Language Support

```yaml
msix:
  languages:
    - en-US     # first entry is the default language
    - de-DE
    - fr-FR
    - ja-JP
```

Declares which languages your app supports for Store listings and localized resources. Defaults to `["en-US"]`. Underscores are normalized to hyphens (`en_US` → `en-US`).

## Version Targeting

```yaml
msix:
  minVersion: "10.0.17763.0"        # Windows 10 1809 minimum (Store minimum). This is the default.
  maxVersionTested: "10.0.22621.0"  # defaults to minVersion when omitted
```

These map to `MinVersion` / `MaxVersionTested` on `<TargetDeviceFamily Name="Windows.Desktop" />`.

```yaml
msix:
  setBuildNumber: false   # include the build number in the package version. Default: false
```

## Package Integrity

```yaml
msix:
  enforcePackageIntegrity: true   # Default: false
```

When enabled, emits `<uap10:PackageIntegrity><uap10:Content Enforcement="on" /></uap10:PackageIntegrity>` inside `<Properties>`. The child `<uap10:Content Enforcement="on" />` element is what actually turns on runtime integrity checks — a bare/childless element is a no-op. Windows then verifies that no package files have been tampered with after signing. **Requires Windows 10 version 2004 (build 19041) or later.**

## Windows Services

Register Windows Services that ship inside the package (`desktop6` schema). **Requires Windows 10 version 1903 (build 18362) or later, and the `packagedServices` (or `localSystemServices`) restricted capability declared via [`capabilities`](#capabilities)** — services will not register without it.

```yaml
msix:
  capabilities:
    - packagedServices                 # restricted capability — required for windows.service
  windowsServices:
    - name: "MyBackgroundService"        # required — Service Control Manager name
      executable: "app/service.exe"      # optional — defaults to the main app executable
      startupType: "auto"                # auto | manual | disabled. Default: manual
      startAccount: "localSystem"        # localSystem | localService | networkService. Default: localSystem
      arguments: "--mode service"        # optional — command-line arguments
```

| Field | Required | Default | Description |
|---|---|---|---|
| `name` | Yes | — | Service name used in the Windows Service Control Manager. |
| `executable` | No | main app executable | Relative path to the service executable within the package. |
| `startupType` | No | `manual` | How Windows starts the service: `auto`, `manual`, or `disabled`. |
| `startAccount` | No | `localSystem` | The account the service runs under: `localSystem`, `localService`, or `networkService`. Required by the `desktop6:Service` schema and always emitted. |
| `arguments` | No | — | Command-line arguments passed to the service executable. |

Each entry produces a `<desktop6:Extension Category="windows.service" Executable="..." EntryPoint="Windows.FullTrustApplication"><desktop6:Service Name="..." StartupType="..." StartAccount="..." Arguments="..." /></desktop6:Extension>` declaration.

## Auto-Launch Extension

```yaml
msix:
  addAutoLaunchExtension: true
```

When `true`, adds a `desktop:Extension Category="windows.startupTask"` so the app runs on user login. When unset, it defaults to `true` only if `electron-winstore-auto-launch` is present in your dependencies, otherwise `false`. Requires the `runFullTrust` capability (always present).

## Protocols and File Associations

MSIX picks up the top-level [`protocols`](configuration.md) and [`fileAssociations`](configuration.md) configuration and emits the corresponding `uap:Extension` declarations — you do not configure these under `msix`:

```yaml
protocols:
  name: "My App Protocol"
  schemes: ["myapp"]
fileAssociations:
  - ext: "myext"
    name: "My Custom File"
```

## Custom Manifest and Extensions

For advanced scenarios, supply your own manifest template or inject extra XML:

```yaml
msix:
  customManifestPath: "msix/AppxManifest.xml"     # relative to build resources
  customExtensionsPath: "msix/extensions.xml"     # injected into <Extensions>
```

The custom manifest supports the same `${}` macros as the [default template](https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/templates/msix/appxmanifest.xml), including the MSIX-specific macro `${packageIntegrity}`. Windows services are injected via the `${extensions}` macro alongside protocol/file-association extensions.

Note the two paths resolve against **different** base directories: `customManifestPath` is relative to the [build resources](configuration.md) directory, whereas `customExtensionsPath` is resolved relative to the **app source directory** (the packaged app dir), not the build resources directory.

:::warning[Namespaces in a custom template]
If you supply your own template, it must declare the namespaces used by the features you enable — `desktop` (auto-launch startup task), `uap` (protocols / file associations), `uap10` (package integrity), and `desktop6` (services) — and list the appropriate prefixes in `IgnorableNamespaces`. Omitting them produces an invalid manifest. Start from the default template above.
:::

## Bundles and Store Uploads

When you build more than one architecture, electron-builder can combine the per-architecture `.msix` packages into a single `.msixbundle`, and optionally wrap it into a `.msixupload` archive for the Microsoft Store Partner Center:

```yaml
win:
  target:
    - target: msix
      arch:
        - x64
        - arm64
msix:
  createMsixbundle: true     # combine architectures into one .msixbundle. Default: true (when >1 arch)
  createMsixupload: true     # produce a .msixupload for Partner Center submission. Default: false
```

- A `.msixbundle` is produced only when more than one architecture is built and `createMsixbundle` is not `false`.
- A `.msixupload` is a plain ZIP of the bundle (or, if no bundle was produced, the single package). It is **not** code-signed — the Store re-signs on submission.

:::note
If you set `createMsixbundle: false` while building multiple architectures and request `createMsixupload: true`, the upload will contain only the first architecture's package. Keep `createMsixbundle` enabled for multi-architecture Store submissions.
:::

## Code Signing {#msix-package-code-signing}

MSIX artifacts (`.msix`, `.msixbundle`) are signed with **SHA-256** (single signature; dual SHA-1/SHA-256 signing does not apply to MSIX).

- **Store distribution** — no manual signing needed. The Store signs the package with a Microsoft certificate during submission; the `publisher` is replaced with a Store-managed identity.
- **Sideloading / enterprise** — the package must be signed with a trusted certificate whose Subject matches `publisher`. See [Windows Code Signing](features/code-signing/code-signing-win.md).

For self-signed certificates during development, see [Microsoft's certificate guide](https://learn.microsoft.com/en-us/windows/msix/package/create-certificate-package-signing).

## Building on macOS

MSIX packaging tools are Windows-only. On macOS, electron-builder runs them inside a Windows VM via [Parallels Desktop](https://www.parallels.com/). Temp files are routed through your home directory so the VM can access them over the always-available `\\Mac\Home` share. Ensure a Parallels Windows VM is configured and the modern `winCodeSign` toolset is available inside it.

## Testing Locally

```powershell
# Install locally (Developer Mode must be enabled for unsigned packages, or import the signing cert first)
Add-AppxPackage -Path ".\MyApp-1.0.0.msix"

# Install a bundle
Add-AppxPackage -Path ".\MyApp-1.0.0.msixbundle"

# Verify it is registered
Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" }

# Inspect the installed manifest
$p = Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" } | Select-Object -First 1
(Get-AppxPackageManifest -Package $p.PackageFullName).OuterXml

# Uninstall
Get-AppxPackage | Where-Object { $_.Name -like "*MyApp*" } | Remove-AppxPackage
```

To install a package signed with a self-signed certificate, first import that certificate into **Trusted People** on the target machine — see [Microsoft's signing guide](https://learn.microsoft.com/en-us/windows/msix/package/sign-app-package-using-signtool).

## Publishing to the Microsoft Store

1. Create a [Microsoft Developer account](https://developer.microsoft.com/en-us/store/register/) and reserve your app name in [Partner Center](https://partner.microsoft.com/en-us/dashboard/windows/overview).
2. Build with `createMsixupload: true` to produce a `.msixupload`.
3. Upload the `.msixupload` via the Partner Center submission flow.
4. Complete the submission (age rating, pricing, screenshots) and wait for certification.

## Auto-Updates

MSIX auto-updates are handled by the Microsoft Store (or your App Installer `.appinstaller` flow), **not** by electron-updater. For self-managed auto-updates outside the Store, use [NSIS](nsis.md) instead.

## Common Questions

**Which toolset version do I need?** `toolsets.winCodeSign` can be any modern bundle `>= 1.0.0` (e.g. `1.0.0`, `1.1.0`, or `1.3.0`). Only an unset toolset or the legacy `0.0.0` bundle (`winCodeSign-2.6.0`, which lacks the required SDK) is rejected.

**Can I build MSIX on Linux?** No. Use Windows, or macOS with Parallels Desktop.

**Why is my multi-arch `.msixbundle` named with one architecture?** Bundle and upload artifacts span all built architectures; a custom `artifactName` containing `${arch}` will still render a single architecture token. Prefer an `artifactName` without `${arch}` for bundles.

**Does MSIX support auto-update via electron-updater?** No — see [Auto-Updates](#auto-updates) above.

## Configuration

{!./app-builder-lib.Interface.MsixOptions.md!}

---

### Notarization (`website/docs/notarization.md`)

# macOS Notarization

Notarization is Apple's security service that scans your app for malicious software before distribution. Starting with macOS 10.15 (Catalina), apps distributed outside the Mac App Store **must** be notarized or Gatekeeper will block them.

## Notarization vs. Code Signing

These are two separate steps, both required:

| Step | What it does | Required since |
|---|---|---|
| **Code signing** | Proves the app came from you and wasn't modified | macOS 10.8 (Mountain Lion) |
| **Notarization** | Apple scans your app for malware; issues a ticket | macOS 10.15 (Catalina) |
| **Stapling** | Attaches the notarization ticket to the app bundle | Recommended (enables offline verification) |

electron-builder handles all three steps automatically when configured.

## Prerequisites

Before notarizing, you need:

1. An active **Apple Developer Program** membership ($99/year)
2. A **Developer ID Application** certificate (not Mac App Store)
3. **Hardened Runtime** enabled in your build configuration
4. Required **entitlements** set (see below)
5. Authentication credentials (Apple ID + app-specific password, or API key)

## Enabling Notarization

```yaml
mac:
  notarize: true
  sign:
    hardenedRuntime: true
    entitlements: build/entitlements.mac.plist
    entitlementsInherit: build/entitlements.mac.inherit.plist
```

:::note[v27]
`notarize` stays on `mac`, but the signing options (`hardenedRuntime`, `entitlements`, `entitlementsInherit`) moved under `mac.sign` in v27. See [macOS signing](./migration/v27-breaking-changes.md#macos-signing-macsign).
:::

### Authentication — Option A: Apple ID

Create an app-specific password at [appleid.apple.com](https://appleid.apple.com) (do **not** use your Apple ID password directly).

Set these environment variables:

```bash
APPLE_ID=you@example.com
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
APPLE_TEAM_ID=ABCDE12345
```

### Authentication — Option B: App Store Connect API Key (Recommended for CI)

API keys don't expire and don't require two-factor authentication, making them ideal for CI.

1. Go to [App Store Connect → Users and Access → Keys](https://appstoreconnect.apple.com/access/integrations/api)
2. Create a new key with "Developer" role
3. Download the `.p8` file (only downloadable once)
4. Note the **Key ID** and **Issuer ID**

Then set the following environment variables:

```bash
# base64-encode the .p8 file for use as a CI secret
APPLE_API_KEY=<base64-encoded-p8-content>
APPLE_API_KEY_ID=KEYID
APPLE_API_ISSUER=your-issuer-id
APPLE_TEAM_ID=ABCDE12345
```

### Authentication — Option C: Keychain Profile (stored credentials)

If you have previously stored notarization credentials via `xcrun notarytool store-credentials`, reference them by profile name:

```bash
APPLE_KEYCHAIN_PROFILE=my-notarization-profile
APPLE_KEYCHAIN=/path/to/keychain.keychain-db  # optional; defaults to system keychain
```

This approach requires credentials to be pre-stored on the build machine and is not suitable for ephemeral CI environments.

## Hardened Runtime Requirements

Notarization requires **Hardened Runtime** (`mac.sign.hardenedRuntime: true`). This restricts what your app process can do unless you explicitly declare entitlements.

Modern Electron needs exactly one exception, and electron-builder grants it by default — **no entitlements file is required to notarize a stock Electron app**:

```xml
<?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/>
</dict>
</plist>
```

Write that to `build/entitlements.mac.plist` only if you need to add capabilities of your own; your file replaces the default rather than extending it, so keep `com.apple.security.cs.allow-jit` in it.

Do **not** add `com.apple.security.cs.allow-unsigned-executable-memory` out of habit. It was needed by Electron versions from the 2019 era, is not needed by modern V8/Electron, and weakens the Hardened Runtime for no benefit.

Leave `build/entitlements.mac.inherit.plist` absent unless you truly need it. Without it, each nested binary is signed with [`@electron/osx-sign`](https://github.com/electron/osx-sign)'s per-file defaults, which mirror Chromium's own entitlements — renderer and GPU helpers get only `allow-jit`, and the looser exceptions go to the plugin helper alone. Supplying the file applies one plist to every nested binary instead.

### Common Entitlements

| Entitlement | When Needed |
|---|---|
| `com.apple.security.cs.allow-jit` | Required by Electron (V8 JIT) — granted by default |
| `com.apple.security.cs.allow-unsigned-executable-memory` | Legacy Electron only — not needed by modern V8/Electron and weakens the Hardened Runtime |
| `com.apple.security.cs.disable-library-validation` | Loading frameworks/native modules signed by another team, or unsigned |
| `com.apple.security.network.client` | Outbound network access |
| `com.apple.security.network.server` | Incoming connections |
| `com.apple.security.files.user-selected.read-write` | Read/write files the user selects |
| `com.apple.security.files.downloads.read-write` | Read/write the Downloads folder |
| `com.apple.security.device.camera` | Camera access |
| `com.apple.security.device.microphone` | Microphone access |
| `com.apple.security.personal-information.location` | Location services |

## Notarization in GitHub Actions

```yaml
jobs:
  build-mac:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Build, sign, and notarize
        run: npx electron-builder --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 }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### Required GitHub Secrets

| Secret | Value |
|---|---|
| `MAC_CSC_LINK` | Base64-encoded `.p12` certificate |
| `MAC_CSC_KEY_PASSWORD` | Certificate password |
| `APPLE_ID` | Your Apple ID email |
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password from appleid.apple.com |
| `APPLE_TEAM_ID` | Your 10-character team ID (e.g., `ABCDE12345`) |

## How Stapling Works

After notarization, Apple returns a "ticket" that confirms your app was approved. electron-builder automatically **staples** this ticket to the app bundle — meaning the notarization check can succeed even without an internet connection.

Stapling is performed automatically when `notarize: true`. You don't need to configure anything extra.

## Testing Notarization

After building, verify the notarization status:

```bash
# Check Gatekeeper assessment (simulates what macOS does on first launch)
spctl --assess --verbose --type exec dist/mac/MyApp.app

# Should output:
# dist/mac/MyApp.app: accepted
# source=Notarized Developer ID

# Check that the stapling ticket is attached
xcrun stapler validate dist/mac/MyApp.app
# Should output: The validate action worked!

# Check code signature
codesign --verify --deep --strict --verbose=2 dist/mac/MyApp.app
```

## Troubleshooting

**"The software certificate used to sign this application is not trusted on this machine"**
: Your Developer ID certificate is not in the system keychain or hasn't been trusted. Verify `CSC_LINK` points to a valid Developer ID certificate — electron-builder imports and configures the certificate automatically.

**Notarization hangs indefinitely**
: Apple's notarization service may be slow. electron-builder polls for completion. If it consistently times out, check your network connection and Apple's [developer system status](https://developer.apple.com/system-status/).

**"Package Invalid: the binary is not signed with a valid Developer ID certificate"**
: You used a Mac App Store certificate for direct distribution, or vice versa. Direct distribution requires a "Developer ID Application" certificate, not "Apple Distribution."

**"App can't be opened because Apple cannot check it for malicious software"**
: Notarization succeeded but the app wasn't stapled, or the user opened the app before stapling completed. Run `xcrun stapler validate` to check. Rebuild with electron-builder to get a properly stapled app.

**"com.apple.security.cs.allow-jit entitlement required"**
: Add the JIT entitlement to your `entitlements.mac.plist`. See the entitlements section above.

**Notarization rejected: "The executable does not have the Hardened Runtime enabled"**
: Set `mac.sign.hardenedRuntime: true` in your mac configuration.

**"Invalid Info.plist (plist or signature have wrong format)"**
: All binaries inside the app bundle must be signed. This often happens with third-party native modules or bundled binaries. Ensure `asar.unpack` is set for any `.node` files and that all binaries are signed before creating the final distributable. Use a custom `afterSign` hook if you need to sign additional binaries.

## Related Pages

- [macOS Code Signing](features/code-signing/code-signing-mac.md) — exporting certificates, keychain management, disabling signing
- [Code Signing Overview](features/code-signing/code-signing.md) — environment variables, CI setup, certificate types
- [macOS Configuration](mac.md) — entitlements, Hardened Runtime, bundle ID setup
- [GitHub Actions](features/github-actions.md) — full workflow examples

---

### Nsis (`website/docs/nsis.md`)

---
title: "NSIS"
---

The top-level [nsis](./configuration.md#nsis) key contains a set of options instructing electron-builder on how it should build NSIS target (default target for Windows).

These options are also applicable for the [Web installer](#web-installer), use top-level `nsisWeb` key.

---

Unicode enabled by default. Large strings are supported (maximum string length of 8192 bytes instead of the default of 1024 bytes).

## 32 bit + 64 bit

If you build both ia32 and x64 arch (`--x64 --ia32`), you will always get one installer. The appropriate arch will be installed automatically.
The same applied to web installer (`nsis-web` [target](win.md#target)). The same dual-arch installer mechanism also applies to `--x64 --arm64`.

:::note[ia32 requires Electron <= 43]
[Electron 44 removed Windows ia32 builds](https://github.com/electron/electron/pull/51816) — building ia32 requires `electronVersion` <= 43.x (supported until the v43 series reaches end-of-life in January 2027).
:::

## Web Installer

To build web installer, set [target](win.md#target) to `nsis-web`. Web Installer automatically detects OS architecture and downloads corresponding package file. So, the user doesn't need to guess what installer to download and at the same time you don't bundle package files for all architectures in one installer (as in case of default `nsis` target). It doesn't matter for common Electron application (due to superb LZMA compression, size difference is acceptable), but if your application is huge, Web Installer is a solution.

To customize web installer, use the top-level `nsisWeb` key (not `nsis`).

If for some reasons web installer cannot download (antivirus, offline):

- Download package file into the same directory where installer located. It will be detected automatically and used instead of downloading from the Internet. Please note — only original package file is allowed (checksum is checked).
- Specify a local package file using `--package-file=path_to_file`. It must be one of the packages built with the installer (any arch; checksum is checked).

The web installer verifies the package it installs against the SHA-512 of the packages built with it:

- A package passed via `--package-file` (electron-updater passes the package it verified against the update manifest) must match one of them, for any arch.
- A package downloaded from the default, publish-derived (versioned) URL must match the package for the detected arch.
- A package downloaded from an explicit `appPackageUrl` (e.g. a version-independent `latest` URL) is **not** verified, since that URL can serve packages of other builds.

A local package (passed via `--package-file`, or found next to the installer) is first copied into the installer's own temporary directory. The installer verifies and installs that copy. The local file itself is left in place; for an update, electron-updater removes the package it passed once the updated app starts. With `allowUnverifiedAppPackage` a package passed via `--package-file` is still copied, but not verified.

A mismatch, a `--package-file` package that cannot be copied, or a package that cannot be downloaded (also a download cancelled in the progress window) aborts the installation (exit code `2`); a package next to the installer that doesn't match or cannot be copied is ignored and the package is downloaded instead. When a download fails, an interactive run first offers to retry, and a silent run (`/S`) aborts right away. The installer copies, verifies and, if needed, downloads the package before it removes an installed version, so an installation aborted for one of these reasons leaves that version in place. Because the download can take a while, a run that isn't an update (`--updated`) then checks again whether the app is running, with the same prompt as at the start (Cancel aborts with exit code `2`), before the installed version is removed; a custom `customCheckAppRunning` macro is not run a second time. Set `nsisWeb.allowUnverifiedAppPackage: true` (default `false`) to skip the `--package-file` and versioned-download checksum checks (a package next to the installer is still checked) only if you intentionally use one web installer with packages of other builds (e.g. a version-independent installer with `--package-file`).

For auto-update, electron-updater installs web-installer updates only when `disableWebInstaller` is `false`; installs made by an `nsis-web` installer built with electron-builder v27+ default to `false` automatically — see [Auto Update](./features/auto-update.md#disablewebinstaller-now-defaults-to-true). Don't set `updateManifest: false` for `nsis-web` apps: [signed update manifests](./features/signed-update-manifests.md) cover the web package's path, SHA-512 and size.

## Custom NSIS script

Two options are available — [include](#include) and [script](#script). `script` allows you to provide completely different NSIS script. For most cases it is not required as you need only to customise some aspects, but still use well-tested and maintained default NSIS script. So, `include` is recommended.

:::warning[Custom `script` disables built-in safeguards]
When you provide a custom `script`, electron-builder no longer generates (and signs) the uninstaller for you and skips installer size verification. Prefer `include` unless you really need to replace the whole script.
:::

For `nsis-web`, a custom `script` that copies installSection.nsh should insert `!insertmacro prepareWebPackage` before `uninstallOldVersion`; otherwise the package is prepared in `installApplicationFiles`, after the installed version is removed.

Keep in mind — if you customize the NSIS script, you should always mention it in issue reports. And don't expect that your issue will be resolved.

:::warning[v27: file-association ProgID format changed]
NSIS installers now register each `fileAssociations` entry under a unique generated **ProgID** (`<program>.<component>`, derived from `productName` + the app GUID) instead of using the association `name`/extension verbatim, which could collide with unrelated apps. `fileAssociations` and its `name`/`ext`/`description` fields are unchanged and nothing needs migrating — **but** if your custom `include`/`script` (or external tooling) hard-codes the old ProgID (the association name or extension) to add shell verbs or registry keys, update it to the new generated value. See [v27 Breaking Changes → NSIS file-association ProgID](./migration/v27-breaking-changes.md#nsis-file-association-progid-format-changed).
:::

1. Add file `build/installer.nsh` (or set [include](#include) explicitly — a single path, or an array of paths that are all included in order, e.g. `"include": ["build/installer.nsh", "build/signing.nsh"]`; each path is resolved relative to the build resources directory first, then relative to the project directory).
2. Define wanted macro to customise: `customHeader`, `preInit`, `customInit`, `customUnInit`, `customInstall`, `customUnInstall`, `customRemoveFiles`, `customInstallMode`, `customWelcomePage`, `customUnWelcomePage`, `customUnInstallSection`.

:::note[Example]

```nsis
!macro customHeader
  !system "echo '' > ${BUILD_RESOURCES_DIR}/customHeader"
!macroend

!macro preInit
  ; This macro is inserted at the beginning of the NSIS .OnInit callback
  !system "echo '' > ${BUILD_RESOURCES_DIR}/preInit"
!macroend

!macro customInit
  !system "echo '' > ${BUILD_RESOURCES_DIR}/customInit"
!macroend

!macro customInstall
  !system "echo '' > ${BUILD_RESOURCES_DIR}/customInstall"
!macroend

!macro customInstallMode
  # set $isForceMachineInstall or $isForceCurrentInstall
  # to enforce one or the other modes.
!macroend

!macro customWelcomePage
  # Welcome Page is not added by default for installer.
  !insertMacro MUI_PAGE_WELCOME
!macroend

!macro customUnWelcomePage
  !define MUI_WELCOMEPAGE_TITLE "custom title for uninstaller welcome page"
  !define MUI_WELCOMEPAGE_TEXT "custom text for uninstaller welcome page $\r$\n more"
  !insertmacro MUI_UNPAGE_WELCOME
!macroend

!macro customUnInstallSection
  Section /o "un.Some cool checkbox"
    ; You can add some uninstall section as component page
    ; If defined, then always run after `customUnInstall`
  SectionEnd
!macroend
```

:::

- `BUILD_RESOURCES_DIR` and `PROJECT_DIR` are defined.
- `build` is added as `addincludedir` (i.e. you don't need to use `BUILD_RESOURCES_DIR` to `!include` sibling files from the build resources directory).
- `build/x86-unicode` and `build/x86-ansi` are added as `addplugindir` (each one only when the directory exists, regardless of the `unicode` option).
- File associations macro `registerFileAssociations` and `unregisterFileAssociations` are still defined.
- All other electron-builder specific flags (e.g. `ONE_CLICK`) are still defined.

:::note[Uninstaller lifecycle — `customUnInstall` changes take effect one version later]
The uninstaller that runs during an uninstall **or during an update** is the `Uninstall <app>.exe` that was written to disk by the **previously installed** version — not the one embedded in the installer that is currently running. So when you add or change `customUnInstall` (or anything else affecting the uninstaller), the change only becomes active after the _next_ install: version N ships the new uninstaller, and it is first executed when version N is uninstalled or updated to N+1. If your `customUnInstall` "does not fire", it is almost always because the machine still runs the old uninstaller from the previous version.
:::

:::warning[Uninstall registry key — do not hard-code `...\Uninstall\<appId>`]
electron-builder registers the uninstall entry under `Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_APP_KEY}`, where `UNINSTALL_APP_KEY` is the application **GUID** (with each `\` replaced by " - ", i.e. space, hyphen, space) — _not_ the `appId`. In a custom script use the provided defines instead of building the path yourself: `${UNINSTALL_REGISTRY_KEY}` (the full key, see `multiUser.nsh`) or `${UNINSTALL_APP_KEY}`. A hard-coded `Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_ID}` points at a key electron-builder never writes.
:::

If you want to include additional resources for use during installation, such as scripts or additional installers, you can place them in the `build` directory and include them with `File`. For example, to include and run `extramsi.msi` during installation, place it in the `build` directory and use the following:

```nsis
!macro customInstall
  File /oname=$PLUGINSDIR\extramsi.msi "${BUILD_RESOURCES_DIR}\extramsi.msi"
  ExecWait '"msiexec" /i "$PLUGINSDIR\extramsi.msi" /passive'
!macroend
```

:::tip[Is there a way to call just when the app is installed (or uninstalled) manually and not on update?]
Use `${isUpdated}`.

```nsis
${ifNot} ${isUpdated}
  # your code
${endIf}
```

:::

## GUID vs Application Name

Windows requires to use registry keys (e.g. INSTALL/UNINSTALL info). Squirrel.Windows simply uses application name as key.
But it is not robust — Google can use key Google Chrome SxS, because it is a Google.

So, it is better to use [GUID](http://stackoverflow.com/a/246935/1910191).
You are not forced to explicitly specify it — name-based [UUID v5](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_5_.28SHA-1_hash_.26_namespace.29) will be generated from your [appId](./configuration.md#appid) or [name](configuration.md#metadata).
It means that you **should not change appId** once your application in use (or name if `appId` was not set). Application product name (title) or description can be safely changed.

You can explicitly set guid using option [nsis.guid](#guid), but it is not recommended — consider using [appId](./configuration.md#appid).

It is also important to set the Application User Model ID (AUMID) to the [appId](./configuration.md#appid) of the application, in order for notifications on Windows 8/8.1 to function and for Window 10 notifications to display the app icon within the notifications by default. The AUMID should be set within the Main process and before any BrowserWindows have been opened, it is normally the first piece of code executed: `app.setAppUserModelId(appId)`

## Portable

To build portable app, set target to `portable` (or pass `--win portable`).

For portable app, following environment variables are available:

- `PORTABLE_EXECUTABLE_FILE` - path to the portable executable.
- `PORTABLE_EXECUTABLE_DIR` - directory where the portable executable is located.
- `PORTABLE_EXECUTABLE_APP_FILENAME` - sanitized app name to use in [file paths](https://github.com/electron-userland/electron-builder/issues/3186#issue-345489962).

The portable target also supports a custom NSIS script via `portable.include` (a single path or an array of paths). Unlike the installer targets, `build/installer.nsh` is **not** auto-discovered for portable builds — a custom script is only included when the option is explicitly set, so an `installer.nsh` written for the installer target does not silently leak into portable builds.

## Common Questions

:::tip[How do I change the default installation directory?]

It is very specific requirement. Do not do if you are not sure. Add [custom macro](#custom-nsis-script):

```nsis
!macro preInit
  SetRegView 64
  WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
  WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
  SetRegView 32
  WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
  WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\MyApp"
!macroend
```

:::

:::tip[Is it possible to make a single installer that will allow configuring user/machine installation?]

Yes, you need to switch to assisted installer (not default one-click).

package.json

```json
"build": {
  "nsis": {
    "oneClick": false
  }
}
```

electron-builder.yml

```yaml
nsis:
  oneClick: false
```

:::

## Configuration

{!./app-builder-lib.Interface.NsisOptions.md!}

---

### Pkg (`website/docs/pkg.md`)

The top-level [pkg](configuration.md) key contains a set of options instructing electron-builder on how it should build macOS [PKG](https://en.wikipedia.org/wiki/Package_(macOS)) product archive installers.

## When to Use PKG

PKG (`.pkg`) files are macOS installer packages that run Apple's native Installer application. Use PKG when:

- **System-level installs are required** — installing launch daemons, kernel extensions (or System Extensions on modern macOS), or files outside `/Applications`
- **Custom install scripts are needed** — pre/postinstall shell scripts run with root privileges during installation
- **Enterprise managed deployment** — PKG integrates with MDM systems and software management tools like Munki, Jamf, and SCCM for macOS
- **Installer UI customization** — displaying welcome screens, license agreements, readme files, and conclusion pages

For standard consumer app distribution, [DMG](dmg.md) is the conventional choice. For Mac App Store distribution, see [MAS](mas.md).

## Certificate Requirements

Signing a PKG file requires a **Developer ID Installer** certificate — this is a different certificate from the **Developer ID Application** certificate used to sign the app bundle inside the PKG.

```yaml
pkg:
  identity: "Developer ID Installer: My Company (TEAM1234AB)"
```

Or use the `CSC_INSTALLER_LINK` and `CSC_INSTALLER_KEY_PASSWORD` environment variables in CI.

:::tip
You can have both `Developer ID Application` and `Developer ID Installer` certificates. The application certificate signs the `.app` bundle, and the installer certificate signs the `.pkg` wrapper.
:::

:::note[v27: pkg.identity is not inherited]
As of v27 the `.pkg` installer identity resolves from `pkg.identity` (or `CSC_INSTALLER_LINK`) **only** — it no longer falls back to the app/mac-level signing identity (the old `mac.identity` fallback is gone, since that field moved to `mac.sign.identity`). If your `.pkg` relied on inheriting the mac-level identity, set `pkg.identity` explicitly.
:::

## Installation Location

By default the app is installed to `/Applications`. Override with `installLocation`:

```yaml
pkg:
  installLocation: "/Applications"
```

Setting a non-standard install location (e.g., `/usr/local/bin`) is unusual for Electron apps but may be needed for CLI tools bundled with the app.

## Install Scripts

Place shell scripts in the `scripts` directory (default: `build/pkg-scripts/`). These run at install time with root privileges:

| Script Name | When It Runs |
|---|---|
| `preinstall` | Before files are installed |
| `postinstall` | After files are installed |
| `preupgrade` | Before an upgrade (rarely used) |
| `postupgrade` | After an upgrade (rarely used) |

```yaml
pkg:
  scripts: build/pkg-scripts   # default
```

Example `postinstall` script:

```bash
#!/bin/bash
# Register a launch daemon after install
launchctl load /Library/LaunchDaemons/com.mycompany.myapp.daemon.plist
exit 0
```

:::warning[Script Permissions]
Scripts must be executable (`chmod +x`). Scripts run as root during installation — test thoroughly. A failing script (non-zero exit code) will abort the installation.
:::

See the [macOS Kernel Extensions tutorial](tutorials/macos-kernel-extensions.md) for a detailed example.

## Installer UI Customization

Customize the pages displayed in Apple's Installer:

```yaml
pkg:
  welcome: build/welcome.html    # or .rtf, .txt
  license: build/license.html    # or .rtf, .txt
  conclusion: build/conclusion.html
```

Supported file formats for each page: HTML, RTF, plain text.

**Background image** — displayed behind the installer pages (macOS 10.5+):

```yaml
pkg:
  background:
    file: build/installer-background.png
    alignment: center       # center | left | right | top | bottom | topleft | topright | bottomleft | bottomright
    scaling: tofit          # tofit | proportional | none
```

## Installation Behavior Options

Control how the installer behaves when re-installing or upgrading:

| Option | Default | Description |
|---|---|---|
| `isRelocatable` | `true` | Allow users to choose a different install location |
| `isVersionChecked` | `true` | Block downgrades (PKG with older version won't install over newer) |
| `hasStrictIdentifier` | `true` | Require matching bundle ID for upgrades |
| `overwriteAction` | `upgrade` | `upgrade` (recommended) or `update` |

For most apps, leave these at their defaults.

## Forcing App Closure Before Install

If your installer or upgrade requires the app to not be running:

```yaml
pkg:
  mustClose:
    - com.mycompany.myapp
    - com.mycompany.myapp.helper
```

The Installer will prompt the user to quit any running processes with matching bundle IDs before proceeding.

## Extra Component Packages

Bundle additional `.pkg` files alongside the main component:

```yaml
pkg:
  extraPkgsDir: build/extra-pkgs
```

All `.pkg` files in this directory will be included as additional components in the installer. Useful for installing additional software alongside your app (e.g., a CLI tool, a system extension).

## Complete Example

```yaml
pkg:
  identity: "Developer ID Installer: My Company (TEAM1234AB)"
  installLocation: "/Applications"
  scripts: build/pkg-scripts
  welcome: build/welcome.html
  license: build/license.rtf
  conclusion: build/conclusion.html
  background:
    file: build/background.png
    alignment: center
    scaling: tofit
  mustClose:
    - com.mycompany.myapp
  isRelocatable: true
  overwriteAction: upgrade
```

## Troubleshooting

**"The package is not signed" error:** Ensure you have a valid Developer ID Installer certificate (not Application). Check the certificate identity string matches exactly.

**Script fails silently:** Run the PKG in verbose mode via Terminal: `sudo installer -verbose -pkg YourApp.pkg -target /`. Non-zero exit from a script aborts installation.

**App not found after install:** Verify `installLocation` is correct and the app bundle ends up at the expected path. Check Console.app for installer logs.

**PKG won't overwrite existing version:** The `isVersionChecked: false` option disables version checking, allowing downgrades.

## Configuration

{!./app-builder-lib.Interface.PkgOptions.md!}

---

### Programmatic Usage (`website/docs/programmatic-usage.md`)

---
title: "Programmatic Usage"
---

:::note
**Node.js >=22.12.0 required.** electron-builder v27 ships as native ESM. CJS `require()` continues to work on Node >=22.12 via Node's stable `require(esm)` support.
:::

The example below uses ESM syntax (recommended). A CJS equivalent is shown at the bottom.

```
/* Detailed source-code truncated for AI context efficiency. */
```

**CJS equivalent (Node >=22.12.0):**
```javascript
const { build, Platform } = require("electron-builder")

build({
  targets: Platform.MAC.createTarget(),
  config: { /* options */ },
})
  .then((result) => console.log(JSON.stringify(result)))
  .catch((error) => console.error(error))
```

---

### Publish (`website/docs/publish.md`)

{!./partials/_upgrading-from-v26.md!}

The [publish](./configuration.md#publish) key contains a set of options instructing electron-builder on how it should publish artifacts and build update info files for [auto update](./features/auto-update.md).

`String | Object | Array<Object | String>` where `Object` it is [Keygen](#keygen), [Generic Server](#byo-generic-create-your-own), [GitHub](#github), [S3](#s3), [Spaces](#spaces), [R2](#r2) or [Snap Store](#snap-store) options. Order is important — first item will be used as a default auto-update server. Can be specified in the [top-level configuration](./configuration.md) or any platform- ([mac](mac.md), [linux](linux.md), [win](win.md)) or target- (e.g. [nsis](nsis.md)) specific configuration.

Note that when using a generic server, you have to upload the built application and metadata files yourself.

Travis and AppVeyor support publishing artifacts. But it requires additional configuration for each CI and you need to configure what to publish.
`electron-builder` makes publishing dead simple.

!!! important "Publishing Must Be Explicitly Requested"
    Publishing is not performed automatically. You must explicitly request publishing using the `--publish` CLI flag (e.g., `--publish always`, `--publish onTag`, `--publish onTagOrDraft`) or by setting the `publish` option in your configuration.

If `GH_TOKEN` or `GITHUB_TOKEN` is defined — defaults to `[{provider: "github"}]`.

If `KEYGEN_TOKEN` is defined and `GH_TOKEN` or `GITHUB_TOKEN` is not — defaults to `[{provider: "keygen"}]`.

If `GITHUB_RELEASE_TOKEN` is defined, it will be used instead of (`GH_TOKEN` or `GITHUB_TOKEN`) to publish your release.
- e.g. mac: ``` export GITHUB_RELEASE_TOKEN=<my token> ```
- the `GITHUB_TOKEN` will still be used when your app checks for updates, etc.
- you could make your `GITHUB_TOKEN` "Read-only" when creating a fine-grained personal access token, and "Read and write" for the `GITHUB_RELEASE_TOKEN`.
- "Contents" fine-grained permission was sufficient. (at time of writing - Apr 2024)

:::warning[Implicit publishing was removed in v27]
In v26 and earlier, electron-builder auto-detected when to publish based on CI environment conditions (npm lifecycle event, a git tag in CI, etc.). **As of v27 this implicit behavior is removed** — publishing never happens unless you request it. This closes a hole where unfinished work or secrets could be published unintentionally.

Explicitly specify your publish intent with the `--publish` CLI flag (e.g. `--publish always`, `--publish onTag`, `--publish onTagOrDraft`, `--publish never`) or set the `publish` option in your configuration. See [v27 Breaking Changes → implicit `--publish` removed](./migration/v27-breaking-changes.md#implicit-publish-removed).
:::

:::info[Snap store]
`snap` target by default publishes to the Snap Store (the app store for Linux). To force publishing to another provider, explicitly specify publish configuration for `snap`.
:::

You can publish to multiple providers. For example, to publish Windows artifacts to both GitHub and Bitbucket (order is important — first item will be used as a default auto-update server, so in this example the app will use GitHub as the auto-update provider):

```json
{
  "build": {
    "win": {
      "publish": ["github", "bitbucket"]
    }
  }
}
```

```yaml
win:
  publish:
      # an object provider for github with additional options
    - provider: github
      protocol: https
      # a string provider for bitbucket that will use default options
    - bitbucket
```

### Which settings become the auto-update feed (`app-update.yml`) {#app-update-yml-feed}

The auto-update feed an installed app polls is the **first** provider written to `app-update.yml` inside the packaged app. One packaged app (for example `win-unpacked`, `linux-unpacked` or the `.app` bundle) holds a single `app-update.yml`, shared by every target built from it. It is resolved like this:

1. The targets of that app that write update info — NSIS/NSIS web installer, AppX with `electronUpdaterAware`, macOS dmg/zip, AppImage, deb/rpm/pacman — each resolve their own `publish`: the target-level value (e.g. `nsis.publish`) first, then the platform level (`win.publish`), then the top level.
2. If one or more of them emit a manifest under those settings, their first provider that receives the manifest becomes the feed — a provider with `publishAutoUpdate: false` gets no `latest*.yml`, so it is skipped (with `publish: [{ provider: "s3", …, publishAutoUpdate: false }, "github"]` the feed is GitHub). They must agree: two such targets whose first providers differ (for example `nsis.publish` pointing at S3 while an AppX inherits `win.publish: github`, or `dmg.publish` differing from `zip.publish`) would leave some installs polling a feed that never receives their manifest. A publishing build then fails at build start with an `InvalidConfigurationError`; a build without a publish policy logs a warning that publishing will fail, and writes no `app-update.yml` for that app. Configure `publish` once at the platform level, or give the targets the same first provider (later providers may differ). Only what decides the feed is compared — the provider and options such as `url`, `bucket`, `region`, `path`, `owner`, `repo` or `channel`; options that only affect the upload (`publishAutoUpdate`, `timeout`, and for S3-compatible providers `acl`, `storageClass`, `encryption`) may differ.
3. If none of them emits a manifest — only non-updating targets such as snap or portable, or a target with `publish: null` or `publishAutoUpdate: false` on every provider — the platform/top-level `publish` is used, as before.
4. If no level configures `publish` at all, the feed falls back to GitHub when the `repository` field points there (see [GitHub Repository](#github-repository-and-bintray-package)).

For example, with only a target-level setting

```yaml
nsis:
  publish:
    provider: s3
    bucket: my-updates
```

installed NSIS apps poll the S3 bucket and embed the update-manifest trust key. Before this rule, the target-level setting was not read for `app-update.yml`: such a build shipped no `app-update.yml` at all (no feed and no trusted key), or — with a GitHub `repository` — a GitHub feed while the manifests went to S3.

You can also configure publishing using CLI arguments, for example, to force publishing snap not to Snap Store, but to GitHub: `-c.snap.publish=github`

A [custom](https://github.com/electron-userland/electron-builder/issues/3261) publish provider can be used if needed.

:::tip[Macros]
In all publish options [File Macros](./file-patterns.md#file-macros) are supported.
:::

## How to Publish

Excerpt from [CLI Usage](./cli.md) of `electron-builder` command:
```
Publishing:
  --publish, -p  [choices: "onTag", "onTagOrDraft", "always", "never"]
```
CLI `--publish` option values:

| Value          |  Description
| -------------- | -----------
| `onTag`        | on tag push only
| `onTagOrDraft` | on tag push or if draft release exists
| `always`       | always publish
| `never`        | never publish

But please consider using automatic rules instead of explicitly specifying `publish`:

* If CI server detected, — `onTagOrDraft`.

* If CI server reports that a tag was pushed, — `onTag`.

  A release will be drafted (if one doesn't already exist) and artifacts published only if a tag was pushed.

* If [npm script](https://docs.npmjs.com/misc/scripts) named `release`, — `always`.

 Add to `scripts` in the development `package.json`:

 ```json
 "release": "electron-builder"
 ```

 and if you run `yarn release`, a release will be drafted (if doesn't already exist) and artifacts published.

### Recommended GitHub Releases Workflow

1. [Draft a new release](https://help.github.com/articles/creating-releases/). Set the "Tag version" to the value of `version` in your application `package.json`, and prefix it with `v`. "Release title" can be anything you want.

    For example, if your application `package.json` version is `1.0`, your draft's "Tag version" would be `v1.0`.

2. Push some commits. Every CI build will update the artifacts attached to this draft.
3. Once you are done, publish the release. GitHub will tag the latest commit for you.

The benefit of this workflow is that it allows you to always have the latest artifacts, and the release can be published once it is ready.

### Continuous Deployment Workflow on Amazon S3 and other non-GitHub

This example workflow is modeled on how releases are handled in Maven (it is an example of one of many possible workflows, you are not forced to follow it).

1. Setup your CI to publish on each commit. E.g. `"dist": "electron-builder --publish always"` in your `package.json`.
2. Set your version in your application `package.json` to `1.9.0-snapshot` (or `1.9.0-master` or whatever you want your development channel to be named). This will publish a file named `snapshot.yml` and a build named `something-snapshot.exe` (and corresponding for mac) to S3.
3. When you are ready to deploy, simply change your package version to `1.9.0` and push. This will then produce a `latest.yml` and `something.exe` on s3. Usually you'll git-tag this version as well (just to keep track of it).
4. Change the version back to a snapshot version right after, i.e. `1.10.0-snapshot`, and commit it.

## GitHub Repository {#github-repository-and-bintray-package}

Detected automatically using:

* [repository](https://docs.npmjs.com/files/package.json#repository) in the application or development `package.json`,
* if not set, env
    * `TRAVIS_REPO_SLUG`
    * or `APPVEYOR_REPO_NAME`
    * or `CIRCLE_PROJECT_USERNAME`/`CIRCLE_PROJECT_REPONAME`,
* if no env, from `.git/config` origin url.

:::warning[The detected repository ships inside your app]
Whatever is detected here becomes the publish/update destination and, for auto-update-capable targets, is written to `app-update.yml` inside every packaged build - that is where electron-updater looks for updates for the lifetime of that install. A build reports the repository it inferred, including the `source` it came from - at info level when it comes from the `repository` field of `package.json` (deliberate configuration), at warn level when it is picked up from CI environment variables or `.git/config`:

```
• update feed inferred from repository info; it will be used as the publish/update destination
  (written to app-update.yml in auto-update-capable targets) - specify it explicitly to be sure
  it stays under your control  reason=owner and repo not specified in the publish configuration
  source=.git/config provider=github owner=my-org repo=my-app
```

Check that it is the repository you expect, and keep control of it. GitHub releases an owner or repository name for re-registration once it is renamed, transferred or deleted, so anyone who then claims that name can publish releases that already-installed copies of your app will download. Set `owner` and `repo` explicitly to avoid depending on detection at all:

```json5
{
  "build": {
    "publish": {
      "provider": "github",
      "owner": "my-org",
      "repo": "my-app"
    }
  }
}
```
:::

# Publishers

## Bitbucket

:::note[v27: authentication mode]
The Bitbucket publisher selects its auth scheme by whether a username is present:

- **With a username** (`bitbucket.username`, or the `BITBUCKET_USERNAME` env var) → HTTP **Basic** auth. Use this for a Bitbucket **app password** or an Atlassian **API token** (the username is your Bitbucket username or Atlassian account email).
- **Without a username** → the token is sent as `Authorization: Bearer <token>` (a repository / project / workspace **access token**). This is new in v27; previously a token was always sent as Basic auth with the repo owner as the username.

If your CI sets `BITBUCKET_TOKEN` to an app password / API token **without** a username, set `BITBUCKET_USERNAME` too, or the request goes out as Bearer and fails authentication.
:::

  {!./builder-util-runtime.Interface.BitbucketOptions.md!}

## Github

:::note[v27: tagNamePrefix replaces vPrefixedTagName]
The GitHub `vPrefixedTagName` boolean was removed — use `tagNamePrefix` to control the tag prefix (defaults to `"v"`; set `tagNamePrefix: ""` for no prefix). `electron-builder migrate-schema` rewrites it. Note that v26 ignored an empty `tagNamePrefix` and still tagged `v<version>`; v27 honors it, so the migrator rewrites a v26 `tagNamePrefix: ""` to `"v"` (with a warning) to keep your existing tag names. (On **GitLab**, `vPrefixedTagName` is unchanged and still works.)
:::

  {!./builder-util-runtime.Interface.GithubOptions.md!}

## GitLab
  {!./builder-util-runtime.Interface.GitlabOptions.md!}

## Keygen
  {!./builder-util-runtime.Interface.KeygenOptions.md!}

## S3
  {!./builder-util-runtime.Interface.S3Options.md!}

## Snap Store
  {!./builder-util-runtime.Interface.SnapStoreOptions.md!}

## Spaces
  {!./builder-util-runtime.Interface.SpacesOptions.md!}

## R2

To publish to [Cloudflare R2](https://developers.cloudflare.com/r2/):

1. Create an [R2 API token](https://developers.cloudflare.com/r2/api/s3/tokens/) with at least **Object Read & Write** permission on your bucket.
2. Expose its credentials to electron-builder via the `CF_R2_ACCESS_KEY_ID` and `CF_R2_SECRET_ACCESS_KEY` environment variables.
3. Enable [public access](https://developers.cloudflare.com/r2/buckets/public-buckets/) for the bucket via an `r2.dev` subdomain or a custom domain — R2's S3 API endpoint cannot serve unauthenticated downloads, so `electron-updater` cannot download updates from it.
4. Set `publicUrl` to that public base URL.

Auto-updating from R2 requires an `electron-updater` version that supports the `r2` provider: electron-updater >= 7.0.0 (the release line containing this feature).

Note: files are uploaded with a single PUT request, and R2 limits single-PUT uploads to ~5 GiB per object.

  {!./builder-util-runtime.Interface.R2Options.md!}

## BYO Generic (create-your-own)
(And maybe submit it upstream in a PR!)
  {!./builder-util-runtime.Interface.GenericServerOptions.md!}

---

### Snap (`website/docs/snap.md`)

The top-level [`snapcraft`](#configuration) key is the recommended way to configure [Snap](https://snapcraft.io) builds. It requires an explicit `base` field and separates per-core options cleanly.

The flat `snap` config key was **removed in v27** — use `snapcraft` instead. `electron-builder migrate-schema` converts an old `snap` config to `snapcraft` automatically. See [Migrating from `snap` to `snapcraft`](#migrating-from-snap-to-snapcraft) if you are upgrading.

---

## Core 24 (Recommended)

`core24` targets Ubuntu 24.04 Noble and requires **Electron 25.0.0+** (28.0.0+ recommended). It uses snapcraft v8 (craft-application framework) and brings first-class Wayland, the GNOME extension, and Launchpad remote builds for multi-arch CI.

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "confinement": "strict"
    }
  }
}
```

Building Snap packages requires a Linux environment (or [Docker](features/multi-platform-build.md#docker)).

### Build Environments

Choose one build environment. They are mutually exclusive.

| Option | Platform | Recommended for |
|---|---|---|
| `useLXD: true` | Linux | Linux CI where nested virtualisation is unavailable |
| `useMultipass: true` | macOS / Windows / Linux | Local development |
| `useDestructiveMode: true` | Linux only | Docker CI containers without any virtualisation |
| `remoteBuild.enabled: true` | Any | Multi-arch CI (amd64, arm64, armhf) via Launchpad |

If none is set and you are on Linux, snapcraft's own default (Multipass) is used. On non-Linux platforms you must choose `useMultipass` or `remoteBuild`.

#### LXD

Container-based isolation. Preferred on most Linux CI systems because it does not require nested virtualisation (unlike Multipass).

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "useLXD": true
    }
  }
}
```

LXD must be installed and the build user must be in the `lxd` group:

```sh
sudo snap install lxd
sudo usermod -aG lxd $USER
lxd init --minimal
```

#### Multipass

VM-based isolation. The default choice for local development on macOS and Windows.

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "useMultipass": true
    }
  }
}
```

#### Destructive Mode

Builds directly on the host without any VM or container (`snapcraft --destructive-mode`).

!!! warning
    **Not recommended for production builds.** Destructive mode pollutes the host environment — any library present on the host at build time can end up in the snap, making builds difficult to reproduce. Prefer `useLXD` or `useMultipass` for clean builds.

Valid reason(s) to use this mode:

- CI test suites where the environment is already fully controlled.

The `gnome` extension is **incompatible** with destructive mode. electron-builder automatically clears `extensions` when `useDestructiveMode` is set; explicitly including `"gnome"` alongside it will throw an error.

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "useDestructiveMode": true,
      "extensions": []
    }
  }
}
```

#### Remote Build on Launchpad

Builds on [Canonical's Launchpad](https://launchpad.net/) infrastructure. Works from any platform and supports building for multiple architectures simultaneously (`amd64`, `arm64`, `armhf`).

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "remoteBuild": {
        "enabled": true,
        "buildFor": ["amd64", "arm64"],
        "acceptPublicUpload": true
      }
    }
  }
}
```

!!! note
    Unless `privateProject` is set, your source code is uploaded to a **public** Launchpad repository. Set `acceptPublicUpload: true` to suppress the interactive consent prompt in CI.

##### Authentication

Remote builds require Snapcraft Store credentials. electron-builder resolves them in this order:

1. `remoteBuild.cscLink` — base64-encoded credentials or a file path in the build config.
2. `SNAP_CSC_LINK` — environment variable, same format (CI-recommended).
3. `SNAPCRAFT_STORE_CREDENTIALS` — plain-text macaroon, read directly by snapcraft.
4. An active interactive `snapcraft login` session.

Credentials are injected **only into the spawned `snapcraft` subprocess** environment and are never exposed through `process.env`.

This follows the same pattern as `WIN_CSC_LINK` / `CSC_LINK` for [code signing](./features/code-signing/code-signing.md).

##### CI Setup

Generate credentials once and store as a CI secret:

```sh
# Run locally — output is your CI secret value
snapcraft export-login - | base64 -w0
```

Then set `SNAP_CSC_LINK` in your CI environment:

```yaml
# GitHub Actions
- name: Build and publish snap
  env:
    SNAP_CSC_LINK: ${{ secrets.SNAP_CSC_LINK }}
  run: npx electron-builder --linux snap
```

:::warning[Classic Confinement]
Snap Store requires manual review and approval for `classic` confinement. Electron apps typically use `strict` with appropriate interface plugs.
:::

To reference credentials file directly via the config (useful for monorepos where the credential is managed per-project):

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "remoteBuild": {
        "enabled": true,
        "cscLink": "file://..."
      }
    }
  }
}
```

---

### GNOME Extension

By default, core24 builds apply the `gnome` extension, which:

- Pulls in the `gnome-46-2404` platform content snap (GTK3, GLib, etc.)
- Adds `gtk-3-themes`, `icon-themes`, `sound-themes` content snaps
- Adds the `gpu-2404` GPU support content snap
- Injects layout, environment variables, and plugs required by Electron on Ubuntu 24.04

You normally do not need to configure `extensions` at all. To opt out:

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "extensions": []
    }
  }
}
```

When `extensions` is empty, electron-builder falls back to the standard Electron plug set and you are responsible for layout and content snap declarations.

:::tip[Minimal Plugs]
Start with only the plugs you actually need. Unnecessary plugs may slow Snap Store review. At minimum, `browser-support`, `network`, `desktop`, and `desktop-legacy` are needed for most Electron apps.
:::

---

### Wayland

core24 always allows native Wayland. The snap runs with `--ozone-platform=wayland` on compositors that support it and falls back to XWayland otherwise.

To force X11-only mode, set `forceX11: true`:

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "forceX11": true
    }
  }
}
```

---

### Stage Packages

The default stage packages are `["libnspr4", "libnss3", "libxss1", "libappindicator3-1", "libsecret-1-0"]`.

Use the `"default"` keyword to extend the defaults rather than replace them:

```json
{
  "snapcraft": {
    "base": "core24",
    "core24": {
      "stagePackages": ["default", "libv4l-0"]
    }
  }
}
```

---

## Core 22

`core22` targets Ubuntu 22.04 Jammy and is supported via the legacy `SnapCoreLegacy` implementation. (As of v27, `core24` is also stable — prefer it for new apps.)

```json
{
  "snapcraft": {
    "base": "core22",
    "core22": {
      "summary": "My Electron app",
      "confinement": "strict",
      "stagePackages": ["default"]
    }
  }
}
```

Key differences from core24:

- **`allowNativeWayland` defaults to `false`** — Wayland is disabled by default for older Electron compatibility. Set to `true` to enable. (The legacy interface uses `allowNativeWayland`; core24 uses `forceX11`.)
- **No extensions** — no GNOME extension support; uses the `desktop-gtk2` part instead.
- **`useTemplateApp`** — when `stagePackages` is not customised, electron-builder uses a pre-built Electron snap template for faster assembly (x64 and armv7l only).
- **Template builds skip `snapcraft`** — the image is packed directly with the `mksquashfs` from the [`toolsets.appimage`](./toolsets.md) bundle (non-template builds run the `snapcraft` CLI). `compression: "lzo"` needs a toolset other than the legacy `"0.0.0"`, whose `mksquashfs` supports only `gzip` and `xz`.

---

## Core 20

`core20` targets Ubuntu 20.04 Focal.

```json
{
  "snapcraft": {
    "base": "core20",
    "core20": {
      "summary": "My Electron app",
      "confinement": "strict"
    }
  }
}
```

Behaviour is identical to core22 above. Use core22 unless the Snap Store requires core20.

---

## Core 18

`core18` targets Ubuntu 18.04 Bionic. Use only if your store listing requires it.

```json
{
  "snapcraft": {
    "base": "core18",
    "core18": {
      "summary": "My Electron app",
      "confinement": "strict"
    }
  }
}
```

---

## Custom Pass-Through

Set `base: "custom"` to pass a `snapcraft.yaml` file (or an inline object) through to snapcraft verbatim. electron-builder performs no injection — no plugs, extensions, organize mappings, or desktop entries are added.

```json
{
  "snapcraft": {
    "base": "custom",
    "custom": {
      "yaml": "build/snapcraft.yaml"
    }
  }
}
```

The `yaml` path is resolved relative to the build resources directory (`build/` by default). You can also supply the YAML content inline as an object in the config.

---

## Migrating from `snap` to `snapcraft`

:::tip[Automated]
`electron-builder migrate-schema` performs this restructuring for you. If the old config has no `base`, it assumes `core20` and warns so you can confirm. See [v27 Breaking Changes → snap → snapcraft](./migration/v27-breaking-changes.md#snap-snapcraft).
:::

The removed `snap` key is equivalent to using `snapcraft` with a per-core options object. The `base` field moves to the top level of `snapcraft`, and all other fields move inside the corresponding core key.

```jsonc
// Before — deprecated snap key
{
  "snap": {
    "base": "core22",
    "summary": "My app",
    "confinement": "strict",
    "stagePackages": ["default", "libfoo"]
  }
}

// After — snapcraft key (recommended)
{
  "snapcraft": {
    "base": "core22",
    "core22": {
      "summary": "My app",
      "confinement": "strict",
      "stagePackages": ["default", "libfoo"]
    }
  }
}
```

For `core22` and older, place the per-core options inside the base-named sub-key (e.g. `core22`). Omit `base` from the inner object — it lives at `snapcraft.base` now.

The `appPartStage` option controls which files are included from the main app part:

```yaml
snapcraft:
  base: core22
  core22:
    appPartStage:
      - -usr/share/doc
      - -usr/share/man
```

## Publishing to the Snap Store

1. Create an account at [snapcraft.io](https://snapcraft.io/account), register your snap name, and install the `snapcraft` CLI — see [Publishing to the Snap Store](https://snapcraft.io/docs/releasing-your-app)
2. Configure publishing in your electron-builder config:

```yaml
publish:
  provider: snapStore
  repo: myapp       # your registered snap name
  channels:
    - stable
```

3. Build and publish: `electron-builder --linux snap --publish always`

### Channel Strategy

| Channel | Purpose |
|---|---|
| `edge` | Latest development builds (auto-published from CI) |
| `beta` | Beta testing builds |
| `candidate` | Release candidates |
| `stable` | Production releases |

## Local Testing

Install a locally built snap without the Snap Store:

```bash
sudo snap install --dangerous dist/myapp_1.0.0_amd64.snap
```

Run and check logs:

```bash
myapp
journalctl --user -xe | grep myapp
```

To uninstall: `sudo snap remove myapp`

## Debugging

Enable verbose electron-builder output:

```bash
DEBUG=electron-builder electron-builder --linux snap
```

For snap runtime issues, check confinement denials:

```bash
snap run --shell myapp
# Inside the snap shell, test what's accessible
```

## Configuration

### `snapcraft` (new)

  {!./app-builder-lib.Interface.SnapcraftOptions.md!}

### `SnapOptions24` (core24)

  {!./app-builder-lib.Interface.SnapOptions24.md!}

### `RemoteBuildOptions`

  {!./app-builder-lib.Interface.RemoteBuildOptions.md!}

### `SnapOptionsCustom`

  {!./app-builder-lib.Interface.SnapOptionsCustom.md!}

### `SnapOptionsLegacy` (core18 / core20 / core22)

  {!./app-builder-lib.Interface.SnapOptionsLegacy.md!}

---

### Squirrel Windows (`website/docs/squirrel-windows.md`)

---
title: "Squirrel.Windows"
---

The top-level [squirrelWindows](./configuration.md#squirrelwindows) key contains a set of options instructing electron-builder on how it should build Squirrel.Windows.

Squirrel.Windows target is maintained, but deprecated. Please use [nsis](nsis.md) instead.

To use Squirrel.Windows please install `electron-builder-squirrel-windows` dependency.
To build for Squirrel.Windows on macOS or Linux, please install `mono` (`brew install mono` / `apt install mono-devel`).

The Squirrel vendor binaries (`Squirrel.exe`, `SyncReleases.exe`, `nuget.exe`, …) are downloaded from the checksummed `squirrel.windows` bundle; `rcedit` comes from the `winCodeSign` bundle. Pin a version or supply your own bundle via [`toolsets.squirrel`](./toolsets.md).

Your app must be able to handle Squirrel.Windows startup events that occur during install and uninstall. See [electron-squirrel-startup](https://github.com/mongodb-js/electron-squirrel-startup).

## Configuration

  {!./app-builder-lib.Interface.SquirrelWindowsOptions.md!}

---

### Targets (`website/docs/targets.md`)

# Target Selection Guide

This page helps you choose the right packaging format for each platform. The right choice depends on your distribution method, target audience, and operational requirements.

## Windows Targets

| Target | Format | Best For | Admin Required | Auto-Update |
|---|---|---|---|---|
| `nsis` | `.exe` installer | Consumer apps (most common) | Optional (per-user mode) | electron-updater |
| `nsis-web` | `.exe` web installer | Apps with large assets | Optional | electron-updater |
| `portable` | `.exe` no-install | USB drives, no-install scenarios | No | Manual |
| `appx` | `.appx` | Windows Store, enterprise MDM | Managed by Store | Store only |
| `msix` | `.msix` / `.msixbundle` / `.msixupload` | Windows Store, modern deployment (successor to AppX) | Managed by Store | Store only |
| `msi` | `.msi` | Enterprise deployment (SCCM/Intune/GPO) | Yes | Not supported via electron-updater |
| `msi-wrapped` | `.msi` wrapping NSIS | Enterprise + existing NSIS installer | Yes | electron-updater |
| `squirrel.windows` | `.exe` / NuGet | Legacy (not recommended) | No | Squirrel |

### When to Use Each Windows Target

**NSIS** (`nsis`) — the default and best choice for most applications.
- Consumer software distributed via your website or GitHub releases
- Both per-user (no admin) and per-machine install modes available
- Script-based customization via NSIS macros (`.nsh` hooks) — not visual UI theming
- Works with electron-updater for auto-updates

**NSIS Web** (`nsis-web`) — use when your app download is very large.
- Produces a small stub installer that downloads the full payload at install time
- Suitable for apps with many platform-specific binaries
- Requires internet connectivity at install time

**Portable** (`portable`) — use when no installation at all is the goal.
- Single executable, runs from any location
- No registry entries, no Start menu entries
- Common for developer tools, USB-stick deployments
- Auto-updates require manual implementation

**AppX** (`appx`) — use for Windows Store distribution or enterprise MDM.
- Required for listing in the Microsoft Store
- Enterprise IT can deploy via Intune/MDM without the Store
- Code signing with a trusted certificate is required for sideloading
- See [AppX Configuration](appx.md)

**MSIX** (`msix`) — the modern successor to AppX (beta); prefer it for new Store/MDM work.
- Produces `.msix`, multi-architecture `.msixbundle`, and Store `.msixupload` artifacts
- Adds modern manifest features: package integrity and Windows services
- Requires the modern `winCodeSign` toolset (the default; only the legacy `0.0.0` bundle is rejected); builds on Windows 10 / Windows Server 2012 R2 (6.3+) or later, or macOS via Parallels
- See [MSIX Configuration](msix.md)

**MSI** (`msi`) — use for enterprise deployment via Group Policy or SCCM.
- IT departments can deploy silently across managed machines
- Supports standard MSI command-line flags (`/quiet`, `/passive`)
- Upgrade code is critical — changing it breaks silent upgrades of existing installs
- See [MSI Configuration](msi.md)

**MSI-Wrapped** (`msi-wrapped`) — use when you need MSI packaging but want NSIS features.
- Wraps your existing NSIS installer inside an MSI container
- Enables Group Policy deployment of apps with NSIS-based setup logic
- See [MSI-Wrapped Configuration](msi-wrapped.md)

### Windows Decision Tree

```
Do you need Windows Store distribution?
  → Yes, modern (recommended): MSIX
  → Yes, legacy: AppX

Do you need enterprise GPO/SCCM/Intune deployment?
  → Yes, native MSI: MSI
  → Yes, but keep NSIS features: MSI-Wrapped

Is this a consumer app?
  → Normal installer: NSIS (default)
  → Very large download: NSIS-Web
  → No installation at all: Portable
```

---

## macOS Targets

| Target | Format | Best For | Code Signing | Notarization |
|---|---|---|---|---|
| `dmg` | `.dmg` disk image | Standard consumer distribution | Developer ID Application | Required (10.15+) |
| `pkg` | `.pkg` installer | System-level installs, launch agents | Developer ID Installer | Required (10.15+) |
| `zip` | `.zip` archive | Update server payload, minimal size | Developer ID Application | Required (10.15+) |
| `mas` | App bundle (Store) | Mac App Store | Apple Distribution | No (Apple signs) |
| `mas-dev` | App bundle (dev) | Testing MAS builds locally | Apple Development | No |
| `dir` | Directory | Development / debugging | Optional | No |

### When to Use Each macOS Target

**DMG** (`dmg`) — the standard for consumer macOS distribution.
- Drag-and-drop install with `/Applications` shortcut
- Customizable window, background, icon layout
- electron-updater supports updates via ZIP payload
- See [DMG Configuration](dmg.md)

**PKG** (`pkg`) — use when you need system-level installation.
- Required for kernel extensions, launch daemons, or files outside `/Applications`
- Pre/post-install scripts via the `scripts` directory
- Requires a separate "Developer ID Installer" certificate
- See [PKG Configuration](pkg.md)

**ZIP** — use as the update payload for electron-updater.
- Smaller than DMG for the same content
- Not user-facing — used internally by the auto-update mechanism
- Commonly built alongside DMG

**MAS** (`mas`) — required for Mac App Store submission.
- Uses App Sandbox — your app must declare all permissions via entitlements
- Requires provisioning profiles
- Apple handles signing during the Store submission process
- See [MAS Configuration](mas.md)

### macOS Decision Tree

```
Is the app going to the Mac App Store?
  → Yes: MAS
  → Testing MAS builds: mas-dev

Does the app install system components (daemons, kernel extensions)?
  → Yes: PKG

Standard consumer distribution?
  → Yes: DMG (+ ZIP for auto-updates)
```

---

## Linux Targets

| Target | Format | Best For | Sandboxed | Distro Support |
|---|---|---|---|---|
| `AppImage` | `.AppImage` | Universal, no install | No | Universal |
| `deb` | `.deb` | Debian, Ubuntu, Mint | No | Debian-based |
| `rpm` | `.rpm` | Fedora, RHEL, openSUSE | No | RPM-based |
| `snap` | Snap package | Ubuntu-primary, Store distribution | Yes | Any (snapd) |
| `flatpak` | Flatpak bundle | Cross-distro sandboxed | Yes | Any (flatpak) |
| `pacman` | `.pkg.tar.zst` | Arch Linux, Manjaro | No | Arch-based |
| `apk` | `.apk` | Alpine Linux, Docker | No | Alpine |
| `freebsd` | `.pkg` | FreeBSD | No | FreeBSD |
| `p5p` | IPS package | Solaris / illumos | No | Solaris-based |
| `zip`, `tar.gz` | Archive | CDN / custom deployment | No | Universal |

### When to Use Each Linux Target

**AppImage** — built by default alongside Snap; best first choice for broad compatibility.
- Single file, no installation, no root required
- Runs on virtually any x86_64 Linux distribution
- [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) handles desktop integration
- electron-updater supports delta updates via embedded blockmap (no separate file needed)
- See [AppImage Configuration](appimage.md)

**DEB** — use for Debian-based distribution (Ubuntu, Mint, etc.).
- Installable via `apt` from a repository or directly
- Best when targeting Ubuntu users who expect `.deb` packages
- See [Linux Configuration → DEB](linux.md#debian-package-deb)

**RPM** — use for Red Hat-based distribution (Fedora, RHEL, CentOS, openSUSE).
- Installable via `dnf`/`yum`
- See [Linux Configuration → RPM](linux.md#rpm-package-rpm)

**Snap** — use for Ubuntu-primary distribution via the Snap Store.
- Automatic updates via Snap Store
- Sandboxed with `strict` confinement
- Required for Snap Store listing
- See [Snap Configuration](snap.md)

**Flatpak** — use for cross-distro sandboxed distribution.
- Works on any distro with `flatpak` installed
- electron-builder produces single-file bundles (not Flathub repository packages)
- See [Flatpak Configuration](flatpak.md)

**Pacman** — use for Arch Linux and Arch-based distributions.
- Beta status — test thoroughly
- See [Linux Configuration → Pacman](linux.md#pacman-package-pacman)

**APK** — use for Alpine Linux or Docker container base images.
- Alpine uses musl libc; some native modules need recompilation
- See [Linux Configuration → Alpine APK](linux.md#alpine-apk-package-apk)

### Linux Decision Tree

```
Universal distribution, no install required?
  → AppImage (default)

Ubuntu Store / auto-update via Snap?
  → Snap

Cross-distro sandbox?
  → Flatpak

Debian/Ubuntu apt repository?
  → DEB

Fedora/RHEL/openSUSE dnf repository?
  → RPM

Arch Linux?
  → Pacman (beta)

Alpine / Docker?
  → APK
```

---

## Combining Multiple Targets

You can build multiple targets in a single run:

```yaml
# electron-builder.yml
win:
  target:
    - nsis
    - portable

mac:
  target:
    - dmg
    - zip

linux:
  target:
    - AppImage
    - deb
```

```bash
# CLI: build all configured targets
electron-builder --mac --win --linux

# Build specific targets
electron-builder --win nsis --win msi

# Build for specific architectures
electron-builder --mac --x64 --arm64
```

## Related Pages

- [Architecture & Multi-Arch](architecture.md) — building for multiple CPU architectures
- [GitHub Actions](features/github-actions.md) — CI/CD workflows for multi-platform builds
- [Code Signing](features/code-signing/code-signing.md) — signing requirements by platform

---

### Toolsets (`website/docs/toolsets.md`)

---
title: "Toolsets"
---

electron-builder relies on a handful of external **binary bundles** — the NSIS compiler, Wine, the Windows code-signing tools, the AppImage runtime, FPM, and a few others — that it downloads on demand and caches locally. The top-level [`toolsets`](./configuration.md) key lets you pin the version of each bundle, or point electron-builder at a bundle you host yourself.

For most projects you never touch this key: every toolset defaults to the newest published bundle. You reach for `toolsets` only to **pin a specific (or legacy) version**, or to **supply a custom bundle** in place of a built-in one.

:::info[New in v27]
Before v27, toolset selection was a mix of fixed version pins and environment-variable overrides (`APPIMAGE_TOOLS_PATH`, `ELECTRON_BUILDER_NSIS_DIR`, `USE_SYSTEM_WINE`, …). v27 **removes every one of those env vars** and replaces them with the typed `toolsets` config on this page. See [Toolsets & environment variables](./migration/v27-breaking-changes.md#toolsets-environment-variables) in the breaking-changes reference and the [Replacing removed environment variables](#replacing-removed-environment-variables) section below.
:::

## The toolsets

Each property of `toolsets` corresponds to one downloadable bundle, hosted at [electron-userland/electron-builder-binaries](https://github.com/electron-userland/electron-builder-binaries/releases):

| Toolset | Used for | `"latest"` resolves to |
|---|---|---|
| `winCodeSign` | Windows code signing & resource editing (`signtool` / `osslsigncode`, `rcedit`, Windows Kits for AppX/MSIX) | `1.3.0` |
| `appimage` | Building `.AppImage` files (`mksquashfs`, `unsquashfs`, the self-executing runtime); its `mksquashfs` also packs legacy-base snaps built from the snap template app | `1.1.0` |
| `nsis` | Compiling Windows installers (`makensis`, plugin DLLs, `elevate.exe`) | `1.2.1` |
| `wine` | Running Windows tools (NSIS, rcedit, signtool) on non-Windows hosts | `system` (host `wine` on `PATH`) |
| `fpm` | Building Linux packages (`.deb`, `.rpm`, `.pacman`, …) on macOS & Linux | `2.2.1` |
| `linuxToolsMac` | Building Linux targets / `.tar.lz` archives on macOS (`ar`, `lzip`, `gtar`) | `1.0.1` |
| `sevenZip` | Extracting `.7z` and `.tar.xz` archives internally | `1.0.1` |
| `icons` | Converting source images to `.icns`, `.ico`, and PNG icon sets | `1.2.3` |
| `squirrel` | Building Squirrel.Windows installers (`Squirrel.exe`, `SyncReleases.exe`, `nuget.exe`, `7z`) — requires `electron-builder-squirrel-windows` | `1.1.1` |

:::note[Platform notes]
- **`wine`** is only needed to build **Windows targets on a non-Windows machine**. On Windows it has no effect. It defaults to the **host-installed `wine`** on `PATH` (`"system"`) on both macOS and Linux, so a macOS host that builds Windows targets needs Wine installed (`brew install --cask wine-stable`). `toolsets.wine: "1.0.1"` still downloads the Wine 11.0 bundle on macOS when pinned explicitly, but the published bundle ships no PE builtins, so it cannot run Windows tools on its own.
- **`winCodeSign`** is used on all platforms (`signtool.exe` on Windows, `osslsigncode` on macOS/Linux).
- **`squirrel`** is only used by the `squirrelWindows` target. It runs natively on Windows; on macOS/Linux it needs a host-installed `mono`, and `rcedit` (from `winCodeSign`) runs under Wine.
- **`fpm`**, **`icons`**, and **`squirrel`** each have only one published version today, so `"latest"` and the listed version are equivalent.
:::

For the full version-by-version breakdown of what each `"latest"` bundle upgrades from — and which are drop-in replacements versus behavior changes — see the migration table in [Toolset defaults resolve to `"latest"`](./migration/v27-breaking-changes.md#toolset-defaults-resolve-to-latest-newest-bundle).

## Default resolution — `"latest"`

Every `toolsets.*` property defaults to **`"latest"`**. An **unset** property and the literal string **`"latest"`** both resolve to the **newest published bundle** for that toolset. These two are interchangeable:

```json5
{ "build": { "toolsets": {} } }                          // unset → latest
{ "build": { "toolsets": { "nsis": "latest" } } }        // explicit latest
```

:::note
`null` is no longer accepted: it was dropped from the `ToolsetConfig` type and the configuration schema rejects it, so use `"latest"` or omit the key. `electron-builder migrate-schema` removes `null` entries (and rewrites the retired `appimage: "1.0.2"` pin to `"1.0.3"`).
:::

Pinning to a concrete version is as simple as naming it:

```yaml
toolsets:
  nsis: "1.2.1"
  winCodeSign: "1.3.0"
```

## Pinning the legacy bundle (`"0.0.0"`)

Every toolset accepts the sentinel version **`"0.0.0"`**, which selects the **pre-v27 legacy bundle** for that tool. It is the escape hatch if a newer bundle introduces a regression:

```json5
{
  "build": {
    "toolsets": {
      "winCodeSign": "0.0.0",
      "nsis": "0.0.0",
      "appimage": "0.0.0",
      "wine": "0.0.0"
    }
  }
}
```

Pinning `"0.0.0"` also changes a few behaviors that are gated on the bundle version — for example, the legacy AppImage bundle (`appimage: "0.0.0"`) is the FUSE2 runtime and re-adds the automatic `--no-sandbox` launch argument, and a `winCodeSign` below `1.3.0` forces the legacy PowerShell path for Azure Trusted Signing (see [Code signing & toolsets](#code-signing-and-toolsets)).

:::warning[Short-term workaround only]
`"0.0.0"` is intended as a temporary fallback while you resolve an incompatibility. The alias **may be removed in a future major release** — prefer moving to a current bundle (or a [custom toolset](#custom-toolsets)) rather than relying on it long-term.
:::

## Custom toolsets

Instead of a version string, any toolset can be set to a **`ToolsetCustom`** object to supply your own bundle:

```typescript
toolsets.<name>: { url: string, checksum?: string, version?: string }
```

| Field | Required | Description |
|---|---|---|
| `url` | **yes** | An `https://` URL or a `file://` path. See below. |
| `checksum` | for downloaded archives | SHA-256 of the archive as 64 hex characters, or its SHA-512 as 88 base64 characters. See [Custom toolset checksums](#custom-toolset-checksum). **Required** for `https://` URLs and for `file://` **archive files**. **Not** needed for a bare `file://` **directory** (used as-is, no caching). |
| `version` | no | Label used only in the local cache directory name. Falls back to the first 8 hex characters of the `checksum` digest when omitted. |

**`url` accepts two forms:**

- **`https://…`** — the bundle is **downloaded, checksum-verified, extracted, and cached** locally.
- **`file://…`** — a local path. A bare **directory** is used **as-is** (no checksum, no extraction). A local **archive file** is **checksum-verified**, then extracted and cached (checksum required); a mismatch fails the build and leaves the file untouched. Relative `file://` paths must resolve inside the project's build-resources directory; absolute paths are used directly.

```json5
// Remote bundle (URL) — checksum required
{ "build": { "toolsets": { "nsis": {
  "url": "https://example.com/my-nsis-bundle-1.0.tar.gz",
  "checksum": "<SHA-256 hex or base64 SHA-512 of the archive>",
  "version": "my-custom-1.0"
} } } }

// Local directory (used as-is, no checksum)
{ "build": { "toolsets": { "appimage": {
  "url": "file:///path/to/my-appimage-tools-dir"
} } } }
```

### Custom toolset checksums {#custom-toolset-checksum}

Every custom toolset bundle accepts the same checksum formats, whichever toolset it replaces: `winCodeSign`, `appimage`, `nsis`, `wine`, `fpm`, `linuxToolsMac`, `sevenZip`, `icons` or `squirrel`.

- **Formats:** either
  - the SHA-256 of the archive file as 64 **hex** characters (an uppercase value is lowercased for you), or
  - the SHA-512 of the archive file as 88 **base64** characters, ending in `==`. v26 used this format for all toolset checksums, so a v26 value keeps working unchanged in `toolsets.<name>.checksum`.
- **Why SHA-256 hex is the default:** v27 downloads toolsets with the official [`@electron/get`](https://github.com/electron/get) package, whose checksum verification (`sumchecker`) only supports SHA-256 hex. electron-builder verifies a base64 SHA-512 itself.
- **Not accepted:** prefixed forms such as `sha256:…` or `sha512-…`, and a SHA-512 written as hex. electron-builder rejects a checksum in any other format before downloading or extracting anything.
- **When it is needed:** for an `https://` URL and for a `file://` archive file. A bare `file://` directory needs none and is not verified.
- **What is verified:** a downloaded archive is verified before it is cached or extracted (a corrupted download is deleted), and an archive already in the electron-builder cache is re-verified before it is extracted again. A `file://` archive is verified each time before it is extracted; on a mismatch the build fails and your file is left in place.

Compute it from the exact archive that `url` points to:

| Platform | SHA-256 (hex) | SHA-512 (base64) |
|---|---|---|
| macOS, Linux | `shasum -a 256 my-bundle.tar.gz` (or `sha256sum my-bundle.tar.gz`) | `openssl dgst -sha512 -binary my-bundle.tar.gz \| openssl base64 -A` |
| Windows (PowerShell) | `(Get-FileHash -Algorithm SHA256 my-bundle.tar.gz).Hash` | `[Convert]::ToBase64String([Security.Cryptography.SHA512]::Create().ComputeHash([IO.File]::ReadAllBytes((Resolve-Path my-bundle.tar.gz))))` |
| Windows (cmd) | `certutil -hashfile my-bundle.tar.gz SHA256` | — |

```json5
{ "build": { "toolsets": { "nsis": {
  "url": "https://example.com/my-nsis-bundle-1.0.tar.gz",
  "checksum": "56997fdefe25e7928a1a68b4583d08b240b66cf660234053b20131a74cc082f4",
  "version": "my-custom-1.0"
} } } }
```

Recompute the checksum whenever you rebuild or repack the archive.

:::warning[The bundle must mirror the built-in layout]
A custom bundle has to match the **directory layout** of the corresponding built-in bundle — electron-builder looks for the same executables in the same relative paths. Use the build scripts in [electron-builder-binaries/packages](https://github.com/electron-userland/electron-builder-binaries/tree/master/packages) as the reference for each toolset's expected structure.
:::

### Supported archive formats

Archives supplied via `url` are extracted automatically. Supported formats: **`.zip`**, **`.7z`**, **`.tar.gz`** / **`.tgz`**, **`.tar.xz`** / **`.txz`** and **`.tar.7z`**. A bare directory (no archive) is used as-is.

The two families are unpacked differently, which decides where the bundle root is:

- **Tar archives** (`.tar.gz`, `.tgz`, `.tar.xz`, `.txz`, `.tar.7z`) are extracted with their first path component stripped, so put everything inside **one top-level folder** (`tar -czf bundle.tar.gz my-bundle`). That folder becomes the bundle root.
- **`.zip` and `.7z`** are extracted as they are, so put the bundle's files **at the root of the archive**, not inside a folder. A release zip that wraps everything in a versioned folder (such as `nsis-3.10/`) has to be repacked.

:::note[sevenZip exception]
Because 7-Zip is the tool electron-builder uses to extract `.7z` and `.tar.xz` archives, a custom **`sevenZip`** bundle can't itself be one of those formats — that would be circular. Supply it only as a **`.tar.gz`**, **`.zip`**, or bare **`file://` directory**. (The bundle must contain `bin/7za` on macOS/Linux or `bin/7za.exe` on Windows.)
:::

### Custom NSIS bundle layout {#custom-nsis-bundle-layout}

:::warning[Did you use deprecated `.customNsisBinary`/`.customNsisResources` properties in `v26` config?]
Checkout our [`v26`-> `v27` migration guide](./migration/v26-to-v27#step-4-custom-nsis-migration-guide) and relevant [breaking changes note](./migration/v27-breaking-changes#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis).
:::


#### Custom NSIS bundle Explanation

The new NSIS bundle is extremely flexible in-that it provides entrypoints to give you full control of how NSIS execution begins & where NSIS Resources are checked for. There is one bash script entry-point for linux/mac builds at `/makensis` in the bundle and two entrypoints for windows cmd/powershell builds respectively `/makensis.cmd` & `/makensis.ps1`. Any time makensis is invoked by electron-builder with a custom-nsis-toolset it passes off all the execution arguments to the relevant flavored script-shim based on the build-machine OS. These scripts also make it trivial to set the `NSISDIR` (NSIS Resources Directory) wherever you want.

The flexible nature of this toolset provides a greener-pasture for custom NSIS development to make some practical and exotic progress.

:::note[NSIS Entrypoint Shim Scripts]
If you do not need to support builds on Linux/MacOS then you can ommit the `/makensis` bash-script entrypoint from your bundle. Similarly if you do not need to support builds on Windows you can ommit the `/makensis.cmd`/`/makensis.ps1` entrypoint shim scripts from your bundle. At present `/makensis.ps1` is not actually used by electron-builder - It is still considered a required file & not including it may cause your bundle to break unexpectedly from a future electron-builder update.
:::

---

#### Default Custom NSIS bundle Anatomy

A functional flushed out example NSIS Bundle can be found from `electron-builder-binaries` [Default NSIS Bundle](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis). It contains some resources not required for builds that we think still could be useful.

This is the same bundle we use for vanilla-builds and can be explored to get an idea of how custom-nsis bundles work. Here's a breakdown of some highlights:

| File Path | Purpose |
|-|-|
| `/makensis` | A bash script to route all builds running from macOS/Linux hosts to the appropriate `makensis` binary. Also sets `NSISDIR` (NSIS Resource Directory) to be `/windows/` |
| `/makensis.cmd` | A command script to route incoming windows builds to the `/windows/makensis.exe` binary. Also sets `NSISDIR` (NSIS Resource Directory) to be `/windows/` |
| `/makensis.ps1` | A PowerShell script wich may be used in the future by electron-builder to route incoming windows builds from to the `/windows/makensis.exe` binary. Also sets `NSISDIR` (NSIS Resource Directory) to be `/windows/` |
| `/elevate.exe` | A script used by 3rd party UAC plugin for privledge management. **You must include this file in your bundle if you use ElectronBuilder's included NSIS scripts**  |
| `/windows/` | This is the directory used as the NSIS dir for Custom NSIS Resources |
| `/windows/Plugins/x86-unicode/` | This is where all Plugin DLLs called into by NSIS scripts live for this bundle. Some plugins ship with NSIS, some are 3rd party plugins. The 3rd party plugins found here will be required in your custom-build if you use ElectronBuilder's standard NSIS scripts. All `makensis/makensis.exe` binaries in this bundle were compiled with the `x86-unicode` target. They will only call DLLs from this directory.|
| `/windows/Contrib` | These files in here were used **during** the compilation of `makensis/makensis.exe` binaries in this bundle. They are not included at build-time but baked into the running binary. |
|`/windows/nsiconfig.nsh` |  The global config file makensis processes automatically at startup, before your NSIS scripts. It sets default flags and defines like NSIS_MAX_STRLEN, NSIS_CONFIG_* options, etc. |
| `/mac/x64/makensis` | A native `makensis` binary compiled for Intel-CPU macintosh computers. Routed to by `/makensis` entrypoint |
| `/mac/arm64/makensis`| A native `makensis` binary compiled for Apple-Silicon macintosh computers. Routed to by `/makensis` entrypoint |
| `/linux/x64/makensis` | A native `makensis` binary compiled for x64 Linux computers. Routed to by `/makensis` entrypoint |
| `/linux/arm64/makensis`| A native `makensis` binary compiled for arm64 Linux computers. Routed to by `/makensis` entrypoint |
| `/windows/makensis.exe` | A native `makensis.exe` binary compiled to run on x64 Windows computers. Routed to by `/makensis.cmd` & `/makensis.ps1` entrypoints |

All `makensis/makensis.exe` binaries in this bundle were compiled with the `x86-unicode` build `Target`.

Many files are omitted from this list, but most of the important ones are covered. See also [NSIS Resources](#nsis-resources)

The default bundle (`nsis-bundle-3.12.tar.gz` from the [`nsis@1.2.1` release](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis)

#### Custom NSIS bundle Anatomy (minimal)

Here are the files that are absolutely required for a functional NSIS bundle. Note, you only have to provide shims for the operating system & architecture you intend to build on, you're also free to use wine if you have no native `makensis` binary. Hell go-wild on JIT compile custom NSIS binaries to bake in aesthetic installer choices at build time... Or you know w/e.

| File | Purpose |
|-|-|
| `/makensis` | A bash script to route all builds running from macOS/Linux hosts to the appropriate `makensis` binary. Also sets `NSISDIR` (NSIS Resource Directory) |
| `/makensis.cmd` | A command script used to route incoming windows builds to the appropriate `makensis.exe` binary. Also sets `NSISDIR` (NSIS Resource Directory) |
| `/makensis.ps1` | A PowerShell script wich may be used in the future by electron-builder to route incoming windows builds to the appropriate `makensis.exe` binary. Also sets `NSISDIR` (NSIS Resource Directory). Technically it is unused now and you don't **need** to include it for a functional bundle. It is still considered required to prevent unexpected breaks in your custom-bundle build that are created in future electron-builder updates |
| `/elevate.exe` | A script used by 3rd party plugins for privledge management. **You must include this file in your bundle if you use ElectronBuilder's included NSIS scripts**  |
| `/<NSISDIR>/Plugins/<Target>/*` | You must include the plugins provided by NSIS in your NSISDIR` (NSIS Resource Directory). **If you use Electron Builder's built-in NSIS scripts. You also must provide DLLs for these required 3rd party plugins [INetC, StdUtils, SpiderBanner, NsProcess, UAC, WinShell, EmbedHTML, Nsisunz, NSISunzU]** |
| `/<SomePath>/makensis` \|\| `<SomePath>/makensis.exe` | You'll need at least one `makensis/makensis.exe` binary in your bundle and your entry script must pass on makensis execution to it. **You only need to include entrypoint-scripts or binaries for the OS/Arch flavor your electron builder flow runs on**. |

Note soft requirements of `elevate.exe` and `3rd party plugin DLLs` only if you use electron-builder's NSIS scripts. In practice these are almost definitely required without a ton of NSIS scripting work to create entirely custom NSIS scripts for electron-builder.

You can grab these soft-required resources for your own custom bundle `electron-builder-binaries` [Default NSIS Bundle](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis)

:::warning[Limited amd64-unicode support]
Tracking in issue [electron-builder-binaries#230](https://github.com/electron-userland/electron-builder-binaries/issues/230) there are no `amd64-unicode` compiled versions of soft-required 3rd party plugin DLLs. For best results with available NSIS resources, compile your custom NSIS binaries under the `x86-unicode` build `Target`.
:::

#### Out of the NSIS Bundle Anatomy

Worth a quick note that while the bundle itself provides the full and complete NSIS build tools. Electron-Builder and anything building with NSIS still passes in it's own NSIS scripts to use it.

---

#### Helpful Context

A custom `toolsets.nsis` bundle provides the whole NSIS toolset. The NSIS toolset is a build-tool used to compile installer/uninstaller executables for Windows build targets. To be frank, NSIS is an old tool which suffers from a complicated structure and a lot of nuance that is not cohesively documented in all areas. On the flip side NSIS is extremely battle-tested and enjoys one of the largest ecosystem of installer related plugins, tweaks and 3rd party tools. Notably it is a go-to installer type for supporting automatic-updates.

Since documentation can be a bit sparse with regards to the artifacts provided by NSIS and their interactions. A brief breakdown of some key things provided by NSIS are in-order.

---

#### NSIS Binary

As you'd expect, a functional NSIS toolset provides a binary `makensis/makensis.exe` for compiling installers/uninstaller executables for Windows build targets.

While NSIS is a windows build-target tool, it can be natively compiled to run on mac/windows/linux. On mac/linux it compiles to the `makensis` executable file and on windows it compiles to `makensis.exe` executable file.

Unlike modern compilers which allow for multiple output types from one compiler, the `Target` output type a `makensis/makensis.exe` binary compiles to is baked in at compile time. Valid NSIS `Target` types include `x86-unicode`,`x86-ansi`,`amd64-unicode` and one must be chosen when the tool is compiled.

A `makensis/makensis.exe` binary will only build outputs of the `Target` type it was compiled for. Also very important to note, a `makensis/makensis.exe` will only be compatible with resources built for the same `Target` type it was compiled for.

A `makensis/makensis.exe` must be compiled with `NSIS_CONFIG_LOG=yes` to make it possible to enable logging in NSIS scripts.

Many images and assets that effect the installer appearance are also baked into the `makensis/makensis.exe` binary when it is compiled. A custom compiled `makensis/makensis.exe` binary is required for certain aesthetic customizations to the install.

---

#### NSIS Resources

The `makensis/makensis.exe` binary can use a whole directory pattern of various resources for various parts of it's compilation execution. These resources are commonly arranged in a folder-tree like so:

| Resource Type | NSIS Directory Location | Description |
|---|--|---|
| Plugins  | `/Plugins/{ x86-unicode \|\| x86-ansi \|\| amd64-unicode }/<name>.dll`  | Compiled binary DLLs to expose advanced execution to NSIS scripts. These must be compiled with the same `Target` as the `makensis/makensis.exe` binary |
| Include | `/Include/<name>.nsh` | These are `.nsh` headers which can contain code/macros used by NSIS scripts |
| Conf File | `/nsiconfig.nsh` |  The global config file makensis processes automatically at startup, before your NSIS scripts. It sets default flags and defines like NSIS_MAX_STRLEN, NSIS_CONFIG_* options, etc. |
| Contrib | `/Contrib/<various>` | These are language/image assets that are baked into the `makensis/makensis.exe` binary when it's created. They are not read during `makensis` exec. |
| Stubs | `/Stubs/<name>` | Functional complete binaries that NSIS uses as a base to customize by appending byte-code to them. In newer versions of NSIS these must also match the `makensis/makensis.exe` binary `Target` flavor |
| Bin | `/Bin/<name>` | Executables that can be used by plugins |

This is a non-exhaustive list of potential resources.

Plugins are particularly of-note here. NSIS scripts do not offer a lot of functionality on their own, so even very basic things can require a Plugin .dll to provide the functionality.


---


#### Debug NSIS logging


:::info[ You can toggle logging on an NSIS bundle via: ]
`.installerDebugLogging` However it additionally needs `makensis/makensis.exe` binaries and the stubs compiled with `NSIS_CONFIG_LOG=yes`, which the default bundle's NSIS versions are not.
:::

## Replacing removed environment variables

v27 **removes** the toolset environment-variable overrides. Replace each with a `toolsets.<name>` custom object:

| Removed env var | Toolset it controlled | Replacement |
|---|---|---|
| `APPIMAGE_TOOLS_PATH` | AppImage build tools | `toolsets.appimage: { url }` |
| `LINUX_TOOLS_MAC_PATH` | linux-tools-mac bundle | `toolsets.linuxToolsMac: { url }` |
| `CUSTOM_FPM_PATH` | FPM executable | `toolsets.fpm: { url }` |
| `ELECTRON_BUILDER_NSIS_DIR` | NSIS compiler bundle | `toolsets.nsis: { url }` |
| `ELECTRON_BUILDER_NSIS_RESOURCES_DIR` | NSIS resources/plugins | `toolsets.nsis: { url }` |
| `CUSTOM_NSIS_RESOURCES` | Alternate NSIS resources | `toolsets.nsis: { url }` |
| `ELECTRON_BUILDER_WINE_TOOLSET_DIR` | Wine bundle | `toolsets.wine: { url }` |
| `USE_SYSTEM_WINE` | Host Wine instead of the bundle | `toolsets.wine: "system"` |
| `USE_SYSTEM_SIGNCODE` | Host `signtool`/`signcode` | Configure via [`win.sign`](./features/code-signing/code-signing-win.md) + `winCodeSign` |
| `USE_SYSTEM_OSSLSIGNCODE` | Host `osslsigncode` | Configure via [`win.sign`](./features/code-signing/code-signing-win.md) + `winCodeSign` |
| `USE_SYSTEM_FPM` | Host `fpm` instead of the bundle | `toolsets.fpm: { url }` |

```json5
{ "build": { "toolsets": { "nsis": {
  "url": "https://example.com/my-nsis-bundle.tar.gz",
  "checksum": "<SHA-256 hex or base64 SHA-512 of the archive>"
} } } }
```

:::warning[No env-var replacement for the signing overrides]
The two signing `USE_SYSTEM_*` variables (`USE_SYSTEM_SIGNCODE`, `USE_SYSTEM_OSSLSIGNCODE`) have **no env-var equivalent** — configure signing through [`win.sign`](./features/code-signing/code-signing-win.md) and the `winCodeSign` toolset instead. `USE_SYSTEM_WINE` is replaced by the config value `toolsets.wine: "system"`.

`USE_SYSTEM_FPM` is likewise removed. On **Windows there is no bundled FPM**, so an FPM-based target now **requires** an explicit custom `toolsets.fpm` (`{ url: "file:///path/to/dir" }`) and otherwise throws a clear configuration error — previously it silently fell back to a host `fpm` on `PATH`.
:::

See [Toolset env-var overrides removed](./migration/v27-breaking-changes.md#toolset-env-var-overrides-removed) for the complete rationale.

## Code signing and toolsets

Windows signing is driven by the `winCodeSign` toolset in combination with [`win.sign`](./features/code-signing/code-signing-win.md):

- The default `winCodeSign` (`"latest"` → `1.3.0`) bundles a modern `signtool` / `osslsigncode` (native arm64) plus the Windows Kits used for AppX/MSIX. The `hsm` and `pkcs11` signing modes require this modern bundle.
- **Azure Trusted Signing** (`win.sign: { type: "azure" }`) uses the faster `signtool /dlib` path **out of the box**, because the default `winCodeSign` ships the ATS `dlib` + .NET 8 payload — no pin needed. To force the legacy PowerShell `Invoke-TrustedSigning` path, pin `winCodeSign` **below** `1.3.0` (e.g. `"1.2.1"` or `"0.0.0"`). A `ToolsetCustom` object uses the `dlib` from your supplied bundle.

For full setup — certificate methods, HSM/PKCS#11, and Azure — see [Code Signing for Windows](./features/code-signing/code-signing-win.md) and [Azure Trusted Signing `signtool /dlib` is the default](./migration/v27-breaking-changes.md#azure-trusted-signing-signtool-dlib-is-the-default).

---

### Troubleshooting (`website/docs/troubleshooting.md`)

# Troubleshooting

This page covers common errors by category. Enable verbose logging to get more detail:

```bash
DEBUG=electron-builder electron-builder build
```

## Code Signing

### macOS

**"No identity found for code signing"**
: No Developer ID certificate is installed. Run `security find-identity -v -p codesigning` to list available identities. On CI, verify `CSC_LINK` is set and contains a valid base64-encoded `.p12`.

**"CSSMERR_TP_CERT_REVOKED"**
: Your certificate was revoked by Apple. Renew it in the Apple Developer portal.

**"The certificate has expired"**
: Renew the certificate in Apple Developer portal, re-export it, and update your CI secrets.

**Build produces unsigned output without an error**
: `CSC_LINK` isn't set, so electron-builder silently skips signing. Set `forceCodeSigning: true` to turn this into a build failure so you catch it early.

**"Keychain not unlocked" / codesign hangs**
: Verify `CSC_LINK` and `CSC_KEY_PASSWORD` are set correctly — electron-builder creates a temporary keychain and runs all required setup commands, including `set-key-partition-list`, automatically. If using a manually managed keychain (via `CSC_KEYCHAIN`), ensure you run `security set-key-partition-list -S apple-tool:,apple: -s -k <password> <keychain>` after importing.

**"App cannot be opened because the developer cannot be verified"** (Gatekeeper)
: The app is unsigned, signed with the wrong certificate type (e.g., Mac App Store cert used for direct distribution), or not notarized. Check the certificate type and notarization status with `spctl --assess --verbose dist/mac/MyApp.app`.

### macOS Notarization

**"Package Invalid: the binary is not signed with a valid Developer ID certificate"**
: You used an Apple Distribution / Mac App Store certificate. Notarization for direct distribution requires a Developer ID Application certificate.

**"The executable does not have the Hardened Runtime enabled"**
: Add `hardenedRuntime: true` to your `mac.sign` config. Also ensure entitlements include `com.apple.security.cs.allow-jit`.

**"Notarization failed: invalid credentials"**
: The `APPLE_ID` / `APPLE_APP_SPECIFIC_PASSWORD` combination is wrong, or the app-specific password was revoked. Regenerate it at [appleid.apple.com](https://appleid.apple.com).

**"Notarization timed out"**
: Apple's notarization service may be slow. Run again later. Check [Apple System Status](https://developer.apple.com/system-status/).

### Windows

**SmartScreen warning at install time**
: Normal for standard OV certificates — trust builds over time based on download count. EV certificates bypass this. No fix needed; warn users in your release notes that the warning is expected.

**"Publisher does not match" (AppX)**
: The `appx.publisher` value in your config must exactly match the Subject field in your certificate. Copy it from the certificate's Properties → Details → Subject.

**"SignTool error: No certificates were found that met all the given criteria"**
: The certificate isn't in the certificate store, or `WIN_CSC_LINK` and `WIN_CSC_KEY_PASSWORD` are wrong. Verify the base64 decodes to a valid `.pfx` and that the password is correct.

---

## ASAR and File Packaging

**Native module crashes at runtime: "Error: Module did not self-register"**
: The native module (`.node` file) is inside the ASAR archive, but native modules can't load from inside ASAR. Add to `asar.unpack`:
```yaml
asar:
  unpack:
    - "node_modules/your-module/**"
    - "**/*.node"
```

**"ENOENT: no such file or directory" at runtime**
: A file that exists in development is missing from the packaged app. Check if it's being excluded by a `files` pattern or by the default exclusions. Build with `DEBUG=electron-builder` and look for "excluding" log lines. Also check `asar.unpack` — files inside ASAR must be accessed via `app.getAppPath()`, not via `__dirname` relative paths.

**App directory is too large**
: Profile the ASAR after building:
```bash
npx asar list dist/mac/MyApp.app/Contents/Resources/app.asar | sort
```
Common culprits: TypeScript source files (`.ts`), source maps (`.map`), test fixtures, large binary assets. Add exclusion patterns to `files`:
```yaml
files:
  - "**/*"
  - "!src/**"
  - "!**/*.ts"
  - "!**/*.map"
  - "!**/__tests__/**"
```

**"Cannot find module" at runtime after packaging**
: A production dependency isn't being included. Check that it's in `dependencies` (not `devDependencies`) in `package.json`. electron-builder only includes production dependencies.

**FileSet `from` path not found**
: In `files`, the `from` path is relative to the **app directory**. In `extraResources` / `extraFiles`, `from` is relative to the **project root**. These are different when `directories.app` is set.

---

## Native Modules

**"Error: The module was compiled against a different Node.js version"**
: electron-builder automatically rebuilds native modules for the target Electron version during the build. If you see this error at runtime during development (not during a build), ensure you are running your app via `electron .` from the project root, not from a globally installed Electron binary compiled against a different version.

**Native module works in development but crashes after packaging**
: The module likely needs to be in `asar.unpack`. Some modules also need their full directory tree unpacked:
```yaml
asar:
  unpack:
    - "node_modules/better-sqlite3/**"
    - "node_modules/sharp/**"
```

---

## Auto-Update

**Updates not detected: app reports "up to date" when a new version exists**
: Check that the `publish` configuration matches your distribution channel. For GitHub, ensure the release isn't a draft (draft releases are invisible to electron-updater). Verify `app.getVersion()` matches the version in `package.json` at build time.

**"Cannot find latest.yml" / update server 404**
: electron-builder generates `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` only when publishing. Ensure you ran `--publish always` (or `--publish onTagOrDraft`) when building the release.

**"sha512 checksum mismatch"**
: The downloaded update file is corrupt or the wrong file is being served. Verify that the artifact and the `.yml` metadata file were generated together in the same build and not mixed from different builds. As of electron-builder 27, the manifest `sha512` should be base64-encoded — a 128-hex-character "expected" value in the error message is the tell that a hand-rolled manifest still uses hex, which is accepted but deprecated in v27 and will be removed in v28; convert it with `sha512sum file.ext | cut -d' ' -f1 | xxd -r -p | base64 -w0`.

**Delta updates (AppImage / NSIS-Web) fail**
: For AppImage delta updates, electron-builder embeds a blockmap directly in the AppImage binary — no separate file needs to be published. If delta updates aren't working, verify the AppImage was built with electron-builder and that a publish provider is configured. For NSIS differential packages, `nsis.differentialPackage` must not be `false` (any other value — `true`, `"compressed"`, `"store-asar"`, or unset — builds a differential-aware package; `"store-asar"` additionally stores `resources/app.asar` uncompressed so app-code deltas stay proportional to what changed).

---

## Build Environment

**"Cannot find module 'electron'"**
: `electron` must be in `devDependencies`, not `dependencies`. electron-builder expects to find it in the project's devDependencies to determine the Electron version to bundle.

**"Resource does not exist" when downloading Electron**
: The Electron version specified in your devDependencies doesn't exist for the target architecture. Check [Electron releases](https://github.com/electron/electron/releases) to confirm the version and architecture combination exists.

**Build hangs with no output**
: Often caused by a native module rebuild that's hanging. Run with `DEBUG=electron-builder` and check the last log line. Also check if a spawned process is waiting for input (some postinstall scripts do this).

**Out-of-memory error during packaging**
: Large apps can exhaust memory during ASAR creation. Set `NODE_OPTIONS=--max-old-space-size=4096` before running electron-builder.

**Build fails in an offline / air-gapped environment**
: electron-builder downloads build toolsets (e.g. WinCodeSign, AppImage tools) the first time they are needed, then caches the archive at `<ELECTRON_BUILDER_CACHE>/<releaseName>/<filename>`. On every subsequent build it reads from that cache and makes no network request.

For a first-time setup in a fully air-gapped environment, run the build once on a machine with internet access so the cache is populated, then copy the entire `ELECTRON_BUILDER_CACHE` directory to the air-gapped machine. Point both machines at the same directory with `ELECTRON_BUILDER_CACHE=/path/to/shared/cache`.

**"Downloading" progress appears for an artifact that should be cached**
: The toolset archive cache lives inside `ELECTRON_BUILDER_CACHE` (default: `~/Library/Caches/electron-builder` on macOS, `%LOCALAPPDATA%/electron-builder/Cache` on Windows, `~/.cache/electron-builder` on Linux). If this directory is cleared or not persisted between builds (common on ephemeral CI runners), the archive is re-downloaded. Persist the cache directory across CI runs to avoid repeated downloads.

---

## Linux-Specific

**"fpm not found"**
: The DEB, RPM, Pacman, APK, FreeBSD, and P5P targets use FPM internally. electron-builder bundles FPM — if it fails to find it, try reinstalling electron-builder or clearing `~/.cache/electron-builder`.

**AppImage won't run: "FUSE not found" / "fusermount not found"**
: As of v27 the default AppImage runtime is static and needs no host FUSE, so this affects only builds pinned to the legacy runtime (`toolsets.appimage: "0.0.0"`). Rebuild on the default runtime (remove the `0.0.0` pin), or install FUSE2 on the host:
```bash
# Ubuntu/Debian
sudo apt install fuse libfuse2

# Fedora
sudo dnf install fuse fuse-libs
```
Or run with `--appimage-extract-and-run`:
```bash
./MyApp.AppImage --appimage-extract-and-run
```

**AppImage won't run: "squashfs: FATAL ERROR aborting"**
: This is a symptom of the legacy FUSE2 runtime. Remove any `toolsets.appimage: "0.0.0"` pin so the build uses the default static runtime.

**Flatpak build fails: "runtime not found"**
: Install the Flatpak runtime before building:
```bash
flatpak install flathub org.freedesktop.Platform//24.08
flatpak install flathub org.freedesktop.Sdk//24.08
```

**Snap confinement issues: permission denied at runtime**
: The snap is trying to access something not declared in its interface plugs. Run `snap run --shell myapp` to get a shell inside the snap, then test access. Add the appropriate plug to `snap.plugs` in your config.

---

## macOS-Specific

**DMG window layout doesn't look right**
: DMG window positions are set by the macOS system and can behave unexpectedly if the dimensions in `dmg.window.size` don't match the `contents` positions. Ensure icon positions fit within the window bounds with some padding. Test by opening the built DMG manually.

**"App is damaged and can't be opened"**
: Usually means the app was downloaded without Gatekeeper quarantine being cleared, and notarization failed or wasn't performed. Run `xattr -dr com.apple.quarantine /path/to/app` temporarily during testing. Ensure notarization succeeds before distributing.

**PKG installer fails: "The package … is not compatible with this version of macOS"**
: Check `pkg.minVersion` matches your actual minimum supported macOS version.

**"The application cannot be opened" on Apple Silicon**
: The app may be x64-only but running on an ARM Mac without Rosetta. Either build a universal binary or ensure Rosetta 2 is installed (`softwareupdate --install-rosetta`).

---

## Windows-Specific

**NSIS installer: "The installer you are trying to use is corrupt or incomplete"**
: The installer binary is corrupted. This can happen when the file is scanned/modified by antivirus during download or build. Check your AV exclusions during builds.

**"The publisher could not be verified" (Windows SmartScreen)**
: The app isn't code-signed, or signed with an untrusted certificate. See [Windows Code Signing](features/code-signing/code-signing-win.md).

**MSI: upgrades don't remove the old version**
: The `upgradeCode` was changed between versions. The `upgradeCode` (a GUID) must remain **identical** across all versions of your application. See [MSI Configuration](msi.md).

**AppX installation fails: "The package could not be installed"**
: Common causes: certificate not trusted on the target machine; `publisher` in config doesn't match certificate Subject; Windows version below the `minVersion` requirement. Check Event Viewer → Applications and Services Logs → Microsoft → Windows → AppxPackagingOM for detailed error codes.

---

## Docker / Cross-Platform Builds

**Linux build on macOS/Windows hangs or fails**
: Use the official Docker image:
```bash
docker run --rm -ti \
  -v ${PWD}:/project \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  electronuserland/builder:wine \
  /bin/bash -c "npm ci && npx electron-builder --linux"
```

**"wine: command not found" when building Windows on Linux**
: Use the `electronuserland/builder:wine` Docker image which includes Wine. Or install Wine on the host system.

---

## Getting More Help

- Run `DEBUG=electron-builder electron-builder build 2>&1 | tee build.log` to capture full debug output
- Search [GitHub Issues](https://github.com/electron-userland/electron-builder/issues)
- When filing a bug report, use [electron/minimal-repro](https://github.com/electron/minimal-repro) to create a minimal reproduction case — issues with a repro get resolved significantly faster

---

### Win (`website/docs/win.md`)

---
title: "Windows"
---

{!./partials/_upgrading-from-v26.md!}

The top-level [win](./configuration.md#win) key contains a set of options instructing electron-builder on how it should build Windows targets. These options are applicable to any Windows target.

---

## Common Questions
## How do you delegate code signing?

In v27, all Windows signing is configured through the single [`win.sign`](./configuration.md#win) discriminated union (`type: "signtool" | "hsm" | "pkcs11" | "azure"`). To delegate signing to a custom function, set the `sign` field on that union — it works for the `signtool`, `hsm` and `pkcs11` types. See the [Windows Code Signing guide](./features/code-signing/code-signing-win.md) and [why sign.js is called 8 times](https://github.com/electron-userland/electron-builder/issues/3995).

```json
"win": {
  "sign": {
    "type": "signtool",
    "sign": "./customSign.js",
    "publisherName": "CN=My Company, O=My Company, C=US"
  }
}
```

electron-builder cannot read the certificate your hook signs with, so set `publisherName` to that certificate's subject: it is written to `app-update.yml`, and electron-updater checks every downloaded update against it. It is never derived from a certificate for a hook, not even one in the config, so without it a build that writes `app-update.yml` (an `nsis`, `nsis-web` or `electronUpdaterAware` `appx` target with a publish configuration, including one inferred from a GitHub `repository`) fails. Set `win.verifyUpdateCodeSignature: false` instead only if your updates are not Authenticode-signed or you don't use electron-updater.

Copy the subject from a binary your hook has already signed (a previous release, or `win-unpacked` from an `electron-builder --win dir` build, which does not write `app-update.yml`) instead of typing it by hand. electron-builder cannot check it against the certificate the hook uses, and every component you list (`CN`, `O`, `C`, …) must match the certificate exactly — otherwise installed apps reject every later update:

- **Windows (PowerShell):** `(Get-AuthenticodeSignature .\dist\win-unpacked\<App>.exe).SignerCertificate.Subject` prints it in the form to use as-is.
- **macOS / Linux:** `osslsigncode verify -in <App>.exe` — the `Subject:` line under "Signer's certificate", printed as `/C=US/ST=California/O=My Company, Inc./CN=My Company, Inc.`. Keep only `CN`, `O` and `C` and write them as `CN="My Company, Inc.", O="My Company, Inc.", C=US`: OpenSSL names some other components differently from Windows (`ST` instead of `S`, for example), and a value that contains a comma must be wrapped in double quotes.

:::note[Upgrading from v26]
The v26 `win.signtoolOptions` / `win.azureSignOptions` keys were removed — `electron-builder migrate-schema` rewrites them to `win.sign` automatically. See [v27 Breaking Changes → Windows signing](./migration/v27-breaking-changes.md#windows-signing-winsign).
:::

File `customSign.js` in the project root directory:
```js
exports.default = async function(configuration) {
  // your custom code
}
```

## How do you use a custom verify function to enable nsis signature verification alternatives instead of powershell?

Use the `NsisUpdater.verifyUpdateFileAuthenticodeSignature` interface:

```ts
export type VerifyUpdateFileResult =
  | { response: "success" }
  | { response: "failure"; message: string }

export type VerifyUpdateFileAuthenticodeSignature = (
  publisherName: string[],
  path: string
) => Promise<VerifyUpdateFileResult>
```

Pass a custom verify function to the nsis updater. For example, if you want to use a native verify function, you can use [win-verify-signature](https://github.com/beyondkmp/win-verify-trust).

```ts
import { NsisUpdater } from "electron-updater"
import { verifySignatureByPublishName } from "win-verify-signature"

export default class AppUpdater {
    constructor() {
        const options = {
            requestHeaders: {
                // Any request headers to include here
            },
            provider: 'generic',
            url: 'https://example.com/auto-updates'
        }

        const autoUpdater = new NsisUpdater(options)
        autoUpdater.verifyUpdateFileAuthenticodeSignature = async (publisherName: string[], path: string) => {
            const result = verifySignatureByPublishName(path, publisherName)
            return result.signed ? { response: "success" } : { response: "failure", message: result.message }
        }
        autoUpdater.addAuthHeader(`Bearer ${token}`)
        autoUpdater.checkForUpdatesAndNotify()
    }
}
```

The built-in default uses [`windowsExecutableCodeSignatureVerifier`](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/windowsExecutableCodeSignatureVerifier.ts).
The older property `verifyUpdateCodeSignature`, which differs only in name and return interface, is deprecated and kept only as a compatibility shim (shall be removed in electron-builder v28). The protected `_verifyUpdateCodeSignature` member is likewise deprecated in favour of `_verifyUpdateFileAuthenticodeSignature`.

## How do you create a Parallels Windows 10 Virtual Machine?

:::warning[Disable "Share Mac user folders with Windows"]
If you use Parallels, you [must not use](https://github.com/electron-userland/electron-builder/issues/865#issuecomment-258105498) "Share Mac user folders with Windows" feature and must not run installers from such folders.
:::

You don't need to have a Windows 10 license. A free license is provided (expires after 90 days, but this is not a problem because no additional setup is required).

1. Open Parallels Desktop.
2. File -> New.
3. Select "Modern.IE" in the "Free Systems".
4. Continue, Continue, Accept software license agreement.
5. Select "Microsoft Edge on Windows 10".
6. The next steps are general, see [Installing Windows on your Mac using Parallels Desktop](https://kb.parallels.com/4729) from "Step 6: Specify a name and location".

Parallels Windows 10 VM will be used automatically to build AppX on macOS. No need even start VM — it will be started automatically on demand and suspended after build. No need to specify VM — it will be detected automatically (first Windows 10 VM will be used).

## How do you create a VirtualBox Windows 10 Virtual Machine?

If you are not on macOS or don't want to buy [Parallels Desktop](https://www.parallels.com/products/desktop/), you can use free [VirtualBox](https://www.virtualbox.org/wiki/Downloads).

1. Download a Windows evaluation ISO from the [Microsoft Evaluation Center](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-11-enterprise) (Microsoft no longer publishes the prebuilt "MSEdge on Win10" virtual machines).
2. Create a new virtual machine in VirtualBox and install Windows from the ISO.

VirtualBox is not supported by electron-builder for now, so you need to set up the build environment on Windows if you want to use VirtualBox to build AppX (and other Windows-only tasks).

## Configuration

  {!./app-builder-lib.Interface.WindowsConfiguration.md!}

---

### Features Auto Update (`website/docs/features/auto-update.md`)

---
title: "Auto Update"
---

Auto updates are enabled by the `electron-updater` package. Ideally, auto updates are configured to run in a CI pipeline to automatically provision new releases. See [publish configuration](../publish.md) for information on how to configure your local or CI environment for automated deployments.

Auto updates work as follows:

- You configure the package to build release metadata (`latest.yml`)
- Electron builder uploads the actual release targets and metadata files to the configured target (except for generic server, where you have to upload manually)
- You configure the Electron application to use auto-updates, which queries the publish server for possible new releases

Read the remainder of this guide to configure everything.

:::info[Code signing is required on macOS]
macOS application must be [signed](./code-signing/code-signing.md) in order for auto updating to work.
:::

## Auto-updatable Targets

* macOS: DMG.
* Linux: AppImage, DEB, Pacman and RPM.
* Windows: NSIS.

All these targets are default, custom configuration is not required. (Though it is possible to [pass in additional configuration, e.g. request headers](#custom-options-instantiating-updater-directly).)

:::info
1. **Squirrel.Windows is not supported.** Simplified auto-update is supported on Windows if you use the default NSIS target, but is not supported for Squirrel.Windows.
You can [easily migrate to NSIS](https://github.com/electron-userland/electron-builder/issues/837#issuecomment-355698368).
2. `zip` target for macOS is **required** for Squirrel.Mac, otherwise `latest-mac.yml` cannot be created, which causes `autoUpdater` error. Default [target](../mac.md#macos-target-overview) for macOS is `dmg`+`zip`, so there is no need to explicitly specify target.
:::

## Differences between electron-updater and built-in autoUpdater

The `electron-updater` package offers a different functionality compared to Electron's built-in auto-updater. Here are the differences:

* Linux is supported (not only macOS and Windows).
* Code signature validation not only on macOS, but also on Windows.
* All required metadata files and artifacts are produced and published automatically.
* Download progress and [staged rollouts](#staged-rollouts) supported on all platforms.
* Different providers supported out of the box: ([GitHub Releases](https://help.github.com/articles/about-releases/), [Amazon S3](https://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/community/tutorials/an-introduction-to-digitalocean-spaces), [Cloudflare R2](https://developers.cloudflare.com/r2/), [Keygen](https://keygen.sh/docs/api/#auto-updates-electron) and generic HTTP(s) server).
* You need only 2 lines of code to make it work.

## Quick Setup Guide

1. Install [electron-updater](https://yarn.pm/electron-updater) as an app dependency.

2. Configure the [`publish`](../publish.md) options depending on where you want to host your release files.

3. Build your application and check that the build directory contains the metadata `.yml` files next to the built application. For most publish targets, the building step will also upload the files, except for the generic server option, where you have to upload your built releases and metadata manually.

4. Use `autoUpdater` from `electron-updater` instead of `electron`:

    CommonJS
    ```js
    const { autoUpdater } = require("electron-updater")
    ```
    ESM
    ```js
    import { autoUpdater } from "electron-updater"
    ```
    TypeScript
    ```typescript
    import electronUpdater, { type AppUpdater } from 'electron-updater';

    export function getAutoUpdater(): AppUpdater {
       // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
       // It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
       const { autoUpdater } = electronUpdater;
       return autoUpdater;
    }
    ```

5. Call `autoUpdater.checkForUpdatesAndNotify()`. Or, if you need custom behaviour, implement `electron-updater` events, check examples below.

:::note
Do not call `setFeedURL`. electron-builder automatically creates `app-update.yml` file for you on build in the `resources` (this file is internal, you don't need to be aware of it). Its feed is the first provider that receives the update manifest (`publishAutoUpdate` not `false`) in the `publish` settings of the targets that write update info — a target-level `publish` such as `nsis.publish` counts — see [which settings become the auto-update feed](../publish.md#app-update-yml-feed).
:::

## Examples

:::note[Example in TypeScript using system notifications]
```typescript
import { autoUpdater } from "electron-updater"

export default class AppUpdater {
  constructor() {
    const log = require("electron-log")
    log.transports.file.level = "debug"
    autoUpdater.logger = log
    autoUpdater.checkForUpdatesAndNotify()
  }
}
```
:::

* A [complete example](https://github.com/iffy/electron-updater-example) showing how to use.
* An [encapsulated manual update via menu](https://github.com/electron-userland/electron-builder/blob/7f6c3fea6fea8cffa00a43413f5335097aca94b0/pages/encapsulated%20manual%20update%20via%20menu.js).

### Custom Options instantiating updater Directly

If you want more control over the updater configuration (e.g. request header for authorization purposes), you can instantiate the updater directly.

```typescript
import { NsisUpdater } from "electron-updater"
// Or MacUpdater, AppImageUpdater

export default class AppUpdater {
    constructor() {
        const options = {
            requestHeaders: {
                // Any request headers to include here
            },
            provider: 'generic',
            url: 'https://example.com/auto-updates'
        }

        const autoUpdater = new NsisUpdater(options)
        autoUpdater.addAuthHeader(`Bearer ${token}`)
        autoUpdater.checkForUpdatesAndNotify()
    }
}
```

:::note[Credentials are only sent to the update feed's origin]
The credential headers in `requestHeaders` / `addAuthHeader` — headers such as `Authorization`, the same set that is removed on a cross-origin redirect — and the query string of the feed `url` are only sent to the feed's origin (scheme, host and port; as on redirects, an `http` → `https` upgrade of the feed host on the default ports keeps the headers and the feed query). With the generic, s3, spaces, r2, keygen, bitbucket, github and gitlab providers, an update file whose URL in `latest*.yml` is on another origin, and the blockmaps and differential range requests derived from it, are downloaded without them; such a URL keeps its own query string. A blockmap is downloaded from `${url}.blockmap` with the file URL's query string, so a pre-signed installer URL's signature does not fit it: set `files[].blockMapUrl` in `latest*.yml` to the blockmap's own (pre-signed) URL. A relative value resolves like `url` (against the feed URL); an absolute URL is used as-is, with its own host and query string. Either follows the same origin rule. With a `blockMapUrl`, the old version's blockmap comes from the local cache or from `previousBlockmapBaseUrlOverride`; when neither has it, that one update is downloaded in full, which fills the cache for the next one (the private GitHub and GitLab providers, which resolve files from the release assets, ignore `blockMapUrl`). With every provider, including custom ones, a URL that electron-updater resolves against the feed URL (such as `files[].url` or a blockmap) gets the feed query only on the feed's origin. Serve the update files from the feed origin (relative `files[].url`, the default) or use pre-signed URLs. [Signed update manifests](./signed-update-manifests.md) cover `files[].url` and `packages.<arch>.path`, so a pre-signed URL has to be in `latest*.yml` before it is signed (see [Key storage](./key-rotation.md#key-storage)). A custom provider (one that does not extend a built-in provider such as `GenericProvider`) declares where these headers go by overriding `Provider.feedBaseUrl`: return its feed URL to send them only to that origin, or `null` to send the request headers to every download URL. If it declares neither and the downloads carry credential headers, `downloadUpdate()` fails with `ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED` before any download request:

```typescript
class MyProvider extends Provider<UpdateInfo> {
  get feedBaseUrl(): URL | null {
    return new URL("https://updates.example.com/") // or null
  }
  // getLatestVersion(), resolveFiles(), …
}
```
:::

### Custom downloaded-file verification

All updater classes inherit `AppUpdater.verifyUpdateFile`, which lets you run your own verification step on an update file before it is allowed to become installable.
If the verification fails, the update is aborted, the file is deleted and an `ERR_UPDATER_INVALID_UPDATE_FILE` error is emitted.

The hook runs on **every** path that can lead to an install, so a rejected file can never reach the installer:

| When | `updateFilePath` points to |
| --- | --- |
| Right after a fresh download | the file under a **temporary** name — it is renamed to `originalUpdateFileName` only after your verification succeeds, so an unverified file is never executable under its real name |
| An update downloaded by an earlier session and reused from the cache | the cached file, under its real name |
| Before an install-on-next-launch spawns the cached installer | the pending installer, under its real name |

Only the first case can quarantine the file under a temporary name; in the other two the bytes are already on disk under their real name from a previous session, and the hook is a re-verification of a file at rest. `cancellationToken` is set only when the verification belongs to a download.

For NSIS web installers, `packageFilePath` is also provided and points to the downloaded companion `.7z` package. It is omitted when no such package was downloaded.

This hook is generic and works across updater implementations. On Windows NSIS updates, the built-in Authenticode verification remains available separately as `NsisUpdater.verifyUpdateFileAuthenticodeSignature`, and both run — Authenticode first, then your hook.

```ts
import { NsisUpdater } from "electron-updater"

const updater = new NsisUpdater()

updater.verifyUpdateFile = async ({ updateFilePath, originalUpdateFileName, packageFilePath, cancellationToken }) => {
  try {
    // Example for your custom code, which can inspect the update file at `updateFilePath`,
    // can use the expected filename `originalUpdateFileName`, and for NSIS web installers can also inspect
    // the downloaded companion package at `packageFilePath`.
    const signatureFile = getCorrespondingSignatureFile(originalUpdateFileName)
    checkSignature(updateFilePath, signatureFile)
    return { response: "success" }
  }
  catch (err) {
    return { response: "failure", message: `${err}` }
  }
}

// assigning null restores the default (a stub that always succeeds)
updater.verifyUpdateFile = null
```

A `failure` must carry a `message`; it is what the emitted error reports as the reason.

## Install on Next Launch (Windows/Linux)

When a downloaded update is automatically installed is controlled by `autoUpdater.autoInstallEvent` (`"manual" | "onQuit" | "onNextLaunch"`, default `"onQuit"`). With the default `"onQuit"`, the update is installed when the app quits: the updater spawns the installer as a detached process while the app is exiting. If the quit happens because the OS session is ending (shutdown, reboot or log off on Windows), the OS can kill that installer mid-install and leave the app in a broken, partially-uninstalled state ([#7807](https://github.com/electron-userland/electron-builder/issues/7807)).

electron-updater ≥ 7.0 (electron-builder v27) mitigates this in two ways:

1. **Session-end guard (always on).** When the OS signals that the session is ending, the on-quit install is skipped and a warning is logged. The downloaded update stays cached and is installed on the next regular quit. Detection is best-effort: `powerMonitor` `shutdown` on macOS/Linux, and the `BrowserWindow` `session-end` event on Windows (windowless, e.g. tray-only, apps cannot be covered on Windows).

2. **Install on next launch (opt-in).** Instead of installing while quitting, persist the downloaded update and install it at the start of the *next* launch, when no session teardown can interrupt it:

    ```js
    autoUpdater.autoInstallEvent = "onNextLaunch"
    ```

    With this set, any app quit records the downloaded update as pending instead of spawning the installer. On the next launch the updater fetches fresh update info from your update server, re-validates the cached installer against it (checksum, and code signature on Windows), verifies the pending version is still an installable change (newer than the running app — or a downgrade when `allowDowngrade` is enabled), then runs the installer silently and restarts the app. If validation fails or the version is no longer an installable change, the pending state is cleared and the app starts normally.

    Set `autoInstallEvent = "manual"` to disable automatic installation entirely (the downloaded update stays cached until you call `quitAndInstall()` yourself).

    A single quit can also be deferred without setting the property:

    ```js
    autoUpdater.quitAndInstall({ isSilent: true, isForceRunAfter: true, waitUntilNextLaunch: true })
    ```

    You can also trigger the pending install explicitly (e.g. before opening your first window):

    ```js
    await autoUpdater.installPendingUpdateIfAvailable()
    ```

### Per-target behavior

The *automatic* install at startup only runs for targets that can install the pending update without an elevation/authentication prompt. Targets that always elevate to install (deb/rpm/pacman via pkexec/sudo, per-machine NSIS via UAC) skip the automatic path with a log message and keep the pending update — call `installPendingUpdateIfAvailable()` explicitly at a moment your app controls to install those.

| Target | Automatic install at next launch | Explicit `installPendingUpdateIfAvailable()` |
| --- | --- | --- |
| NSIS (per-user) | ✓ | ✓ |
| NSIS (per-machine, `isAdminRightsRequired`) | skipped — would show a UAC prompt at startup | ✓ (UAC prompt) |
| AppImage | ✓ | ✓ |
| deb / rpm / pacman | skipped — package managers always elevate (pkexec/sudo) | ✓ (auth prompt) |
| macOS | n/a — Squirrel.Mac stages updates natively and applies them on relaunch | resolves `false` |

electron-builder v27 sets `isAdminRightsRequired` in the update info of every per-machine `nsis` and `nsis-web` build (`perMachine: true`), including assisted installers (`oneClick: false`) and builds with `differentialPackage: false`, so their updates are started with `elevate.exe` and skipped by the automatic install at launch.

:::note[Planned default change in v28]
`autoInstallEvent` defaults to `"onQuit"` in v27; `"onNextLaunch"` is planned to become the **default** in v28 to resolve this class of session-end corruption once and for all. macOS is unaffected: Squirrel.Mac natively stages downloaded updates and applies them on relaunch, without a killable installer process (there `"onQuit"` and `"onNextLaunch"` behave identically).
:::

:::caution[Launching during an OS shutdown is a residual, best-effort-only race]
Deferring the install to the next launch moves the installer out of the session-end quit — the common [#7807](https://github.com/electron-userland/electron-builder/issues/7807) trigger — but it does not make the install itself atomic. If the user *launches* the app while the OS is already shutting down or rebooting (for example, right before a scheduled restart fires), the automatic startup install spawns the same detached NSIS/AppImage installer, and the OS can still terminate it mid-write.

The session-end guard does **not** cover this launch window: it is wired into the on-quit path only, and on a fresh launch the `shutdown` / `session-end` signal generally arrives *after* the startup install has already been spawned, so it cannot reliably win the race. The probability is very low — it requires a deferred update, a launch inside the shutdown window, *and* the OS killing the installer mid-write — and macOS is immune (Squirrel.Mac swaps the app bundle atomically). The only complete fix for NSIS/AppImage is an atomic install, which is out of scope for this feature.
:::

## Debugging

You don't need to listen to all events to understand what's wrong. Just set `logger`.
[electron-log](https://github.com/megahertz/electron-log) is recommended (it is an additional dependency that you can install if needed).

```js
autoUpdater.logger = require("electron-log")
autoUpdater.logger.transports.file.level = "info"
```

Note that in order to develop/test UI/UX of updating without packaging the application you need to have a file named `dev-app-update.yml` in the root of your project, which matches your `publish` setting from electron-builder config (but in [yaml](https://www.json2yaml.com) format).
In latest version you need [force the updater](https://github.com/electron-userland/electron-builder/issues/6863) to work in "dev" mode:
```js
autoUpdater.forceDevUpdateConfig = true
```
:::note
If you see this in logs:
```
APPIMAGE env is not defined, current application is not an AppImage
```
you need to apply [this workaround](https://github.com/electron-userland/electron-builder/issues/3167#issuecomment-627696277) otherwise update won't continue:
```js
process.env.APPIMAGE = path.join(__dirname, 'dist', `app_name-${app.getVersion()}.AppImage`)
```
:::

But it is not recommended, better to test auto-update for installed application (especially on Windows). [Minio](https://github.com/electron-userland/electron-builder/issues/3053#issuecomment-401001573) is recommended as a local server for testing updates.

## Compatibility

Since electron-builder 27 the generated `latest*.yml` targets the modern `files[]` metadata format by default and no longer includes the legacy top-level `path`/`sha512` fields, which are needed only by electron-updater 1.x – 2.15.0.

The `electronUpdaterCompatibility` option sets the electron-updater compatibility semver range (e.g. `>=2.16`, `>=1.0.0`). Can be specified per platform (e.g. `win.electronUpdaterCompatibility`). When the declared range intersects legacy electron-updater versions, the corresponding legacy fields are emitted in addition to `files[]`: the top-level `path`/`sha512` and the Windows `sha2` checksum for ranges intersecting `<2.16.0`, and the old `latest-mac.json` for ranges intersecting `<2.0.0`. Defaults to `>=2.16`.

Metadata format history:

* `1.0.0` latest-mac.json
* `2.15.0` path
* `2.16.0` files

:::warning[sha2-only metadata is deprecated]
Update metadata validated only by the legacy SHA-256 `sha2` checksum is deprecated — v27 warns and **v28 rejects it (fail-closed)**. Avoid pinning `electronUpdaterCompatibility` to a legacy range unless you actually ship 1.x–2.15 clients.
:::

## Update security options

Two `AppUpdater` settings changed or were added in v27. See the [Security & Hardening](./security.md#update-security-electron-updater) page for the full rationale.

The update manifest (`latest*.yml`) itself **is** cryptographically signed and verified before any download as of v27. Publishing a build that emits update metadata fails unless an Ed25519 signing key resolves; `updateManifest: false` is the only opt-out — see [Signed Update Manifests](./signed-update-manifests.md).

Changing a signing key or certificate that installs in the field already trust requires a transition release — see [Key Rotation](./key-rotation.md).

### `disableWebInstaller` (now defaults to `true`)

NSIS **web** installers download their full payload at install time from a manifest-supplied URL, which may not undergo signature verification. As of v27, `AppUpdater.disableWebInstaller` defaults to **`true`**, so a web-installer update is not loaded unless you opt in.

Unless the flag is `false`, a web-installer update is rejected with `ERR_UPDATER_WEB_INSTALLER_DISABLED` — at download time (including an update already cached by a previous launch), before an [install on next launch](#install-on-next-launch-windowslinux), and at install time.

Installs made by an `nsis-web` installer built with electron-builder v27+ carry a `resources/package-type` marker, and `NsisUpdater` then defaults the flag to `false` automatically. Set it yourself only for web-installer installs without that marker (installed by an installer built before v27, or with a custom script) or when switching an app from `nsis` to `nsis-web`:

```ts
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.disableWebInstaller = false // only if you intentionally ship a web installer
```

Set it before the app is `ready`, e.g. right after creating the updater: with `autoInstallEvent: "onNextLaunch"`, a pending web-installer update is checked against the flag when the app is ready, and rejected (its pending-install marker cleared) while the flag is `true`.

A web update cached by a previous launch, or pending an install on next launch, is used only if its web package still matches the freshly fetched manifest; otherwise it is discarded. The `nsis-web` installer itself verifies the package electron-updater passes to it and installs its own copy — see [Web Installer](../nsis.md#web-installer). Once the app runs the version of that update, `NsisUpdater` removes the package from its `pending` cache directory at startup (app `ready`); the package of an update that is not installed yet, or whose install failed, is kept for the install or a retry. Because the manifest vouches for the web package, don't set `updateManifest: false` for `nsis-web` apps: [signed update manifests](./signed-update-manifests.md) cover its path, SHA-512 and size.

### `allowUnverifiedLinuxPackages` (new)

`AppUpdater.allowUnverifiedLinuxPackages` (default **`true`**, preserving historical behavior) controls GPG signature enforcement when installing `.deb` / `.rpm` auto-updates. Because electron-builder does not sign Linux packages itself, the default is permissive; set it to **`false`** to enforce signature checks where the package manager supports them:

```ts
import { autoUpdater } from "electron-updater"
autoUpdater.allowUnverifiedLinuxPackages = false
```

## Staged Rollouts

Staged rollouts allow you to distribute the latest version of your app to a subset of users that you can increase over time, similar to rollouts on platforms like Google Play.

Staged rollouts are controlled by manually editing your `latest.yml` / `latest-mac.yml` (channel update info file).

```yml
version: 1.1.0
files:
  - url: TestApp Setup 1.1.0.exe
    sha512: Dj51I0q8aPQ3ioaz9LMqGYujAYRbDNblAQbodDRXAMxmY6hsHqEl3F6SvhfJj5oPhcqdX1ldsgEvfMNXGUXBIw==
    size: 62021782
stagingPercentage: 10
```

Update will be shipped to 10% of userbase.

If you want to pull a staged release because it hasn't gone well, you **must** increment the version number higher than your broken release.
Because some of your users will be on the broken 1.0.1, releasing a new 1.0.1 would result in them staying on a broken version.

## File Generated and Uploaded in Addition

`latest.yml` (or `latest-mac.yml` for macOS, or `latest-linux.yml` for Linux) will be generated and uploaded for all providers except `bintray` (because not required, `bintray` doesn't use `latest.yml`).

## Private GitHub Update Repo

You can use a private repository for updates with electron-updater by setting the `GH_TOKEN` environment variable (on user machine) and `private` option.
If `GH_TOKEN` is set, electron-updater will use the GitHub API for updates allowing private repositories to work.


:::warning
Private GitHub provider only for [very special](https://github.com/electron-userland/electron-builder/issues/1393#issuecomment-288191885) cases — not intended and not suitable for all users.
:::

:::note
The GitHub API currently has a rate limit of 5000 requests per user per hour. An update check uses up to 3 requests per check.
:::

## Events

The `autoUpdater` object emits the following events:

#### Event: `error`

* `error` Error

Emitted when there is an error while updating.

#### Event: `checking-for-update`

Emitted when checking if an update has started.

#### Event: `update-available`

* `info` [UpdateInfo](#updateinfo) (for generic and github providers)
Emitted when there is an available update. The update is downloaded automatically if `autoDownload` is `true`.

#### Event: `update-not-available`

Emitted when there is no available update.

* `info` [UpdateInfo](#updateinfo) (for generic and github providers)
#### Event: `download-progress`
* `progress` ProgressInfo
  * `bytesPerSecond`
  * `percent`
  * `total`
  * `transferred`

Emitted on progress.

#### Event: `update-downloaded`

* `info` [UpdateInfo](#updateinfo) — for generic and github providers.

#### Event: `update-cancelled`

* `info` [UpdateInfo](#updateinfo)

Emitted when an update is cancelled by the user or the download is otherwise aborted.

#### Event: `appimage-filename-updated`

* `path` string

Emitted after a successful AppImage update when the file is moved to its final location.
Only fires on Linux AppImage builds.

## UpdateInfo

  {!./electron-updater.Interface.UpdateInfo.md!}

---

### Features Build Lifecycle (`website/docs/features/build-lifecycle.md`)

# Build Lifecycle

Every `electron-builder build` run passes through a fixed sequence of phases: dependency resolution, app staging, code signing, distributable assembly, and artifact publication. Understanding this sequence tells you **which hook to use**, **when it fires**, and **what state the filesystem is in** when your code runs.

## Complete Build Flow

```
electron-builder build
│
├─ 1. Configuration & Validation
│   └─ Load config, merge platform defaults, resolve output paths
│
├─ 2. For each Platform × Architecture
│   │
│   ├─ 2a. Install / Rebuild Native Deps
│   │   └─ 🪝 beforeBuild
│   │
│   ├─ 2b. Pre-Pack
│   │   └─ 🪝 beforePack
│   │
│   ├─ 2c. Extract Electron Binary → staging dir
│   │   └─ 🪝 afterExtract
│   │
│   ├─ 2d. Copy App Files
│   │   ├─ Filter via files / file patterns
│   │   ├─ Pack node_modules (honor onNodeModuleFile)
│   │   ├─ Create ASAR archive (if enabled)
│   │   ├─ Copy extraResources
│   │   └─ Copy extraFiles
│   │
│   ├─ 2e. Post-Pack (before signing)
│   │   └─ 🪝 afterPack
│   │
│   ├─ 2f. Apply Electron Fuses (if configured)
│   │
│   ├─ 2g. Code Sign the .app / .exe
│   │   └─ 🪝 afterSign  (only fires if signing actually ran)
│   │
│   └─ 2h. Build Distributables — For each Target
│       ├─ 🪝 artifactBuildStarted
│       ├─ [Target-specific build — see per-target section below]
│       └─ 🪝 artifactBuildCompleted
│           └─ Publish Manager schedules artifact upload
│
├─ 3. All Builds Complete
│   └─ 🪝 afterAllArtifactBuild
│
└─ 4. Publish
    └─ Upload all queued artifacts to configured providers
```

---

## Phase-by-Phase Reference

### Phase 1 — Configuration & Validation

electron-builder reads and merges configuration from (in priority order): CLI flags, `electron-builder.config.*`, `package.json#build`. Platform-specific options (`mac`, `win`, `linux`) are overlaid on top of the base config. Output directories are resolved and created.

No hooks fire here.

---

### Phase 2a — Install / Rebuild Native Dependencies

Before staging the app, electron-builder optionally rebuilds native Node.js add-ons (`node-gyp rebuild`) for the target platform and architecture. This happens once per platform/arch combination.

#### 🪝 `beforeBuild`

| Property | Value |
|---|---|
| **Fires** | Before native dependency install/rebuild |
| **Return `false`** | Skips native dependency installation entirely |
| **Context type** | `BeforeBuildContext` |

```js
beforeBuild: async ({ appDir, electronVersion, platform, arch }) => {
  console.log(`Building native deps for ${platform.name} ${arch}`)
  // return false to skip; useful when node_modules are managed externally
}
```

See also: [Loading App Dependencies Manually](/docs/tutorials/loading-app-dependencies-manually)

---

### Phase 2b — Pre-Pack

Fires before any file copying begins. The staging directory (`appOutDir`) has been created but is empty.

#### 🪝 `beforePack`

| Property | Value |
|---|---|
| **Fires** | Before files are copied into the app bundle |
| **Staging dir state** | Empty |
| **Context type** | `BeforePackContext` |

```js
beforePack: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
  // Generate files that need to be in the build
}
```

---

### Phase 2c — Extract Electron Binary

The Electron binary for the target platform/arch is downloaded (or read from cache) and extracted into the staging directory. After this phase the directory contains the unmodified Electron shell — no app code yet.

#### 🪝 `afterExtract`

| Property | Value |
|---|---|
| **Fires** | After Electron is extracted; before any app files are placed |
| **Staging dir state** | Contains Electron binary only |
| **Context type** | `AfterExtractContext` (same shape as `PackContext`) |

```js
afterExtract: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
  // Modify the Electron binary itself, e.g. replace embedded resources
}
```

To provide a fully custom Electron build instead of downloading the standard release, use the `electronDist` hook:

#### 🪝 `electronDist`

| Property | Value |
|---|---|
| **Fires** | Before Electron extraction, to override the source |
| **Return** | Path to a custom Electron directory or a folder of zip files |

```js
electronDist: async (context) => {
  return "/path/to/custom-electron-dist"
}
```

---

### Phase 2d — Copy App Files

This is the main packaging phase. electron-builder:

1. Computes the file set based on the `files` glob patterns (defaulting to everything except `node_modules` and dev artifacts).
2. Filters `node_modules` — only production deps are included by default.
3. Creates an ASAR archive (enabled by default). In v27 the `asar: true` sentinel was removed — omit the key to keep defaults, pass an `asar: {}` object to configure it, or `asar: false` to disable.
4. Copies `extraResources` into the platform resources directory.
5. Copies `extraFiles` into the app root.

#### 🪝 `onNodeModuleFile`

| Property | Value |
|---|---|
| **Fires** | Once per file inside `node_modules` during packaging |
| **Return `true`** | Force-include the file |
| **Return `false`** | Force-exclude the file |
| **Return `undefined`/`void`** | Use default inclusion logic |

```js
onNodeModuleFile: (filePath) => {
  if (filePath.includes("__tests__") || filePath.endsWith(".test.js")) {
    return false  // exclude test files from all modules
  }
}
```

No other hooks fire during file copying. Use `afterPack` (next phase) to inspect or modify the result.

---

### Phase 2e — Post-Pack (before signing)

All app files are in the staging directory. The app bundle is complete but **unsigned**. This is the primary hook for modifying the packaged app.

#### 🪝 `afterPack`

| Property | Value |
|---|---|
| **Fires** | After all files are packaged; **before** code signing |
| **Staging dir state** | Complete app bundle, unsigned |
| **Context type** | `AfterPackContext` |

```js
afterPack: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
  // appOutDir points to the staged .app / win-unpacked / linux-unpacked dir
  const { join } = require("path")
  const { writeFileSync } = require("fs")
  writeFileSync(join(appOutDir, "VERSION"), packager.appInfo.version)
}
```

**Common uses:**
- Inject a `VERSION` or `BUILD_ID` file into the bundle
- Modify `Info.plist` (macOS) or `resources/app.asar` contents
- Strip debug symbols / strip binaries before signing

---

### Phase 2f — Apply Electron Fuses

If `electronFuses` is configured, fuse bits are flipped in the Electron binary at this point — after packing, before signing. Fuses are baked into the binary and cannot be changed post-sign.

See: [Adding Electron Fuses](/docs/tutorials/adding-electron-fuses)

---

### Phase 2g — Code Sign

The app bundle is signed using the platform-native toolchain:

| Platform | Tool | What is signed |
|---|---|---|
| macOS | `codesign` | `.app` bundle, all frameworks and dylibs |
| Windows | `signtool.exe` | EXE and DLL files |
| Linux | _(no signing in this phase)_ | — |

Notarization (macOS) runs here when `mac.notarize: true` is set.

#### 🪝 `afterSign`

| Property | Value |
|---|---|
| **Fires** | After signing completes; **only if signing actually ran** |
| **Not fired** | When signing is skipped (no certificate configured) |
| **Context type** | `AfterPackContext` |

```js
afterSign: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
  // App is signed. Distributables have not been built yet.
}
```

:::tip[Built-in notarization]
Use `mac.notarize: true` for standard notarization — electron-builder handles it automatically. Only reach for `afterSign` when you need a custom notarization flow. See [Notarization](/docs/features/code-signing/notarization).
:::

For a fully custom signing implementation (replacing the built-in signer):

```yaml
# electron-builder.config.yml
mac:
  sign: ./scripts/custom-sign.js
win:
  sign:
    type: signtool
    sign: ./scripts/custom-sign.js
    publisherName: "CN=My Company, O=My Company, C=US"
```

On Windows, electron-builder cannot read the certificate a custom hook signs with, so set `win.sign.publisherName` to that certificate's subject — it is written to `app-update.yml` for update signature verification, and a build that writes `app-update.yml` fails without it. Copy it from a binary the hook already signed, as shown in [How do you delegate code signing?](../win.md#how-do-you-delegate-code-signing); every component you list must match exactly.

---

### Phase 2h — Build Distributables (per target)

For each requested output format (NSIS, DMG, AppImage, etc.), electron-builder assembles the final distributable from the staged app. Target builds run after signing, so the inputs are always signed binaries.

Two hooks bracket every target build:

#### 🪝 `artifactBuildStarted`

| Property | Value |
|---|---|
| **Fires** | Immediately before a single artifact starts building |
| **Context type** | `ArtifactBuildStarted` |

```js
artifactBuildStarted: async ({ targetPresentableName, file, safeArtifactName, packager, arch }) => {
  console.log(`Starting: ${targetPresentableName}`)
}
```

#### 🪝 `artifactBuildCompleted`

| Property | Value |
|---|---|
| **Fires** | Immediately after a single artifact finishes building |
| **Context type** | `ArtifactCreated` |

```js
artifactBuildCompleted: async ({ file, safeArtifactName, target, packager, arch, sha512 }) => {
  console.log(`Done: ${file}  SHA512: ${sha512}`)
}
```

After `artifactBuildCompleted` fires, the Publish Manager queues the artifact for upload and schedules update-metadata generation (`latest.yml`, `latest-mac.yml`, etc.).

#### Target-specific hooks

Some targets expose additional hooks that fire inside their own build sequence:

| Hook | Target | Fires when |
|---|---|---|
| `msiProjectCreated` | MSI (WiX) | After WiX `.wxs` file is written to disk, before `candle.exe` / `light.exe` |
| `appxManifestCreated` | AppX / MSIX | After `AppxManifest.xml` is written to disk, before `makeappx.exe` |

```js
// Edit the WiX XML before MSI compilation
msiProjectCreated: async (wixProjectPath) => {
  const fs = require("fs")
  const wxsPath = require("path").join(wixProjectPath, "installer.wxs")
  let xml = fs.readFileSync(wxsPath, "utf8")
  xml = xml.replace('Manufacturer="PLACEHOLDER"', 'Manufacturer="Acme Corp"')
  fs.writeFileSync(wxsPath, xml)
}
```

```js
// Edit the AppX manifest before packaging
appxManifestCreated: async (manifestPath) => {
  const fs = require("fs")
  let manifest = fs.readFileSync(manifestPath, "utf8")
  manifest = manifest.replace(/<DisplayName>.*?<\/DisplayName>/, "<DisplayName>My App</DisplayName>")
  fs.writeFileSync(manifestPath, manifest)
}
```

---

### Phase 3 — All Builds Complete

Every platform, architecture, and target has finished. All artifacts are on disk.

#### 🪝 `afterAllArtifactBuild`

| Property | Value |
|---|---|
| **Fires** | After **every** platform/arch/target finishes; before publishing |
| **Context type** | `BuildResult` |
| **Return** | `string[]` — additional file paths to include in publish |

```js
afterAllArtifactBuild: async ({ outDir, artifactPaths, platformToTargets, configuration }) => {
  // Upload debug symbols
  const dsymPaths = artifactPaths.filter(p => p.endsWith(".dSYM"))
  for (const p of dsymPaths) {
    await uploadToSentry(p)
  }

  // Return extra files to include in the publish step
  const changelog = `${outDir}/CHANGELOG.md`
  writeChangelog(changelog)
  return [changelog]
}
```

---

### Phase 4 — Publish

The Publish Manager uploads all queued artifacts (and any extra paths returned from `afterAllArtifactBuild`) to the configured providers: GitHub Releases, S3, DigitalOcean Spaces, Generic server, GitLab, Keygen, Snap Store, etc.

Update metadata files (`latest.yml`, `latest-mac.yml`, `latest-linux.yml`) are written to the output directory and included in the publish.

See [Publish Configuration](/docs/publish) for provider setup.

---

## Hook Summary Table

| Hook | Phase | What the filesystem looks like | Common use |
|---|---|---|---|
| `beforeBuild` | 2a | Source tree only | Skip or customize native rebuild |
| `beforePack` | 2b | Empty staging dir | Inject generated source files |
| `afterExtract` | 2c | Staging: Electron binary only | Modify the Electron binary |
| `electronDist` | 2c | _(before extraction)_ | Supply a custom Electron build |
| `onNodeModuleFile` | 2d | _(during file copy)_ | Filter node_modules inclusions |
| `afterPack` | 2e | Staging: full app bundle, unsigned | Modify bundle before signing |
| `afterSign` | 2g | Staging: full app bundle, signed | Custom post-sign steps |
| `artifactBuildStarted` | 2h | Target build starting | Logging, timing |
| `msiProjectCreated` | 2h (MSI) | WiX `.wxs` file on disk | Edit WiX XML |
| `appxManifestCreated` | 2h (AppX) | `AppxManifest.xml` on disk | Edit AppX manifest |
| `artifactBuildCompleted` | 2h | Artifact file on disk | Checksums, per-artifact upload |
| `afterAllArtifactBuild` | 3 | All artifacts on disk | Symbol upload, extra publish paths |

---

## Target Build Sequences

Each target runs inside Phase 2h. Here is what happens inside each one:

### Windows

| Target | Sequence |
|---|---|
| **NSIS** | Generate script → compile with `makensis` → sign installer → emit artifact |
| **MSI (WiX)** | Write `.wxs` → `msiProjectCreated` hook → `candle.exe` + `light.exe` → sign → emit |
| **AppX / MSIX** | Write manifest → `appxManifestCreated` hook → `makeappx.exe` + `makepri.exe` → sign → emit |
| **Portable** | Copy staged app into a self-extracting archive → emit |

### macOS

| Target | Sequence |
|---|---|
| **DMG** | Create `.dmg` via `dmgbuild` → emit |
| **ZIP** | Archive `.app` bundle → emit |
| **PKG** | Build `.pkg` via `pkgbuild` + `productbuild` → sign → emit |
| **MAS** | Re-sign with MAS provisioning profile → build `pkg` → emit |

### Linux

| Target | Sequence |
|---|---|
| **AppImage** | Create squashfs image → embed ELF header → emit |
| **DEB / RPM / Pacman** | Package via `fpm` → emit |
| **Snap** | Write `snapcraft.yaml` → run `snapcraft` → emit |
| **Flatpak** | Write manifest → run `flatpak-builder` → emit |

---

## Concurrency

By default electron-builder packs one platform/architecture combination at a time (`concurrency.jobs: 1`). Raising this value runs multiple platform × arch pack operations in parallel using an async pool.

```yaml
# electron-builder.config.yml
concurrency:
  jobs: 2   # pack up to 2 platform/arch combos simultaneously
```

### What concurrency affects

| Scope | Behavior |
|---|---|
| **Platform × arch loop** | Up to `jobs` pack operations run in parallel |
| **Hooks within one pack** | Always serial — each hook is fully awaited before the build advances |
| **`afterAllArtifactBuild`** | Fires once, after **all** concurrent packs complete |

So with `jobs: 2` and a build targeting `mac/x64`, `mac/arm64`, and `win/x64`:

- `mac/x64` and `mac/arm64` start concurrently (2 slots)
- `win/x64` starts as soon as one of the mac slots finishes
- `afterAllArtifactBuild` fires after the last one completes

### Concurrency limit

Setting `jobs` above `8` (the internal `MAX_FILE_REQUESTS` constant) logs a warning because each concurrent pack opens many file handles simultaneously. Exceeding the OS file descriptor limit causes `EMFILE` errors. The value is floored to an integer; any value less than `1` is reset to `1`.

```yaml
concurrency:
  jobs: 4   # safe upper bound on most systems
```

### Hook author implications

Because hooks for different platform/arch combinations may run at the same time, avoid writing to shared paths without coordination. Each hook receives its own `appOutDir`, `outDir`, and `arch` so there is no conflict when writing into the staging directory — the risk is only if your hook writes to a fixed global path (a log file, a shared temp directory, etc.).

See [Multi Platform Build](/docs/features/multi-platform-build) and [Build Architectures](/docs/architecture).

---

## Further Reading

- [Hooks Reference](/docs/features/hooks) — full API for every hook, with examples
- [File Patterns](/docs/file-patterns) — how `files`, `extraResources`, and `extraFiles` work
- [Code Signing](/docs/features/code-signing) — signing setup for macOS and Windows
- [Publish Configuration](/docs/publish) — provider setup and update metadata
- [Programmatic Usage](/docs/programmatic-usage) — drive the build from Node.js

---

### Features Electron Forge (`website/docs/features/electron-forge.md`)

---
title: "Electron Forge Integration"
---

electron-builder ships four [Electron Forge](https://www.electronforge.io/) maker packages that let you produce electron-builder targets from a Forge project without migrating your entire build pipeline.

:::tip[Consider using electron-builder directly]
[Publishing](../publish.md), [Auto Update](./auto-update.md), and [Code Signing](./code-signing/code-signing.md) are only available when using electron-builder as your primary build tool. If you need any of those features, migrate fully to electron-builder rather than using these makers.
:::

## How it works

Each maker is a thin wrapper around `buildForge()` from `app-builder-lib`. When Electron Forge calls `make`, the maker delegates to electron-builder's full packaging pipeline for that target, then returns the paths to the produced artifacts. All electron-builder configuration options for the target are available via the maker's `config` key.

## Available makers

| Package | Target | Platform |
|---|---|---|
| `electron-forge-maker-nsis` | NSIS installer (`.exe`) | Windows |
| `electron-forge-maker-nsis-web` | Web installer (`.exe`, downloads payload at install time) | Windows |
| `electron-forge-maker-appimage` | AppImage (`.AppImage`) | Linux |
| `electron-forge-maker-snap` | Snap package (`.snap`) | Linux |

---

## NSIS installer

```bash
npm install --save-dev electron-forge-maker-nsis
```

```js title="forge.config.js"
const { MakerNsis } = require("electron-forge-maker-nsis")

module.exports = {
  makers: [
    new MakerNsis({
      // All NsisOptions are accepted here — see /docs/nsis for the full reference
      oneClick: false,
      perMachine: true,
    }),
  ],
}
```

Full configuration reference: [NSIS](../nsis.md)

---

## NSIS web installer

```bash
npm install --save-dev electron-forge-maker-nsis-web
```

```js title="forge.config.js"
const { MakerNsisWeb } = require("electron-forge-maker-nsis-web")

module.exports = {
  makers: [
    new MakerNsisWeb({
      // NsisWebOptions — extends NsisOptions
    }),
  ],
}
```

The web installer produces a small stub executable that detects the user's architecture and downloads the appropriate payload from your publish provider at install time. Full configuration reference: [NSIS](../nsis.md)

---

## AppImage

```bash
npm install --save-dev electron-forge-maker-appimage
```

```js title="forge.config.js"
const { MakerAppImage } = require("electron-forge-maker-appimage")

module.exports = {
  makers: [
    new MakerAppImage({
      // AppImageOptions — see /docs/appimage for the full reference
      license: "LICENSE",
    }),
  ],
}
```

Full configuration reference: [AppImage](../appimage.md)

---

## Snap

```bash
npm install --save-dev electron-forge-maker-snap
```

```js title="forge.config.js"
const { MakerSnap } = require("electron-forge-maker-snap")

module.exports = {
  makers: [
    new MakerSnap({
      // SnapOptions — see /docs/snap for the full reference
      confinement: "strict",
      grade: "stable",
    }),
  ],
}
```

Full configuration reference: [Snap](../snap.md)

---

## Limitations

These makers delegate to electron-builder's packaging only. The following electron-builder features are **not available** through Forge makers:

- **Publishing** — use `electron-builder --publish` directly
- **Auto Update** — requires electron-builder's full publish pipeline; see [Auto Update](./auto-update.md)
- **Code Signing** — requires electron-builder's signing configuration; see [Code Signing](./code-signing/code-signing.md)

For projects that need any of these, the recommended path is to use electron-builder as the primary build tool instead of Electron Forge.

---

### Features Github Actions (`website/docs/features/github-actions.md`)

# GitHub Actions CI/CD

This guide shows how to build, sign, and publish Electron apps using GitHub Actions for all three platforms (macOS, Windows, Linux).

## Basic Concepts

- **Secrets** — store certificates, passwords, and API tokens in [GitHub repository secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) (Settings → Secrets and variables → Actions)
- **Matrix builds** — build for multiple platforms in parallel using a matrix strategy
- **`GH_TOKEN`** — the GitHub token electron-builder uses to upload release assets. Use `GITHUB_TOKEN` (provided automatically) or a PAT with `repo` scope

## Minimal Multi-Platform Workflow

This workflow builds for macOS, Windows, and Linux on every push to `main` and on version tags:

```yaml
name: Build and Release

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npx electron-builder --publish never
        env:
          # macOS signing
          CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
          CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
          # Windows signing
          WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist-${{ matrix.os }}
          path: dist/
```

## Publish on Tag Push

To publish GitHub releases automatically when you push a version tag (`v1.2.3`):

```yaml
name: Release

on:
  push:
    tags: ['v*']

jobs:
  release:
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Publish
        run: npx electron-builder --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # macOS signing + notarization
          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 }}
          # Windows signing
          WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
```

The `--publish always` flag uploads artifacts to the GitHub release for the pushed tag. If no release exists, electron-builder creates a draft release.

## Draft Release Workflow

A common pattern is to publish a **draft** release on every tagged push, then manually promote it to a full release after reviewing:

```yaml
# In electron-builder.yml
publish:
  provider: github
  releaseType: draft
```

Then in GitHub: go to Releases → find the draft → click "Edit" → "Publish release".

## macOS Code Signing Setup

### Preparing the Certificate

```bash
# Export your Developer ID Application certificate from Keychain Access
# File → Export Items → save as .p12 with a password

# Encode to base64
base64 -i certificate.p12 | pbcopy   # copies to clipboard
```

Add to GitHub Secrets:
- `MAC_CSC_LINK` — the base64-encoded `.p12` content
- `MAC_CSC_KEY_PASSWORD` — the certificate password

### macOS Signing

Pass `CSC_LINK` and `CSC_KEY_PASSWORD` directly — electron-builder creates and manages a temporary keychain automatically:

```yaml
  build-mac:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build and notarize
        run: npx electron-builder --mac --publish always
        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 }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### macOS Secrets Reference

| Secret | How to Get |
|---|---|
| `MAC_CSC_LINK` | `base64 -i certificate.p12` |
| `MAC_CSC_KEY_PASSWORD` | The password you set when exporting the `.p12` |
| `APPLE_ID` | Your Apple ID email (developer account) |
| `APPLE_APP_SPECIFIC_PASSWORD` | Create at [appleid.apple.com](https://appleid.apple.com) → App-Specific Passwords |
| `APPLE_TEAM_ID` | 10-character team ID from [developer.apple.com/account](https://developer.apple.com/account) |

## Windows Code Signing Setup

```bash
# Encode your Windows certificate (.pfx) to base64
# PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("certificate.pfx")) | Out-File cert_encoded.txt

# bash (Linux/macOS):
base64 certificate.pfx > cert_encoded.txt
```

Add to GitHub Secrets:
- `WIN_CSC_LINK` — base64-encoded `.pfx` content
- `WIN_CSC_KEY_PASSWORD` — certificate password

### Using Azure Trusted Signing

```yaml
      - name: Build and sign (Azure)
        run: npx electron-builder --win --publish always
        env:
          AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Configure `win.sign` with `type: "azure"` in your electron-builder config (this replaces the v26 `win.azureSignOptions` key). `endpoint`, `codeSigningAccountName`, `certificateProfileName`, and `publisherName` are required. See [Windows Code Signing](./code-signing/code-signing-win.md#azure-trusted-signing-type-azure-beta).

## Caching for Faster Builds

Cache npm packages and Electron binaries to avoid redundant downloads:

```yaml
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm    # caches ~/.npm

      - name: Cache Electron
        uses: actions/cache@v4
        with:
          path: |
            ~/.cache/electron
            ~/.cache/electron-builder
          key: ${{ runner.os }}-electron-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-electron-
```

## Linux Snapcraft Publishing

To publish to the Snap Store, add a `SNAPCRAFT_STORE_CREDENTIALS` secret (obtained by running `snapcraft export-login -`):

```yaml
      - name: Build and publish Snap
        run: npx electron-builder --linux snap --publish always
        env:
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Complete Production Workflow

A production-grade workflow with separate jobs per platform, caching, and GitHub release publishing:

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Troubleshooting

**"Resource not accessible by integration"**
: The workflow doesn't have permission to create releases. Add `permissions: contents: write` to the job or workflow. Also ensure `GH_TOKEN` is set.

**macOS: "No identity found"**
: `CSC_LINK` or `CSC_KEY_PASSWORD` is wrong or missing. Verify the base64-encoded certificate decodes to a valid `.p12` and that the password is correct. Run `echo "$CSC_LINK" | base64 --decode | openssl pkcs12 -info -passin pass:"$CSC_KEY_PASSWORD"` locally to verify.

**Windows: certificate base64 too long**
: Windows CI environments may truncate environment variables over 8192 characters. Re-export the `.pfx` without the full certificate chain included. See [Code Signing](./code-signing/code-signing.md#encoding-a-certificate-for-ci).

**Build fails with "Cannot find module"**
: Native modules need to be rebuilt for the target Electron version. Ensure `npm ci` (not `npm install`) is used. electron-builder handles native module rebuilding automatically.

## Related Pages

- [Code Signing](./code-signing/code-signing.md) — environment variables, certificate types
- [macOS Notarization](./code-signing/notarization.md) — notarization setup and troubleshooting
- [Auto Update](./auto-update.md) — configuring electron-updater with GitHub releases
- [Multi Platform Build](./multi-platform-build.md) — building for other platforms using Docker

---

### Features Hooks (`website/docs/features/hooks.md`)

## Build Hooks

Build hooks let you run custom code at specific points in the electron-builder build lifecycle. Use them to modify the app bundle, perform additional signing, upload symbols, or integrate with external services.

## Build Lifecycle

Hooks fire in this order during every build. Pick the hook that runs at the right stage for your task — modifying app files before signing, triggering external tools after all artifacts are ready, etc.

| Hook | When it fires | Common use |
|---|---|---|
| `beforeBuild` | Before native deps are installed/rebuilt | Skip or customize native module rebuild |
| `beforePack` | Before files are copied into the app bundle | Inject generated files, modify source before pack |
| `afterExtract` | After Electron binary is extracted | Modify the Electron binary or its resources |
| `afterPack` | After files are packaged, **before signing** | Modify app bundle structure, add unpacked files |
| `afterSign` | After signing, **before distributable is created** | Custom post-sign steps, extra notarization workflows |
| `artifactBuildStarted` | When each installer/image starts building | Per-artifact logging or tracking |
| `artifactBuildCompleted` | When each installer/image finishes | Checksum capture, per-artifact upload |
| `afterAllArtifactBuild` | After **all** artifacts are done | Upload debug symbols, generate changelogs, return extra publish paths |

Hook execution is serial within each platform/arch combination. Async hooks are awaited before the build proceeds.

**Target-specific hooks** (only fire for their respective target):
- `electronDist` — supply a custom Electron binary
- `msiProjectCreated` — edit WiX XML before MSI compilation
- `appxManifestCreated` — edit AppX manifest XML before packaging
- `onNodeModuleFile` — filter which `node_modules` files are included in the bundle

## Defining Hooks

Hooks can be defined in three ways:

### 1. Inline Function (JS/TS config only)

```js
// electron-builder.config.js
module.exports = {
  afterPack: async (context) => {
    console.log("afterPack:", context.appOutDir)
  }
}
```

### 2. Path to File

Use a file path string when using JSON/YAML config, or when you want to keep hooks in separate files:

```yaml
# electron-builder.yml
afterPack: ./scripts/afterPack.js
```

```js
// scripts/afterPack.js
exports.default = async function(context) {
  console.log("afterPack:", context.appOutDir)
}
```

The function must be the **default export** of the module.

### 3. Module ID

Reference a node module:

```yaml
afterPack: my-electron-builder-plugin
```

## Hook Reference

### `beforeBuild`

Runs before native dependencies are installed or rebuilt (before `npm rebuild`). Resolving to `false` skips dependency installation entirely.

```js
beforeBuild: async (context) => {
  // context: BeforeBuildContext
  // { appDir, electronVersion, platform, arch }
  console.log("Building for:", context.platform.name, context.arch)
  // return false to skip dependency install
}
```

### `beforePack`

Runs before files are copied into the app bundle.

```js
beforePack: async (context) => {
  // context: BeforePackContext
  // { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}
```

### `afterExtract`

Runs after the Electron binary has been extracted into the output directory, before app files are placed. Useful for manipulating the Electron binary or its resources.

```js
afterExtract: async (context) => {
  // context: AfterExtractContext (same shape as PackContext)
  // { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}
```

### `afterPack`

Runs after the app is fully packaged but before code signing. This is the ideal hook for:
- Modifying the app bundle structure
- Adding files that should not be ASAR-archived
- Running post-processing on the packaged app

```js
afterPack: async (context) => {
  // context: AfterPackContext
  // { outDir, appOutDir, packager, electronPlatformName, arch, targets }
  const path = require("path")
  const fs = require("fs")

  // Example: add a VERSION file to the app bundle
  const versionFile = path.join(context.appOutDir, "VERSION")
  fs.writeFileSync(versionFile, context.packager.appInfo.version)
}
```

### `afterSign`

Runs after the app is signed but before the distributable (DMG, NSIS installer, etc.) is created. Commonly used for macOS notarization in custom workflows.

```js
afterSign: async (context) => {
  // context: AfterPackContext
  // { outDir, appOutDir, packager, electronPlatformName, arch, targets }
}
```

:::tip[Notarization]
electron-builder has built-in notarization support via `mac.notarize: true`. Only use `afterSign` for notarization if you need a custom notarization workflow. See [Notarization](/docs/features/code-signing/notarization).
:::

### `artifactBuildStarted`

Runs when an individual artifact (e.g., a DMG, an NSIS installer) starts being built.

```js
artifactBuildStarted: async (context) => {
  // context: ArtifactBuildStarted
  // { targetPresentableName, file, safeArtifactName, packager, arch }
  console.log("Building artifact:", context.targetPresentableName)
}
```

### `artifactBuildCompleted`

Runs when an individual artifact finishes building.

```js
artifactBuildCompleted: async (context) => {
  // context: ArtifactCreated
  // { file, safeArtifactName, target, packager, publishConfig, isWriteUpdateInfo, updateInfo, sha2, sha512, artifactName, arch }
  console.log("Built:", context.file, "SHA512:", context.sha512)
}
```

### `afterAllArtifactBuild`

Runs after all artifacts have been built. Can return an array of additional file paths to publish.

```js
afterAllArtifactBuild: async (buildResult) => {
  // buildResult: BuildResult
  // { outDir, artifactPaths, platformToTargets, configuration }

  // Example: upload debug symbols to Sentry
  for (const artifactPath of buildResult.artifactPaths) {
    if (artifactPath.endsWith('.dSYM') || artifactPath.endsWith('.pdb')) {
      await uploadSymbols(artifactPath)
    }
  }

  // Return additional files to publish
  return ["/path/to/additional-artifact.zip"]
}
```

### `electronDist`

Provide a custom Electron binary instead of the standard downloaded one. The hook receives `PrepareApplicationStageDirectoryOptions` and should return the path to a custom Electron build or a directory of Electron zip files.

```js
electronDist: async (context) => {
  // Return path to custom Electron directory
  return "/path/to/custom/electron-dist"
}
```

### `msiProjectCreated`

Runs after the WiX project has been created on disk but before it is compiled into an MSI. Receives the path to the WiX project directory.

```js
msiProjectCreated: async (wixProjectPath) => {
  // Modify WiX XML files at wixProjectPath
}
```

### `appxManifestCreated`

Runs after the AppX manifest has been created but before the package is assembled. Receives the path to the manifest file.

```js
appxManifestCreated: async (manifestPath) => {
  // Modify the AppX manifest XML
  const fs = require("fs")
  let manifest = fs.readFileSync(manifestPath, "utf8")
  // ... modify manifest ...
  fs.writeFileSync(manifestPath, manifest)
}
```

### `onNodeModuleFile`

Called for each file in `node_modules` during packaging. Return `true` to force-include, `false` to force-exclude, or `undefined`/`void` to use default behavior.

```js
onNodeModuleFile: async (filePath) => {
  // Exclude test files from all node_modules
  if (filePath.includes("__tests__") || filePath.endsWith(".test.js")) {
    return false
  }
}
```

## Common Hook Patterns

### Modifying Info.plist After Pack

```js
const path = require("path")
const plist = require("plist")
const fs = require("fs")

afterPack: async (context) => {
  if (context.electronPlatformName !== "darwin") return

  const plistPath = path.join(
    context.appOutDir,
    `${context.packager.appInfo.productFilename}.app`,
    "Contents",
    "Info.plist"
  )
  const plistData = plist.parse(fs.readFileSync(plistPath, "utf8"))
  plistData.LSUIElement = true   // Hide from dock
  fs.writeFileSync(plistPath, plist.build(plistData))
}
```

### Custom Code Signing

```js
mac:
  sign: "./scripts/sign.js"
```

```js
// scripts/sign.js
exports.default = async function(configuration) {
  // configuration: CustomMacSign
  // { path, packager, entitlements, ... }
  const { execSync } = require("child_process")
  execSync(`codesign --deep --sign "${configuration.identity}" "${configuration.path}"`)
}
```

### Uploading Symbols to Sentry

```js
afterAllArtifactBuild: async (buildResult) => {
  const { execSync } = require("child_process")
  const dsymPaths = buildResult.artifactPaths.filter(p => p.endsWith(".dSYM"))
  for (const dsym of dsymPaths) {
    execSync(`sentry-cli upload-dif ${dsym}`)
  }
}
```

### Generating a Changelog File

```js
afterAllArtifactBuild: async (buildResult) => {
  const fs = require("fs")
  const changelogPath = `${buildResult.outDir}/CHANGELOG.txt`
  fs.writeFileSync(changelogPath, generateChangelog())
  return [changelogPath]   // include in publish
}
```

## Async and Error Handling

All hooks support async functions. If a hook throws an error, the build fails:

```js
afterPack: async (context) => {
  try {
    await doSomething()
  } catch (err) {
    console.error("afterPack failed:", err)
    throw err   // rethrow to fail the build
  }
}
```

## Debugging Hooks

Add `console.log` or `console.error` inside your hooks — output appears in the electron-builder terminal output.

Enable verbose electron-builder logging to see the full build lifecycle:

```bash
DEBUG=electron-builder electron-builder build
```

## Interface Reference

{!./app-builder-lib.Interface.Hooks.md!}

---

### Features Icons And Images (`website/docs/features/icons-and-images.md`)

---
title: "Icons & Images"
---

electron-builder handles icon and image assets differently per platform. Give it a single high-quality source — a 1024×1024 PNG or an SVG — and it auto-converts to every format the target platform needs. No manual export pipelines, no platform-specific tooling required.

All icon and image files are **optional** — if omitted, the default Electron icon is used. Files are resolved from the [`buildResources`](../contents.md#extraresources) directory, which defaults to `build/` in your project root.

:::tip[Icon creation tools]
[Icon Composer](https://developer.apple.com/icon-composer/), [AppIcon Generator](http://www.tweaknow.com/appicongenerator.php), [MakeAppIcon](https://makeappicon.com/)
:::

## TL;DR — Recommended build/ layout

```
build/
├── icon.svg            ← master icon (SVG — scales perfectly to all sizes)
├── icon.png            ← or use PNG (1024×1024 recommended)
├── icon.icns           ← optional: pre-built macOS icon (skips conversion)
├── icon.ico            ← optional: pre-built Windows icon (skips conversion)
├── background.png      ← macOS DMG background (540×380 px)
├── background@2x.png   ← Retina DMG background (1080×760 px)
└── icons/              ← optional: manual Linux PNG set
    ├── 16x16.png
    ├── 32x32.png
    └── 512x512.png
```

A single `icon.svg` or `icon.png` in your `build/` directory is enough to cover all three platforms — electron-builder discovers and converts it automatically.

---

## Application Icon

The application icon appears in the Dock, taskbar, Start menu, and application menus. electron-builder searches for your icon in this order:

1. The explicit `mac.icon`, `win.icon`, or `linux.icon` config path
2. `icon.<format>` (e.g. `icon.icns` for macOS, `icon.ico` for Windows)
3. `icon.png`
4. `icon.svg` — auto-discovered, no config required
5. `icon.icns` (cross-platform source for ICNS-based conversion)
6. A directory named `icons/` or `icon/`

### Format & size requirements

| Platform | Accepted input formats | Minimum | Recommended |
|---|---|---|---|
| macOS | `.svg`, `.png`, `.icns`, `.icon` | 512×512 | 1024×1024 (or SVG) |
| Windows | `.svg`, `.png`, `.ico` | 256×256 | 512×512 (or SVG) |
| Linux | `.svg`, `.png`, directory of PNGs | 256×256 | SVG or 1024×1024 PNG |

:::tip[SVG is best]
SVG icons are rasterized at 1024px before conversion, giving every generated size a clean starting point. Use SVG when your icon is available in vector form.
:::

---

## SVG Support

SVG is supported as an input format for all three platforms. Place `icon.svg` in your `build/` directory and electron-builder finds it automatically — no `mac.icon` or `win.icon` config required.

- **macOS / Windows**: The SVG is rasterized to 1024×1024 before ICNS or ICO conversion.
- **Linux (`set` format)**: The SVG is passed through directly to the freedesktop `scalable/` icon hierarchy. Desktop environments render it at any scale — ideal for HiDPI and fractional-scaling displays.

Explicit config also works:

```yaml
mac:
  icon: build/icon.svg

win:
  icon: build/icon.svg
```

---

## macOS Icon Sizes (ICNS)

When converting to `.icns`, electron-builder generates all standard Apple icon sizes:

| Size | Usage |
|---|---|
| 16×16 | Finder list view |
| 32×32 | Finder column view, Retina 16px |
| 64×64 | Retina 32px |
| 128×128 | Finder preview |
| 256×256 | Standard Retina |
| 512×512 | Large Retina |
| 1024×1024 | Full Retina (macOS 10.7+) |

All sizes are generated from the input regardless of the source dimensions — SVG sources and large PNGs produce the sharpest results at every size.

:::note
If you provide a pre-built `.icns`, electron-builder uses it as-is — no re-encoding.
:::

### macOS `.icon` format (Asset Catalog)

If you set `mac.icon` to a `.icon` file (Apple Icon Composer asset), electron-builder compiles it into an `Assets.car` via `actool` and wires it via `CFBundleIconName`. This requires **Xcode 26+** and **macOS 15+** on the build machine.

For `.icns` files, the path is referenced via `CFBundleIconFile` and does not require Xcode.

:::note
If you only provide a `.icon` file and rely on the default DMG volume icon, consider setting `dmg.icon` explicitly to an `.icns` file — the DMG volume icon still uses `.icns`.
:::

---

## Linux Icon Set

electron-builder generates a full set of PNG icons for Linux, named `icon_NxN.png`, at these standard sizes:

| Size | Freedesktop target directory |
|---|---|
| 16×16 | `hicolor/16x16/apps/` |
| 24×24 | `hicolor/24x24/apps/` |
| 32×32 | `hicolor/32x32/apps/` |
| 48×48 | `hicolor/48x48/apps/` |
| 64×64 | `hicolor/64x64/apps/` |
| 128×128 | `hicolor/128x128/apps/` |
| 256×256 | `hicolor/256x256/apps/` |
| 512×512 | `hicolor/512x512/apps/` |

SVG sources are placed in `scalable/` instead and are not resized.

To provide a pre-built Linux set, create a directory and name each PNG with its pixel dimensions:

```yaml
linux:
  icon: build/icons/
```

```
build/icons/
├── 16x16.png
├── 32x32.png
├── 48x48.png
├── 128x128.png
├── 256x256.png
└── 512x512.png
```

Files must match the pattern `NxN.png` or `N.png` (e.g., `256x256.png` or `512.png`). Files with incidental digits in their names (e.g., `app2.png`) are ignored.

---

## macOS DMG Background

The DMG installer window background image. The window dimensions match the image dimensions — getting this right ensures your DMG looks polished.

| Asset | Dimensions | Location |
|---|---|---|
| Standard background | **540×380 px** | `build/background.png` or `build/background.tiff` |
| Retina background | **1080×760 px** | `build/background@2x.png` |
| Volume icon | `.icns` | `build/icon.icns` (or `dmg.icon`) |

```yaml
dmg:
  background: build/background.png
  backgroundColor: "#ffffff"   # fallback if no background image
  icon: build/dmg-icon.icns    # volume badge icon (optional)
  iconSize: 80
  iconTextSize: 12
```

Supply both `background.png` and `background@2x.png` for crisp rendering on Retina displays.

---

## Windows NSIS Installer Images

The NSIS installer has two visual modes:

- **One-click installer** — minimal UI, only uses the app icon
- **Assisted installer** — a wizard with a sidebar and header banner

### Assisted installer assets

| Asset | Dimensions | Format | Location |
|---|---|---|---|
| Installer header banner | **150×57 px** | `.bmp` | `build/installerHeader.bmp` |
| Installer sidebar | **164×314 px** | `.bmp` | `build/installerSidebar.bmp` |
| Uninstaller sidebar | **164×314 px** | `.bmp` | `build/uninstallerSidebar.bmp` |
| Installer icon | `.ico` | | `build/installerIcon.ico` |
| Uninstaller icon | `.ico` | | `build/uninstallerIcon.ico` |

```yaml
nsis:
  installerHeader: build/installerHeader.bmp
  installerSidebar: build/installerSidebar.bmp
  uninstallerSidebar: build/uninstallerSidebar.bmp
  installerIcon: build/installerIcon.ico
  uninstallerIcon: build/uninstallerIcon.ico
```

:::note
BMP files must be 24-bit RGB (no alpha). Use any image editor to export in this format.
:::

---

## Windows AppX (Microsoft Store) Tile Images

AppX packages require a set of tile images for the Windows Start screen and Store listing. Place them in `build/appx/`:

| Asset | Dimensions | Filename |
|---|---|---|
| Store logo | **50×50 px** | `StoreLogo.png` |
| Small tile | **44×44 px** | `Square44x44Logo.png` |
| Medium tile | **150×150 px** | `Square150x150Logo.png` |
| Wide tile | **310×150 px** | `Wide310x150Logo.png` |

All AppX images must be PNG with transparency support. Microsoft recommends providing 1x, 1.25x, 1.5x, 2x, and 4x scale variants using the naming convention `Square44x44Logo.scale-200.png` (for 2x), but electron-builder only requires the base filenames listed above.

See the [AppX configuration](../appx.md) page for the full set of AppX options, including custom manifest support.

---

## Icons Toolset

electron-builder bundles a portable icon conversion toolset (`icon-tool.js` + `resvg.wasm`) that handles all PNG/SVG → ICNS/ICO/set conversions without native dependencies. The toolset is downloaded automatically on first use and cached in the electron-builder cache directory.

**Override with a local bundle** (useful for offline builds or development). Supply a [custom toolset](../toolsets.md#custom-toolsets) via `toolsets.icons` (there is no `ELECTRON_BUILDER_ICONS_TOOLSET_PATH` environment variable):

```json5
{ "build": { "toolsets": { "icons": { "url": "file:///path/to/icons-bundle" } } } }
```

The directory must contain `icon-tool.js` and `resvg.wasm` (the same layout as the built-in bundle).

---

## Source Resolution Reference

When you specify `icon: build/icon` (no extension), electron-builder probes the following paths for each target format:

| Priority | Probe path | Notes |
|---|---|---|
| 1 | `build/icon.<format>` | e.g. `icon.icns` for macOS |
| 2 | `build/icon` | exact path |
| 3 | `build/icon.png` | |
| 4 | `build/icon.svg` | auto-discovered; converted on the fly |
| 5 | `build/icon.icns` | used as PNG source for non-ICNS targets |
| 6 | `build/icons/` or `build/icon/` | directory of pre-sized PNGs |
| 7 | `build/icon.ico` | used as source for ICO targets |

ICNS files can be used as a source for all platforms — electron-builder extracts the largest embedded PNG frame and converts from there.

---

### Features Key Rotation (`website/docs/features/key-rotation.md`)

---
title: "Key Rotation"
---

# Key Rotation

This page is a runbook for rotating the keys that an Electron app's release pipeline depends on: the **Ed25519 key that signs update manifests** and the **platform code-signing certificates** for Windows, macOS, and Linux. It is written for maintainers who already ship auto-updating apps with electron-builder and electron-updater.

## Why rotation is a transition problem

Every install in the field trusts exactly what it was built with. electron-updater does not fetch trust anchors from your update server, and there is no revocation list it consults. Concretely:

| Trust anchor | Where it is fixed | What checks it |
| --- | --- | --- |
| Update-manifest trust list (`updateManifestPublicKey`, one or more Ed25519 public keys) | `app-update.yml`, written at build time — present whenever a signing key or an explicit `updateManifest.publicKey` resolves (a publishing build fails without a key unless `updateManifest: false`) | electron-updater, on every platform, before any download ([Signed Update Manifests](./signed-update-manifests.md)) |
| Windows publisher name (`publisherName`) | `app-update.yml`, written at build time | electron-updater (NSIS target), against the Authenticode signature of the downloaded installer |
| macOS Team ID | The running app's code signature | Squirrel.Mac, when it installs the downloaded update |
| Linux repository GPG key | The user's package-manager keyring | `apt` / `dnf` / `zypper`, only when `allowUnverifiedLinuxPackages` is `false` |

Because the anchor travels *inside* the previous release, you cannot simply start signing with a new key: installs that trust only the old anchor would reject everything you publish from that point on. Rotation is therefore always a **transition window**: you first ship a release that installs the new anchor while still passing the old check, wait for it to reach your users, and only then switch.

### The bridge release

Throughout this page, a **bridge release** is an ordinary release with one property: it is *verified with the old key* and *carries the new trust anchor*. Users who install it will accept updates signed with the new key; users who never install it stay pinned to the old key. Where the anchor is a **list** (the manifest trust list, Windows `publisherName`), a bridge release simply lists both old and new, and the switch itself needs no flag day. The remaining sections describe what "carries the new anchor" means for each mechanism, and how long you may need to keep the old key alive.

:::tip[Plan the window before you need it]
Rotation is much calmer when it is routine. Decide in advance how you will measure adoption of a bridge release (download counts on your update server, telemetry, or simply time elapsed against your `stagingPercentage` schedule) and what you will do with installs that never update.
:::

## Rotating the update-manifest signing key (Ed25519)

electron-builder signs `latest*.yml` with one or more Ed25519 private keys and embeds the matching public keys into `app-update.yml`; electron-updater verifies the manifest against that list before downloading anything. See [Signed Update Manifests](./signed-update-manifests.md) for the feature itself. Four facts drive the rotation procedure:

1. **An install trusts a LIST of keys.** `updateManifestPublicKey` in `app-update.yml` is either a single public key or a list of them. A manifest is accepted when *any* listed key validates *any* signature it carries. Verification is fail-closed as soon as at least one key is configured — and since v27 configures one by default, treat fail-closed as the normal state. electron-updater never fetches replacement keys from the update server.
2. **A manifest may carry several signatures.** Every configured signing key signs each `latest*.yml`; the result is written as a `signatures` list (one `{ keyId, signature }` entry per key) plus the legacy single `signature` field holding the first key's signature. `keyId` is the hex SHA-256 of the public key's SPKI DER encoding (printed by `create-update-key`), so you can see at a glance which keys signed a release.
3. **The private keys are resolved from, in order:** `updateManifest.signingKey` → `updateManifest.signingKeyFile` → `ELECTRON_BUILDER_UPDATE_SIGN_KEY` → `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE`. The first source that is set wins, but that source may hold several keys: config values accept an array, a PEM value may contain several concatenated `-----BEGIN PRIVATE KEY-----` blocks, and `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE` accepts several paths joined with the OS path delimiter (`:` on Linux/macOS, `;` on Windows). Relative `signingKeyFile` paths resolve against the project directory; relative `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE` paths against the current working directory. Setting `updateManifest` to `false` disables signing outright and short-circuits this whole ladder, environment variables included.
4. **The embedded trust list is either explicit or derived.** If `updateManifest.publicKey` is set (string or array) it is embedded as-is; otherwise the public half of *every* signing key is derived and embedded. This lets you trust a key before you sign with it.

:::note[Platform blocks replace, they don't merge]
A platform-specific `updateManifest` block (for example `linux.updateManifest`) *replaces* the top-level `updateManifest` block for that platform rather than being merged with it. If you set `publicKey` at the top level but also have a per-platform block, put `publicKey` in the per-platform block too.

The same replacement rule makes `linux.updateManifest: false` an opt-out for Linux alone, leaving a top-level signing config in force for the other platforms. A platform value of `null` is *not* an opt-out — it falls back to the top-level block.
:::

### Recommended steady state: always trust the next key

The cheapest rotation is the one you prepared for. Generate the *next* key today, keep its private half offline, and embed both public keys in every release:

```yaml
# electron-builder.yml
updateManifest:
  publicKey:
    - |               # CURRENT key — the one CI signs with
      -----BEGIN PUBLIC KEY-----
      MCowBQYDK2VwAyEA...
      -----END PUBLIC KEY-----
    - |               # NEXT key — private half stored offline, not yet used
      -----BEGIN PUBLIC KEY-----
      MCowBQYDK2VwAyEA...
      -----END PUBLIC KEY-----
```

Every install in the field then already trusts a successor. If the current key leaks, you can start signing with the next key immediately: no bridge release, no waiting for adoption. Rotation becomes "promote next to current, generate a new next", and the bridge release described below collapses into an ordinary release.

### Planned rotation, step by step

**1. Generate the new keypair.** Run this on a trusted machine, not in a shared CI log:

```sh
npx electron-builder create-update-key --out ./update-private-key.new.pem
```

The private key is written with mode `0600`; the public key and its key id are printed to stdout. Store the private key as a **new** CI secret alongside the old one. Do not replace the old secret yet.

**2. Ship the bridge release: sign with BOTH keys, trust BOTH keys.** Provide both private keys to the build. The trust list is derived automatically, so no `publicKey` config is needed:

```sh
# two files, joined with the OS path delimiter (":" on Linux/macOS, ";" on Windows)
ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=/run/secrets/update-key.old.pem:/run/secrets/update-key.new.pem electron-builder --publish always

# or both PEMs concatenated in one variable
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-key.old.pem update-key.new.pem)" electron-builder --publish always
```

Put the **old** key first: its signature also fills the legacy `signature` field, which is what installs built before trust lists existed verify. The resulting `latest*.yml` carries two entries in `signatures`; installs in the field verify it with the old key, and after updating trust `[old, new]`. Nothing else about the release needs to change.

**3. Keep dual-signing for your support window.** Every release you publish while both keys are configured is a bridge release: old installs verify it via the old signature, updated installs via either. How long to keep the old key depends on how far back you support updating from; measure adoption of the first dual-signed release and decide.

**4. Drop the old key.** Remove the old private key from the environment. From this release on, manifests are signed by the new key only and `app-update.yml` trusts only the new key. Installs that took any dual-signed release verify them; installs that skipped the whole window fail with `ERR_UPDATER_MANIFEST_SIGNATURE_INVALID` and stop updating.

**5. Deal with stragglers.** Installs older than the first dual-signed release trust only the old key. Your options are:

- **Accept it** and tell users on very old versions to reinstall from your download page.
- **Keep dual-signing longer**, or serve them a separate feed (for example a `latest-legacy.yml` on a different `channel`) whose manifests are still signed with the old key. This only works if the old builds can be steered there (`autoUpdater.channel`, `setFeedURL`, or a feed URL you controlled at the time).
- **Runtime override.** Newer app code can set `autoUpdater.updateManifestPublicKey` to a list explicitly; this overrides the value from `app-update.yml`, which helps if the trust list must change without rebuilding `app-update.yml`, but it does not help installs that are already in the field.

**6. Retire the old key.** Once no feed is signed with it, delete the old private key from CI. Keep the *public* key and its key id on file for auditing.

### If the private key is compromised

An attacker holding a private key that installs still trust can produce a `latest*.yml` those installs will accept, provided they can also place it (and a payload) on your update server or intercept the connection. A trust list does **not** protect against the leaked key itself: as long as an install lists that key, a signature made with it verifies. The remedy is to get the leaked key out of the trust list on every install, and to stop signing with it, as fast as possible.

1. **Lock down the feed first.** Revoke the publish credentials (`GH_TOKEN`, S3/R2 keys, and so on) and audit the manifests currently served. The manifest signature protects the metadata, but only if the storage it is served from has not already been overwritten.
2. **If installs already trust a successor key** (the recommended steady state above): switch signing to that key now, embed `[successor, new-next]`, and never sign with the leaked key again. Every install that trusts the successor is protected from this point on.
3. **If they do not:** ship a bridge release immediately. It must still be signed with the compromised key — that is the only key the installs trust — so sign with `[compromised, new]` and publish from a clean pipeline; you are racing the attacker. Follow with a release signed by the new key only as soon as adoption allows.
4. **Remove the leaked key from the trust list** in the very next release (stop providing its private key; if you list keys explicitly, delete its entry). Until an install has taken that release it remains exposed to the leaked key; verification is otherwise fail-closed, so once it has, anything the attacker signs is rejected with `ERR_UPDATER_MANIFEST_SIGNATURE_INVALID`, and unsigned manifests with `ERR_UPDATER_MANIFEST_NOT_SIGNED`.

### Key storage

- **Never commit private keys.** `updateManifest.signingKey` exists for completeness; in practice use `ELECTRON_BUILDER_UPDATE_SIGN_KEY` (PEM contents) or `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE` (paths to mounted secret files) from your CI secret store.
- Prefer the `_FILE` variant where your CI can mount secrets as files; it keeps the keys out of process listings and environment dumps.
- If a private key lives in an HSM or KMS that signs on your behalf, electron-builder cannot call it directly. Sign `latest*.yml` in a post-publish step of your own (append an entry to `signatures` with the matching `keyId`, or set `signature`, over the payload from `canonicalizeForSigning` in `builder-util-runtime`), and list the public key in `updateManifest.publicKey` so it is embedded. electron-builder warns at build time when none of the keys it signs with is in an explicit `publicKey` list, since such a release cannot verify its own manifests.

  A `publicKey` alone does not satisfy the v27 signing requirement, so this workflow takes one of two routes:

  - **Build without a publish policy.** The requirement only warns there, and electron-builder writes the unsigned `latest*.yml` locally; your step signs it and publishes the artifacts and the manifest.
  - **Keep publishing, with `publishAutoUpdate: false` on every publish provider.** electron-builder uploads the artifacts and embeds the `publicKey`, but writes no `latest*.yml` at all, so your step has to generate the manifest as well as sign and upload it.
- **Pre-signed download URLs** (`files[].url`, `files[].blockMapUrl`, `packages.<arch>.path`) are covered by the signature, so a step that writes them into `latest*.yml` has to run before the manifest is signed — or sign it again afterwards, which needs the private key in that step. A server that mints them per request cannot serve a signed manifest.
- Use one key per app (or per release channel if channels are operated by different teams). Sharing a key across unrelated apps means one compromise affects all of them.

## Windows: code-signing certificate rotation

Two things happen when a Windows certificate changes: the OS and SmartScreen see a new signer (reputation may need to be rebuilt for an OV certificate), and **electron-updater checks the new signature against the publisher name embedded in the app**. Only the second is electron-builder's concern.

### How the updater checks the certificate

When `win.verifyUpdateCodeSignature` is `true` (the default), electron-builder writes the resolved publisher name into `app-update.yml` as `publisherName`. It is either what you configured in `win.sign.publisherName` (a string or an array) or, when not configured, the Common Name of the signing certificate. When electron-builder cannot read the signing certificate (a custom `sign` hook without a certificate, or a certificate without a CN), the build fails unless `publisherName` is set. At update time the NSIS updater reads that value and runs `Get-AuthenticodeSignature` on the downloaded installer:

- The installer must carry a **valid** Authenticode signature.
- The signer's Subject is compared against **each** configured publisher name. A full Distinguished Name (`CN=..., O=..., C=...`) matches when every component you listed equals the certificate's; a bare `CN=` value matches on the Common Name alone (with a warning asking you to configure the full DN).
- If **any** entry matches, the update is installed. If none matches, the update is rejected.
- If `app-update.yml` has no `publisherName` at all, verification is skipped with a deprecation warning (this fail-open behavior becomes fail-closed in v28).

The same check runs again before an [install-on-next-launch](./auto-update.md#install-on-next-launch-windowslinux) installer is executed.

### Renewal vs. rotation

- **Renewal with the same Subject** (same CA, same legal entity, same DN): the embedded publisher name still matches and existing installs update without any special release. Verify the DN is byte-for-byte identical: a changed `O=`, `L=`, or `C=` component is a rotation, not a renewal, if you configured a full DN.
- **Rotation to a certificate with a different Subject** (new CA, company rename, moving to Azure Trusted Signing, switching from OV to EV): existing installs will reject installers signed with it until they have received a bridge release.

### Step by step

**1. Ship the bridge release with both names.** Before signing anything with the new certificate, publish a release (still signed with the **old** certificate) whose `win.sign.publisherName` lists both subjects:

```yaml
win:
  sign:
    type: signtool            # or hsm / pkcs11 / azure
    publisherName:
      - "CN=Old Name, O=Old Company Inc, C=US"
      - "CN=New Name, O=New Company Ltd, C=GB"
```

Installs that take this release accept updates signed by either certificate. Because `publisherName` is now explicit, keep the entries in sync with reality; electron-builder validates the list at build time (see below).

**2. Wait for adoption**, then **switch the signing certificate** (`WIN_CSC_LINK`/`WIN_CSC_KEY_PASSWORD`, the HSM/PKCS#11 token, or the Azure account). Keep both names in `publisherName` for as long as you still want installs on the bridge release to keep updating, and until you are sure no pre-bridge installs matter.

**3. Remove the old name** once the transition is complete.

### Build-time validation

When `publisherName` is configured explicitly and electron-builder can read the signing certificate (a `.pfx`/`.p12` file, a Windows certificate-store entry, or an `.crt`/`.cer` file with a CN), it fails the build with `InvalidConfigurationError` if **none** of the configured names matches the certificate's subject. This catches a wrong `WIN_CSC_LINK` before it produces installers that every install would reject. An array containing both old and new names passes as long as one of them matches the certificate actually in use, so the bridge configuration above never trips it. The check is skipped for custom `sign` hooks and for Azure Trusted Signing (no local certificate to compare against). Setting `publisherName: null` opts out of embedding entirely.

### Notes per signing method

- **signtool (file / store)** — rotate by pointing `certificateFile`/`WIN_CSC_LINK` (or `certificateSubjectName`/`certificateSha1`) at the new certificate. Timestamp your signatures (`rfc3161TimeStampServer`, on by default) so installers signed with the old certificate stay valid after it expires.
- **HSM / PKCS#11** — the certificate lives on the token; rotation means a new token or key label plus an explicit `publisherName` for PKCS#11 without an extractable certificate, an HSM/PKCS#11 `.crt`/`.cer` without a CN, or HSM with a custom `sign` hook and no certificate identifier.
- **Custom `sign` hook** — electron-builder cannot read the certificate the hook signs with, so set `publisherName` (the build fails without it, even if a certificate is configured too). It is **not** checked against the hook's certificate at build time. Copy the subject from a binary the hook signed (see [How do you delegate code signing?](../win.md#how-do-you-delegate-code-signing)); a wrong component makes installs reject every update signed with it.
- **Azure Trusted Signing** — there is no local certificate, so `publisherName` is required and is embedded verbatim. Confirm the Subject Azure signs with before the bridge release.
- **Custom `verifyUpdateFileAuthenticodeSignature` function** — if your app replaces the verifier (see the [Windows target docs](../win.md)), the rules above are yours to reimplement.
- **Squirrel.Windows** is not supported by electron-updater's auto-update flow, so nothing here applies to it.

See [Windows Code Signing](./code-signing/code-signing-win.md) for configuration details of each method.

## macOS: certificate and Team ID

On macOS, electron-updater downloads the `zip` artifact and hands it to the native **Squirrel.Mac** framework, which verifies that the update's code signature matches the running app's before it is installed. That requirement — the update must be signed by the same Apple Team ID as the installed app — is enforced by Squirrel.Mac and macOS, not by electron-builder or electron-updater, and there is no publisher-name list to widen.

- **Renewing or replacing a Developer ID Application certificate within the same Team** is transparent: the Team ID does not change, so Squirrel.Mac accepts the new signature. Update `CSC_LINK`/`CSC_KEY_PASSWORD` (or the keychain identity selected via `CSC_NAME`/`mac.sign.identity`) and keep shipping.
- **Changing Team ID** (moving the app to a different Apple developer account) cannot be bridged through the updater: installs signed by the old team will not accept an update signed by the new one. Users must download and install the new build manually. Announce it in-app in the last release under the old team.
- **Notarization** is per build and does not create a trust anchor in the installed app. Rotate notarization credentials (`APPLE_ID`/`APPLE_APP_SPECIFIC_PASSWORD`, App Store Connect API key) whenever you like.
- The Ed25519 **manifest signature** applies to `latest-mac.yml` exactly as on other platforms, and rotates as described [above](#rotating-the-update-manifest-signing-key-ed25519).

See [macOS Code Signing](./code-signing/code-signing-mac.md) and [Notarization](./code-signing/notarization.md).

## Linux: repository GPG keys

electron-builder does **not** sign `.deb`, `.rpm`, or `AppImage` artifacts itself. What exists:

- **Manifest signature.** The Ed25519 signature on `latest-linux.yml` is the integrity layer electron-builder provides for every Linux target, and it rotates as described [above](#rotating-the-update-manifest-signing-key-ed25519). The downloaded file's `sha512` is checked against that manifest like on every other platform.
- **AppImage.** electron-updater performs no package-level signature check on AppImages beyond the manifest and its `sha512`.
- **deb / rpm.** `AppUpdater.allowUnverifiedLinuxPackages` (default `true`) controls whether the package manager's own signature checks are bypassed when a downloaded package is installed. With `false`, `apt`, `dnf`/`yum`, and `zypper` enforce their normal verification; plain `dpkg` performs no signature verification at all, and a bare `rpm` install cannot be made to enforce it from the command line, so the updater logs a warning in those cases.

If you sign your packages with GPG and enforce it (`allowUnverifiedLinuxPackages = false`), the trust anchor is the public key in the user's keyring (`/etc/apt/trusted.gpg.d`, `/etc/pki/rpm-gpg`, ...). That keyring is populated by your packaging, not by electron-builder, so rotation follows the standard distribution playbook:

1. **Publish the new public key** in your repository metadata and on your download site.
2. **Ship a bridge release** signed with the **old** GPG key whose package installs the **new** public key into the keyring (via a post-install script or a keyring package dependency). Users who install it now trust both keys.
3. **Switch package signing to the new key** after adoption; installs that skipped the bridge will hit a package-manager signature error and need a manual key import or reinstall.
4. Remove the old public key from the keyring in a later release.

## Rotation checklist

Use this as a template for a rotation ticket.

**Before**

- [ ] Generate the new key/certificate on a trusted machine; store it as a *new* secret — do not overwrite the old one yet.
- [ ] Decide how you will measure adoption of the bridge release and how long the window lasts.
- [ ] Decide what happens to installs that never take the bridge (reinstall, legacy feed).

**Bridge release** (verified with the old key, carries the new anchor)

- [ ] Ed25519: sign with `[old, new]` private keys (old first); the derived trust list embeds both. If you list keys explicitly, `updateManifest.publicKey` = `[old, new]`.
- [ ] Windows: `win.sign.publisherName` = `[old subject, new subject]`; sign with the old certificate.
- [ ] Linux GPG (if enforced): package installs the new public key; sign with the old GPG key.
- [ ] macOS: nothing to bridge for a same-Team certificate change; announce a Team ID change in-app.
- [ ] Verify the built `app-update.yml` lists both keys under `updateManifestPublicKey` / both `publisherName` entries, and `latest*.yml` has two `signatures` entries, before publishing.
- [ ] Never reach for `updateManifest: false` to get a rotation release out: it is not a rotation tool, and it produces manifests that every install carrying a key will refuse.

**Switch**

- [ ] Point CI at the new private key / certificate.
- [ ] Ed25519: keep dual-signing for the support window, then remove the old private key (and its `publicKey` entry, if explicit). Consider embedding the *next* key already.
- [ ] Windows: keep both names until the window closes.
- [ ] Confirm an install on the bridge release updates successfully from the first release signed with the new key.

**After**

- [ ] Remove the old subject from `publisherName`, the old GPG key from the keyring.
- [ ] Delete the old private key / certificate from CI; keep public halves (and the Ed25519 key id) for audit.
- [ ] Record the rotation date and the last version signed with the old key.

## Related

- [Signed Update Manifests](./signed-update-manifests.md) — the Ed25519 manifest signing feature
- [Auto Update](./auto-update.md) — updater behavior and options, including `allowUnverifiedLinuxPackages`
- [Security & Hardening](./security.md) — overview of update security controls
- [Code Signing](./code-signing/code-signing.md) — certificates and environment variables per platform

---

### Features Multi Platform Build (`website/docs/features/multi-platform-build.md`)

# Multi Platform Build

:::info
Don't expect that you can build an app for all platforms on one platform.
:::

- If your app has native dependencies, they can only be compiled on the target platform unless [prebuild](https://www.npmjs.com/package/prebuild) is used. Most node modules [don't provide](https://github.com/atom/node-keytar/issues/27) prebuilt binaries.
- macOS Code Signing works only on macOS. [Cannot be fixed](http://stackoverflow.com/a/12156576).

For CI workflow examples across all platforms, see [GitHub Actions](github-actions.md).

## macOS

All required system dependencies (except rpm) are downloaded automatically on demand on macOS 10.12+.

To build rpm:
```bash
brew install rpm
```

## Linux

You can use [Docker](#docker) to avoid installing system dependencies.

To build in distributable format:
```bash
sudo apt-get install --no-install-recommends -y libopenjp2-tools
```

To build rpm:
```bash
sudo apt-get install --no-install-recommends -y rpm
# or
sudo yum install rpm-build
```

To build pacman:
```bash
sudo apt-get install --no-install-recommends -y bsdtar
```

To build snap (only needed with custom stage packages):
```bash
sudo snap install snapcraft --classic
sudo snap install multipass --beta --classic
```

### Build for Windows on Linux

[Docker](#docker) (`electronuserland/builder:wine`) is recommended to avoid installing system dependencies.

- Install Wine 2.0+ — see [WineHQ Binary Packages](https://www.winehq.org/download#binary).
- Install [Mono](http://www.mono-project.com/download/#download-lin) 4.2+ if you want to use Squirrel.Windows (NSIS doesn't require Mono).

### Build 32-bit from a 64-bit Machine

```bash
sudo apt-get install --no-install-recommends -y gcc-multilib g++-multilib
```

:::warning[Electron 44 removed 32-bit Windows/Linux]
Windows `ia32` and Linux `armv7l` builds require **`electronVersion` &lt;= 43.x** — [Electron 44 removed them](https://github.com/electron/electron/pull/51816). On Electron 44+ electron-builder fails fast with a configuration error (a warning if a custom `electronDist`/mirror is set). The v43 series is supported until January 2027. See [Build Architectures](../architecture.md#supported-architectures-by-platform).
:::

## Docker

Build Linux or Windows targets on any platform using Docker.

:::warning
You cannot build for Windows using Docker if your app has native dependencies that don't use [prebuild](https://www.npmjs.com/package/prebuild).
:::

:::note
Do not use Docker Toolbox on macOS. Only [Docker for Mac](https://docs.docker.com/docker-for-mac/install/) works.
:::

### Build on a Local Machine

```sh
docker run --rm -ti \
  --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
  --env ELECTRON_CACHE="/root/.cache/electron" \
  --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
  -v ${PWD}:/project \
  -v ${PWD##*/}-node-modules:/project/node_modules \
  -v ~/.cache/electron:/root/.cache/electron \
  -v ~/.cache/electron-builder:/root/.cache/electron-builder \
  electronuserland/builder:wine
```

Then run `yarn && yarn dist` inside the container.

:::tip
If you don't need to build for Windows, use `electronuserland/builder` — Wine is not included in that image.
:::

### Provided Docker Images

:::tip
Lock your `FROM` to a specific date-tag (e.g. `builder:24-07.23`) or digest rather than `latest`, to prevent unexpected toolset upgrades.
:::

:::note[v27 requires Node.js &gt;=22.12]
The images are now published in **Node.js 22 and 24** flavors (e.g. `builder:22`, `builder:24`). The old Node.js 20 tags do not satisfy v27's minimum runtime — use a `22`/`24` tag. See the [tags list](https://hub.docker.com/r/electronuserland/builder/tags).
:::

| Image | Contents |
|---|---|
| `electronuserland/builder` or `electronuserland/builder:24` | Node.js 24 (also available as `:22`) + Linux build dependencies. Use for Linux-only targets. |
| `electronuserland/builder:wine` | Wine + Node.js. Use for Windows targets. |
| `electronuserland/builder:wine-mono` | Mono for Squirrel.Windows. |
| `electronuserland/builder:wine-chrome` | `google-chrome-stable` + `xvfb`. Use for headless Electron testing. |
| `electronuserland/builder:base` | System dependencies only. Not meant for direct use. |

Images are also tagged with a date suffix `-%m.%y` (e.g. `builder:24-07.23`) for pinning. Full build script: [build.sh](https://github.com/electron-userland/electron-builder/blob/master/docker/build.sh)

---

### Features Security (`website/docs/features/security.md`)

---
title: "Security & Hardening"
---

# Security & Hardening

electron-builder aims to be secure-by-default across the whole release pipeline — from how updates are verified, to how your app's contents are integrity-checked, to how artifacts are signed and published. This page is a map of those controls; each section links out to the detailed guide rather than repeating it.

:::info[v27 tightened several defaults]
Several security-relevant defaults changed in v27 — auto-update web installers, update-metadata format, and explicit publishing. If you are upgrading, read this page alongside the [v27 Breaking Changes](../migration/v27-breaking-changes.md).
:::

## Update security (electron-updater)

- **Signed update manifests (required by default).** The `latest*.yml` update metadata is Ed25519-signed at publish time and verified by electron-updater — before any download — against a public key embedded in the app. Verification fails closed on unsigned or tampered manifests. Publishing a build that emits update metadata fails unless a signing key resolves; `updateManifest: false` is the only opt-out and warns on every build. Unlike the `sha2`-only metadata grace period below, this one is enforced in **v27**. See [Signed Update Manifests](./signed-update-manifests.md).

- **Web-installer payloads are disabled by default.** `AppUpdater.disableWebInstaller` now defaults to **`true`**. NSIS *web* installers (the small installer that downloads its full payload at install time from a manifest-supplied URL) are no longer loaded unless you opt in, because that payload may not undergo signature verification. A web-installer update is rejected with `ERR_UPDATER_WEB_INSTALLER_DISABLED` — at download (including an update cached by a previous launch), before an install on next launch, and at install time. Installs made by an `nsis-web` installer built with electron-builder v27+ opt in automatically (via the `resources/package-type` marker); set `autoUpdater.disableWebInstaller = false` yourself only for web-installer installs without that marker (built before v27, or with a custom script) or when switching an app from `nsis` to `nsis-web`. Cached web updates are re-verified against the freshly fetched manifest, and the `nsis-web` installer verifies the package electron-updater passes to it against the packages built with it (see [Web Installer](../nsis.md#web-installer)). [Signed update manifests](./signed-update-manifests.md) cover the web package's path, SHA-512 and size, so don't set `updateManifest: false` for `nsis-web` apps.

- **Update credentials stay on the feed's origin.** The credential headers from `requestHeaders` / `addAuthHeader` (headers such as `Authorization`, the same set that is removed on a cross-origin redirect) and the query string of the feed URL are only sent to the update feed's origin. A download on another origin, including the same host on another port or over `http` instead of `https`, is requested without them. See [Auto Update](./auto-update.md#custom-options-instantiating-updater-directly).

- **Linux package signatures.** `AppUpdater.allowUnverifiedLinuxPackages` (default **`true`**) preserves historical behavior, since electron-builder does not sign Linux packages itself. Set it to **`false`** to enforce GPG signature checks when installing `.deb` / `.rpm` auto-updates on package managers that support verification.

- **Modern, verified update metadata.** Generated `latest*.yml` targets the `files[]` format and the default `electronUpdaterCompatibility` is now `>=2.16`. Update metadata validated only by the legacy SHA-256 `sha2` checksum is deprecated.

- **Windows update signature verification.** On Windows, downloaded NSIS updates are Authenticode-verified before they are applied. `win.verifyUpdateCodeSignature` (default `true`) embeds your signing `publisherName` into `app-update.yml`, and electron-updater checks each downloaded update against the certificate subject. Supply an array of `publisherName` values when rotating certificates so updates signed by either the old or new certificate still verify.

:::warning[sha2-only metadata is rejected in v28]
v27 is a grace period for `sha2`-only update metadata. In **v28**, it is **rejected**. Confirm your updates use modern `files[]` metadata before upgrading to v28.
:::

**See also:** [Auto Update](./auto-update.md) · [Update metadata & compatibility](./auto-update.md#compatibility)

## Package integrity

Two independent mechanisms protect the code inside your packaged app:

- **ASAR integrity.** When your app is packaged into an `app.asar` archive (the default), electron-builder embeds integrity hashes so Electron can detect a tampered or swapped archive at runtime. It is on by default; `asar.disableIntegrity: true` disables it (not recommended). See the [ASAR options](../migration/v27-breaking-changes.md#asar-options-asar) for the v27 config shape.

- **Electron Fuses.** Fuses are "magic bits" flipped at package time — *before* code signing — that disable risky Electron features for the entire app (e.g. `runAsNode`, `enableNodeCliInspectArguments`, `enableNodeOptionsEnvironmentVariable`). Pairing `enableEmbeddedAsarIntegrityValidation` with `onlyLoadAppFromAsar` binds the app to its verified ASAR. Because the bits are flipped before signing, the OS (Gatekeeper / App Locker) is responsible for ensuring they can't be flipped back.

**See also:** [Adding Electron Fuses](../tutorials/adding-electron-fuses.md)

## Code signing & notarization

Signing proves your app's identity and lets the OS confirm it hasn't been tampered with since it was signed. On macOS, notarization is additionally required for Gatekeeper when distributing outside the Mac App Store. electron-builder discovers signing credentials from the environment and signs automatically when they are present.

- **macOS signing** (Developer ID, `mac.sign`) — [macOS Signing](./code-signing/code-signing-mac.md)
- **Windows signing** — signtool, HSM, PKCS#11, and Azure Trusted Signing via the `win.sign` discriminated union — [Windows Signing](./code-signing/code-signing-win.md)
- **Apple notarization** — [Notarization](./code-signing/notarization.md)

**See also:** [Code Signing overview](./code-signing/code-signing.md)

## Safe publishing

v27 **removed implicit `--publish`**. Earlier versions could auto-publish based on the presence of CI tag environment variables, git tags, or npm lifecycle events — which risked accidentally exposing secrets or shipping unfinished releases. v27 never publishes unless you ask it to: pass `--publish <always|onTag|onTagOrDraft|never>` explicitly in your release scripts, or set the `publish` option in your configuration.

```bash
electron-builder --publish always   # explicit — required in v27
```

**See also:** [Publish configuration](../publish.md) · [Implicit `--publish` removed](../migration/v27-breaking-changes.md#implicit-publish-removed)

## Update feed ownership

When a GitHub or Bitbucket publish configuration omits `owner`/`repo`, electron-builder fills them in from the repository info (`package.json` `repository`, CI environment variables, then `.git/config`). The result becomes the publish/update destination and, for auto-update-capable targets, is written to `app-update.yml` inside the packaged app, so it is the update feed every installed copy keeps using — long after the build machine is gone. A build reports the repository it resolved to and the `source` it came from (info level for `package.json` `repository`, warn level for CI environment variables or `.git/config`), so read that line and confirm it is correct:

```
• update feed inferred from repository info; it will be used as the publish/update destination
  (written to app-update.yml in auto-update-capable targets) - specify it explicitly to be sure
  it stays under your control  reason=owner and repo not specified in the publish configuration
  source=.git/config provider=github owner=my-org repo=my-app
```

That name has to stay yours. GitHub frees an owner or repository name for re-registration as soon as it is renamed, transferred or deleted, and whoever claims it next can publish a release that installed copies of your app will download and run. Set `owner` and `repo` explicitly rather than relying on detection, and do not retire the namespace while builds are still in the field.

**See also:** [GitHub repository detection](../publish.md#github-repository-and-bintray-package)

## Toolset integrity

The external build toolsets electron-builder downloads (NSIS, winCodeSign, AppImage, Wine, FPM, …) are checksum-verified after download and cached, so a corrupted or substituted bundle is rejected. When you supply a **custom** toolset from a remote `url`, a `checksum` is **required**; a local `file://` directory is used as-is and needs no checksum.

```json5
{
  "build": {
    "toolsets": {
      "nsis": {
        "url": "https://example.com/my-nsis-bundle.tar.gz",
        "checksum": "sha256:abc123…"
      }
    }
  }
}
```

**See also:** [Toolsets](../toolsets.md) · [Custom toolset bundles](../migration/v27-breaking-changes.md#toolset-env-var-overrides-removed)

---

### Features Signed Update Manifests (`website/docs/features/signed-update-manifests.md`)

---
title: "Signed Update Manifests"
---

# Signed Update Manifests

electron-updater always verifies the sha512 checksum of a downloaded artifact against the update manifest (`latest.yml` / `latest-mac.yml` / `latest-linux.yml`). That protects the *artifact*, but the *manifest itself* is only protected by the transport (HTTPS) and the storage it is served from — anyone who can modify the file on your update server can point clients at an arbitrary payload with a matching checksum.

Signed update manifests close that gap: electron-builder can sign each `latest*.yml` with one or more **Ed25519** private keys at publish time, and electron-updater verifies the signature — before anything is downloaded — against a list of trusted public keys baked into the app at build time. Signing uses Node's built-in `crypto`; no additional dependencies or external tools are involved.

Signing is **required**: publishing a build that emits auto-update metadata fails unless a signing key resolves, and `updateManifest: false` is the only way to opt out. See [Opting out](#opting-out).

Verification remains backward compatible in both directions:

- Apps built **before** a public key existed ignore the `signature`/`signatures` fields in the manifest (a one-time warning notes that verification is disabled).
- Apps built **with** a public key **fail closed**: an unsigned or tampered manifest aborts the update check before any download starts.

:::warning[Full protection arrives one release after adoption]
Installs already in the field carry whatever `app-update.yml` they shipped with. Those built before you adopted signing have no trusted key, so they keep accepting unsigned manifests — signing a release does not retroactively protect the installs that will consume it. The installs protected by a signed manifest are the ones built *after* the public key was embedded.
:::

## Quick setup

**1. Generate a keypair** (once per app or release channel):

```sh
npx electron-builder create-update-key
```

This writes `update-private-key.pem` (mode `0600`) to the current directory (use `--out <path>` to choose the location) and prints the matching public key and its **key id** (the hex SHA-256 of the public key's SPKI DER encoding, which appears as `keyId` next to each signature in the manifest) for reference. Store the private key as a CI secret — it never needs to be committed anywhere.

**2. Provide the private key at build/publish time**, preferably via an environment variable in CI:

```sh
# either the PEM content itself…
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-private-key.pem)" electron-builder --publish always
# …or a path to the key file
ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=/run/secrets/update-private-key.pem electron-builder --publish always
```

No configuration block is required — the environment-variable route is the whole setup. Every generated `latest*.yml` gains a base64 `signature` field plus a `signatures` list (see [What is written](#what-is-written)), and the derived public key is embedded into the app's `app-update.yml` as `updateManifestPublicKey` automatically.

A publishing build with no resolvable key fails instead — at build start, before anything is packed or uploaded, so none of that build's artifacts are published:

```
auto-update manifests must be signed, but no Ed25519 signing key was found for mac. Generate one with
`electron-builder create-update-key`, then supply it via the ELECTRON_BUILDER_UPDATE_SIGN_KEY (PEM contents)
or ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE (path) environment variable, or via updateManifest.signingKey /
updateManifest.signingKeyFile in the build configuration. To publish unsigned manifests anyway, opt out
explicitly with `updateManifest: false`.
```

A build **without** a publish policy only warns, so local builds keep working while your release pipeline is the thing that has to hold the key.

**3. That's it.** electron-updater picks the public key up from `app-update.yml` and enforces verification from the first update check.

## Configuration reference

Instead of (or in addition to) the environment variables, you can configure signing in the [build configuration](../configuration.md) — at the top level, or per platform (e.g. under `linux` or `win`) to sign only some platforms' manifests:

```yaml
updateManifest:
  # Ed25519 private key(s), PEM (PKCS#8). Secret — prefer ELECTRON_BUILDER_UPDATE_SIGN_KEY in CI.
  # A string (which may contain several concatenated PEM blocks) or an array of strings.
  signingKey: null
  # Path(s) to file(s) containing private key(s). Alternative to signingKey. String or array.
  # Relative paths resolve against the project directory.
  signingKeyFile: null
  # Public key(s) (PEM or base64 SPKI) embedded into app-update.yml as the updater's trust list.
  # Optional — derived automatically from the private key(s) when omitted. String or array.
  publicKey: null
```

The whole option may instead be set to `false`, the only opt-out:

```yaml
updateManifest: false # publishes UNSIGNED manifests; warns on every build
```

Key resolution order is: `signingKey` → `signingKeyFile` → `ELECTRON_BUILDER_UPDATE_SIGN_KEY` → `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE`. The first source that is set wins, but that source may provide **several keys**:

| Source | Several keys |
| --- | --- |
| `signingKey` / `ELECTRON_BUILDER_UPDATE_SIGN_KEY` | concatenate the PEM blocks (`cat a.pem b.pem`), or use an array in config |
| `signingKeyFile` | an array of paths in config |
| `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE` | join the paths with the OS path delimiter: `a.pem:b.pem` on Linux/macOS, `a.pem;b.pem` on Windows |

A relative `signingKeyFile` path resolves against the project directory (where the build configuration lives), like every other path in the configuration; a relative `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE` resolves against the current working directory, as environment-variable paths usually do.

Every key must be a distinct Ed25519 key; duplicates and other key types fail the build. When several keys are configured, **each manifest is signed by all of them** and the trust list embeds all of their public keys — this is the dual-signing used during [key rotation](#key-rotation).

An explicit `publicKey` takes precedence over derivation and is embedded as-is. Use it to trust keys you do not (yet) sign with — for example the *next* key ahead of a rotation. If none of the keys the build signs with is in an explicit `publicKey` list, electron-builder logs a warning: installs of that release could not verify its own manifests, which is only ever intended for a deliberate bridge release.

A `publicKey` on its own does **not** satisfy the signing requirement — electron-builder still has no key to sign with, so a publishing build fails. An HSM/KMS-style signer that signs `latest*.yml` in a later step of its own therefore either builds without a publish policy, where the requirement only warns and the unsigned manifest is written locally for it to sign, or keeps publishing with `publishAutoUpdate: false` on every publish provider — electron-builder then uploads the artifacts and embeds the `publicKey`, but writes no `latest*.yml`, so the external step has to generate the manifest too. See [Key storage](./key-rotation.md#key-storage).

At runtime you can also set the trust list on the updater directly; it overrides the value from `app-update.yml`:

```ts
import { autoUpdater } from "electron-updater"
autoUpdater.updateManifestPublicKey = "-----BEGIN PUBLIC KEY-----\n…" // or an array of keys
```

## Opting out

Set `updateManifest: false` — at the top level, or under a platform key — to publish unsigned manifests. This is the only opt-out, and every build that produces a manifest logs a warning naming the consequence.

```yaml
updateManifest: false
```

Two properties are worth knowing:

- **It short-circuits the environment variables.** A leftover `ELECTRON_BUILDER_UPDATE_SIGN_KEY` in CI cannot re-enable signing behind the back of the opt-out, so `false` means unsigned everywhere, deterministically.
- **A platform value wins over the root**, so `linux: { updateManifest: false }` opts out for Linux only while a root signing config still covers the other platforms — and the converse works too. A platform value of `null`, on the other hand, falls back to the root rather than opting out.

The requirement is also waived, without any configuration, where no manifest is emitted at all: a build whose publish providers all set `publishAutoUpdate: false`, a build whose targets write no update info (for example only snap, flatpak, MSI/MSIX, portable, mas/pkg or plain archives), and any build without a publish policy (which only warns).

:::danger[Opting out is not reversible for a release line]
Once a release embeds a public key, its installs are **fail-closed**: an unsigned manifest is rejected with `ERR_UPDATER_MANIFEST_NOT_SIGNED` and those installs stop updating entirely. Do not switch to `updateManifest: false` after shipping signed manifests, and make sure *every* pipeline that can publish a release — including a hotfix built from a laptop — has the key.
:::

Legitimate reasons to opt out: an app that does not auto-update at all. A pipeline that signs `latest*.yml` itself in a later step should **not** opt out — `false` also drops `updateManifest.publicKey`, so installs would embed no trust list and never verify those signatures; see [Key storage](./key-rotation.md#key-storage) for its two routes.

## What is written

`app-update.yml` gains `updateManifestPublicKey`: a single string when one key is trusted (unchanged from the single-key format), a YAML list when several are.

Each signed `latest*.yml` gains:

```yaml
signature: b0Qk…               # legacy single field: the FIRST signing key's signature
signatures:
  - keyId: 3f9c…e1             # hex SHA-256 of the signing key's public key (SPKI DER), as printed by create-update-key
    signature: b0Qk…           # same value as `signature` above
  - keyId: a71d…04             # one more entry per additional signing key
    signature: Xr2w…
```

Both fields are always written, even with a single key, so the manifest shape does not change when a second key is added. Each entry signs the same payload; the `keyId` lets an updater holding several trusted keys pick the matching signature directly, and lets you tell at a glance which keys signed a release.

## What is signed

Every signature covers the integrity-critical fields of the manifest: `version`, `stagingPercentage`, `minimumSystemVersion` (its absence is signed too, so an OS-version gate cannot be added or removed after signing), every file entry's `url`, `sha512`, and `size` (and its `blockMapUrl` when present), and — for the NSIS web installer — every `packages` entry's `path`, `sha512`, `size`, `blockMapSize`, and `isAdminRightsRequired` (all in a canonical, order-independent form). Cosmetic fields — `releaseName`, `releaseNotes`, and `releaseDate` — are not covered, so they remain editable after publishing without invalidating the signature. The `signature`/`signatures` fields themselves (including `keyId`) are not part of the signed payload, so signatures can be added or removed without invalidating the others.

`files[].isAdminRightsRequired` is intentionally not signed either. It only decides how electron-updater launches the already hash-verified installer (directly, or through `elevate.exe`), so tampering with it cannot change what gets installed — at worst it causes an unexpected UAC prompt — while leaving it unsigned lets you correct a misconfigured value in `latest.yml` after publishing without re-signing.

The canonical form is injective: every string value is JSON-quoted (so a value can never contain an unescaped newline or tab and cannot impersonate another record), numbers are written bare, and an absent optional field is encoded differently from an empty one. Two manifests therefore produce the same signed bytes only if every covered field is identical.

A signed manifest must also have the shape the signature is meant to protect. Before trying any key, the updater rejects a manifest whose `files` list is empty or missing, whose `version`, `files[].url`/`sha512`, `minimumSystemVersion` or `packages` entries are not plain strings/numbers, or whose signed string fields contain control characters, with `ERR_UPDATER_MANIFEST_SIGNATURE_INVALID` naming the problem. In particular, the deprecated top-level `path`/`sha512` fields (an unsigned mirror of `files[0]`) are never used to download a verified update. electron-builder applies the same checks when signing, so a build cannot produce a signed manifest that its updaters would refuse.

## Verification behavior in electron-updater

When at least one public key is configured (embedded or set at runtime), verification is enforced on every update check, for every provider. The manifest is accepted when **any trusted key validates any of its signatures** (`signatures` entries tagged with that key's id are tried first, then the untagged legacy `signature`):

| Manifest state | Result |
| --- | --- |
| At least one signature validates against a trusted key | Update proceeds |
| No `signature` and no `signatures` | Error `ERR_UPDATER_MANIFEST_NOT_SIGNED` |
| Signed, but tampered or signed only by keys the install does not trust | Error `ERR_UPDATER_MANIFEST_SIGNATURE_INVALID` |

Both errors are emitted through the updater's regular `error` event and abort the update **before any download begins**.

:::warning[The hazard is turning it off, not turning it on]
Verification is enforced by the *installed* app, and since signing is on by default there is nothing to roll out — your first build already embeds the public key and signs its manifests. Older installs without the key simply skip verification. What matters from then on is that every future manifest is signed: publishing an unsigned one makes every install that carries the key refuse the update.
:::

## Key rotation

An install trusts the **list** of public keys embedded in its `app-update.yml`, and a manifest can carry a signature from **each** of several keys. Rotation therefore needs no flag day:

1. **Bridge release(s):** configure both the **old** and the **new** private key (old first, so it also fills the legacy `signature` field), e.g. `ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE=old.pem:new.pem`. Manifests are now dual-signed and `app-update.yml` trusts `[old, new]`. Installs in the field verify via the old signature; once updated they trust both keys.
2. **Keep dual-signing** for as long as you want installs that predate the first bridge release to be able to update.
3. **Drop the old key** from the environment. Manifests are signed by the new key only; every install that took a bridge release keeps verifying. Installs that never did stop updating (`ERR_UPDATER_MANIFEST_SIGNATURE_INVALID`) and must be reinstalled or served from a separate feed still signed with the old key.

Better still, embed the *next* key before you need it: set `updateManifest.publicKey` to `[current, next]` and keep the next private key offline. A compromise of the current key can then be answered by switching to the next key at once, with no bridge release. Note that a trust list does not protect against a leaked key itself while installs still list it — remove it from the list as fast as adoption allows. The full procedure — including compromise handling, key storage advice, and the equivalent steps for Windows, macOS, and Linux code-signing keys — is in the [Key Rotation](./key-rotation.md) runbook.

## Related: Linux package signature enforcement

Manifest signing protects the update *metadata*. Separately, `AppUpdater.allowUnverifiedLinuxPackages` (default `true`) controls whether the OS package manager's own GPG/signature checks are bypassed when installing `.deb` / `.rpm` auto-updates. If you sign your Linux packages, set it to `false` to enforce them — see [Auto Update](./auto-update.md#allowunverifiedlinuxpackages-new) and [Security & Hardening](./security.md#update-security-electron-updater).

---

### Migration V26 To V27 (`website/docs/migration/v26-to-v27.md`)

---
title: "Migrating from v26 to v27"
---

# Migrating from v26 to v27

This is the **step-by-step walkthrough** for upgrading a project from v26 to v27. For the authoritative catalogue of **everything that changed**, see **[v27 Breaking Changes →](./v27-breaking-changes)**.

v27 migrates the entire electron-builder ecosystem to native ES modules, raises the minimum Node.js version to **22.12.0**, and hard-deletes the deprecated APIs that accumulated since v22.

:::danger[Don't upgrade without reading the breaking changes]
Every breaking change — what's removed, renamed, restructured, and what action each requires — is catalogued on the **[v27 Breaking Changes](./v27-breaking-changes)** page. Skim it before you start so nothing surprises you.
:::

**Most projects need only a Node.js version bump plus one command.** The `build()` API is unchanged and CJS `require()` continues to work without any code changes on supported Node.js versions. Configuration keys that were renamed or restructured (`win.sign`, `mac.sign`, `electronGet`, `asar`, `nativeModules`, …) are rewritten for you automatically by `migrate-schema` in Step 1 — they keep the same runtime behavior. A few type exports were renamed or removed; see the [breaking changes](./v27-breaking-changes#removed-exports).

The upgrade is three steps:

1. [Run the automated migrator](#step-1-run-the-automated-migrator) — rewrites your config to v27 form
2. [Update Node.js](#step-2-update-nodejs) — to >=22.12.0
3. [Apply the manual steps](#step-3-apply-the-manual-steps) — the few things the migrator can't do

Then walk the [summary checklist](#summary-checklist).

---

## Step 1: Run the automated migrator

Run the built-in command to apply every **config-level** breaking change to your project automatically:

```bash
electron-builder migrate-schema            # apply changes in place
electron-builder migrate-schema --dry-run  # preview without writing (alias: -n)
```

This rewrites your `electron-builder.json`, `electron-builder.yml`, `package.json` (build key), or any other static config format in place. It also rewrites **programmatic** configs (`.js`/`.ts`/`.cjs`/`.mjs`) via an AST-located codemod that preserves comments, imports, functions, and formatting.

Without `--config`, it looks for a `build` object in `package.json` and then for `electron-builder.{yml,yaml,json,json5,toml,js,cjs,mjs,ts}` in the project root. **If your config file has another name** (for example one your build scripts pass as `electron-builder --config my-config.cjs`), pass the same path: `electron-builder migrate-schema --config my-config.cjs`. Pass `--project-dir <dir>` to specify the project root (default: current directory).

### What it migrates automatically

| Config change | Before | After |
|---|---|---|
| `electronCompile` removed | `"electronCompile": true` | *(deleted)* |
| `disableDefaultIgnoredFiles` removed | `"disableDefaultIgnoredFiles": true` | *(deleted)* |
| `framework` / `nodeVersion` / `launchUiVersion` removed | `"framework": "electron"` | *(deleted)* |
| `npmSkipBuildFromSource` removed | `"npmSkipBuildFromSource": true` | `"nativeModules": { "buildDependenciesFromSource": false }` |
| Native-module options grouped | `"npmRebuild": true` | `"nativeModules": { "npmRebuild": true }` |
| `nativeRebuilder` renamed | `"nativeRebuilder": "parallel"` | `"nativeModules": { "rebuildMode": "parallel" }` |
| `nativeRebuilder: "legacy"` removed | `"nativeRebuilder": "legacy"` | *(dropped with a warning — `@electron/rebuild` `"sequential"` applies)* |
| `appImage.systemIntegration` removed | `"appImage": { "systemIntegration": "ask" }` | *(deleted)* |
| `linux.syncDesktopName` removed | `"linux": { "syncDesktopName": true }` | *(deleted — always synced in v27)* |
| Legacy asar keys | `"asar-unpack": "**/*.node"` | `"asar": { "unpack": ["**/*.node"] }` |
| `asarUnpack` consolidated | `"asarUnpack": ["**/*.node"]` | `"asar": { "unpack": ["**/*.node"] }` |
| `disableSanityCheckAsar` moved | `"disableSanityCheckAsar": true` | `"asar": { "disableSanityCheck": true }` |
| `disableAsarIntegrity` moved | `"disableAsarIntegrity": true` | `"asar": { "disableIntegrity": true }` |
| `asar: true` removed | `"asar": true` | *(deleted — absence means enabled)* |
| Platform `asarUnpack` moved | `"asarUnpack": ["**/*.node"], "mac": { "asarUnpack": ["x/**"] }` | `"asar": { "unpack": ["**/*.node"] }, "mac": { "asar": { "unpack": ["**/*.node", "x/**"] } }` *(root options merged in — a platform `asar` replaces the root one)* |
| Keys with no effect under `asar: false` | `"asar": false, "asarUnpack": "x"` | `"asar": false` |
| macOS signing fields → `sign` | `"mac": { "identity": "Developer ID...", "hardenedRuntime": true }` | `"mac": { "sign": { "identity": "Developer ID...", "hardenedRuntime": true } }` |
| `signIgnore` renamed | `"mac": { "signIgnore": ["**/*.txt"] }` | `"mac": { "sign": { "ignore": ["**/*.txt"] } }` |
| `mac.type` moved | `"mac": { "type": "development" }` | `"mac": { "sign": { "type": "development" } }` |
| `gatekeeperAssess` removed | `"mac": { "gatekeeperAssess": false }` | *(deleted — `@electron/osx-sign` 2.x has no `spctl --assess` step)* |
| v26 `null` = unset on mac options | `"mac": { "provisioningProfile": null }` | *(deleted — the v27 option rejects `null`; `identity: null` is kept)* |
| `mac.universal` fields → `universal` | `"mac": { "mergeASARs": true }` | `"mac": { "universal": { "mergeASARs": true } }` |
| `electronDownload` → `electronGet` | `"electronDownload": { "mirror": "https://m/" }` | `"electronGet": { "mirrorOptions": { "mirror": "https://m/" } }` |
| `electronDownload` fields with no v5 equivalent | `"electronDownload": { "strictSSL": false, "force": true }` | *(dropped with a warning)* |
| `GithubOptions.vPrefixedTagName` | `"vPrefixedTagName": false` | `"tagNamePrefix": ""` |
| GitHub empty `tagNamePrefix` (ignored by v26) | `{ "provider": "github", "tagNamePrefix": "" }` | `{ "provider": "github", "tagNamePrefix": "v" }` *(keeps your tag names; warns)* |
| `win.azureSignOptions` → `win.sign` | `"win": { "azureSignOptions": { "endpoint": "…" } }` | `"win": { "sign": { "type": "azure", "endpoint": "…" } }` |
| `win.signtoolOptions` → `win.sign` | `"win": { "signtoolOptions": { "certificateFile": "…" } }` | `"win": { "sign": { "type": "signtool", "certificateFile": "…" } }` |
| Azure extra sign keys | `"win": { "azureSignOptions": { "ExcludeCredentials": "X" } }` | `"win": { "sign": { "type": "azure", "additionalMetadata": { "ExcludeCredentials": "X" } } }` |
| `win.signExecutable: false` | `"win": { "signExecutable": false }` | `"win": { "sign": false }` |
| `win.signExecutable: true` removed | `"win": { "signExecutable": true }` | *(deleted — enabled by default)* |
| `win.signAndEditExecutable: true` removed | `"win": { "signAndEditExecutable": true }` | *(deleted — always enabled in v27)* |
| `win.signAndEditExecutable: false` | `"win": { "signAndEditExecutable": false }` | `"win": { "sign": false }` *(resource editing now always runs; warns)* |
| Signing options under disabled signing | `"win": { "signExecutable": false, "signtoolOptions": { … } }` | `"win": { "sign": false }` *(the unused block is dropped; warns)* |
| `snap` → `snapcraft` | `"snap": { "confinement": "strict", "base": "core22" }` | `"snapcraft": { "base": "core22", "core22": { "confinement": "strict" } }` |
| `helper-bundle-id` moved | `"helper-bundle-id": "com.x.helper"` | `"mac": { "helperBundleId": "com.x.helper" }` |
| `squirrelWindows.noMsi` inverted | `"squirrelWindows": { "noMsi": true }` | `"squirrelWindows": { "msi": false }` |
| Root-level `directories` moved | `{ "directories": { "output": "dist" } }` (package.json root) | `{ "build": { "directories": { "output": "dist" } } }` |
| `toolsets.*: null` rejected | `"toolsets": { "wine": null }` | *(deleted — unset resolves to `"latest"`)* |
| Retired `appimage` pin | `"toolsets": { "appimage": "1.0.2" }` | `"toolsets": { "appimage": "1.0.3" }` |

It also **warns without rewriting** where there is no mechanical equivalent — `squirrelWindows.customSquirrelVendorDir` and a custom NSIS bundle in `nsis.customNsisBinary` / `customNsisResources` (see Step 3; it does move `customNsisBinary.debugLogging` → `installerDebugLogging`), a custom `mac.sign` signer next to sibling signing options, and, in JS/TS configs only, a platform-level `asarUnpack` next to root-level ASAR options — and prints **advisories** (never a rewrite) for an `nsis-web` target, `nsis.perMachine` or `nsisWeb.perMachine` set to `true`, a custom `win.sign.sign` hook without `win.sign.publisherName`, a `generic` publish configuration whose `url` has a query string, and a `mac`/`mas`/`masDev` section that names no entitlements file. See [what `migrate-schema` does and does not do](./v27-breaking-changes#new-command-migrate-schema).

> Each row links to its full explanation on the [v27 Breaking Changes](./v27-breaking-changes#breaking-changes-at-a-glance) page.

### Serialization caveats

- **JSON5 → JSON**: when `migrate-schema` rewrites a `.json5` file it produces valid JSON (no comments, standard quoting) and prints a warning. If preserving comments matters, apply the changes manually.
- **TOML**: the `toml` npm package is read-only. `migrate-schema` detects TOML configs, prints the required changes, and exits without writing.
- **YAML**: `js-yaml` preserves key order when round-tripping but does not preserve comments.
- **Programmatic configs** (`.js`/`.ts`/`.cjs`/`.mjs`): rewritten in place when reducible to a single object literal; falls back to printing manual steps for dynamic function bodies, spreads, or computed keys, or when `typescript` is not installed.
- **`snap` base defaulting**: when your old `snap` config has no `base` field, the tool assumes `"core20"` (v27's 1-to-1 migration target) and prints a warning so you can confirm or change it.

---

## Step 2: Update Node.js

v27 requires **Node.js 22.12.0 or later** — the version where Node's [`require(esm)`](https://nodejs.org/en/blog/release/v22.12.0) support was stabilized, allowing both CJS and ESM consumers to use these packages without code changes.

Update your local environment:

```bash
# nvm
nvm install 22 && nvm use 22

# fnm
fnm install 22 && fnm use 22
```

Update CI (GitHub Actions):

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: '22'
```

Update your `package.json` engines field if you declare one:

```json
{ "engines": { "node": ">=22.12.0" } }
```

Confirm your CI and Docker images run Node.js 22.12 or later:

```dockerfile
FROM node:22-bookworm-slim
```

electron-builder's own Docker images for Linux builds have been updated and are available in both node 22 and 24 flavors: https://hub.docker.com/r/electronuserland/builder/tags

### ESM/CJS — no code changes needed on Node >=22.12

```js
// CJS require() — still works
const { build } = require("electron-builder")

// ESM import — now the preferred style
import { build } from "electron-builder"
```

`moduleResolution` settings `"node"`, `"node16"`/`"nodenext"`, and `"bundler"` (recommended) all work. See [Native ESM output](./v27-breaking-changes#native-esm-output) for details.

---

## Step 3: Apply the manual steps

`migrate-schema` handles every config-level change except `squirrelWindows.customSquirrelVendorDir` and a custom NSIS bundle in `nsis.customNsisBinary` / `customNsisResources`. The following require manual action — each links to its full explanation:

- [ ] [Migrate off `electron-compile`](./v27-breaking-changes#electroncompile) to a modern bundler (electron-vite / esbuild / webpack), if you used `electronCompile`.
- [ ] [Add `--publish` explicitly](./v27-breaking-changes#implicit-publish-removed) to your release scripts — auto-publish is gone.
- [ ] **Update-manifest signing:** [it is required on publishing builds](./v27-breaking-changes#signed-update-manifests-are-required) — run `npx electron-builder create-update-key` once, store the PEM as a CI secret and expose it as `ELECTRON_BUILDER_UPDATE_SIGN_KEY`, or set `updateManifest: false` to keep publishing unsigned manifests (warned about on every build). Every pipeline that can publish a release needs the key.
- [ ] [Replace `devMetadata` / `extraMetadata`](./v27-breaking-changes#devmetadata-extrametadata-programmatic-packageroptions) in the programmatic API with `config.extraMetadata`.
- [ ] [Replace `--em.build` / `--em.directories`](./v27-breaking-changes#removed-flags-embuild-emdirectories) CLI flags with `-c` / `-c.directories`.
- [ ] [`linux.syncDesktopName` is removed automatically](./v27-breaking-changes#linuxsyncdesktopname-always-synced) by `migrate-schema` — the behaviour is now always on. Only if you had it set to `false`: set `desktopName` to control the installed `.desktop` filename (the migrator warns you).
- [ ] [Switch `<%= var %>` → `${var}`](./v27-breaking-changes#linux-maintainer-script-ejs-template-syntax) in Linux maintainer scripts.
- [ ] [Update custom NSIS scripts](./v27-breaking-changes#nsis-file-association-progid-format-changed) that hard-code the old file-association ProgID.
- [ ] [Replace `CI_BUILD_TAG`](./v27-breaking-changes#ci_build_tag-environment-variable) with `CI_COMMIT_TAG`.
- [ ] [Replace toolset env-var overrides](./v27-breaking-changes#toolset-env-var-overrides-removed) (`APPIMAGE_TOOLS_PATH`, `ELECTRON_BUILDER_NSIS_DIR`, `USE_SYSTEM_WINE`, …) with `toolsets.X: { url, checksum }`.
- [ ] [Validate toolset pins](./v27-breaking-changes#toolset-defaults-resolve-to-latest-newest-bundle) — defaults now resolve to `"latest"` (newest bundle); pin to `"0.0.0"` only if a legacy bundle is needed.
- [ ] **32-bit builds:** [`arch: "all"` now expands to x64 + arm64](./v27-breaking-changes#arch-all-now-expands-to-x64-and-arm64-32-bit-fails-fast-on-electron-44) (was x64 + ia32) — request `ia32` explicitly to keep it. Building Windows `ia32` / Linux `armv7l` on Electron 44+ now fails fast; pin `electronVersion` to `43.x` or earlier (supported until January 2027) or drop those targets.
- [ ] **macOS names:** [`productName` / `executableName` are now validated, not silently sanitized](./v27-breaking-changes#macos-productname-and-executablename-are-validated-not-sanitized) — if a build starts erroring on the name, choose one that needs no filename sanitization.
- [ ] **Bitbucket publishing:** [a token set without a username is now sent as Bearer auth](./v27-breaking-changes#bitbucket-cloud-publishing-token-without-username-uses-bearer-auth) — if `BITBUCKET_TOKEN` holds an app password / API token, also set `BITBUCKET_USERNAME` so Basic auth is used; genuine access tokens need no username.
- [ ] **node_modules arch/os filtering:** [dependencies are now `cpu`/`os`-filtered against the target on every build](./v27-breaking-changes#node_modules-are-now-archos-filtered-on-every-build) — a package whose `cpu`/`os` mismatches the target is excluded (was effectively universal-macOS-only). Re-include an intentionally cross-arch binary via `extraResources` / `files`.
- [ ] **macOS DMG:** [the DMG `filesystem` default is now APFS](./v27-breaking-changes#dmg-filesystem-defaults-to-apfs) — set `dmg.filesystem: "HFS+"` only if you need pre-10.13 (High Sierra) compatibility.
- [ ] **electron-updater:** [`disableWebInstaller` now defaults to `true`](./v27-breaking-changes#disablewebinstaller-defaults-to-true) — web-installer updates are rejected (`ERR_UPDATER_WEB_INSTALLER_DISABLED`) unless it is `false`. Installs made by a v27+ `nsis-web` installer opt in automatically; set `disableWebInstaller: false` only for web-installer installs without the `resources/package-type` marker (pre-v27 installers, custom scripts) or when switching from `nsis` to `nsis-web`. The `nsis-web` installer now verifies and installs its own copy of the app package and leaves a local package file in place; set `nsisWeb.allowUnverifiedAppPackage: true` only if you use one web installer with packages of other builds.
- [ ] **NSIS per-machine updates:** [per-machine builds set `isAdminRightsRequired` in the update info](./v27-breaking-changes#nsis-per-machine-builds-set-isadminrightsrequired) — their updates are started with `elevate.exe`; with `autoInstallEvent: "onNextLaunch"`, per-machine apps call `installPendingUpdateIfAvailable()` to install a pending update.
- [ ] **electron-updater:** [update credentials stay on the feed's origin](./v27-breaking-changes#update-credentials-stay-on-the-feeds-origin) — if `latest*.yml` points downloads at another origin that needs your `requestHeaders` / `addAuthHeader` credentials or a token in the feed URL's query, serve the files from the feed origin or use pre-signed URLs. A custom provider used with credential headers must override `Provider.feedBaseUrl` (its feed URL, or `null`), else downloads fail with `ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED`.
- [ ] **electron-updater:** [`latest*.yml` no longer includes legacy top-level `path`/`sha512`](./v27-breaking-changes#latestyml-drops-legacy-top-level-pathsha512) — no action for updater >=2.16; migrate any app code reading `info.path`/`info.sha512` to `info.files[0]`, and set `electronUpdaterCompatibility` to a legacy-inclusive range only if you still ship 1.x–2.15 updaters.
- [ ] **Production dependencies:** [redundant production `dependencies` are excluded, not rejected](./v27-breaking-changes#redundant-production-dependencies-are-excluded-not-rejected) — `electron`/`electron-builder` are excluded from the copied `node_modules` (was a hard error) and the `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY` env var is removed (if you used it to bundle `electron-builder`, override `ignoredProductionDependencies` without that name); tune the set via `ignoredProductionDependencies` (e.g. add bundler-inlined deps like `react`). `electron-prebuilt`/`electron-rebuild` no longer error and are NOT excluded — remove them from `dependencies` (they are deprecated) or add them to the list.
- [ ] [Remove `squirrelWindows.customSquirrelVendorDir`](./v27-breaking-changes#squirrelwindowscustomsquirrelvendordir) — the option is deleted; supply a custom Squirrel bundle via `toolsets.squirrel` (a `ToolsetCustom` object) instead (note the different bundle layout).
- [ ] [Migrate any custom NSIS-binary and/or custom NSIS-resources to a `toolsets.nsis` bundle](./v27-breaking-changes#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis) —Potentially relevant for builds reliant on NSIS (`nsisWeb`, `portable`, `nsis`). The `.customNsisBinary` and `.customNsisResources` config properties have been removed in favor of a new NSIS-bundle configured via `toolsets.nsis`. The new toolset consolidates the removed properties and provides both the NSIS binary and resources. Configuring the removed properties fails the build. `customNsisBinary.debugLogging` is now `nsis.installerDebugLogging` (moved by `migrate-schema`); it needs a log-enabled NSIS (`NSIS_CONFIG_LOG=yes`), which the default toolset is not, so it requires a custom `toolsets.nsis` bundle.
- [ ] **Wine on macOS:** [`toolsets.wine` now defaults to the host install](./v27-breaking-changes#toolset-defaults-resolve-to-latest-newest-bundle) — a macOS host that builds Windows targets needs Wine installed (`brew install --cask wine-stable`).
- [ ] **macOS entitlements:** [the bundled default now grants only `allow-jit`](./v27-breaking-changes#macos-default-entitlements-tightened) — if your app loads native modules, frameworks, or plugins signed by another Team ID (or unsigned), add `disable-library-validation` (and `allow-unsigned-executable-memory` if needed) back in `build/entitlements.mac.plist`.
- [ ] **Missing dependencies:** [a production dependency that cannot be resolved now fails the build](./v27-breaking-changes#allowmissingdependencies-now-fails-the-build) — fix the install or set `allowMissingDependencies`.
- [ ] **Native binaries:** [a `.node` addon built for another platform/arch now fails the build](./v27-breaking-changes#native-binaries-are-verified-against-the-target) — rebuild it for the target, or set `nativeModules.verifyNativeBinaries` to `"warn"`/`false`.
- [ ] **`extraFiles` / `extraResources`:** [a `to` that is absolute or escapes the output dir now throws](./v27-breaking-changes#extrafiles--extraresources-destinations-are-validated).
- [ ] **Windows `publisherName`:** [is validated against the signing certificate](./v27-breaking-changes#windows-publishername-is-validated-against-the-signing-certificate) — update `win.sign.publisherName` after a certificate rotation. A custom `win.sign.sign` hook (even with a certificate configured), or a certificate without a CN, now needs `win.sign.publisherName` for builds that write `app-update.yml` (`nsis` / `nsis-web` / `electronUpdaterAware` `appx` with a publish config, including one inferred from a GitHub `repository`); set `win.verifyUpdateCodeSignature: false` instead only if your updates are not Authenticode-signed or you don't use electron-updater.
- [ ] **NSIS scripts:** [rename a `customInstallmode` macro](./v27-breaking-changes#nsis-custominstallmode-macro-guard-casing-corrected) to the documented `customInstallMode`.
- [ ] **Linux launcher:** [`.desktop` `Exec` now runs a `*-launcher` script and `executableArgs` field codes are literal](./v27-breaking-changes#linux-launcher-entrypoint) — update custom `.desktop` / AppArmor / MIME tooling and move `%F`/`%U` to `linux.desktop.entry.Exec`.
- [ ] **Update channels:** [suffixed channels (e.g. `beta-arm64`) now expand to lower channels](./v27-breaking-changes#suffixed-update-channels-now-expand-to-lower-channels) with `generateUpdatesFilesForAllChannels` — set it to `false` to keep one file per suffixed channel.
- [ ] **electron-updater API:** [`quitAndInstall` takes an options object](./v27-breaking-changes#quitandinstall-takes-an-options-object), [`autoInstallOnAppQuit` → `autoInstallEvent`](./v27-breaking-changes#autoinstallevent-replaces-autoinstallonappquit), and [`downloadUpdate()` resolves with `{ updateFile, packageFile }`](./v27-breaking-changes#downloadupdate-resolves-with-a-downloadexecutorresult-object) — update app code.
- [ ] **TypeScript imports:** [renamed type exports](./v27-breaking-changes#renamed-type-exports) (`ElectronDownloadOptions` → `ElectronGetOptions`, …) have no compat aliases.
- [ ] **Plugin/custom-target authors:** [replace `packager.info.X` and `platformSpecificBuildOptions`](./v27-breaking-changes#programmatic-plugin-author-api-changes) with the new pass-through getters.

---

## Step 4: Custom NSIS Migration Guide

If you do do not use either `.customNsisBinary` or `.customNsisResources` configurations on `nsis` `nsis-web` or `portable` in your <=`v26` config there are no actions required and you can skip this step.

To migrate from `.customNsisBinary`/`.customNsisResources` to a new custom unified `toolsets.nsis` bundle requires manual steps.

A [brief overview](./v27-breaking-changes/#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis) of this breaking change is good context before you start this migration. The [NSIS Toolset Bundle docs](../toolsets#custom-nsis-bundle-layout) may also be useful.

### **If you previously used `.customNsisBinary` in `v26` or prior:**

- [ ] Download & extract a copy of the new default NSIS Toolset Bundle from [electron-builder-binaries](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis) as well as the archive you provided via `.customNsisBinary`.
- [ ] Depending on what OS(es) your `.customNsisBinary` was built to support, you may have any subset of these three binaries that need to be copied over.
    - a. `/Bin/makensis.exe`
    - b. `/linux/makensis`
    - c. `/mac/makensis`
- [ ] The NSIS Toolset Bundle you download is a valid `toolsets.nsis` bundle and provides easy locations you can drop any `makensis` binaries you into.
    - a. `/windows/` ← *Copy `/Bin/makensis.exe` From your `.customNsisBinary` archive to this path to migrate*
    - b. `/mac/x64` ← *Copy `/mac/makensis` from a `.customNsisBinary` archive to this path to migrate*
    - c. `/mac/arm64`
    - d. `/linux/x64` ← *Copy `/linux/makensis` from a `.customNsisBinary` archive to this path to migrate*
    - e. `/linux/arm64`
- [ ] When copying your custom binaries across you should overwrite the makensis executables already present in the new-bundle.
- [ ] (optional) - As you can see, the new `toolset.nsis` bundle now provides native makensis binaries for newly supported targets like `Mac (arm64)` & `Linux (arm64)`. You're welcome to provide binaries for as many (or as few) Operating-System/Architecture targets as you want in your custom toolset. You only need to provide a `makensis/makensis.exe` executable for the OS & arch combos you intend to run your builds with.
- [ ] (optional) - The new `toolsets.nsis` is more flexible than previous `.customNsisBinary`/`.customNsisResources` in that the layout is customizable. The following files hook incoming makensis executions. This shim-script-layer opens up a path for custom dynamic logic to adjust binary/resources conditionally and on the fly.
    - a. `/makensis` ← *Bash script, routes incoming makensis execution requests for macOS/Linux builds*
    - b. `/makensis.cmd` ← *Cmd script, routes incoming makensis execution requests for Windows builds running via Command Prompt*
    - c. `/makensis.ps1` ← *PowerShell script, may be used in the future by electron-builder to route incoming windows builds*
- [ ] Include your newly created bundle in your electron builder config via `toolsets.nsis` by either:
    - a. Providing a `file://<path-to-your-uncompressed-nsis-toolset-bundle-folder>` as the `toolsets.nsis.url`.
    - b. Similar to how `.customNsisBinary`/`.customNsisResources` worked - host the bundle as a `.7z` file remotely. Provide the remote url as the `toolsets.nsis.url` & and provide the hash of the archive (either sha256-hex or sha512-base64) to the `toolsets.nsis.checksum` property.


### **If you previously used `.customNsisResources` in `v26` or prior:**

- [ ] Download & extract a copy of the new default NSIS Toolset Bundle from [electron-builder-binaries](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis) as well as the archive you provided via `.customNsisResources`.
- [ ] For the `.customNsisResources`, all 3rd party resource .dlls were located as such:
    - a. `/plugins/x86-ansi/<plugin(s).dll>`
    - b. `/plugins/x86-ansi/<plugin(s).dll>`
    - c. `plugins/<Target>/<plugin(s)>.dll`
- [ ] The NSIS Toolset Bundle you download is a valid `toolsets.nsis` bundle and provides an easy locations you can drop in your resources. The root NSISDIR (NSIS Resources Directory) is now `/windows/`. The move of just resources is pretty straightforward:
    - a. `/windows/Plugins/x86-ansi/<plugin(s).dll>`
    - b. `/windows/Plugins/x86-ansi/<plugin(s).dll>`
    - c. `plugins/<Target>/<plugin(s)>.dll`
- [ ] When copying your custom resources across you should overwrite the makensis executables already present in the new-bundle.
- [ ] The complete common NSIS resources pulled by `makensis` binary runtime are `/Stubs`, `/Include`, `/Contrib`, `/Plugins`. Nothing outside of `/plugins` was previously supported by `.customNsisResources`. With the new NSIS toolset, if you have any of these resources then they can go here:
    - a. `/windows/Plugins/`
    - b. `/windows/Include/`
    - c. `/windows/Contrib/`
    - d. `/windows/Stubs/`
    - e. `/windows/` ← *Root NSISDIR (Nsis Resources Dir) by default*
- [ ] (optional) - The new `toolsets.nsis` is more flexible than previous `.customNsisBinary`/`.customNsisResources` in that the layout is customizable. The following files configure NSISDIR (NSIS Resource Directory). This shim-script-layer opens up a path to dynamically control NSISDIR on the fly.
    - a. `/makensis` ← *Bash script, routes incoming makensis execution requests for macOS/Linux builds*
    - b. `/makensis.cmd` ← *Cmd script, routes incoming makensis execution requests for Windows builds running via Command Prompt*
    - c. `/makensis.ps1` ← *PowerShell script, routes incoming makensis execution requests for Windows builds running via Powershell*
- [ ] Include your newly created bundle in your electron builder config via `toolsets.nsis` by either:
    - a. Providing a `file://<path-to-your-uncompressed-nsis-toolset-bundle-folder>` as the `toolsets.nsis.url`.
    - b. Similar to how `.customNsisBinary`/`.customNsisResources` worked - host the bundle as a `.7z` file remotely. Provide the remote url as the `toolsets.nsis.url` & and provide the hash of the archive (either sha256-hex or sha512-base64) to the `toolsets.nsis.checksum` property.

- **The `toolsets.nsis` bundle must carry `elevate.exe` at the bundle's root directory** You can grab a copy one from the root of Electron Builder's default NSIS Toolset Bundle provided in [electron-builder-binaries](https://github.com/electron-userland/electron-builder-binaries/tree/master/artifacts/nsis). It is used for privledge-control features by the UAC nsis plugin.

- **For more information/guidance you can checkout the [`toolsets.nsis` documentation](../toolsets#custom-nsis-bundle-layout)**

## Summary checklist

Run `electron-builder migrate-schema` first — it handles the items marked ✓ automatically.

**Runtime and CI**
- [ ] Node.js runtime updated to >=22.12.0
- [ ] CI `node-version` updated to `'22'`
- [ ] `"engines"` field in `package.json` updated (if declared)

**Build config — auto-migrated by `migrate-schema`**
- [ ] ✓ `electronCompile` removed (if present)
- [ ] ✓ `disableDefaultIgnoredFiles` removed (if present; re-include specific files via a `files` glob such as `**/*.obj`)
- [ ] ✓ `framework`, `nodeVersion`, `launchUiVersion` removed (if present)
- [ ] ✓ `npmSkipBuildFromSource` → `nativeModules.buildDependenciesFromSource` (if present)
- [ ] ✓ `buildDependenciesFromSource`, `nodeGypRebuild`, `npmRebuild` moved into `nativeModules`; `nativeRebuilder` → `rebuildMode`
- [ ] ✓ Legacy asar keys + `asarUnpack` → `asar.unpack` (platform-level too); `disableSanityCheckAsar` → `asar.disableSanityCheck`; `disableAsarIntegrity` → `asar.disableIntegrity`; `asar: true` removed
- [ ] ✓ macOS signing fields → `mac.sign.*`; `signIgnore` → `sign.ignore` (also `mas`/`masDev`)
- [ ] ✓ `mac.universal` fields → `mac.universal.*`
- [ ] ✓ `electronDownload` → `electronGet` (verify dropped `cache`/`customDir`/`customFilename`/`strictSSL`/`platform`/`arch`/`version`/`force`)
- [ ] ✓ `appImage.systemIntegration` removed (if present)
- [ ] ✓ `linux.syncDesktopName` removed (if present; always synced in v27 — set `desktopName` if you had it `false`)
- [ ] ✓ `GithubOptions.vPrefixedTagName` → `tagNamePrefix` (if present)
- [ ] ✓ `win.azureSignOptions` / `win.signtoolOptions` → `win.sign`; `win.signExecutable` / `win.signAndEditExecutable` handled
- [ ] ✓ `snap` restructured to `snapcraft` (verify the assumed `base` if none was set)
- [ ] ✓ `helper-bundle-id` → `mac.helperBundleId`; `squirrelWindows.noMsi` → `msi`; root-level `directories` → `build.directories`
- [ ] ✓ `toolsets.*: null` removed; `toolsets.appimage: "1.0.2"` → `"1.0.3"`; `nativeRebuilder: "legacy"` dropped

**Runtime defaults & auto-update**
- [ ] DMG `filesystem` default is now APFS — set `dmg.filesystem: "HFS+"` only for pre-10.13 macOS compatibility
- [ ] macOS default entitlements now grant only `allow-jit` — add `allow-unsigned-executable-memory` / `disable-library-validation` back in `build/entitlements.mac.plist` (and `.inherit.plist` for nested binaries) only if your app loads foreign-signed or unsigned binaries; MAS builds without `build/entitlements.mas.plist` now get osx-sign's sandboxed default
- [ ] [electron-updater `disableWebInstaller` defaults to `true`](./v27-breaking-changes#disablewebinstaller-defaults-to-true) — web-installer updates are rejected unless it is `false`; v27+ `nsis-web` installs opt in automatically, set `disableWebInstaller: false` for older web-installer installs or when switching from `nsis` to `nsis-web`; the `nsis-web` installer verifies its app package (`nsisWeb.allowUnverifiedAppPackage` opts out)
- [ ] [A code-signed Windows build that writes `app-update.yml` needs a publisher name](./v27-breaking-changes#windows-publishername-is-validated-against-the-signing-certificate) — set `win.sign.publisherName` for a custom `win.sign.sign` hook or a certificate without a CN, or `win.verifyUpdateCodeSignature: false` if your updates are not Authenticode-signed
- [ ] [NSIS per-machine builds set `isAdminRightsRequired`](./v27-breaking-changes#nsis-per-machine-builds-set-isadminrightsrequired) — their updates are started with `elevate.exe` and not installed automatically at launch; with `autoInstallEvent: "onNextLaunch"`, call `installPendingUpdateIfAvailable()`
- [ ] [Update credentials stay on the feed's origin](./v27-breaking-changes#update-credentials-stay-on-the-feeds-origin) — `requestHeaders` / `addAuthHeader` credentials and the feed URL's query are not sent to downloads on another origin
- [ ] `latest*.yml` drops legacy top-level `path`/`sha512`; default `electronUpdaterCompatibility` is now `>=2.16` — read `files[]` instead; pin a legacy-inclusive range only for legacy embedded updaters
- [ ] Production `dependencies` that electron-builder provides or that your bundler inlines (`electron`/`electron-builder`/`react`/…) are excluded from the copied `node_modules` instead of erroring; `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY` removed and `electron-prebuilt`/`electron-rebuild` no longer guarded — tune via `ignoredProductionDependencies`
- [ ] Building Windows targets on macOS uses the host Wine (`toolsets.wine` defaults to `"system"`) — install Wine on macOS build machines

**Manual steps** — see [Step 3](#step-3-apply-the-manual-steps) & [Step 4](#step-4-custom-nsis-migration-guide) above.

**Plugin and custom-target authors** — see the [programmatic API changes](./v27-breaking-changes#programmatic-plugin-author-api-changes).

---

## Full breaking-changes reference

Every change above is documented in detail — with rationale (design notes) and before/after for each — on the **[v27 Breaking Changes](./v27-breaking-changes)** page.

---

### Migration V27 Breaking Changes (`website/docs/migration/v27-breaking-changes.md`)

---
title: "v27 Breaking Changes"
---

# v27 Breaking Changes

:::danger[Read this before upgrading to v27]
v27 moves the entire electron-builder ecosystem to **native ES modules**, raises the minimum runtime to **Node.js 22.12.0**, and **hard-deletes the deprecated APIs** that accumulated since v22. Every breaking change in the release is catalogued on this page.

**Most projects need only a Node.js bump plus one command.** Run the automated migrator and follow the step-by-step **[migration walkthrough →](./v26-to-v27)**.
:::

This page is the **canonical reference for _what_ changed** in v27. For the **_how_ — the ordered upgrade steps, the automated migrator, and the checklist** — see the [v26 → v27 migration walkthrough](./v26-to-v27).

- The `build()` API is unchanged; configuration keys that were renamed or restructured are rewritten automatically by `migrate-schema` (see each entry below) and keep their v26 behavior, and exported types that survive keep their shape.
- CJS `require()` continues to work without code changes on supported Node.js versions.
- Every **config-level** breaking change below that has a mechanical v27 equivalent is rewritten automatically by `electron-builder migrate-schema` (look for the **Auto** ✓ marker). The config keys it cannot rewrite, [`squirrelWindows.customSquirrelVendorDir`](#squirrelwindowscustomsquirrelvendordir) and a custom NSIS bundle in [`nsis.customNsisBinary` / `customNsisResources`](#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis), are reported with a warning. Runtime, CLI, env-var, and behavior changes require manual action — see [what `migrate-schema` does and does not do](#new-command-migrate-schema).

:::tip[Run the automated migrator first]
```bash
electron-builder migrate-schema            # apply changes in place
electron-builder migrate-schema --dry-run  # preview only
```
This handles every change marked **Auto ✓** below. See the [walkthrough](./v26-to-v27#step-1-run-the-automated-migrator) for flags and serialization caveats.
:::

:::info[Toolsets now default to the newest bundle ("latest")]
In v27 every `toolsets.*` property defaults to **`"latest"`** — an **unset** property and the literal `"latest"` both resolve to the **newest published bundle** for that toolset (previously each property defaulted to a fixed pinned version; `null` is no longer accepted). No config change is required, but the effective defaults moved:

- **`wine` → `"system"`** — the **host-installed `wine`** on `PATH`, on macOS as well as Linux (v26 downloaded a Wine 4.0.1 bundle on macOS). **Building Windows targets on macOS now requires Wine on the host** (`brew install --cask wine-stable`). The `"1.0.1"` Wine 11.0 bundle still downloads when pinned explicitly, but the published bundle ships no PE builtins, so it cannot run Windows tools on its own.
- **`winCodeSign` → `1.3.0`** — Windows Kits 10.0.26100.0, `osslsigncode` 2.11 (native arm64), and the Azure Trusted Signing `dlib` + .NET 8 payload.
- **`appimage` → `1.1.0`** — static FUSE3-compatible runtime; adds `unsquashfs` support.
- `icons` → `1.2.3` — newer `wasm-vips` / `@resvg/resvg-wasm` bundle that writes 16/32 px ICNS entries as ARGB (was `1.1.0`).
- `linuxToolsMac` → `1.0.1` (runs on macOS 15+) and `sevenZip` → `1.0.1` (correct per-arch Windows binaries).
- `nsis` (`1.2.1`) and `fpm` (`2.2.1`) are unchanged; the new `squirrel` toolset resolves to `1.1.1`.

To stay on a legacy bundle, pin the toolset to `"0.0.0"`. Because `winCodeSign` now defaults to `1.3.0`, **Azure Trusted Signing uses the faster `signtool /dlib` path out of the box**. Full breakdown in [Toolsets & environment variables](#toolsets-environment-variables).
:::

---

## Breaking changes at a glance

:::tip[Already ran migrate-schema?]
Rows marked **Auto ✓** are rewritten for you. For the shortlist of changes the migrator can **not** apply — the new defaults and runtime behaviors you may still trip over — see **[What's New in v27 → new defaults & behavior changes](./whats-new-v27#new-defaults-and-behavior-changes-you-may-trip-over)**.
:::

| Change | Auto | Action required |
|--------|:---:|----------------|
| [Node.js >=22.12.0 required](#nodejs-22120-required) | — | Update your runtime and CI Node version |
| [All packages are native ESM](#native-esm-output) | — | None — `require()` still works on Node >=22.12 |
| [`electron-forge-maker-*` are now ESM](#electron-forge-maker-plugins) | — | None — same API, same export shape |
| [`electronCompile` removed](#electroncompile) | ✓ | Remove from config; migrate off `electron-compile` |
| [`disableDefaultIgnoredFiles` removed](#disabledefaultignoredfiles) | ✓ | Removed automatically; re-include specific files via a `files` glob (e.g. `**/*.obj`) |
| [`framework` / `nodeVersion` / `launchUiVersion` removed](#framework-nodeversion-launchuiversion) | ✓ | Removed automatically (Electron is the only framework) |
| [ProtonFramework & LibUiFramework removed](#removed-exports) | — | Migrate to an Electron-based build setup |
| [`appImage.systemIntegration` removed](#appimagesystemintegration) | ✓ | Removed automatically |
| [`npmSkipBuildFromSource` removed](#npmskipbuildfromsource) | ✓ | Replaced by `nativeModules.buildDependenciesFromSource` |
| [Native-module options grouped under `nativeModules`](#native-module-options-nativemodules) | ✓ | `nativeRebuilder` → `rebuildMode`; the `"legacy"` rebuilder is removed (dropped with a warning) |
| [ASAR options consolidated under `asar`](#asar-options-asar) | ✓ | `asarUnpack` → `asar.unpack`, etc., including platform-level `mac.asarUnpack` → `mac.asar.unpack` (`asar: true` is still valid, just redundant) |
| [`asar.ordering` is now applied](#asar-ordering-is-now-applied) | — | v26 ignored the option; v27 packs the listed files first and fails when the file is missing. Remove the option to keep the dependencies-first layout |
| [macOS signing consolidated under `mac.sign`](#macos-signing-macsign) | ✓ | `identity`/`entitlements`/`hardenedRuntime`/… → `mac.sign.*`; `signIgnore` → `sign.ignore` |
| [`mac.universal` options consolidated](#macuniversal) | ✓ | `mergeASARs`/`singleArchFiles`/`x64ArchFiles` → `mac.universal.*` |
| [Windows signing unified under `win.sign`](#windows-signing-winsign) | ✓ | Discriminated union `type: "signtool" \| "hsm" \| "pkcs11" \| "azure"` |
| [`win.signExecutable` / `win.signAndEditExecutable` removed](#winsignexecutable-winsignandeditexecutable-removed) | ✓ | `false` → `win.sign: false`; resource editing always runs |
| [`electronDownload` → `electronGet`](#electrondownload-electronget) | ✓ | Reshaped to `@electron/get` options; some legacy fields dropped (warned) |
| [`snap` config key removed](#snap-snapcraft) | ✓ | Restructured to `snapcraft` with an explicit `base` |
| [Root-level `directories` removed](#root-level-directories-in-packagejson) | ✓ | Move under `build.directories` |
| [`build.helper-bundle-id` removed](#buildhelper-bundle-id) | ✓ | Moved to `mac.helperBundleId` |
| [`squirrelWindows.noMsi` removed](#squirrelwindowsnomsi) | ✓ | Replaced by `msi` (inverted) |
| [`squirrelWindows.customSquirrelVendorDir` removed](#squirrelwindowscustomsquirrelvendordir) | — | Supply a custom Squirrel bundle via `toolsets.squirrel` (a `ToolsetCustom` object); `migrate-schema` warns but cannot rewrite it (different bundle layout) |
| [`.customNsisBinary` / `.customNsisResources` removed](#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis) | partial | Originally avail on nsis, nsisWeb, portable. Now combined and moved to a [NSIS Toolset Bundle](../toolsets#custom-nsis-bundle-layout) configured under `toolsets.nsis`. **Manual action required** - [A complete migration guide is available](./v26-to-v27#step-4-custom-nsis-migration-guide). A config that still sets removed key(s) fails the build with a message naming the replacement. (`migrate-schema` warns, cannot convert it).  |
| [`GithubOptions.vPrefixedTagName` removed](#githuboptions-gitlaboptions-vprefixedtagname) | ✓ | Use `tagNamePrefix`; an empty `tagNamePrefix: ""` is now honored (v26 ignored it) |
| [`GitlabOptions.vPrefixedTagName` retained](#githuboptions-gitlaboptions-vprefixedtagname) | — | None — still functional; the migrator leaves GitLab entries untouched |
| [`devMetadata` / `extraMetadata` in `PackagerOptions` removed](#devmetadata-extrametadata-programmatic-packageroptions) | — | Use `config` / `config.extraMetadata` |
| [Implicit `--publish` removed](#implicit-publish-removed) | — | Pass `--publish` explicitly |
| [`--em.build` / `--em.directories` CLI flags removed](#removed-flags-embuild-emdirectories) | — | Use `-c` / `-c.directories` |
| [`linux.syncDesktopName` removed](#linuxsyncdesktopname-always-synced) | ✓ | Removed automatically; behaviour is always on. If it was `false`, set `desktopName` to control the filename |
| [Linux maintainer-script EJS syntax removed](#linux-maintainer-script-ejs-template-syntax) | — | Use `${var}` instead of `<%= var %>` |
| [NSIS file-association ProgID format changed](#nsis-file-association-progid-format-changed) | — | Update custom NSIS scripts that hard-code the old ProgID |
| [NSIS `customInstallMode` macro guard casing corrected](#nsis-custominstallmode-macro-guard-casing-corrected) | — | Rename a `customInstallmode` (lowercase `m`) macro definition in custom NSIS scripts to the documented `customInstallMode` |
| [Toolset defaults resolve to `"latest"`](#toolset-defaults-resolve-to-latest-newest-bundle) | — | No action; pin to `"0.0.0"` to restore a legacy bundle. **Wine is now the host install** — building Windows targets on macOS needs Wine installed |
| [`toolsets.*: null` rejected; `appimage: "1.0.2"` retired](#toolset-defaults-resolve-to-latest-newest-bundle) | ✓ | `null` entries are removed (unset = `"latest"`); `"1.0.2"` becomes `"1.0.3"` |
| [Toolset env-var overrides removed](#toolset-env-var-overrides-removed) | — | Replace `APPIMAGE_TOOLS_PATH`, `ELECTRON_BUILDER_NSIS_DIR`, `USE_SYSTEM_WINE`, … with `toolsets.X: { url, checksum }` — setting a removed variable now **fails the build** |
| [`CI_BUILD_TAG` env var removed](#ci_build_tag-environment-variable) | — | Use `CI_COMMIT_TAG` |
| [Azure Trusted Signing `/dlib` is the default](#azure-trusted-signing-signtool-dlib-is-the-default) | — | Pin `winCodeSign` below `1.3.0` only to force the legacy PowerShell path |
| [New: `win.target: "msix"` (beta)](#new-msix-target-beta) | — | Optional, additive — the default `winCodeSign` works (only the legacy `0.0.0` bundle is rejected) |
| [`PlatformPackager.info` & `platformSpecificBuildOptions` now `protected`](#programmatic-plugin-author-api-changes) | — | Plugin authors: hard break — `.info` no longer compiles externally; use the new pass-through getters |
| [Linux `.desktop` `Exec` now runs a generated `*-launcher` script](#linux-launcher-entrypoint) | — | Update custom `.desktop`/AppArmor/MIME tooling that hard-codes the `Exec` command |
| [`node_modules` arch/os-filtered on every build](#node_modules-are-now-archos-filtered-on-every-build) | — | Awareness — packages whose `cpu`/`os` mismatch the target are now excluded |
| [`arch: "all"` now expands to x64 + arm64; 32-bit fails fast on Electron 44+](#arch-all-now-expands-to-x64-and-arm64-32-bit-fails-fast-on-electron-44) | — | `arch: "all"` drops `ia32` — request `ia32` explicitly; ia32/armv7l require `electronVersion` <= 43.x |
| [macOS `productName`/`executableName` validated, not silently sanitized](#macos-productname-and-executablename-are-validated-not-sanitized) | — | A name needing filename sanitization now throws — pick a name that needs none |
| [macOS default entitlements tightened](#macos-default-entitlements-tightened) | — | The bundled default now grants only `allow-jit`; add `allow-unsigned-executable-memory` / `disable-library-validation` back in `build/entitlements.mac.plist` (and `.inherit.plist`) only if your app needs them |
| [Bitbucket Cloud publishing: token without username → Bearer auth](#bitbucket-cloud-publishing-token-without-username-uses-bearer-auth) | — | Set `BITBUCKET_USERNAME` if your token is an app password / API token |
| [Redundant production `dependencies` excluded, not rejected](#redundant-production-dependencies-are-excluded-not-rejected) | — | `electron`/`electron-builder` are excluded from the copied `node_modules` (was a hard error); tune the set via `ignoredProductionDependencies`. If you set `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY` (removed) to bundle `electron-builder`, override the list instead; `electron-prebuilt`/`electron-rebuild` no longer error and now ship if declared — remove them from `dependencies` |
| [`allowMissingDependencies` now fails the build](#allowmissingdependencies-now-fails-the-build) | — | A missing production dependency is a hard error; set `allowMissingDependencies: true` to restore v26 warn-only behavior |
| [Native binaries are verified against the target](#native-binaries-are-verified-against-the-target) | — | A `.node` addon built for another platform/arch now fails the build; set `nativeModules.verifyNativeBinaries: "warn"` (or `false`) to package anyway |
| [`extraFiles` / `extraResources` `to` is validated](#extrafiles--extraresources-destinations-are-validated) | — | An absolute `to`, or one escaping the output dir, now throws |
| [Windows `publisherName` validated against the certificate](#windows-publishername-is-validated-against-the-signing-certificate) | — | Update `publisherName` after a certificate rotation, or drop it. Custom `win.sign.sign` hooks (even with a certificate configured) and certificates without a CN now **require** it when `app-update.yml` is written (or set `win.verifyUpdateCodeSignature: false` if your updates are not Authenticode-signed) |
| [Custom Windows signing hook moved to `win.sign.sign`](#custom-windows-signing-hooks-move-to-winsignsign) | ✓ | `win.signtoolOptions.sign` moves with the rest of `signtoolOptions` to `win.sign.sign` |
| [macOS names are no longer NFD-normalized](#macos-productname-and-executablename-are-validated-not-sanitized) | — | Update tooling that matches the normalized on-disk bundle filename |
| [Linux `executableArgs` field codes are now literal](#linux-launcher-entrypoint) | — | Remove `%F`/`%U` from `executableArgs`; use `linux.desktop.entry.Exec` |
| [`http-parser` dropped from default pacman `depends`](#pacman-default-dependencies-changed) | — | Add it back with `"depends": ["default", "http-parser"]` only if your app really needs it |
| [New: Cloudflare R2 publish provider](#new-cloudflare-r2-publish-provider-additive) | — | None — additive; needs `accountId` and an `https` `publicUrl` |
| [New command: `migrate-schema`](#new-command-migrate-schema) | — | None — run it to apply every **Auto ✓** change above |
| [DMG `filesystem` defaults to APFS](#dmg-filesystem-defaults-to-apfs) | — | Set `dmg.filesystem: "HFS+"` only if you need pre-10.13 macOS compatibility |
| [`disableWebInstaller` defaults to `true` (electron-updater)](#disablewebinstaller-defaults-to-true) | — | Web-installer updates are rejected (`ERR_UPDATER_WEB_INSTALLER_DISABLED`) unless `disableWebInstaller` is `false`; v27+ `nsis-web` installs opt in automatically; older installs, and apps switching from `nsis` to `nsis-web`, need `disableWebInstaller: false` |
| [`nsis-web` installer verifies and installs its own copy of the app package](#disablewebinstaller-defaults-to-true) | — | A `--package-file` package or a publish-URL download that does not match, or a `--package-file` package that cannot be copied, aborts the install (exit code `2`); the local package file is now left in place. Only if you run one web installer with a `--package-file` from another build: set `nsisWeb.allowUnverifiedAppPackage: true` |
| [NSIS: per-machine builds set `isAdminRightsRequired` in the update info](#nsis-per-machine-builds-set-isadminrightsrequired) | — | None for most apps. Their updates are started with `elevate.exe`; with `autoInstallEvent: "onNextLaunch"`, per-machine apps call `installPendingUpdateIfAvailable()` to install a pending update |
| [Update credentials are only sent to the feed's origin (electron-updater)](#update-credentials-stay-on-the-feeds-origin) | — | Only if `latest*.yml` points downloads at another origin (another host, or another port or scheme) that needs your `requestHeaders` / `addAuthHeader` credentials or a token in the feed URL's query: serve the files from the feed origin or use pre-signed URLs. A custom provider used with credential headers must override `Provider.feedBaseUrl` (else `ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED`) |
| [Suffixed channels expand to lower channels](#suffixed-update-channels-now-expand-to-lower-channels) | — | Only with `generateUpdatesFilesForAllChannels`: a `beta-*`/`latest-*` channel now writes 2–3 yml files instead of 1 |
| [Signed update manifests are required](#signed-update-manifests-are-required) | — | Generate a key with `electron-builder create-update-key` and set `ELECTRON_BUILDER_UPDATE_SIGN_KEY`, or set `updateManifest: false` to keep publishing unsigned manifests |
| [`app-update.yml` feed follows the update-writing targets](#app-updateyml-feed-follows-the-update-writing-targets) | — | Only with target-level `publish` (e.g. `nsis.publish`), targets of one app with different feeds, or a first provider with `publishAutoUpdate: false`: check which feed installs will poll |
| [`latest*.yml` drops legacy top-level `path`/`sha512`](#latestyml-drops-legacy-top-level-pathsha512) | — | None for electron-updater >=2.16 (all modern clients); set `electronUpdaterCompatibility` to a legacy-inclusive range only if you still ship apps embedding electron-updater 1.x–2.15 |
| [`quitAndInstall` takes an options object (electron-updater)](#quitandinstall-takes-an-options-object) | — | Replace positional args: `quitAndInstall(true, false)` → `quitAndInstall({ isSilent: true, isForceRunAfter: false })` |
| [`autoInstallOnAppQuit` replaced by `autoInstallEvent` enum (electron-updater)](#autoinstallevent-replaces-autoinstallonappquit) | — | `autoInstallOnAppQuit = false` → `autoInstallEvent = "manual"`; default `"onQuit"` preserves behavior |
| [`downloadUpdate()` resolves with an object, not an array (electron-updater)](#downloadupdate-resolves-with-a-downloadexecutorresult-object) | — | `const [installer] = await downloadUpdate()` → `const { updateFile } = await downloadUpdate()`; same for `checkForUpdates()` → `downloadPromise` |
| [`verifyUpdateCodeSignature` renamed and returns a result object (electron-updater)](#verifyupdatecodesignature-renamed-to-verifyupdatefileauthenticodesignature) | — | `NsisUpdater.verifyUpdateCodeSignature` → `verifyUpdateFileAuthenticodeSignature`; return `{ response: "success" }` / `{ response: "failure", message }` instead of `null` / `string` |
| [Renamed type exports (`ElectronDownloadOptions`, `WindowsAzureSigningConfiguration`, …)](#removed-exports) | — | Import the new names — no compat aliases |
| [`SnapOptions`, `ProtonFramework`, `LibUiFramework` exports removed](#removed-exports) | — | Use the `snapcraft` config shape / Electron framework |

---

## Runtime & ESM {#runtime-esm}

### Node.js >=22.12.0 required

v27 requires **Node.js 22.12.0 or later**. This is the version where Node's [`require(esm)`](https://nodejs.org/en/blog/release/v22.12.0) support was stabilized (no flags needed), which allows both CJS and ESM consumers to use these packages without any code changes.

```json
{
  "engines": {
    "node": ">=22.12.0"
  }
}
```

See the walkthrough for [updating your runtime, CI, and Docker images](./v26-to-v27#step-2-update-nodejs).

### Native ESM output

electron-builder packages now ship as native ES modules. On **Node >=22.12.0** both styles continue to work:

```js
// CJS require() — still works
const { build, Platform } = require("electron-builder")

// ESM import — now the preferred style
import { build, Platform } from "electron-builder"
```

If your project uses `"type": "module"` or ESM imports, no changes are needed. If it uses CJS on Node >=22.12, `require()` continues to work as before. The `build()` function signature, all configuration options, and all exported types are otherwise unchanged.

**`moduleResolution`** — v27 packages include `exports` maps and full TypeScript declarations. `"node"` (legacy), `"node16"`/`"nodenext"`, and `"bundler"` (recommended) all work.

### `electron-forge-maker-*` plugins {#electron-forge-maker-plugins}

The four Forge maker plugins (`electron-forge-maker-appimage`, `electron-forge-maker-nsis`, `electron-forge-maker-nsis-web`, `electron-forge-maker-snap`) are now native ES modules. The public API is identical — Electron Forge loads them via dynamic `import()` internally, so no config changes are required.

---

## Removed configuration options

### `electronCompile`

The `electronCompile` configuration option has been **removed**. `electron-compile` is an unmaintained library with no releases since 2019.

```json5
{ "build": { "electronCompile": true } }  // ← delete this line
```

If your project relies on `electron-compile` for source compilation, migrate to a modern build tool before upgrading:

- **[electron-vite](https://electron-vite.org/)** — recommended; fast, first-class ESM and Electron integration
- **[esbuild](https://esbuild.github.io/)** — extremely fast, minimal configuration
- **[webpack](https://webpack.electron.build/)** — mature, widely used

### `framework` / `nodeVersion` / `launchUiVersion` {#framework-nodeversion-launchuiversion}

These three fields are removed. Only Electron is supported as a target framework — `proton`/`proton-native` and `libui` support has been removed (see [Removed exports](#removed-exports)).

```json5
{
  "build": {
    "framework": "electron",    // ← delete; "electron" was and remains the default
    "nodeVersion": "current",   // ← delete; no effect
    "launchUiVersion": "0.1.0"  // ← delete; no effect
  }
}
```

### `disableDefaultIgnoredFiles`

The `disableDefaultIgnoredFiles` option has been **removed**. To include a file that is excluded by default (for example a Wavefront `.obj` 3D model, or any other default-excluded extension/name), add an explicit `files` glob that targets it — an explicit include now overrides the matching default exclusion:

```json5
{
  "build": {
    "disableDefaultIgnoredFiles": true,  // ← delete this line
    "files": [
      "**/*",
      "**/*.obj"  // ← add a glob for the default-excluded files you want to keep
    ]
  }
}
```

Broad patterns such as `**/*` still honor the defaults; only a pattern that names the extension or directory concretely opts it back in.

### `appImage.systemIntegration`

Removed. Desktop integration is handled automatically by [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher).

### `npmSkipBuildFromSource`

Removed. Use `buildDependenciesFromSource` (now under [`nativeModules`](#native-module-options-nativemodules)). It is the logical inverse:

```json5
// v26
{ "npmSkipBuildFromSource": true }
// v27 equivalent
{ "nativeModules": { "buildDependenciesFromSource": false } }
```

### `asar: true` (still valid — no action needed)

**`asar: true` remains valid in v27.** The type is `AsarOptions | boolean | null`, the schema accepts a boolean, and the runtime maps `true` to `{}` — so an existing `asar: true` keeps working exactly as before. It is simply redundant, because ASAR is enabled by default:

```json5
{ "build": { "asar": true } }  // Still works — but redundant
{ "build": { "asar": {} } }    // Equivalent; or omit the key entirely
```

`migrate-schema` drops the redundant root-level `true` (folding any other ASAR keys into an `asar` object instead). The full ASAR restructuring is documented under [ASAR options → `asar`](#asar-options-asar).

### Root-level `directories` in `package.json`

Specifying `directories` at the root of `package.json` is removed. Move it under the `build` key.

```json
{ "name": "my-app", "directories": { "output": "dist" } }              // Before
{ "name": "my-app", "build": { "directories": { "output": "dist" } } } // After
```

### `build.helper-bundle-id`

The hyphenated root-level `helper-bundle-id` is removed. Use `mac.helperBundleId`.

```json5
{ "build": { "helper-bundle-id": "com.example.helper" } }            // Before
{ "build": { "mac": { "helperBundleId": "com.example.helper" } } }   // After
```

### `squirrelWindows.noMsi`

The `noMsi` boolean is removed in favor of its inverse, `msi`.

```json5
{ "build": { "squirrelWindows": { "noMsi": true } } }   // Before
{ "build": { "squirrelWindows": { "msi": false } } }    // After
```

### `squirrelWindows.customSquirrelVendorDir`

Removed. v27 inlines the Squirrel.Windows installer logic and drops the `electron-winstaller` npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained `squirrel.windows` electron-builder-binaries bundle. To pin a version or supply a custom/local bundle — for example an air-gapped mirror — use the standard `toolsets.squirrel` setting instead, the same way you would pin `toolsets.nsis` or `toolsets.winCodeSign`.

The shape and behaviour differ, so this is not a 1-to-1 rename:

- The old `customSquirrelVendorDir` pointed at a directory **whose contents were the vendor files** (`Squirrel.exe`, `nuget.exe`, …) and was copied verbatim, bypassing all provisioning.
- `toolsets.squirrel` accepts a version pin (`"1.1.1"` / `"latest"`) **or** a `ToolsetCustom` object whose `url` points at a bundle that **contains an `electron-winstaller/vendor/` subtree**. The bundle still goes through normal provisioning: on every platform `rcedit.exe` is supplied from the `winCodeSign` toolset (it runs under Wine on non-Windows hosts), and for `msi: true` the shared WiX toolset is merged in.

```json5
// Before (removed):
{ "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } }
// After: a custom toolset bundle whose electron-winstaller/vendor/ holds the binaries.
// A bare file:// directory is used as-is (no checksum); a remote/archive url needs a checksum.
{ "toolsets": { "squirrel": { "url": "file:///abs/path/to/squirrel-toolset" } } }
```

`migrate-schema` cannot rewrite this key (the bundle layout differs), so it warns and leaves it in place; the next build then fails with a message pointing at `toolsets.squirrel`. Remove the key to use the default bundle, or point `toolsets.squirrel` at a bundle with the layout above.

For a fully offline build, seed the `squirrel.windows@<version>` archive (plus `winCodeSign` and, for MSI, the WiX bundle) in the toolset cache or point `toolsets.squirrel` at a local bundle — see [Offline / Air-Gapped Builds](../tutorials/offline-air-gapped-builds.md). Nothing is downloaded outside the checksummed toolset bundles.

### customNsisBinary and customNsisResources replaced by `toolsets.nsis`

Removed (`.customNsisBinary` / `customNsisResources`), on `nsis`, `nsisWeb` and `portable` configs, together with the exported `CustomNsisBinary` type.

The 1 to 1 replacement for either/both is the new [`toolsets.nsis`](../toolsets#custom-nsis-bundle-layout) bundle. A config that still sets either key fails the build with a message naming the replacement.

In general, the NSIS resources/binary are married quite closely and in nuanced ways - the new unified structure gives easy access to both in the same place and simplifies build-flow.

See [Custom NSIS Migration Guide](./v26-to-v27#step-4-custom-nsis-migration-guide) & [NSIS toolset documentation](../toolsets.md#custom-nsis-bundle-layout) for more details.

- **A Custom NSIS Binary** Custom compiled NSIS binaries are practical to facillitate builds with unique requirements (like bypassing the 2Gib limit on installer.exe generation). A custom NSIS binary can also be used to adjust to the appearance of the installer in a variety of ways through the many images that get baked into it. If you were using (`.customNsisBinary`) in `v26`, now you must provide that binary via a `toolsets.nsis` bundle for parity on `v27`.
- **Custom NSIS Resources** Custom NSIS resources are required to expose any additional 3rd party plugins that may be needed in custom `.nsh` scripts. If you were using (`.customNsisResources`) in `v26`, now you must provide that resource via a `toolsets.nsis` bundle for parity on `v27`.
- **`customNsisBinary.debugLogging`** moves to `nsis.installerDebugLogging` (or `nsisWeb.installerDebugLogging`). It never had an effect on `portable`, which has no equivalent.



```json5
// Before (v26):
// Custom NSIS artifacts provided via 2 seperate bundles
{
  "nsis": {
    "customNsisBinary": {
      "url": "https://example.com/my-nsis-executable.7z",
      "checksum": "<checksum>",
      "version": "3.10",
      "debugLogging": true
    },
    "customNsisResources": {
      "url": "https://example.com/my-nsis-resources.7z",
      "checksum": "<checksum>",
      "version": "3.4.1"
    }
  }
}
// After:
// Custom NSIS artifacts provided by a unified toolset
// Note: Debug-logging still controlled on NSIS and not by toolsets.nsis
{
  "toolsets": {
    "nsis": {
      "url": "https://example.com/my-nsis.7z",
      "checksum": "<checksum>",
      "version": "3.10"
    }
  },
  "nsis": { "installerDebugLogging": true }
}
```

You can see the relative escape hatch of providing an archive for the full toolset is generally the same from `v26` -> `v27` simply in a more consolidated form.

Of note, `v27` also accepts a `file://` path in the `url` field. A `file://` path to an unpacked bundle directory needs no checksum and is used as-is with no caching; a `file://` path to an archive still needs a `checksum`.

- `https://` — downloaded and cached locally; the archive is extracted before use.
- `file:///absolute/path` — used directly; relative paths must be within the project resources dir.

**`installerDebugLogging` needs a log-enabled NSIS**, that is `makensis` and its stubs compiled with `NSIS_CONFIG_LOG=yes`; any other `makensis` rejects the `LogSet` it emits. The bundled `toolsets.nsis` versions are not log-enabled, so the build fails with a configuration error when `installerDebugLogging` is set without a custom `toolsets.nsis` bundle. This is unchanged from v26, where the option also only worked with your own `customNsisBinary`. When building on Linux or macOS, the bundle's `makensis` for that host must be log-enabled too.

`migrate-schema` moves `debugLogging` to `installerDebugLogging` (dropping it, with a warning, for `portable`) and removes a `customNsisBinary` that sets nothing else. It cannot convert a custom bundle, so it leaves a `customNsisBinary` with a `url` / `checksum` / `version`, and any `customNsisResources`, in place and warns; the build then fails with the message above until you move them.

### `GithubOptions` / `GitlabOptions` `vPrefixedTagName` {#githuboptions-gitlaboptions-vprefixedtagname}

The `vPrefixedTagName` boolean on `GithubOptions` is removed. Use `tagNamePrefix` to control the tag prefix.

```json5
{ "publish": { "provider": "github", "vPrefixedTagName": false } }   // Before
{ "publish": { "provider": "github", "tagNamePrefix": "" } }         // After (empty string = no prefix)
```

To keep the default `v` prefix, simply remove `vPrefixedTagName` — `tagNamePrefix` defaults to `"v"`.

**An empty `tagNamePrefix` is now honored.** v26 ignored `tagNamePrefix: ""` and still tagged `v1.2.3` (unless `vPrefixedTagName: false`); v27 uses it as-is and tags `1.2.3`. `migrate-schema` rewrites a GitHub `tagNamePrefix: ""` without `vPrefixedTagName: false` to `"v"` — keeping your existing tag names — and warns so you can set it back if unprefixed tags are what you wanted. A non-empty `tagNamePrefix` always won in v26 and is kept. The migrator applies to GitHub publish entries anywhere in the config (root, `mac`/`win`/`linux`, and target sections such as `nsis.publish`).

:::note[GitlabOptions.vPrefixedTagName is not removed]
Only the **GitHub** field was removed. On **GitLab**, `vPrefixedTagName` is unchanged in v27 — it still exists in the type, the schema, and the runtime, and continues to control the tag prefix (`vPrefixedTagName: false` → `1.2.3`; omit it → `v1.2.3`). It has no `tagNamePrefix` equivalent, so `migrate-schema` leaves GitLab publish entries untouched. No action is required.
:::

### `linux.syncDesktopName` (always synced)

The `linux.syncDesktopName` flag is removed. The behaviour it gated is now **always on**: the installed `.desktop` filename is always derived from the `desktopName` field in `package.json` — installed as `${desktopName}.desktop`, with any trailing `.desktop` in the value stripped first — falling back to `executableName` only when `desktopName` is absent.

```json5
// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } }  // ← delete this line
```

`migrate-schema` removes the flag automatically. No replacement is needed. If you had `syncDesktopName: false` (opting out of syncing in v26), the migrator warns you: the installed filename is now always derived from `desktopName`, so set (or omit) `desktopName` to control it.

**Why this changed:** Electron derives its `app_id` / `WM_CLASS` from `desktopName`, and desktop environments match a running window to its launcher entry by comparing `WM_CLASS` against the installed `.desktop` filename. When the two diverged — which is exactly what happened with `syncDesktopName: false` (the v26 default) whenever a `desktopName` was set — GNOME, KDE, and others failed to associate the window with its launcher, breaking taskbar grouping, dock icons, and launcher highlighting (see [#9103](https://github.com/electron-userland/electron-builder/issues/9103)). Removing the flag eliminates a footgun rather than removing functionality. Path-traversal/NUL validation on the resulting filename is unchanged.

### Linux maintainer-script EJS template syntax

The legacy `<%= varName %>` EJS interpolation in Linux maintainer scripts (e.g. FPM `after-install`/`after-remove`) is removed. Use shell-style `${varName}` instead.

### `devMetadata` / `extraMetadata` (programmatic `PackagerOptions`) {#devmetadata-extrametadata-programmatic-packageroptions}

These fields in the programmatic `PackagerOptions` API are removed (they have thrown `InvalidConfigurationError` since v22).

```ts
// Before
await build({ targets: Platform.MAC.createTarget(), devMetadata: { … }, extraMetadata: { … } })
// After
await build({ targets: Platform.MAC.createTarget(), config: { extraMetadata: { … } } })
```

---

## Restructured configuration

### Native-module options → `nativeModules` {#native-module-options-nativemodules}

Four root-level configuration properties are moved into a new `nativeModules` sub-key, and `nativeRebuilder` is renamed to `rebuildMode`.

```json5
// Before (v26)
{ "build": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "nativeRebuilder": "parallel" } }

// After (v27)
{ "build": { "nativeModules": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "rebuildMode": "parallel" } } }
```

`npmArgs` is **not** affected — it controls the package-manager install phase and remains at the root level. `npmSkipBuildFromSource` (deprecated in v26) is removed; `migrate-schema` converts it to its inverse, `nativeModules.buildDependenciesFromSource`.

The `"legacy"` value of `nativeRebuilder` (the old app-builder binary rebuilder) is **removed** — `rebuildMode` only accepts `"sequential"` or `"parallel"`, and native modules are always rebuilt with `@electron/rebuild`. `migrate-schema` drops a `"legacy"` value with a warning, so the default `"sequential"` mode applies.

### ASAR options → `asar` {#asar-options-asar}

All ASAR-related configuration is now nested under a single `asar` key. Flat root-level properties are removed. The `asar` type is now `AsarOptions | false | null`.

| Removed key | Replacement |
|---|---|
| `asar-unpack` | `asar.unpack` |
| `asar-unpack-dir` | `asar.unpack` |
| `asar.unpackDir` | `asar.unpack` |
| `asarUnpack` | `asar.unpack` |
| `disableSanityCheckAsar` | `asar.disableSanityCheck` |
| `disableAsarIntegrity` | `asar.disableIntegrity` |
| `asar: true` | *(still valid — redundant, since absence means enabled)* |
| `mac.asarUnpack` / `win.asarUnpack` / `linux.asarUnpack` / `mas.asarUnpack` / `masDev.asarUnpack` | `<platform>.asar.unpack` |

```json5
// Before
{ "build": { "asarUnpack": ["**/*.node"], "disableSanityCheckAsar": true, "disableAsarIntegrity": true } }

// After
{
  "build": {
    "asar": {
      "unpack": ["**/*.node"],
      "disableSanityCheck": true,
      "disableIntegrity": true
    }
  }
}
```

When `asar: false`, the root-level sub-options have no effect and the migrator removes them.

**Platform-level `asar` now replaces the root one.** In v26 the root `asarUnpack`, `disableSanityCheckAsar`, and `disableAsarIntegrity` applied to every platform, and a platform's own `asarUnpack` was *added* to the root patterns. In v27 all three live inside `asar`, and a platform-level `asar` (e.g. `mac.asar`) replaces the root `asar` object entirely. `migrate-schema` keeps the v26 result: a platform `asarUnpack` becomes `<platform>.asar.unpack` merged with the root patterns and options, and a platform that already overrides `asar` gets the former root-level values copied in. (For programmatic JS/TS configs, the codemod only rewrites the cases where a plain move is equivalent and warns about the rest.)

```json5
// Before (v26)
{ "asarUnpack": ["**/*.node"], "mac": { "asarUnpack": ["Resources/**"] } }
// After (v27)
{ "asar": { "unpack": ["**/*.node"] }, "mac": { "asar": { "unpack": ["**/*.node", "Resources/**"] } } }
```

### `asar.ordering` is now applied {#asar-ordering-is-now-applied}

`asar.ordering` was documented and validated in v26 but never read: the archive was always packed dependencies first, app files last. v27 honors it: directories keep their positions, the files listed in the ordering file are packed first in the order of the file, and everything else follows in its original order.

Two consequences for projects that already set the option:

- **The archive layout changes once.** Differential updates download most of `app.asar` on the first update after upgrading; later updates are unaffected.
- **A missing ordering file is now an error.** The path is resolved relative to the build resources directory, then the project directory, and the build fails with `InvalidConfigurationError` when neither has it.

Remove the option to keep the default layout, which is tuned for small differential updates.

### macOS signing → `mac.sign` {#macos-signing-macsign}

All macOS code-signing options now live inside a single `sign` object on `mac` (and `mas` / `masDev`), mirroring the Windows `win.sign` grouping. `mac.sign` is typed as `CustomMacSign | ElectronSignOptions | string | null`, where `ElectronSignOptions` is a typed pass-through to [`@electron/osx-sign`](https://github.com/electron/osx-sign).

| Removed (`mac.*`) | Replacement (`mac.sign.*`) |
|---|---|
| `identity` | `sign.identity` |
| `entitlements` | `sign.entitlements` |
| `entitlementsInherit` | `sign.entitlementsInherit` |
| `entitlementsLoginHelper` | `sign.entitlementsLoginHelper` |
| `provisioningProfile` | `sign.provisioningProfile` |
| `type` | `sign.type` *(defaults to a value derived from the target: `mas-dev` builds default to `development`, everything else to `distribution`; an explicit value wins)* |
| `binaries` | `sign.binaries` |
| `requirements` | `sign.requirements` |
| `hardenedRuntime` | `sign.hardenedRuntime` |
| `gatekeeperAssess` | *(removed — `@electron/osx-sign` 2.x dropped the `spctl --assess` step; there is no `sign.gatekeeperAssess`)* |
| `strictVerify` | `sign.strictVerify` |
| `preAutoEntitlements` | `sign.preAutoEntitlements` |
| `timestamp` | `sign.timestamp` |
| `additionalArguments` | `sign.additionalArguments` |
| `signIgnore` | `sign.ignore` *(renamed to the `@electron/osx-sign` canonical name)* |

```json5
// Before
{ "mac": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "signIgnore": ["**/*.txt"] } }

// After
{ "mac": { "sign": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "ignore": ["**/*.txt"] } } }
```

To skip signing, use `mac.sign.identity: null` (or `mac.sign: null`). The same structure applies to `mas` and `masDev`.

A few of these accepted `null` in v26 as "unset" but do not in v27 (`type`, `provisioningProfile`, `binaries`, `signIgnore`, `singleArchFiles`, `x64ArchFiles`); `migrate-schema` drops those `null`s instead of moving them. `identity: null` is kept — it still means "skip signing".

:::warning[Custom signing functions]
If you used `mac.sign` as a **custom signing function or module path** (`sign: "./customSign.js"`) together with sibling fields like `identity`, the two can no longer coexist — `sign` is now a single union. `migrate-schema` leaves your custom signer untouched and prints a warning so you can decide whether to keep the custom function or switch to an `ElectronSignOptions` object.
:::

### macOS default entitlements tightened {#macos-default-entitlements-tightened}

The bundled default entitlements file (used when neither `mac.sign.entitlements` nor `build/entitlements.mac.plist` is present) now grants **only** `com.apple.security.cs.allow-jit`. `com.apple.security.cs.allow-unsigned-executable-memory` and `com.apple.security.cs.disable-library-validation` are **no longer granted by default** — modern Electron does not need either in the main process, and both materially weaken the Hardened Runtime.

- **Nested binaries** (helpers, frameworks, `.node` modules, executables in `app.asar.unpacked`) are no longer signed with one blanket plist. They now receive [`@electron/osx-sign`](https://github.com/electron/osx-sign)'s per-file defaults — renderer/GPU helpers: `allow-jit`; plugin helper: Chromium's looser set; everything else: `default.darwin.plist` — unless you supply `build/entitlements.mac.inherit.plist` / `mac.sign.entitlementsInherit`.
- **MAS builds** without a `build/entitlements.mas.plist` fall back to `@electron/osx-sign`'s sandboxed `default.mas.plist` (previously they received the non-sandboxed `mac` template).
- **Ad-hoc builds** (`mac.sign.identity: "-"`) keep working: a built-in ad-hoc entitlements file that includes `disable-library-validation` is applied to the app and its nested binaries.
- **New post-sign warning:** after signing, electron-builder lists Mach-O libraries and bundles under `Contents/Resources/app.asar.unpacked` and `Contents/PlugIns` (which electron-builder never re-signs) whose signature carries a different Team ID (or none) while `disable-library-validation` is not granted — these would fail library validation when loaded. Executables are spawned rather than loaded, so they are not subject to library validation and are not flagged.

**Action is required only if** your app loads frameworks, plugins, or native modules signed by another Team ID (or unsigned), or still relies on `allow-unsigned-executable-memory`: add the keys back in `build/entitlements.mac.plist` (and in `build/entitlements.mac.inherit.plist` when a helper process or a sidecar executable does the loading — library validation is enforced by the loading process based on its own entitlements, not by what the nested file was signed with). See [Loading third-party or unsigned binaries](../mac#loading-third-party-or-unsigned-binaries).

This is a runtime default, not a config-key rename, so `migrate-schema` does not change it.

### `mac.universal`

The universal-build options move from `mac` root into a `universal` object (typed as `ElectronUniversalOptions`, a pass-through to [`@electron/universal`](https://github.com/electron/universal)). They have no effect unless the target arch is `universal`.

| Removed (`mac.*`) | Replacement (`mac.universal.*`) |
|---|---|
| `mergeASARs` | `universal.mergeASARs` |
| `singleArchFiles` | `universal.singleArchFiles` |
| `x64ArchFiles` | `universal.x64ArchFiles` |

```json5
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } }                       // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } }      // After
```

### Windows signing → `win.sign` {#windows-signing-winsign}

In v26, Windows signing used two separate root-level keys (`signtoolOptions` and `azureSignOptions`). In v27, all Windows signing modes are expressed through a single `win.sign` key typed as a discriminated union:

```typescript
win.sign: { type: "signtool" | "hsm" | "pkcs11" | "azure", … } | false | null
```

`false` / `null` disables signing entirely. Unset means electron-builder discovers credentials from the environment (e.g. `WIN_CSC_LINK`).

**`win.signtoolOptions` → `win.sign: { type: "signtool", … }`** — all fields move verbatim; add `type: "signtool"`.

```json5
{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } }              // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } }     // After
```

**`win.azureSignOptions` → `win.sign: { type: "azure", … }`** — fields move into `win.sign` with `type: "azure"`. The old index signature (`[k: string]: string`) that allowed arbitrary extra keys is replaced by an explicit `additionalMetadata` object. `migrate-schema` both restructures the key and moves any unrecognized extra keys into `additionalMetadata`.

```json5
// Before
{ "win": { "azureSignOptions": { "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "ExcludeCredentials": "ManagedIdentityCredential" } } }

// After
{ "win": { "sign": { "type": "azure", "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "additionalMetadata": { "ExcludeCredentials": "ManagedIdentityCredential" } } } }
```

#### New signing modes (beta): `hsm` and `pkcs11`

v27 introduces two new signing modes in `win.sign`:

- **`type: "hsm"`** — Hardware Security Module via signtool `/csp /kc` (Windows-only; requires `toolsets.winCodeSign: "1.x"`).
- **`type: "pkcs11"`** — PKCS#11 token via osslsigncode (cross-platform; runs on macOS/Linux CI without a Windows VM).

```json5
{
  "win": {
    "sign": {
      "type": "pkcs11",
      "pkcs11Module": "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
      "pkcs11KeyUri": "pkcs11:token=MyToken;object=MyKey;type=private",
      "certificateFile": "cert.pem"
    }
  },
  "toolsets": { "winCodeSign": "1.3.0" }
}
```

Both HSM and PKCS#11 are **beta** — the interfaces are stable but real-hardware test coverage is limited.

#### Custom Windows signing hooks move to `win.sign.sign`

In v26 the **custom signer hook** lived at `win.signtoolOptions.sign`. v27 reuses the `win.sign` key for the discriminated union, and the hook lives inside the signtool-family config, so it moves together with the rest of `signtoolOptions`:

```json5
{ "win": { "signtoolOptions": { "sign": "./customSign.js" } } }          // Before (v26)
{ "win": { "sign": { "type": "signtool", "sign": "./customSign.js" } } } // After (v27)
```

`migrate-schema` performs this move. (A top-level `win.sign: "./customSign.js"` is a pre-v26 shape that v26 already rejected; move it under `{ "type": "signtool", "sign": … }` by hand.)

A hook now also needs `win.sign.publisherName`, even with a certificate configured (the publisher name of a hook is never derived from a certificate) — see [Windows `publisherName`](#windows-publishername-is-validated-against-the-signing-certificate).

#### `win.signExecutable` / `win.signAndEditExecutable` removed {#winsignexecutable-winsignandeditexecutable-removed}

| Removed | Replacement |
|---------|-------------|
| `win.signExecutable: false` | `win.sign: false` (disables signing; resource editing still runs) |
| `win.signExecutable: true` | *(deleted — signing is enabled by default when credentials are available)* |
| `win.signAndEditExecutable: true` | *(deleted — resource editing always runs)* |
| `win.signAndEditExecutable: false` | `win.sign: false` for the signing half; no equivalent for skipping resource editing — see note below |

:::warning
`win.signAndEditExecutable: false` formerly skipped both resource editing (icon, metadata) and signing. In v27 resource editing always runs. `migrate-schema` maps the signing half to `win.sign: false` and warns about the rest. If you need to skip resource editing for a specific artifact, apply resources manually after building.
:::

Both flags set to `false` skipped **all** signing in v26, so any `win.signtoolOptions` / `win.azureSignOptions` next to them never applied. `migrate-schema` removes those blocks (naming them in a warning, without printing their values) rather than turning signing on; to sign in v27, replace `win.sign: false` with a `win.sign: { type: … }` object.

### `electronDownload` → `electronGet` {#electrondownload-electronget}

The `electronDownload` configuration key is renamed to `electronGet` and reshaped to match [`@electron/get`](https://github.com/electron/get)'s options directly (v27 upgrades to `@electron/get` v5, which downloads via `fetch`).

| Old `electronDownload.*` | New `electronGet.*` |
|---|---|
| `mirror` | `mirrorOptions.mirror` |
| `isVerifyChecksum: false` | `unsafelyDisableChecksums: true` |
| `cache` | *(not a config key — set the `ELECTRON_BUILDER_CACHE` environment variable)* |
| `customDir`, `customFilename`, `strictSSL`, `force`, `platform`, `arch`, `version` | *(no equivalent — dropped by `migrate-schema` with a warning)* |

```json5
{ "electronDownload": { "mirror": "https://my-mirror/" } }                  // Before
{ "electronGet": { "mirrorOptions": { "mirror": "https://my-mirror/" } } }  // After
```

### `snap` → `snapcraft` {#snap-snapcraft}

The top-level `snap` configuration key is removed. Use `snapcraft` with an explicit `base` field and per-base options nested under a sub-key named after the base.

```json5
// Before
{ "build": { "snap": { "confinement": "strict", "stagePackages": ["libfoo"], "base": "core22" } } }

// After
{ "build": { "snapcraft": { "base": "core22", "core22": { "confinement": "strict", "stagePackages": ["libfoo"] } } } }
```

Supported `base` values: `"core18"`, `"core20"`, `"core22"`, `"core24"`, and `"custom"`. `migrate-schema` performs this restructuring; when the old config has no `base`, it assumes `"core20"` and warns so you can confirm. A `base: "custom"` config (inline/path `snapcraft.yaml`) is moved verbatim. The `SnapOptions` export is also removed — see [Removed exports](#removed-exports).

---

## CLI changes

### Implicit `--publish` removed {#implicit-publish-removed}

v27 no longer auto-publishes based on the presence of CI tag environment variables, git tags, or npm lifecycle events. Pass `--publish <always|onTag|onTagOrDraft|never>` explicitly in your release scripts, or set the `publish` option in your configuration.

> **Why:** unexpected auto-publishing could accidentally expose secrets or publish unfinished work. Making publishing explicit closes that hole.

### Removed flags: `--em.build`, `--em.directories` {#removed-flags-embuild-emdirectories}

These flags are removed (they have thrown since v22).

| Removed flag | Replacement |
|---|---|
| `--em.build` | `-c` (pass build config inline) |
| `--em.directories` | `-c.directories` |

### New command: `migrate-schema`

v27 adds `electron-builder migrate-schema`, which rewrites your config to v27 form in place and auto-migrates **static** (`json`/`json5`/`yaml`/package.json) **and programmatic** (`.js`/`.ts`/`.cjs`/`.mjs`) configs. TOML configs are parsed and the required changes printed, but not written. See the [walkthrough](./v26-to-v27#step-1-run-the-automated-migrator) for flags and the before/after of every rewrite.

**It rewrites** every entry marked **Auto ✓** on this page: removed keys (`electronCompile`, `framework`/`nodeVersion`/`launchUiVersion`, `disableDefaultIgnoredFiles`, `appImage.systemIntegration`, `linux.syncDesktopName`, `mac.gatekeeperAssess`), the `nativeModules`, `asar` (root and platform-level), `mac.sign`, `mac.universal`, `win.sign`, `electronGet`, and `snapcraft` restructurings, GitHub `vPrefixedTagName` → `tagNamePrefix`, `helper-bundle-id`, `squirrelWindows.noMsi`, root-level `directories` in `package.json`, and `toolsets.*` values v27 rejects (`null`, the retired `appimage: "1.0.2"`). Values with no v27 equivalent (for example `nativeRebuilder: "legacy"` or `electronDownload.strictSSL`) are dropped with a warning.

**It warns but leaves in place** what cannot be rewritten mechanically: `squirrelWindows.customSquirrelVendorDir`, a custom NSIS bundle in `customNsisBinary` / `customNsisResources` ([→ `toolsets.nsis`](#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis); it does move `customNsisBinary.debugLogging` → `installerDebugLogging`), a custom `mac.sign` signer combined with sibling signing options, and (in JS/TS configs) platform-level `asarUnpack` next to root-level ASAR options.

**It prints an advisory** (without changing the config) for runtime defaults and behavior changes you should check: an `nsis-web` target ([`disableWebInstaller`](#disablewebinstaller-defaults-to-true)), `nsis.perMachine` or `nsisWeb.perMachine` set to `true` ([per-machine NSIS updates](#nsis-per-machine-builds-set-isadminrightsrequired)), a custom `win.sign.sign` hook without `win.sign.publisherName`, unless `win.verifyUpdateCodeSignature` is `false` ([publisher name](#windows-publishername-is-validated-against-the-signing-certificate)), a `generic` publish configuration whose `url` has a query string ([update credentials](#update-credentials-stay-on-the-feeds-origin)), and a `mac`/`mas`/`masDev` section that names no entitlements file ([tightened default entitlements](#macos-default-entitlements-tightened)).

**It does not touch** anything outside the config: CLI flags, environment variables, custom NSIS scripts, Linux maintainer scripts, app code using the electron-updater API, or plugins using `PlatformPackager`. Every other row marked **—** in the [table above](#breaking-changes-at-a-glance) is a manual step.

---

## Publishing (electron-publish)

### Bitbucket Cloud publishing: token without username uses Bearer auth

The Bitbucket publisher now selects its authentication scheme by whether a username is present:

- **Username present** (`bitbucket.username`, or the `BITBUCKET_USERNAME` env var) → HTTP **Basic** auth, as before. Use this for an **app password** or an Atlassian **API token** (the username is your Bitbucket username or Atlassian account email).
- **No username** → the token is sent as `Authorization: Bearer <token>` (a repository / project / workspace **access token**). This is the new behavior; previously a token was always sent as Basic auth using the repository owner as the username.

**Action is required only if** your CI sets `BITBUCKET_TOKEN` (or `bitbucket.token`) to an **app password / API token without a username** — those requests now go out as Bearer and will fail authentication. Set `BITBUCKET_USERNAME` (or `bitbucket.username`) so Basic auth is used. Genuine access tokens need no username.

### New: Cloudflare R2 publish provider (additive)

v27 adds an opt-in `provider: "r2"` publish target (Cloudflare R2, an S3-compatible object store) with matching `electron-updater` support. Credentials come from `CF_R2_ACCESS_KEY_ID` / `CF_R2_SECRET_ACCESS_KEY`; the config requires a Cloudflare `accountId`, and an **`https` `publicUrl`** (custom domain or `pub-<hash>.r2.dev`) is needed for auto-update downloads. This is purely additive — no migration action is required.

---

## Toolsets & environment variables {#toolsets-environment-variables}

### Toolset defaults resolve to `"latest"` (newest bundle)

In v27 every `toolsets.*` property defaults to **`"latest"`** — an unset property or the literal `"latest"` resolves to the **newest published bundle** for that toolset. (Earlier v27 prereleases pinned a fixed default per toolset; those fixed defaults are gone.) The `null` value is no longer part of the `ToolsetConfig` type, and it is **rejected by schema validation** — each `toolsets.*` property is typed as a `ToolsetCustom` object or a version string, with no `null` branch. Switch `null` → `"latest"` or omit the key entirely; both resolve to the newest bundle. `migrate-schema` removes `null` entries. (In v26 an unset or `null` toolset resolved to the legacy `"0.0.0"` bundle, so pin `"0.0.0"` if you need to stay on it.) The `appimage` `"1.0.2"` pin is no longer offered; `migrate-schema` rewrites it to `"1.0.3"`.

| Toolset | v26 default | v27 `"latest"` resolves to | What the upgrade entails |
|---------|-------------|----------------------------|--------------------------|
| `wine` | `0.0.0` (Wine 4.0.1, macOS only) | `system` | Host-installed `wine` on `PATH`, on macOS and Linux. **Requires a host Wine on macOS** — see the note below. The `"1.0.1"` Wine 11.0 bundle still downloads when pinned, but ships no PE builtins and cannot run Windows tools on its own |
| `winCodeSign` | `0.0.0` (winCodeSign 2.6.0) | `1.3.0` | Windows Kits 10.0.26100.0; `osslsigncode` 2.11 + native arm64; bundles the Azure Trusted Signing `dlib` + .NET 8 runtime |
| `appimage` | `0.0.0` (FUSE2 runtime) | `1.1.0` | Static FUSE3-compatible runtime (runs without a host FUSE install); adds `unsquashfs` support |
| `nsis` | `0.0.0` (NSIS 3.0.4.1, split bundle) | `1.2.1` | NSIS 3.12; unified single-archive bundle; entrypoint scripts auto-set `NSISDIR` |
| `fpm` | `2.2.1` | `2.2.1` | Unchanged — FPM 1.17.0 / Ruby 3.4.3 |
| `icons` | `1.1.0` | `1.2.3` | Newer bundle — `wasm-vips` + `@resvg/resvg-wasm`; 16/32 px ICNS entries written as ARGB |
| `linuxToolsMac` | `1.0.0` | `1.0.1` | Same tools rebuilt to run on macOS 15+ (1.0.0 required macOS 26) |
| `sevenZip` | `1.0.0` | `1.0.1` | Correct per-arch Windows `7za.exe` (x64, ia32, arm64) |
| `squirrel` | *(electron-winstaller npm package)* | `1.1.1` | New toolset — Squirrel.Windows vendor bundle, replaces the `electron-winstaller` dependency |

**No action required** for most projects — the new bundles are drop-in replacements and produce identical output. **`wine` is the exception:** its default is no longer a bundle at all, so a macOS host that builds Windows targets now needs Wine installed (`brew install --cask wine-stable`). If you hit a regression introduced by a newer bundle, pin back by setting the toolset version to `"0.0.0"`:

```json5
{ "build": { "toolsets": { "winCodeSign": "0.0.0", "nsis": "0.0.0", "appimage": "0.0.0", "wine": "0.0.0" } } }
```

This escape hatch is intended as a short-term workaround. The `"0.0.0"` alias may be removed in a future major release.

Legacy-base snap template builds (`core18`/`core20`/`core22` with `useTemplateApp`) now pack with the `mksquashfs` from `toolsets.appimage` instead of always using the legacy `"0.0.0"` bundle, so an `appimage: "0.0.0"` pin applies to them too — and its `mksquashfs` supports only `gzip`/`xz`, so snap `compression: "lzo"` (or `linux.compression: "store"`) needs a newer toolset.

### Toolset env-var overrides removed

**This is a breaking change if you used env-var toolset overrides.** The following environment variables are **removed** — replace each with a [`ToolsetCustom`](#toolset-env-var-overrides-removed) object on the relevant `toolsets` key:

| Removed env var | Toolset it controlled | Replace with |
|---|---|---|
| `APPIMAGE_TOOLS_PATH` | AppImage build tools (`mksquashfs`, runtime) | `toolsets.appimage` |
| `MKSQUASHFS_PATH` | `mksquashfs` for legacy (core18/20/22) snap builds | `toolsets.appimage` |
| `LINUX_TOOLS_MAC_PATH` | Linux-tools-mac bundle (`ar`, `lzip`, `gtar`) | `toolsets.linuxToolsMac` |
| `CUSTOM_FPM_PATH` | FPM executable | `toolsets.fpm` |
| `ELECTRON_BUILDER_NSIS_DIR` | NSIS compiler bundle directory | `toolsets.nsis` |
| `ELECTRON_BUILDER_NSIS_RESOURCES_DIR` | NSIS resources/plugins directory | `toolsets.nsis` |
| `CUSTOM_NSIS_RESOURCES` | Alternate NSIS resources bundle | `toolsets.nsis` |
| `ELECTRON_BUILDER_WINE_TOOLSET_DIR` | Wine bundle directory | `toolsets.wine` |
| `ELECTRON_BUILDER_7ZIP_PATH` | 7-Zip executable | `toolsets.sevenZip` |
| `ELECTRON_BUILDER_ICONS_TOOLSET_DIR` | Icon-conversion tools bundle | `toolsets.icons` |
| `ELECTRON_BUILDER_OSSL_SIGNCODE_PATH` | `osslsigncode` binary | `toolsets.winCodeSign` |
| `ELECTRON_BUILDER_RCEDIT_PATH` | `rcedit` binary | `toolsets.winCodeSign` |
| `ELECTRON_BUILDER_WINDOWS_KITS_PATH` | Windows SDK / signtool kit directory | `toolsets.winCodeSign` |
| `SIGNTOOL_PATH` | Path to `signtool.exe` | `win.sign` / `toolsets.winCodeSign` |
| `USE_SYSTEM_WINE` | Forced the host-installed Wine instead of the downloaded bundle (now the default — `toolsets.wine: "system"`) | `toolsets.wine` (see note) |
| `USE_SYSTEM_SIGNCODE` | Forced the host `signtool`/`signcode` instead of the bundled `winCodeSign` toolset | `win.sign` + `winCodeSign` |
| `USE_SYSTEM_OSSLSIGNCODE` | Forced the host `osslsigncode` instead of the bundled one | `win.sign` + `winCodeSign` |
| `USE_SYSTEM_FPM` | Forced the host-installed `fpm` instead of the bundled FPM | `toolsets.fpm` |

> **v27 warns you at build start.** If any removed variable in this section is still set in the build environment, electron-builder logs a one-time deprecation warning naming the variable and its replacement (it is otherwise silently ignored). Unset it once you have migrated to the `toolsets`/`win.sign` config.

Setting any of these now **fails the build** with a message naming the `toolsets` replacement, rather than being silently ignored. The two signing `USE_SYSTEM_*` variables (`USE_SYSTEM_SIGNCODE`, `USE_SYSTEM_OSSLSIGNCODE`) have **no env-var replacement** — configure signing through [`win.sign`](#windows-signing-winsign) and the `winCodeSign` toolset instead. `USE_SYSTEM_WINE` is replaced by `toolsets.wine: "system"`, which is already the default. `USE_SYSTEM_FPM` is now **also removed** (it was still functional in earlier v27 prereleases): supply a custom FPM via `toolsets.fpm: { url: "file:///path/to/dir" }`. On Windows there is no bundled FPM, so an FPM-based target now **requires** an explicit custom `toolsets.fpm` and otherwise throws a clear configuration error (previously it silently fell back to a host `fpm` on `PATH`).

The `url` accepts an `https://` URL (downloaded and cached automatically) or a `file://` path (used as-is). The bundle must mirror the directory layout of the corresponding built-in bundle (see [electron-builder-binaries/packages](https://github.com/electron-userland/electron-builder-binaries/tree/master/packages)).

```json5
// Remote bundle (URL)
{ "build": { "toolsets": { "nsis": { "url": "https://example.com/my-nsis-bundle-1.0.tar.gz", "checksum": "<SHA-256 hex or base64 SHA-512 of the archive>", "version": "my-custom-1.0" } } } }

// Local directory (no checksum required)
{ "build": { "toolsets": { "appimage": { "url": "file:///path/to/my-appimage-tools-dir" } } } }
```

> **Wine note:** macOS and Linux both use the host-installed `wine` on `PATH` by default (`"latest"` resolves to `"system"`), so `USE_SYSTEM_WINE` can simply be dropped. To point at a custom Wine build instead, supply a `ToolsetCustom` object on `toolsets.wine`; note that such a directory must contain a prebuilt `wine-home` prefix alongside `bin/` and `lib/`, so the host install is the simpler option for a stock Wine installation.

Supported archive formats: `.zip`, `.7z`, `.tar.gz`, `.tar.xz`. **Exception for `sevenZip`**: because 7-Zip is used to extract `.7z` and `.tar.xz` archives, a custom `sevenZip` bundle can only be supplied as a `.tar.gz`, `.zip`, or bare `file://` directory.

### `pacman` default dependencies changed

`http-parser` was dropped from the default pacman `depends` list. Additionally, `depends` for deb/rpm/pacman now accepts the `"default"` keyword, which expands to the built-in list so you can extend it instead of replacing it:

```json5
{ "build": { "pacman": { "depends": ["default", "my-extra-package"] } } }
```

### `ElectronGetOptions.force` removed

The `force` field has no equivalent in `@electron/get` v5 and was dropped from `ElectronGetOptions`. Clear the cache directory instead (or set `ELECTRON_BUILDER_CACHE` to a fresh path) to force a re-download. `migrate-schema` drops it with a warning.

### `CI_BUILD_TAG` environment variable

Removed. Use `CI_COMMIT_TAG` (the standard GitLab CI variable) to provide the release tag.

### `ELECTRON_BUILDER_BINARIES_ALLOW_HTTP` renamed

Renamed to `ELECTRON_BUILDER_DANGEROUSLY_ALLOW_HTTP` to make the security implication explicit. The behavior is unchanged: set it to `true` to permit non-`localhost` plain-HTTP mirror / download-override URLs that would otherwise be rejected. Set the new name instead; the old one is no longer read.

### Implicit publish on `npm run release` removed

electron-builder no longer inspects `npm_lifecycle_event`: naming your npm/yarn/pnpm script `release` (e.g. `npm run release`) no longer forces publishing on. Publish explicitly instead — pass a `--publish` flag (`--publish always`, `--publish onTag`, `--publish onTagOrDraft`) or set the `publish` option in your electron-builder config.

### Azure Trusted Signing `signtool /dlib` is the default

Azure Trusted Signing (`win.sign: { type: "azure" }`) uses the faster `signtool /dlib` path automatically: the default `winCodeSign` (`"latest"` → `1.3.0`) ships the ATS `dlib` + .NET 8 payload, so no pin is required. To **force the legacy** PowerShell `Invoke-TrustedSigning` path instead (which requires the `TrustedSigning` PS module in the signing environment), pin `winCodeSign` below `1.3.0`:

```json5
{ "build": { "toolsets": { "winCodeSign": "1.2.1" } } } // or "0.0.0" — forces the legacy PowerShell signing path
```

The signer resolves the path from the `winCodeSign` value: unset / `null` / `"latest"` and any explicit version `>= 1.3.0` use `signtool /dlib`; a version below `1.3.0` (no `dlib` in the bundle) uses PowerShell. A `ToolsetCustom` object uses `dlib` from your supplied bundle.

---

## New: MSIX target (beta)

v27 adds a beta `msix` Windows target alongside `appx`, producing `.msix`, `.msixbundle` (multi-arch), and `.msixupload` (Store) artifacts with modern manifest features that AppX cannot express — Package Integrity (`uap10`) and Windows Services (`desktop6`). It is **purely additive: no migration is required.** It works with the default `winCodeSign` toolset (only the legacy `0.0.0` bundle, which lacks the MSIX-capable Windows SDK, is rejected). MSIX builds on Windows 10+ natively or on macOS via Parallels Desktop (Linux is unsupported), and does **not** auto-update via electron-updater (updates come from the Microsoft Store or App Installer). See the [MSIX target documentation](../msix) for the full configuration surface.

```yaml
win:
  target: msix
# no toolsets pin needed — the default winCodeSign is MSIX-capable
```

---

## NSIS & behavior changes {#nsis-behavior-changes}

### NSIS file-association ProgID format changed

NSIS installers that declare `fileAssociations` now register each association under a unique, Microsoft-recommendation-compliant **ProgID** instead of using the association `name` (or extension) verbatim. The previous value could collide with unrelated applications — most easily when forking a project without changing `fileAssociations[].name`.

The generated ProgID has the form `<program>.<component>`: `<program>` is derived from your `productName` (so it stays readable in the registry) and `<component>` mixes a short readable prefix with a value derived from the app GUID. It is at most 39 characters, contains only letters, digits, and a single period, and never starts with a digit.

**No config change is required, and there is nothing to auto-migrate.** `fileAssociations` and its `name` / `ext` / `description` fields are unchanged; the new ProgID is produced automatically. The installer and uninstaller derive the same ProgID, so registration and removal stay in sync.

**Action is required only if** you ship a custom NSIS script (`nsis.include` / `nsis.script`) or external tooling that hard-codes the old ProgID — the association `name` or extension — for example to add extra shell verbs or registry entries under that key. Update those references to the new generated value.

> On upgrade, an installer built with v27 registers the new ProgID. One-click installers run the previous version's uninstaller during the upgrade, which removes the old-format entry; with assisted installers that do not uninstall the prior version first, the old ProgID may remain in the registry until that version is removed.

### NSIS `customInstallMode` macro guard casing corrected {#nsis-custominstallmode-macro-guard-casing-corrected}

The guard around the `customInstallMode` macro in `multiUserUi.nsh` previously checked `!ifmacrodef customInstallmode` (lowercase `m`), while the [documentation](../nsis.md) and the adjacent `!insertmacro customInstallMode` call use `customInstallMode`. In v27 the guard checks the documented name, `customInstallMode`.

**No action is required if** your custom NSIS script (`build/installer.nsh` or `nsis.include`) defines the macro with the documented casing — it is now reliably applied on the install-mode page of assisted multi-user installers (e.g. to set `$isForceMachineInstall`).

**Action is required only if** you defined the macro under the exact lowercase name `customInstallmode`, which relied on the undocumented casing of the old guard — rename the definition to `customInstallMode`:

```nsis
!macro customInstallMode
  # set $isForceCurrentInstall or $isForceMachineInstall here
!macroend
```

### Linux launcher entrypoint

Every Linux target (deb/rpm, AppImage, snap, flatpak) now launches through a generated `<executableName>-launcher` shell script rather than invoking the executable directly. Two things change in the built artifact:

- The package ships a **new file** — `<executableName>-launcher` (e.g. `/opt/<app>/MyApp-launcher`).
- The generated `.desktop` **`Exec` key points at the launcher** instead of the executable (e.g. `Exec=/opt/MyApp/MyApp-launcher %U`), and `executableArgs` / `forceX11` flags are injected into the launcher rather than inlined into `Exec`.

This makes `executableArgs` apply consistently across all Linux targets and keeps the `.desktop` `Exec` a plain command.

**Action is required only if** you ship a custom `.desktop` override, an AppArmor/snap profile, a MIME handler, or external tooling that hard-codes the `Exec` command or assumes the executable itself is the launch target. Point those at the `*-launcher` script (or the executable, as appropriate).

**Desktop-entry field codes in `executableArgs` are now literal.** v26 passed an argument like `%F` into the `.desktop` `Exec` key unquoted (and skipped appending `%U`). In v27 `executableArgs` are injected into the launcher script and quoted, so `%F` reaches your app as the literal string `"%F"` instead of the file list. If you relied on a field code to receive opened files, remove it from `executableArgs` and set `linux.desktop.entry.Exec` instead. electron-builder warns when it sees one.

**AppImage `--no-sandbox` default changed.** The default `--no-sandbox` launch argument is now injected **only for the legacy FUSE2 runtime** (`toolsets.appimage: "0.0.0"`). With the default static FUSE3 runtime (unset / `"latest"` → `1.1.0`), `--no-sandbox` is no longer added automatically — `AppRun` adds it on its own only when user namespaces are unavailable. If you need the Chromium sandbox disabled unconditionally, set `executableArgs: ["--no-sandbox"]` explicitly.

### `node_modules` are now arch/os-filtered on every build

v27 filters `node_modules` by each package's `package.json` `cpu` / `os` fields against the **target** arch and platform on **every** build (previously this effectively only mattered for `universal` macOS builds). A dependency that declares an incompatible `cpu`/`os` for the target is excluded from the packaged app, whereas v26 copied host-installed `node_modules` verbatim.

**No action is required for typical projects** — this fixes universal-build failures and produces correctly-scoped output. Be aware of it only if you *intentionally* bundled a cross-arch or cross-os optional binary that is now dropped; in that rare case, include it explicitly via `extraResources` / `files`.

### `arch: "all"` now expands to x64 and arm64; 32-bit fails fast on Electron 44+

Two related architecture changes ship in v27:

- **`arch: "all"` no longer includes `ia32`.** `Platform.WINDOWS.createTarget(target, Arch.all)` (and the equivalent `--arch all` / default multi-arch build) now expands to **x64 + arm64** on Windows and Linux, instead of the old **x64 + ia32**. Electron 44 removed Windows `ia32` builds, and Linux `ia32` zips already ended at Electron 19, so the old expansion produced broken or impossible builds on current Electron. **To keep building 32-bit, request `ia32` explicitly** (e.g. `Arch.ia32`, or `--ia32`).
- **Windows `ia32` / Linux `armv7l` fail fast on Electron ≥ 44.** Requesting either of those arches against Electron `>= 44` (which [removed those builds](https://github.com/electron/electron/pull/51816)) now throws a clear `InvalidConfigurationError` **before** downloading, instead of dying on an opaque `404`. The check is downgraded to a **warning** when a custom `electronDist` or Electron mirror is configured (it may still provide 32-bit builds).

```
Error: Electron 44.0.0 does not provide Windows ia32 builds — Electron 44 removed
Windows ia32 and Linux armv7l support. Use electronVersion <= 43.x to keep building
for ia32 (32-bit is supported until the v43 series reaches end-of-life in January 2027),
or drop the ia32 target.
```

**Action:** if you ship 32-bit Windows/Linux builds, either pin `electronVersion` to `43.x` or earlier (supported until January 2027) and request `ia32`/`armv7l` explicitly, or drop those targets. Projects that relied on `"all"` implicitly producing `ia32` must now list it explicitly.

### macOS `productName` and `executableName` are validated, not sanitized

electron-builder now **rejects** a macOS `productName` or `executableName` that would require filename sanitization, throwing an `InvalidConfigurationError` at build start (`assertSafeHelperName`). Previously such names were silently normalized/sanitized, which could make the generated `<Name> Helper.app` bundles diverge from `CFBundleName` and break Electron's helper-process discovery.

**Action is required only if** a build starts failing with an "is not a valid macOS app bundle name" error — choose a `productName`/`executableName` that needs no sanitization (avoid path separators, control characters, and other characters stripped by filename sanitization).

**Names are also no longer NFD-normalized.** The `.app` bundle, the helper bundles, and `CFBundleName` now use the product name exactly as configured. For any name containing accented characters this changes the **bytes** of the on-disk filename, which is what external notarization and upload tooling matches on — update anything that hard-codes the normalized form. Relatedly, archive creation falls back from 7z to zip for NFD-normalized filenames.

### Redundant production `dependencies` are excluded, not rejected

In v26, listing `electron` or `electron-builder` under `dependencies` aborted the build with a hard `InvalidConfigurationError`. In v27 these packages are **excluded from the copied `node_modules`** (logged once) and the build proceeds. They stay valid production dependencies for tooling such as SBOM, license, and vulnerability tracking — electron-builder simply provides them another way (the Electron runtime is embedded separately), so a copy inside `app.asar` would be redundant.

The excluded set is configurable through the new [`ignoredProductionDependencies`](../configuration.md) build option, which defaults to:

```json5
{
  "build": {
    "ignoredProductionDependencies": ["electron", "electron-builder"]
  }
}
```

Overriding the option **replaces** the default list, so keep `electron` and `electron-builder` in it unless you intend to ship them.

- **Add** a name to exclude a production dependency that does not need a copy in `node_modules`. This is the common case when a bundler (Vite, webpack, esbuild, …) already inlines the package into your app code: keep it in `dependencies` so SBOM/license tooling still sees it, and add it here so a duplicate copy is not packaged.

  ```json5
  {
    "build": {
      "ignoredProductionDependencies": ["electron", "electron-builder", "react", "react-dom"]
    }
  }
  ```

- **Remove** a name to keep it in the copied `node_modules` after all (e.g. drop `"electron-builder"` from the list to ship it inside the app).

Only dependencies **your app declares directly** are eligible for exclusion. A listed name is dropped together with the transitive dependencies required **only** by it, while anything also required by a legitimate production dependency (npm/pnpm dedupe it into a single hoisted entry) is kept — and a matching name that appears solely as a transitive dependency of a kept package is never excluded. One footgun to be aware of: a kept package that `require()`s an excluded name at runtime **without declaring it** (relying on hoisting of your app's copy) will crash with `MODULE_NOT_FOUND` — declare that dependency properly in the package that needs it, or remove the name from the list. Matching is by the declared dependency name, so an npm alias (`"custom-electron": "npm:electron@^30.0.0"`) must be listed by its alias key.

#### `ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY` is removed — and the default flipped {#allow_electron_builder_as_production_dependency-is-removed-and-the-default-flipped}

In v26, this (undocumented) environment variable was the only way to keep `electron-builder` in `dependencies` without erroring, and setting it meant the package **was bundled** into the app. In v27 the variable is ignored and the behavior is the opposite: `electron-builder` is in the default ignore list, so it is **silently excluded** from the copied `node_modules` (an info-level log is the only trace). If you relied on the env var to actually ship `electron-builder` inside your app, you must now override the list and drop `"electron-builder"` from it, e.g. `"ignoredProductionDependencies": ["electron"]`.

#### `electron-prebuilt` / `electron-rebuild` no longer error — and are NOT excluded {#electron-prebuilt-electron-rebuild-no-longer-error-and-are-not-excluded}

The v26 hard error also rejected `electron-prebuilt` and `electron-rebuild` in `dependencies`. v27 removes that guard **without** adding them to the default ignore list, so if you still declare them they now **ship inside your app** silently — `electron-prebuilt` drags a full Electron binary along. Both packages are long deprecated (`electron-prebuilt` was renamed to `electron` in 2016; `electron-rebuild` moved to `@electron/rebuild`), which is why this major release drops the special-casing instead of carrying it forward. `electron-nightly` was never guarded and is likewise not in the default list. If you use any of them:

- **Preferred**: migrate — replace `electron-prebuilt` with `electron`, and `electron-rebuild` with `@electron/rebuild` in `devDependencies`.
- Otherwise: move them to `devDependencies`, or add them to `ignoredProductionDependencies` (e.g. `["electron", "electron-builder", "electron-prebuilt", "electron-nightly"]`) if they must stay declared as production dependencies for tooling.

This is a runtime behavior change, not a config-key rename, so `electron-builder migrate-schema` does not modify your config for it.

---

### Suffixed update channels now expand to lower channels

With `generateUpdatesFilesForAllChannels` enabled and a non-GitHub provider, `beta` published `beta.yml` + `alpha.yml` and `latest` published all three — but only when the channel was *exactly* `alpha`, `beta`, or `latest`. A channel with a suffix, such as the per-arch `channel: "${channel}-${arch}"` pattern that resolves to `beta-arm64`, fell through and published a single file, so suffixed pre-release users were never promoted.

v27 reads the base channel off the front of the name and reattaches the suffix:

- `beta-arm64` → `beta-arm64.yml`, `alpha-arm64.yml`
- `latest-arm64` → `latest-arm64.yml`, `alpha-arm64.yml`, `beta-arm64.yml`
- `alpha-arm64` → unchanged (lowest channel)
- bare `alpha` / `beta` / `latest` → unchanged

**This only affects you if you use `generateUpdatesFilesForAllChannels` with a suffixed channel name on a non-GitHub provider.** In that case a suffixed channel now writes 2–3 update files where it previously wrote one, and a `latest-x64` publish overwrites `beta-x64.yml` / `alpha-x64.yml` in the same bucket — changing what live `beta-x64` users are offered. That is the same behaviour bare `latest` has always had, but it is new for suffixed channels. Set `generateUpdatesFilesForAllChannels: false` if you want suffixed channels to keep publishing a single file.

---

## Dependencies & file copying

### `allowMissingDependencies` now fails the build

A production dependency that cannot be resolved during `node_modules` collection is now a **hard error**. In v26 this logged `cannot find path for dependency` / `dependency not found on disk` warnings and produced a package that failed at runtime with `MODULE_NOT_FOUND`; v27 fails the build up front, listing every missing dependency at once.

```json5
{ "build": { "allowMissingDependencies": true } }   // restore the v26 warn-only behavior
{ "build": { "allowMissingDependencies": ["some-pkg"] } }  // allow only specific names
```

`false` / `null` / omitted (the default) fails on any missing production dependency. Missing **optional** dependencies (declared in `optionalDependencies`, e.g. `fsevents` on Linux/Windows) are always allowed and never fail the build.

**Action:** if a build that previously succeeded now fails here, fix the installation, list the affected names in `allowMissingDependencies`, or set it to `true` to keep the v26 behavior.

### Native binaries are verified against the target

After the app is packed, electron-builder reads the header of every `.node` addon in `app.asar`, `app.asar.unpacked` (or the unpacked `app` directory) and checks its format and machine type (ELF `e_machine`, Mach-O `cputype` including every slice of a fat binary, PE `Machine`) against the target platform and arch. A mismatch is now a **hard error** naming the file, what it was built for and what was expected. In v26 a stale or host-platform binary left in `node_modules` by a skipped or cached rebuild was packaged silently and only failed at runtime (`invalid ELF header`, `not a valid Win32 application`, `incompatible architecture`). For a macOS universal build each per-arch slice is checked before the slices are merged.

Other native files (`.so`, `.dylib`, `.dll`, `.exe`, extensionless executables) that mismatch only log a warning, since packages legitimately bundle helper binaries for several platforms. Files whose package declares another platform in `package.json` `os`/`cpu`, or whose path names another platform/arch (prebuildify's `prebuilds/linux-arm64/`, `@img/sharp-darwin-arm64`), are skipped.

```json5
{ "build": { "nativeModules": { "verifyNativeBinaries": "warn" } } }  // log mismatches, keep packaging
{ "build": { "nativeModules": { "verifyNativeBinaries": false } } }   // skip the check
```

Relatedly, `nativeModules.buildDependenciesFromSource` no longer skips the native rebuild for a target whose platform differs from the host (which shipped the host's binary). Native modules cannot be cross-compiled from source, so for such a target the rebuild runs with prebuilt binaries for the target instead, and a warning is logged.

**Action is required only if** a build fails here — rebuild native dependencies for the target (for a cross-platform target, the module needs a prebuilt binary for it), or opt out as above.

### `extraFiles` / `extraResources` destinations are validated {#extrafiles--extraresources-destinations-are-validated}

A `to` path that is absolute (POSIX, drive-letter, or UNC) or that escapes the build output directory now throws an `InvalidConfigurationError`. v26 accepted these and copied files onto the build machine outside the packaged app.

**Action is required only if** a build starts failing here — make the `to` path relative and keep it inside the output directory.

### Windows `publisherName` is validated against the signing certificate

A configured `win.sign.publisherName` (v26: `win.signtoolOptions.publisherName` / `win.azureSignOptions.publisherName`) that does not match the signing certificate's subject now fails the build instead of producing an installer that electron-updater would reject every update for. The common triggers are a certificate rotation and the wrong certificate leaking into `WIN_CSC_LINK` / `CSC_LINK` in CI.

**Action is required only if** a build fails here — update `win.sign.publisherName` to the new certificate subject, sign with the intended certificate, or set `publisherName` to `null` to opt out of update signature verification entirely.

A code-signed build whose publisher name cannot be determined now also fails with `InvalidConfigurationError`. This covers every custom `win.sign.sign` hook without `win.sign.publisherName`: a hook may sign with another certificate than any electron-builder can read, so its publisher name is never derived from a certificate, not even one in the config (`certificateFile`, `certificateSubjectName`, `certificateSha1`, `cscLink`) or from `WIN_CSC_LINK` / `CSC_LINK`. It also covers a certificate without a Common Name (including a certificate-store subject). It applies only to builds that write `app-update.yml`: `nsis`, `nsis-web` and `electronUpdaterAware` `appx` targets with a publish configuration, including one inferred from a GitHub `repository`.

**Action:** set `win.sign.publisherName` to the subject of the certificate that signs your app, copied from a binary it already signed ([how](../win.md#how-do-you-delegate-code-signing)) — every component you list must match. Set `win.verifyUpdateCodeSignature: false` instead only if your updates are not Authenticode-signed or you don't use electron-updater.

> **Tip:** `electron-builder migrate-schema` prints an advisory for a custom `win.sign.sign` hook without `win.sign.publisherName`, whatever certificate the config names.

> Related, on the updater side: an `app-update.yml` with no `publisherName` means signature verification is silently skipped. v27 warns; **v28 will fail closed.**

---

## macOS DMG

### DMG `filesystem` defaults to APFS

The default DMG volume filesystem changed from **`HFS+`** to **`APFS`**. APFS is the modern macOS filesystem and produces smaller, faster-to-mount images on current macOS.

- **No action** for most projects.
- **If you must support pre-10.13 (High Sierra) macOS**, which cannot mount APFS volumes, set the filesystem back to `HFS+` explicitly:

```json5
{ "dmg": { "filesystem": "HFS+" } }
```

This is a runtime default, not a config-key rename, so `migrate-schema` does not change it.

### New: `dmg.format: "ULMO"` (additive)

v27 adds a new `dmg.format` value, **`"ULMO"`** — an LZMA-compressed disk image (macOS 10.15+ only) — alongside the existing `UDZO` / `ULFO` / `UDBZ` / … options. This is purely additive; the default format is unchanged and no migration action is required.

---

## Auto-update (electron-updater)

### `disableWebInstaller` defaults to `true`

`AppUpdater.disableWebInstaller` now defaults to **`true`**. NSIS *web* installers (the small installer that downloads the full payload at install time from a manifest-supplied URL) are no longer loaded unless you opt in, because that payload may not undergo signature verification.

Unless `disableWebInstaller` is `false`, a web-installer update is rejected with `ERR_UPDATER_WEB_INSTALLER_DISABLED` — at download time (including an update already cached by a previous launch), before an install on next launch, and at install time.

- **You do not use a web installer** (the common case): no action — this is the safer default.
- **Your installs were made by an `nsis-web` installer built with v27+**: no action — they opt in automatically. NSIS installers now write a `resources/package-type` marker, and `NsisUpdater` reads it to default `disableWebInstaller` to `false` for web-installer installs.
- **Your web-installer installs lack that marker** (installed by an installer built before v27, or with a custom script), **or you are switching an app from `nsis` to `nsis-web`**: opt in manually (below), or those installs will not update.

A web update cached by a previous launch, or pending an install on next launch, is used only if its web package still matches the freshly fetched manifest. A web-installer update whose update info has no `sha512` for the web package is rejected with `ERR_UPDATER_NO_CHECKSUM`. The `nsis-web` installer now also verifies the package it installs: an explicit `--package-file` must match the SHA-512 of one of the packages built with it (any arch), and a package downloaded from the default publish-derived (versioned) URL must match the package for the detected arch; a package downloaded from an explicit `appPackageUrl` (e.g. a version-independent `latest` URL) is not verified. A mismatch aborts the installation (exit code `2`) before the installed version is removed, so that version stays installed. The new `nsisWeb.allowUnverifiedAppPackage` option (default `false`) opts out, for one web installer used with packages of other builds. See [Web Installer](../nsis.md#web-installer).

A local package — passed via `--package-file` (electron-updater does this for updates) or found next to the installer — is first copied into the installer's own temporary directory, and the installer verifies and installs that copy. The local package file is now left in place (the installer's copy is moved into the app's update cache instead; electron-updater removes the package it passed from its `pending` directory once the updated app starts), and a `--package-file` package that cannot be copied aborts the installation (exit code `2`), also before the installed version is removed; a package next to the installer that cannot be copied is ignored and downloaded instead, as when its checksum doesn't match.

Because the manifest vouches for the web package, don't set `updateManifest: false` for `nsis-web` apps: [signed update manifests](../features/signed-update-manifests.md) cover its path, SHA-512 and size.

**Action:** if you publish and rely on an NSIS web installer and your installs lack the marker, or you are switching from `nsis` to `nsis-web`, opt in by setting `disableWebInstaller: false` in your main process:

```ts
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.disableWebInstaller = false // only if you intentionally ship a web installer
```

> **Tip:** running `electron-builder migrate-schema` on a project that builds an `nsis-web` target now prints an advisory: web-installer updates are rejected unless the app opts in, when to set `autoUpdater.disableWebInstaller = false` at runtime, and when to set `nsisWeb.allowUnverifiedAppPackage`. The advisory is informational only — it never rewrites your config (`disableWebInstaller` is an electron-updater runtime setting, not a build-config key).

### NSIS: per-machine builds set `isAdminRightsRequired` in the update info {#nsis-per-machine-builds-set-isadminrightsrequired}

electron-builder now sets `isAdminRightsRequired: true` in the update info of every per-machine `nsis` and `nsis-web` build (`perMachine: true`), including assisted installers (`oneClick: false`) that don't set `packElevateHelper` and builds with `differentialPackage: false`. For `nsis-web` builds it is written into the file entry of the installer, where electron-updater reads it. electron-updater starts these updates with `elevate.exe` right away and, like other per-machine updates, does not install them automatically at launch with `autoInstallEvent: "onNextLaunch"`.

**Action:** none for most apps. With `autoInstallEvent: "onNextLaunch"`, per-machine apps call `installPendingUpdateIfAvailable()` to install a pending update.

> **Tip:** `electron-builder migrate-schema` prints an advisory when `nsis.perMachine` or `nsisWeb.perMachine` is `true`.

### Update credentials stay on the feed's origin {#update-credentials-stay-on-the-feeds-origin}

electron-updater now sends the credential headers from `requestHeaders` / `addAuthHeader` — headers such as `Authorization`, the same set that is removed on a cross-origin redirect — and the query string of the feed `url` only to the feed's origin. This applies to the generic, s3, spaces, r2, keygen, bitbucket, github and gitlab providers.

- An installer, web package, blockmap or differential range request on another origin is requested without those headers and without the feed query. Another origin is another host, or the same host with another port or scheme (`https://updates.example.com` → `https://updates.example.com:8443` or `http://updates.example.com`); as on redirects, an `http` → `https` upgrade of the same host on the default ports keeps the headers and the feed query. Such a URL keeps its own query string, so pre-signed URLs work. With every provider, including custom ones, a URL that electron-updater resolves against the feed URL (such as `files[].url` or a blockmap) gets the feed query only on the feed's origin.
- Downloads on the feed origin (relative `files[].url`, the default) are unchanged. For GitLab the feed origin is `https://<host>`, where electron-builder publishes the release assets.
- When the app sets `previousBlockmapBaseUrlOverride`, the old blockmap is downloaded with the credentials if it is on that origin.
- A blockmap is downloaded from `${url}.blockmap` with the file URL's query string, which does not fit a pre-signed installer URL. The new, optional `files[].blockMapUrl` in `latest*.yml` names the blockmap's own URL; a relative value resolves like `url`, an absolute one is used as-is; it follows the same origin rule and is covered by the [manifest signature](../features/signed-update-manifests.md#what-is-signed) when present. With it, the old blockmap comes from the local cache or `previousBlockmapBaseUrlOverride`, else that update is downloaded in full. electron-builder does not write it.
- A custom provider that extends a built-in provider (e.g. `GenericProvider` or `GitHubProvider`) inherits its `feedBaseUrl`. Any other custom provider must declare it by overriding `Provider.feedBaseUrl`: its feed URL (the credential headers then only go to that origin) or `null` (the request headers go to every download URL, as before). If it declares neither and the download headers include a credential header (from `requestHeaders`, `addAuthHeader` or the provider's `fileExtraDownloadHeaders`), `downloadUpdate()` fails with `ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED` before any download request. Without credential headers nothing changes.

electron-builder warns once per feed when a `generic` publish `url` (the feed written to `app-update.yml`) has a query string, naming its parameters, and electron-updater announces the change at run time: the first download that loses credential headers logs a warning naming the headers and the two origins, and the first one that does not get the feed query logs one naming the query parameters (never their values). A signature failure on a manifest with a `blockMapUrl` says that it is covered by the signature.

**Action:** a custom provider that does not extend a built-in one and is used with credential headers must override `Provider.feedBaseUrl` (see above). Otherwise none if your update files are served from the feed origin. If `latest*.yml` uses absolute URLs on another origin that authenticate with your `requestHeaders` / `addAuthHeader`, or with a token in the feed URL's query (e.g. `url: https://updates.example.com/?key=…`), those downloads now fail (for example with 401 or 403). Serve the files from the feed origin, or use pre-signed URLs. The manifest signature, [required by default](#signed-update-manifests-are-required) in v27, covers `files[].url` and `packages.<arch>.path`, so pre-signed URLs have to be in `latest*.yml` before it is signed (see [Key storage](../features/key-rotation.md#key-storage)); installs with a public key refuse a manifest whose URLs changed after signing.

> **Tip:** `electron-builder migrate-schema` prints an advisory for a `generic` publish configuration whose `url` has a query string.

### `latest*.yml` drops legacy top-level `path`/`sha512`

The deprecated top-level `path` and `sha512` fields are **no longer written** to the generated update-metadata files (`latest.yml` / `latest-mac.yml` / `latest-linux.yml`) by default. They were kept for electron-updater **1.x – 2.15.0**, which predate the `files[]` array (introduced in 2.16.0, released 2017). Both fields are now emitted only when `electronUpdaterCompatibility` declares a range that intersects those legacy versions — the same mechanism that already controlled the Windows `sha2` field and the legacy `latest-mac.json`. The default `electronUpdaterCompatibility` is now `>=2.16` (previously `>=2.15`), and `UpdateInfo.path` / `UpdateInfo.sha512` are now **optional** on the exported TypeScript type.

- **No action** for virtually all projects: every electron-updater since 2.16.0 reads `files[]` and ignores the top-level fields. Already-published `latest*.yml` files are unaffected.
- **If your app code reads `info.path` / `info.sha512`** (e.g. in an `update-downloaded` handler), switch to `info.files[0].url` / `info.files[0].sha512`.
- **If external tooling parses `latest.yml`** (a custom update server or dashboard), point it at `files[]`.
- **If you still ship apps embedding electron-updater 1.x – 2.15**, keep emitting the legacy descriptor by declaring a compatibility range that includes them, e.g. `"electronUpdaterCompatibility": ">=1.0.0"` (also settable per platform, e.g. `win.electronUpdaterCompatibility`).

> Related: metadata validated only by the legacy SHA-256 `sha2` checksum is deprecated — v27 warns and **v28 will reject sha2-only metadata (fail-closed)**. Avoid pinning `electronUpdaterCompatibility` to a legacy range unless you actually ship 1.x–2.15 clients.

### Signed update manifests are required

Ed25519 signing of `latest*.yml` is new in v27 and **required by default**. A build with a publish policy that includes a target writing auto-update metadata — NSIS, AppImage, deb/rpm/pacman, macOS zip/dmg, or AppX with `electronUpdaterAware` — now fails unless a signing key resolves. The check runs at build start, before anything is packed or uploaded:

```
auto-update manifests must be signed, but no Ed25519 signing key was found for mac. Generate one with
`electron-builder create-update-key`, then supply it via the ELECTRON_BUILDER_UPDATE_SIGN_KEY (PEM contents)
or ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE (path) environment variable, or via updateManifest.signingKey /
updateManifest.signingKeyFile in the build configuration. To publish unsigned manifests anyway, opt out
explicitly with `updateManifest: false`.
```

Only a **publishing** build fails. A build without a publish policy warns instead, so local builds keep working — which matters because `app-update.yml` is generated whenever a publish configuration exists *or* can be inferred from your `repository` field.

**To adopt it** (recommended):

```bash
npx electron-builder create-update-key          # once; store the PEM as a CI secret
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-private-key.pem)" electron-builder --publish always
```

**To opt out**, set `updateManifest: false` at the top level or under a platform key. Every build that emits a manifest then logs a warning. `false` also short-circuits the environment variables, so a leftover `ELECTRON_BUILDER_UPDATE_SIGN_KEY` cannot silently re-enable signing. A platform value of `null` is **not** an opt-out — it falls back to the top-level value.

A publishing build whose targets write no update info (for example only snap, flatpak, MSI/MSIX, portable, mas/pkg or plain archives) needs no key. Two more cases need no configuration at all: when every publish provider sets `publishAutoUpdate: false` no manifest is emitted, so the requirement is waived; and a `publicKey` with no private key does **not** satisfy it, since electron-builder still has nothing to sign with (that HSM/KMS workflow either builds without a publish policy, or publishes with `publishAutoUpdate: false` and generates the manifest itself — see [Key storage](../features/key-rotation.md#key-storage)).

:::danger[Opting out is not reversible for a release line]
Once a release embeds a public key, its installs are **fail-closed**: an unsigned manifest is rejected with `ERR_UPDATER_MANIFEST_NOT_SIGNED` and those installs stop updating entirely. Do not switch to `updateManifest: false` after shipping signed manifests, and make sure every pipeline that can publish a release — including a hotfix built from a laptop — has the key.
:::

Note that installs already in the field are unaffected but also unprotected: they carry whatever `app-update.yml` they shipped with, so those built before you adopted signing keep accepting unsigned manifests. Full protection begins with the installs built after the public key was embedded.

See [Signed Update Manifests](../features/signed-update-manifests.md) and [Key Rotation](../features/key-rotation.md).

### `app-update.yml` feed follows the update-writing targets

The feed an installed app polls — the first provider embedded in its `app-update.yml`, together with the manifest public key — is now resolved from the `publish` settings of the targets that write update info, not only from the platform/top-level `publish`. This changes package contents in three cases:

- **Target-level `publish`** (e.g. `nsis.publish`, `appImage.publish`, `dmg.publish`) now decides the feed. An `nsis.publish`-only build used to ship no `app-update.yml` (or, with a GitHub `repository`, a GitHub feed while the manifests went elsewhere); it now ships that feed and the key that verifies its signed manifests.
- **Providers with `publishAutoUpdate: false` are skipped** when a later provider receives the manifest, also in platform/top-level lists: `win.publish: [{ provider: "s3", …, publishAutoUpdate: false }, "github"]` now embeds GitHub, where v26 embedded the S3 feed that never gets a `latest*.yml`. When every provider sets `publishAutoUpdate: false` (the external-signing route), the first one is still embedded.
- **Targets of one app must agree.** One packaged app holds a single `app-update.yml`, so two update-writing targets built from it (for example `dmg` and `zip`, or `nsis` and an updater-aware `appx`) whose feeds differ fail a publishing build at build start with an `InvalidConfigurationError`; a build without a publish policy warns that publishing will fail and writes no `app-update.yml` for that app. Only feed-identifying options are compared — upload-only ones such as `publishAutoUpdate` or `timeout` may differ.

Builds with no update-writing target, or whose only such target opts out (`publish: null`), keep the platform/top-level feed and the GitHub fallback as before. **Action:** only if you use target-level `publish`, per-target feeds within one app, or a disabled first provider — configure `publish` once at the platform level, or check that the embedded feed is the one you publish `latest*.yml` to. See [which settings become the auto-update feed](../publish.md#app-update-yml-feed).

### New: `allowUnverifiedLinuxPackages` (opt-in Linux package-signature verification)

`AppUpdater.allowUnverifiedLinuxPackages` is a new flag (default **`true`**, preserving historical behavior). Set it to `false` to enforce GPG signature checks when installing `.deb` / `.rpm` auto-updates on package managers that support verification. Because electron-builder does **not** sign Linux packages itself, the default remains permissive; this is additive and requires no migration action.

```ts
import { autoUpdater } from "electron-updater"
autoUpdater.allowUnverifiedLinuxPackages = false // enforce GPG signature checks on .deb/.rpm updates
```

### `quitAndInstall` takes an options object

`AppUpdater.quitAndInstall(isSilent?, isForceRunAfter?)` replaced its positional boolean arguments with a single destructured options object. This is a **hard compile break in TypeScript** (plain JavaScript callers must update by hand — positional booleans are silently ignored):

```ts
// Before (v26)
autoUpdater.quitAndInstall(true, false)

// After (v27)
autoUpdater.quitAndInstall({ isSilent: true, isForceRunAfter: false })
```

Defaults are unchanged (`isSilent: false`, `isForceRunAfter: false`), so `quitAndInstall()` with no arguments behaves exactly as before. The object form also carries the new v27 `waitUntilNextLaunch` flag, which defers the install to the next application launch instead of spawning the installer on quit — see [Install on Next Launch](../features/auto-update#install-on-next-launch-windowslinux).

### `autoInstallEvent` replaces `autoInstallOnAppQuit`

The `autoInstallOnAppQuit` boolean on `AppUpdater` is replaced by a single enum, **`autoInstallEvent: "manual" | "onQuit" | "onNextLaunch"`** (default `"onQuit"`), and is **removed** with no compat alias. A single boolean cannot represent the three states, so this is a clean replacement rather than a shim.

| Before | After |
|---|---|
| `autoInstallOnAppQuit = true` (default) | `autoInstallEvent = "onQuit"` (default) |
| `autoInstallOnAppQuit = false` | `autoInstallEvent = "manual"` |
| — *(new in v27)* | `autoInstallEvent = "onNextLaunch"` |

```ts
// Before
autoUpdater.autoInstallOnAppQuit = false

// After
autoUpdater.autoInstallEvent = "manual"
```

The default `"onQuit"` preserves prior behavior, so most apps need no change. `"onNextLaunch"` defers the install to the next launch to avoid the OS killing the installer during session end, and is planned to become the default in v28 — see [Install on Next Launch](../features/auto-update#install-on-next-launch-windowslinux).

### `downloadUpdate()` resolves with a `DownloadExecutorResult` object

`AppUpdater.downloadUpdate()` — and `UpdateCheckResult.downloadPromise`, the same promise handed back by `checkForUpdates()` when `autoDownload` is enabled — used to resolve with a positional `Array<string>`: `[updateFile]`, or `[updateFile, packageFile]` for NSIS *web* installers. It now resolves with a **`DownloadExecutorResult`** object (exported from `electron-updater`) that names both files:

```ts
interface DownloadExecutorResult {
  readonly updateFile: string // the downloaded installer / AppImage / zip
  readonly packageFile?: string // the NSIS web-installer package, only set for web installers
}
```

This is a **hard compile break in TypeScript**; plain JavaScript callers that index the result (`files[0]`) receive `undefined` and must update by hand:

```ts
// Before (v26 / electron-updater 6)
const files = await autoUpdater.downloadUpdate()
const installer = files[0]

const result = await autoUpdater.checkForUpdates()
const [downloaded] = (await result?.downloadPromise) ?? []

// After (v27 / electron-updater 7)
const { updateFile, packageFile } = await autoUpdater.downloadUpdate()

const result = await autoUpdater.checkForUpdates()
const downloaded = (await result?.downloadPromise)?.updateFile
```

Code that only awaits the promise for its side effects, or that relies on the `update-downloaded` event, needs no change. Additively, the `update-downloaded` event payload (`UpdateDownloadedEvent`) now also carries `packageFile` for web installers, next to the existing `downloadedFile`.

### `verifyUpdateCodeSignature` renamed to `verifyUpdateFileAuthenticodeSignature`

NSIS Authenticode verification no longer uses the `null`-means-success / `string`-means-error convention. `NsisUpdater.verifyUpdateCodeSignature` is renamed to `verifyUpdateFileAuthenticodeSignature` and returns a result object:

```ts
type VerifyUpdateFileResult = { response: "success" } | { response: "failure"; message: string }
```

```ts
// Before (v6)
autoUpdater.verifyUpdateCodeSignature = async (publisherNames, path) => (isValid ? null : "why it failed")

// After (v7)
autoUpdater.verifyUpdateFileAuthenticodeSignature = async (publisherNames, path) =>
  isValid ? { response: "success" } : { response: "failure", message: "why it failed" }
```

The old name is kept as a compatibility shim that translates in both directions, so existing code keeps working; it is deprecated and **will be removed in v28**. The same applies to the protected `_verifyUpdateCodeSignature` member, which is renamed to `_verifyUpdateFileAuthenticodeSignature` — a subclass that *assigns* the old name still works through a deprecated accessor, but a subclass that *redeclares* it as a class field shadows that accessor and must be migrated.

If you override the protected `BaseUpdater.verifyInstallerSignatureOnLaunch` (added earlier in the same v27 pre-release cycle, so only 7.0.0-alpha users are affected), return `{ response: "success" }` rather than `null` — `null` is now read as a verification failure and blocks every install-on-next-launch.

Additively, all updaters gain `AppUpdater.verifyUpdateFile` for app-defined verification of the downloaded file on every path that can lead to an install. See [Custom downloaded-file verification](../features/auto-update.md#custom-downloaded-file-verification).

---

## Programmatic & plugin-author API changes {#programmatic-plugin-author-api-changes}

> **This section only affects you if you import from `app-builder-lib` and access the `packager` or `platformPackager` objects directly.** Standard project configurations are unaffected.

### `info: Packager` is now `protected`

`PlatformPackager` exposed a **public** `info: Packager` field in v26. In v27 it is **`protected`** — a **hard compile break**, not a soft deprecation: external code that chains through `.info.` (a custom target, or a plugin importing `app-builder-lib`) no longer type-checks. All commonly-needed properties are now directly accessible on `PlatformPackager` — drop the `.info.` chain:

| Before (deprecated) | After |
|---|---|
| `packager.info.tempDirManager` | `packager.tempDirManager` |
| `packager.info.metadata` | `packager.metadata` |
| `packager.info.framework` | `packager.framework` |
| `packager.info.cancellationToken` | `packager.cancellationToken` |
| `packager.info.repositoryInfo` | `packager.repositoryInfo` |
| `packager.info.relativeBuildResourcesDirname` | `packager.relativeBuildResourcesDirname` |
| `packager.info.stageDirPathCustomizer` | `packager.stageDirPathCustomizer` |
| `packager.info.areNodeModulesHandledExternally` | `packager.areNodeModulesHandledExternally` |
| `packager.info.isPrepackedAppAsar` | `packager.isPrepackedAppAsar` |
| `packager.info.appDir` | `packager.appDir` |
| `packager.info.getWorkspaceRoot()` | `packager.getWorkspaceRoot()` |
| `packager.info.emitArtifactBuildStarted(e)` | `packager.emitArtifactBuildStarted(e)` |
| `packager.info.emitArtifactBuildCompleted(e)` | `packager.emitArtifactBuildCompleted(e)` |
| `packager.info.emitArtifactCreated(e)` | `packager.emitArtifactCreated(e)` |
| `packager.info.emitMsiProjectCreated(p)` | `packager.emitMsiProjectCreated(p)` |
| `packager.info.emitAppxManifestCreated(p)` | `packager.emitAppxManifestCreated(p)` |

The getters already on `PlatformPackager` in v26 are unchanged (`config`, `projectDir`, `buildResourcesDir`, `packagerOptions`, `appInfo`, `debugLogger`).

### `platformSpecificBuildOptions` is now `protected`

External consumers should use the two new public helpers instead:

| Access pattern | v27 replacement |
|---|---|
| `packager.platformSpecificBuildOptions` (direct read) | `packager.platformOptions` |
| `deepAssign({}, packager.platformSpecificBuildOptions, config.X)` | `packager.getOptionsForTarget<T>("X")` |

`platformOptions` is a typed getter returning the same platform-level config object. `getOptionsForTarget<T>(key)` performs the standard merge of platform options with the named per-target key (e.g. `"appx"`, `"msi"`) and returns the result as `T`. All built-in targets have been migrated; custom targets extending `PlatformPackager` must update any direct `.platformSpecificBuildOptions` access.

---

## Removed exports

`ProtonFramework`, `LibUiFramework`, and `SnapOptions` are removed from the public exports of `app-builder-lib` and `electron-builder`. `CustomNsisBinary` is removed from the public exports of `app-builder-lib`, together with the [`customNsisBinary` option](#customnsisbinary-and-customnsisresources-replaced-by-toolsetsnsis); use `ToolsetCustom` (for `toolsets.nsis`) instead. Use the Electron framework and the [`snapcraft`](#snap-snapcraft) config shape respectively. The removed framework support means `framework: "proton" | "libui"` no longer has any effect — see [`framework` removed](#framework-nodeversion-launchuiversion).

### Renamed type exports

These exported TypeScript types were **renamed with no compatibility alias** — `import { OldName }` is now a hard compile error. Update the import to the new name:

| v26 export | v27 export |
|---|---|
| `ElectronDownloadOptions` | `ElectronGetOptions` |
| `WindowsAzureSigningConfiguration` | `WindowsAzureSigningConfig` |
| `WindowsSigntoolConfiguration` | `WindowsSigntoolSigningConfig` |

`ElectronGetOptions` is re-exported from the top-level `electron-builder` package, so this affects ordinary TypeScript consumers, not just `app-builder-lib` plugin authors.

---

## Design notes

Rationale behind the user-facing breaking changes, for those who want full transparency.

### Why native ESM?

`electron-builder` historically shipped CommonJS. The move to native ESM was driven by the ecosystem: major dependencies (`chalk`, `figures`, `ora`, etc.) have dropped CJS support, and Node.js 22.12's stabilized `require(esm)` makes it safe to ship ESM without breaking CJS consumers. The minimum was set to 22.12.0 specifically because earlier Node.js versions require `--experimental-require-module` to `require()` an ESM package.

### Why move native-module options under `nativeModules`?

In v26, options like `buildDependenciesFromSource`, `nodeGypRebuild`, `npmRebuild`, and `nativeRebuilder` were scattered at the root of `Configuration` alongside unrelated properties. They were grouped under `nativeModules` for the same reason `directories` and `toolsets` are sub-keys: related options should be co-located. The rename `nativeRebuilder → rebuildMode` makes the field name consistent with other enum-style selectors.

### Why unify all Windows signing under `win.sign`?

In v26, Windows signing was split across two sibling root-level keys — `signtoolOptions` and `azureSignOptions` — with precedence rules that were easy to misconfigure (both present → Azure wins, silently). v27 replaces them with a single `win.sign` key typed as a discriminated union. This makes the active signing mode explicit, self-documenting in IntelliSense, and mirrors the macOS `mac.sign` shape so both platforms have the same mental model. The `win.sign: false` sentinel explicitly disables signing, distinguishing it from "not configured" (env-based discovery).

### Why replace `[k: string]` in `WindowsAzureSigningConfiguration`?

The v27 Azure integration switches from PowerShell `Invoke-TrustedSigning` to `signtool.exe /dlib /dmdf`, which reads a `metadata.json` file. An explicit `additionalMetadata: Record<string, string>` field is cleaner than an index signature because it makes intent visible in IntelliSense and prevents accidental shadowing of the typed fields. The legacy PowerShell path remains available (triggered when `toolsets.winCodeSign` is pinned below `"1.3.0"`).

### Why restructure `snap` into `snapcraft`?

The `snapcraft` shape makes the `base` explicit and nests per-base options under a base-named sub-key, so a single config can describe multiple bases unambiguously and core24 (which uses the snapcraft CLI directly) can coexist with legacy bases. `migrate-schema` automates the move but assumes `core20` when no base is present, because that is the v27 1-to-1 migration target — verify the assumption for your project.

### Why consolidate macOS signing under `mac.sign`?

In v26, ~15 signing options were scattered across the root of `MacConfiguration` intermixed with unrelated packaging options. Grouping them under a single `sign` object makes the signing surface self-documenting and matches the Windows `win.sign` grouping. `sign` is now a typed pass-through (`ElectronSignOptions`) to `@electron/osx-sign`, so upstream options are forwarded directly. Note that `ElectronSignOptions` is a closed type (`additionalProperties: false`): a field added by a newer `@electron/osx-sign` is rejected until electron-builder regenerates its schema, so it is a pass-through of the options that exist, not an open bag. `signIgnore` was renamed to `sign.ignore` to match the osx-sign canonical name. The same grouping rationale produced `mac.universal` (a pass-through to `@electron/universal`).

### Why rename `electronDownload` to `electronGet`?

v27 upgrades to `@electron/get` v5, which downloads via `fetch` (replacing the `got`-based path) and exposes a `mirrorOptions` object rather than the old flat `mirror`/`customDir` fields. Renaming the config key to `electronGet` and typing it directly as the library's own options removes electron-builder's hand-maintained translation layer — what you set is what `@electron/get` receives. A few legacy fields (`customDir`, `customFilename`, `strictSSL`) have no v5 equivalent; `migrate-schema` drops them with a warning. `cache` is not gone — it moved out of the config surface and is set with the `ELECTRON_BUILDER_CACHE` environment variable instead.

### Why consolidate ASAR options under `asar`?

In v26, `asarUnpack` lived alongside `asar`, and `disableSanityCheckAsar` / `disableAsarIntegrity` lived at the root of `Configuration`. This was confusing: `asarUnpack` is meaningless when `asar: false`, yet nothing in the type system expressed that relationship. Moving all options under a single `asar` key makes the dependency explicit. The `asar: true` sentinel was left in place — it is redundant next to `{}`, but rejecting it would break working configs for no benefit, so `migrate-schema` merely drops it when folding in other ASAR keys.

### `PlatformPackager.info` is now protected

The public `info: Packager` field created a hard coupling: any internal `Packager` refactor became a breaking change for all plugins. In v27, direct pass-through getters and methods were added for the properties plugin authors actually need, and `info` was changed from `public` to **`protected`**. This is a hard break in v27 (not a deferred deprecation): external `packager.info.X` access no longer compiles, so migrate to the direct getters listed above.

---

## Internal changes (non-user-facing)

These changes have no impact on your build configuration or the public API. They are listed for transparency about what shipped in v27.

- **Native ESM build pipeline.** Babel was removed entirely; packages are compiled by `tsc` to native ESM with `exports` maps and full type declarations.
- **Code-quality modernization.** Production code paths adopt native Node.js APIs and modern TypeScript patterns (`sleep()` from `timers/promises`, native process signal handlers, `Error.cause`, `Reflect.get`/`set`, the WHATWG `URL` API, SHA-256 for WiX directory-ID generation).
- **Dead-code removal.** `ProtonFramework`, `LibUiFramework`, and `binDownload.ts` were deleted; all binary downloads were consolidated into a single `downloadBuilderToolset` path.
- **Flags consolidation.** All boolean `process.env` flags were consolidated into a single `flags.ts`, and `validateShellEmbeddable` moved to `builder-util/envUtil`.
- **Source reorganization.** Platform-specific files were split into per-platform subdirectories. The public API surface is unchanged.
- **Test suite.** Duplicated test files were replaced by runtime-generated tests that fan out across toolset version combinations.
- **`@electron/*` dependency major bumps.** `@electron/get` 3→5 (now `fetch`-based; drives the [`electronGet`](#electrondownload-electronget) rename), `@electron/osx-sign` 1→2 and `@electron/universal` 2→3 (drive the [`mac.sign`](#macos-signing-macsign) / [`mac.universal`](#macuniversal) pass-throughs), plus `@electron/asar` 3→4, `@electron/notarize` 2→3, and `@electron/fuses` 1→2. The fuses bump adds an optional `wasmTrapHandlers?: boolean` field to `electronFuses`; no existing fuse field was removed.

---

--- METRICS ---
- Files Extracted: 58
- Estimated Token Budget: ~142645 tokens
- Canonical Reference: https://codewiki.google/github.com/electron-userland/electron-builder
