Skip to main content

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

PropertyValue
FiresBefore native dependency install/rebuild
Return falseSkips native dependency installation entirely
Context typeBeforeBuildContext
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


Phase 2b — Pre-Pack

Fires before any file copying begins. The staging directory (appOutDir) has been created but is empty.

🪝 beforePack

PropertyValue
FiresBefore files are copied into the app bundle
Staging dir stateEmpty
Context typeBeforePackContext
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

PropertyValue
FiresAfter Electron is extracted; before any app files are placed
Staging dir stateContains Electron binary only
Context typeAfterExtractContext (same shape as PackContext)
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

PropertyValue
FiresBefore Electron extraction, to override the source
ReturnPath to a custom Electron directory or a folder of zip files
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 if asar: true (the default).
  4. Copies extraResources into the platform resources directory.
  5. Copies extraFiles into the app root.

🪝 onNodeModuleFile

PropertyValue
FiresOnce per file inside node_modules during packaging
Return trueForce-include the file
Return falseForce-exclude the file
Return undefined/voidUse default inclusion logic
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

PropertyValue
FiresAfter all files are packaged; before code signing
Staging dir stateComplete app bundle, unsigned
Context typeAfterPackContext
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


Phase 2g — Code Sign

The app bundle is signed using the platform-native toolchain:

PlatformToolWhat is signed
macOScodesign.app bundle, all frameworks and dylibs
Windowssigntool.exeEXE and DLL files
Linux(no signing in this phase)

Notarization (macOS) runs here when mac.notarize: true is set.

🪝 afterSign

PropertyValue
FiresAfter signing completes; only if signing actually ran
Not firedWhen signing is skipped (no certificate configured)
Context typeAfterPackContext
afterSign: async ({ outDir, appOutDir, packager, electronPlatformName, arch, targets }) => {
// App is signed. Distributables have not been built yet.
}
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.

For a fully custom signing implementation (replacing the built-in signer):

# electron-builder.config.yml
mac:
sign: ./scripts/custom-sign.js
win:
signtoolOptions:
sign: ./scripts/custom-sign.js

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

PropertyValue
FiresImmediately before a single artifact starts building
Context typeArtifactBuildStarted
artifactBuildStarted: async ({ targetPresentableName, file, safeArtifactName, packager, arch }) => {
console.log(`Starting: ${targetPresentableName}`)
}

🪝 artifactBuildCompleted

PropertyValue
FiresImmediately after a single artifact finishes building
Context typeArtifactCreated
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:

HookTargetFires when
msiProjectCreatedMSI (WiX)After WiX .wxs file is written to disk, before candle.exe / light.exe
appxManifestCreatedAppX / MSIXAfter AppxManifest.xml is written to disk, before makeappx.exe
// 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)
}
// 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

PropertyValue
FiresAfter every platform/arch/target finishes; before publishing
Context typeBuildResult
Returnstring[] — additional file paths to include in publish
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 for provider setup.


Hook Summary Table

HookPhaseWhat the filesystem looks likeCommon use
beforeBuild2aSource tree onlySkip or customize native rebuild
beforePack2bEmpty staging dirInject generated source files
afterExtract2cStaging: Electron binary onlyModify the Electron binary
electronDist2c(before extraction)Supply a custom Electron build
onNodeModuleFile2d(during file copy)Filter node_modules inclusions
afterPack2eStaging: full app bundle, unsignedModify bundle before signing
afterSign2gStaging: full app bundle, signedCustom post-sign steps
artifactBuildStarted2hTarget build startingLogging, timing
msiProjectCreated2h (MSI)WiX .wxs file on diskEdit WiX XML
appxManifestCreated2h (AppX)AppxManifest.xml on diskEdit AppX manifest
artifactBuildCompleted2hArtifact file on diskChecksums, per-artifact upload
afterAllArtifactBuild3All artifacts on diskSymbol upload, extra publish paths

Target Build Sequences

Each target runs inside Phase 2h. Here is what happens inside each one:

Windows

TargetSequence
NSISGenerate script → compile with makensis → sign installer → emit artifact
MSI (WiX)Write .wxsmsiProjectCreated hook → candle.exe + light.exe → sign → emit
AppX / MSIXWrite manifest → appxManifestCreated hook → makeappx.exe + makepri.exe → sign → emit
PortableCopy staged app into a self-extracting archive → emit

macOS

TargetSequence
DMGCreate .dmg via dmgbuild → emit
ZIPArchive .app bundle → emit
PKGBuild .pkg via pkgbuild + productbuild → sign → emit
MASRe-sign with MAS provisioning profile → build pkg → emit

Linux

TargetSequence
AppImageCreate squashfs image → embed ELF header → emit
DEB / RPM / PacmanPackage via fpm → emit
SnapWrite snapcraft.yaml → run snapcraft → emit
FlatpakWrite 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.

# electron-builder.config.yml
concurrency:
jobs: 2 # pack up to 2 platform/arch combos simultaneously

What concurrency affects

ScopeBehavior
Platform × arch loopUp to jobs pack operations run in parallel
Hooks within one packAlways serial — each hook is fully awaited before the build advances
afterAllArtifactBuildFires 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.

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 and Build Architectures.


Further Reading