Skip to main content

v27 Breaking Changes

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 →.

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.

  • The build() API, all runtime configuration options, and all exported types that survive are unchanged.
  • CJS require() continues to work without code changes on supported Node.js versions.
  • Every config-level breaking change below is rewritten automatically by electron-builder migrate-schema (look for the Auto ✓ marker). Runtime, CLI, env-var, and behavior changes require manual action.
Run the automated migrator first
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 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, null, and the literal "latest" all resolve to the newest published bundle for that toolset (previously each property defaulted to a fixed pinned version). No config change is required, but the effective defaults moved:

  • wine1.0.1 — Wine 11.0 (was Wine 4.0.1); macOS arm64 via Rosetta. Linux still uses host-installed wine.
  • winCodeSign1.3.0 — Windows Kits 10.0.26100.0, osslsigncode 2.11 (native arm64), and the Azure Trusted Signing dlib + .NET 8 payload.
  • appimage1.1.0 — static FUSE3-compatible runtime; adds unsquashfs support.
  • nsis (1.2.1), fpm (2.2.1), icons (1.2.1), linuxToolsMac (1.0.0), and sevenZip (1.0.0) are unchanged.

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.


Breaking changes at a glance

ChangeAutoAction required
Node.js >=22.12.0 requiredUpdate your runtime and CI Node version
All packages are native ESMNone — require() still works on Node >=22.12
electron-forge-maker-* are now ESMNone — same API, same export shape
electronCompile removedRemove from config; migrate off electron-compile
disableDefaultIgnoredFiles removedRemoved automatically; re-include specific files via a files glob (e.g. **/*.obj)
framework / nodeVersion / launchUiVersion removedRemoved automatically (Electron is the only framework)
ProtonFramework & LibUiFramework removedMigrate to an Electron-based build setup
appImage.systemIntegration removedRemoved automatically
npmSkipBuildFromSource removedReplaced by nativeModules.buildDependenciesFromSource
Native-module options grouped under nativeModulesnativeRebuilderrebuildMode
ASAR options consolidated under asarasarUnpackasar.unpack, etc.; asar: true removed
macOS signing consolidated under mac.signidentity/entitlements/hardenedRuntime/… → mac.sign.*; signIgnoresign.ignore
mac.universal options consolidatedmergeASARs/singleArchFiles/x64ArchFilesmac.universal.*
Windows signing unified under win.signDiscriminated union type: "signtool" | "hsm" | "pkcs11" | "azure"
win.signExecutable / win.signAndEditExecutable removedfalsewin.sign: false; resource editing always runs
electronDownloadelectronGetReshaped to @electron/get options; some legacy fields dropped (warned)
snap config key removedRestructured to snapcraft with an explicit base
Root-level directories removedMove under build.directories
build.helper-bundle-id removedMoved to mac.helperBundleId
squirrelWindows.noMsi removedReplaced by msi (inverted)
GithubOptions.vPrefixedTagName removedUse tagNamePrefix
GitlabOptions.vPrefixedTagName retainedNone — still functional; the migrator leaves GitLab entries untouched
devMetadata / extraMetadata in PackagerOptions removedUse config / config.extraMetadata
Implicit --publish removedPass --publish explicitly
--em.build / --em.directories CLI flags removedUse -c / -c.directories
linux.syncDesktopName removedBehaviour is always on; set desktopName to control the filename
Linux maintainer-script EJS syntax removedUse ${var} instead of <%= var %>
NSIS file-association ProgID format changedUpdate custom NSIS scripts that hard-code the old ProgID
Toolset defaults resolve to "latest"No action; pin to "0.0.0" to restore a legacy bundle
Toolset env-var overrides removedReplace APPIMAGE_TOOLS_PATH, ELECTRON_BUILDER_NSIS_DIR, USE_SYSTEM_WINE, … with toolsets.X: { url, checksum }
CI_BUILD_TAG env var removedUse CI_COMMIT_TAG
Azure Trusted Signing /dlib is the defaultPin winCodeSign below 1.3.0 only to force the legacy PowerShell path
PlatformPackager.info & platformSpecificBuildOptions now protectedPlugin authors: hard break — .info no longer compiles externally; use the new pass-through getters
Linux .desktop Exec now runs a generated *-launcher scriptUpdate custom .desktop/AppArmor/MIME tooling that hard-codes the Exec command
node_modules arch/os-filtered on every buildAwareness — packages whose cpu/os mismatch the target are now excluded
Renamed type exports (ElectronDownloadOptions, WindowsAzureSigningConfiguration, …)Import the new names — no compat aliases
SnapOptions, ProtonFramework, LibUiFramework exports removedUse the snapcraft config shape / Electron framework

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) support was stabilized (no flags needed), which allows both CJS and ESM consumers to use these packages without any code changes.

{
"engines": {
"node": ">=22.12.0"
}
}

See the walkthrough for updating your runtime, CI, and Docker images.

Native ESM output

electron-builder packages now ship as native ES modules. On Node >=22.12.0 both styles continue to work:

// 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

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.

{ "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 — recommended; fast, first-class ESM and Electron integration
  • esbuild — extremely fast, minimal configuration
  • webpack — mature, widely used

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).

{
"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:

{
"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.

npmSkipBuildFromSource

Removed. Use buildDependenciesFromSource (now under nativeModules). It is the logical inverse:

// v26
{ "npmSkipBuildFromSource": true }
// v27 equivalent
{ "nativeModules": { "buildDependenciesFromSource": false } }

asar: true sentinel

asar: true is no longer valid. Omit the asar key entirely to enable ASAR with defaults, or specify an object. The full ASAR restructuring is documented under ASAR options → asar.

{ "build": { "asar": true } } // Before — enable with defaults
{ "build": { "asar": {} } } // After — omit entirely (enabled by default) or use an object

Root-level directories in package.json

Specifying directories at the root of package.json is removed. Move it under the build key.

{ "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.

{ "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.

{ "build": { "squirrelWindows": { "noMsi": true } } } // Before
{ "build": { "squirrelWindows": { "msi": false } } } // After

GithubOptions / GitlabOptions vPrefixedTagName

The vPrefixedTagName boolean on GithubOptions is removed. Use tagNamePrefix to control the tag prefix.

{ "publish": { "provider": "github", "vPrefixedTagName": false } } // Before
{ "publish": { "provider": "github", "tagNamePrefix": "" } } // After (empty string = no prefix)

To keep the default v prefix, simply remove vPrefixedTagNametagNamePrefix defaults to "v".

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: false1.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.

// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } } // ← delete this line

No replacement is needed — simply remove the flag. To control the installed filename, set (or omit) desktopName in your root package.json.

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). 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)

These fields in the programmatic PackagerOptions API are removed (they have thrown InvalidConfigurationError since v22).

// Before
await build({ targets: Platform.MAC.createTarget(), devMetadata: {}, extraMetadata: {} })
// After
await build({ targets: Platform.MAC.createTarget(), config: { extraMetadata: {} } })

Restructured configuration

Native-module options → nativeModules

Four root-level configuration properties are moved into a new nativeModules sub-key, and nativeRebuilder is renamed to rebuildMode.

// 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.

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 keyReplacement
asar-unpackasar.unpack
asar-unpack-dirasar.unpack
asar.unpackDirasar.unpack
asarUnpackasar.unpack
disableSanityCheckAsarasar.disableSanityCheck
disableAsarIntegrityasar.disableIntegrity
asar: true(removed — absence means enabled)
// Before
{ "build": { "asarUnpack": ["**/*.node"], "disableSanityCheckAsar": true, "disableAsarIntegrity": true } }

// After
{
"build": {
"asar": {
"unpack": ["**/*.node"],
"disableSanityCheck": true,
"disableIntegrity": true
}
}
}

When asar: false, all the sub-options are irrelevant and the migrator skips them.

macOS signing → mac.sign

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.

Removed (mac.*)Replacement (mac.sign.*)
identitysign.identity
entitlementssign.entitlements
entitlementsInheritsign.entitlementsInherit
entitlementsLoginHelpersign.entitlementsLoginHelper
provisioningProfilesign.provisioningProfile
typesign.type
binariessign.binaries
requirementssign.requirements
hardenedRuntimesign.hardenedRuntime
gatekeeperAssesssign.gatekeeperAssess
strictVerifysign.strictVerify
preAutoEntitlementssign.preAutoEntitlements
timestampsign.timestamp
additionalArgumentssign.additionalArguments
signIgnoresign.ignore (renamed to the @electron/osx-sign canonical name)
// 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.

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.

mac.universal

The universal-build options move from mac root into a universal object (typed as ElectronUniversalOptions, a pass-through to @electron/universal). They have no effect unless the target arch is universal.

Removed (mac.*)Replacement (mac.universal.*)
mergeASARsuniversal.mergeASARs
singleArchFilesuniversal.singleArchFiles
x64ArchFilesuniversal.x64ArchFiles
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } } // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } } // After

Windows signing → win.sign

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:

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.signtoolOptionswin.sign: { type: "signtool", … } — all fields move verbatim; add type: "signtool".

{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // After

win.azureSignOptionswin.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.

// 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).
{
"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.

win.signExecutable / win.signAndEditExecutable removed

RemovedReplacement
win.signExecutable: falsewin.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: falseNo direct equivalent — see note below
warning

win.signAndEditExecutable: false formerly skipped both resource editing (icon, metadata) and signing. In v27 resource editing always runs. To skip only signing, use win.sign: false. If you need to skip resource editing for a specific artifact, apply resources manually after building.

electronDownloadelectronGet

The electronDownload configuration key is renamed to electronGet and reshaped to match @electron/get's options directly (v27 upgrades to @electron/get v5, which downloads via fetch).

Old electronDownload.*New electronGet.*
mirrormirrorOptions.mirror
isVerifyChecksum: falseunsafelyDisableChecksums: true
cache, customDir, customFilename, strictSSL(no equivalent — dropped by migrate-schema with a warning)
{ "electronDownload": { "mirror": "https://my-mirror/" } } // Before
{ "electronGet": { "mirrorOptions": { "mirror": "https://my-mirror/" } } } // After

snapsnapcraft

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.

// 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.


CLI changes

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

These flags are removed (they have thrown since v22).

Removed flagReplacement
--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. See the walkthrough.


Toolsets & environment variables

Toolset defaults resolve to "latest" (newest bundle)

In v27 every toolsets.* property defaults to "latest" — an unset property, null, or the literal "latest" all resolve 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 — it still works at runtime, but TypeScript/programmatic configs typed against Configuration should switch null"latest" or omit the key. migrate-schema does not rewrite this.

Toolsetv26 defaultv27 "latest" resolves toWhat the upgrade entails
wine0.0.0 (Wine 4.0.1, macOS only)1.0.1Wine 11.0; macOS arm64 via Rosetta. Linux uses host-installed wine (no bundle shipped)
winCodeSign0.0.0 (winCodeSign 2.6.0)1.3.0Windows Kits 10.0.26100.0; osslsigncode 2.11 + native arm64; bundles the Azure Trusted Signing dlib + .NET 8 runtime
appimage0.0.0 (FUSE2 runtime)1.1.0Static FUSE3-compatible runtime (runs without a host FUSE install); adds unsquashfs support
nsis0.0.0 (NSIS 3.0.4.1, split bundle)1.2.1NSIS 3.12; unified single-archive bundle; entrypoint scripts auto-set NSISDIR
fpm2.2.12.2.1Unchanged — FPM 1.17.0 / Ruby 3.4.3
icons1.2.11.2.1Unchanged — wasm-vips + @resvg/resvg-wasm
linuxToolsMac1.0.01.0.0Unchanged — gnu-tar, lzip, binutils, etc. (macOS → Linux archives)
sevenZip1.0.01.0.0Unchanged — only published version

No action required for most projects — the new bundles are drop-in replacements and produce identical output. If you hit a regression introduced by a newer bundle, pin back by setting the toolset version to "0.0.0":

{ "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.

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 object on the relevant toolsets key:

Removed env varToolset it controlled
APPIMAGE_TOOLS_PATHAppImage build tools (mksquashfs, runtime)
LINUX_TOOLS_MAC_PATHLinux-tools-mac bundle (ar, lzip, gtar)
CUSTOM_FPM_PATHFPM executable
ELECTRON_BUILDER_NSIS_DIRNSIS compiler bundle directory
ELECTRON_BUILDER_NSIS_RESOURCES_DIRNSIS resources/plugins directory
CUSTOM_NSIS_RESOURCESAlternate NSIS resources bundle
ELECTRON_BUILDER_WINE_TOOLSET_DIRWine bundle directory
USE_SYSTEM_WINEForced the host-installed Wine instead of the downloaded bundle
USE_SYSTEM_SIGNCODEForced the host signtool/signcode instead of the bundled winCodeSign toolset
USE_SYSTEM_OSSLSIGNCODEForced the host osslsigncode instead of the bundled one

The three USE_SYSTEM_* variables above have no env-var replacement — configure signing through win.sign and the winCodeSign toolset instead. (USE_SYSTEM_FPM is unchanged and still works.)

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).

// Remote bundle (URL)
{ "build": { "toolsets": { "nsis": { "url": "https://example.com/my-nsis-bundle-1.0.tar.gz", "checksum": "sha256:abc123…", "version": "my-custom-1.0" } } } }

// Local directory (no checksum required)
{ "build": { "toolsets": { "appimage": { "url": "file:///path/to/my-appimage-tools-dir" } } } }

Wine note: with USE_SYSTEM_WINE gone, Linux uses the host-installed wine by default (no bundle is shipped for Linux), and macOS uses the downloaded Wine 11.0 bundle. To point at a custom Wine build, supply a ToolsetCustom object on toolsets.wine.

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.

CI_BUILD_TAG environment variable

Removed. Use CI_COMMIT_TAG (the standard GitLab CI variable) to provide the release tag.

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:

{ "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.


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.

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).

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.


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.tempDirManagerpackager.tempDirManager
packager.info.metadatapackager.metadata
packager.info.frameworkpackager.framework
packager.info.cancellationTokenpackager.cancellationToken
packager.info.repositoryInfopackager.repositoryInfo
packager.info.relativeBuildResourcesDirnamepackager.relativeBuildResourcesDirname
packager.info.stageDirPathCustomizerpackager.stageDirPathCustomizer
packager.info.areNodeModulesHandledExternallypackager.areNodeModulesHandledExternally
packager.info.isPrepackedAppAsarpackager.isPrepackedAppAsar
packager.info.appDirpackager.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 patternv27 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. Use the Electron framework and the snapcraft config shape respectively. The removed framework support means framework: "proton" | "libui" no longer has any effect — see framework removed.

Renamed type exports

These exported TypeScript types were renamed with no compatibility aliasimport { OldName } is now a hard compile error. Update the import to the new name:

v26 exportv27 export
ElectronDownloadOptionsElectronGetOptions
WindowsAzureSigningConfigurationWindowsAzureSigningConfig
WindowsSigntoolConfigurationWindowsSigntoolSigningConfig

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 and new osx-sign fields are picked up automatically. 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 (cache, customDir, customFilename, strictSSL) have no v5 equivalent; migrate-schema drops them with a warning.

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 removed because having both true (enable with defaults) and {} (same meaning) was redundant.

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 rename), @electron/osx-sign 1→2 and @electron/universal 2→3 (drive the mac.sign / mac.universal 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.