Packaging Desktop Builds
The desktop build is Electron + electron-builder. package.json exposes package:linux / package:mac / package:win; CI runs each on its native runner and creates a GitHub Release on v* tags.
Three gates
- Web bundle —
pnpm build:webwritesdist/. - Electron main —
pnpm build:electroncompiles TypeScript todist-electron/. - electron-builder — packages native installers into
release/.
pnpm package:* chains all three.
Goal
Produce installers for all three platforms locally:
- Linux: AppImage + .deb
- macOS: DMG (universal)
- Windows: NSIS installer + portable zip
Prerequisites
- Node 22.22.1+, pnpm 11.5.2.
- macOS packages MUST be built on macOS (signing is local-only).
- Windows can cross-build from Linux (unsigned). Signed NSIS requires Windows + cert token.
- License activation flow already verified (see Issuing License Keys).
Packaging pipeline
Step-by-step
1. Clean Linux build
pnpm install --frozen-lockfile
pnpm package:linux
ls release/
# Apollo Map Studio-1.0.0-linux-x64.AppImage
# Apollo Map Studio-1.0.0-linux-amd64.deb2
3
4
5
release/ also contains builder-debug.yml and builder-effective-config.yaml (builder diagnostics; CI excludes them).
2. electron-builder.yml essentials
appId: com.apollo-map-studio.app
productName: Apollo Map Studio
directories:
output: release
files:
- dist/**/*
- dist-electron/**/*
- package.json
asar: true
extraMetadata:
main: dist-electron/main.cjs
dependencies: {} # critical — see warning below
publish: null2
3
4
5
6
7
8
9
10
11
12
13
dependencies: {} is intentional
electron-builder runs npm install for package.json.dependencies by default, but Vite already bundles all runtime code into dist-electron/. Reinstalling adds hundreds of MB of dead weight. Force-empty.
3. macOS configuration
mac:
category: public.app-category.developer-tools
target:
- target: dmg
arch: [x64, arm64]
- target: zip
arch: [x64, arm64]
hardenedRuntime: false # dev only; release needs true + entitlements2
3
4
5
6
7
8
pnpm package:mac
# release/Apollo Map Studio-1.0.0-mac-x64.dmg
# release/Apollo Map Studio-1.0.0-mac-arm64.dmg2
3
4. Code signing (macOS)
Unsigned builds trigger Gatekeeper "is damaged" warnings. Sign with:
security import developer-id.p12 -P 'password'
export CSC_LINK=$(base64 < developer-id.p12)
export CSC_KEY_PASSWORD='password'
export APPLE_ID='your@dev.account'
export APPLE_APP_SPECIFIC_PASSWORD='abcd-efgh-ijkl-mnop'
export APPLE_TEAM_ID='ABCDE12345'
pnpm package:mac2
3
4
5
6
7
8
9
Never commit certificates
developer-id.p12 is never in the repo. CI uses GitHub secrets: secrets.MAC_CSC_LINK, secrets.MAC_CSC_KEY_PASSWORD, etc.
5. Notarization (macOS)
mac:
hardenedRuntime: true
notarize:
teamId: ABCDE123452
3
4
pnpm package:mac then auto-uploads to Apple Notary Service. Wait 2–15 min.
6. Windows NSIS
win:
target:
- target: nsis
arch: [x64]
- target: zip
arch: [x64]
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true2
3
4
5
6
7
8
9
10
11
Code signing (EV / OV cert):
export CSC_LINK=$(base64 < windows-cert.pfx)
export CSC_KEY_PASSWORD='password'
pnpm package:win2
3
EV certificates require USB tokens
EV private keys can't be exported; signing happens on the token. EV signing in CI is painful — common pattern: sign locally before delivery and upload to the GitHub Release.
7. Linux AppImage / .deb
linux:
category: Development
maintainer: Apollo Map Studio <maintainers@apollo-map-studio.local>
target:
- target: AppImage
arch: [x64]
- target: deb
arch: [x64]2
3
4
5
6
7
8
No signing required; AppImage gains incremental updates with a zsync file (not yet enabled).
CI release workflow
Full workflow: .github/workflows/ci.yml.
Files modified
| File | Change |
|---|---|
electron-builder.yml | Targets, signing, notarization |
electron/main.cts | Main-process entry |
electron/preload.ts | Renderer bridge |
package.json scripts | Packaging commands |
.github/workflows/ci.yml | CI release matrix |
Testing checklist
Common pitfalls
dependencies drags node_modules in
You forgot extraMetadata.dependencies: {}. If asar is over 200 MB, suspect this first.
macOS "App is damaged"
Unsigned / unnotarized. Self-test workaround:
xattr -d com.apple.quarantine /Applications/Apollo\ Map\ Studio.appCustomer-facing builds MUST be signed and notarized.
Windows SmartScreen warning
Without an EV cert you'll see "Microsoft Defender SmartScreen blocked". Build reputation slowly with Defender's reputation service or buy an EV cert and ship clean immediately.
Linux .deb missing dependencies
linux:
desktop:
Categories: 'Development;Graphics'
deb:
depends: ['libgtk-3-0', 'libnotify4', 'libnss3']2
3
4
5
Electron usually infers these — occasionally one slips.
Cross-platform path case
Linux is case-sensitive, macOS defaults to insensitive, Windows is insensitive. Misspelled imports work locally on macOS but fail on Linux CI. Run pnpm build:web on Linux before push (CI does this for you).
Cannot find module 'dist-electron/main.cjs' at start
pnpm build:electron did not run, or tsconfig.electron.json changed output path. Keep package.json.main and electron-builder.yml.extraMetadata.main in sync.
Source links
electron-builder.ymlelectron/main.ctstsconfig.electron.json.github/workflows/ci.yml—desktop-packagematrix- electron-builder docs
Advanced
Auto-update (electron-updater)
Not enabled today. To enable:
pnpm add electron-updater- Add
publish: { provider: 'github' }toelectron-builder.yml. - Call
autoUpdater.checkForUpdatesAndNotify()inelectron/main.cts.
Auto-update requires signing
Unsigned builds reject new packages to avoid MITM substitution.
Multilingual NSIS
nsis:
installerLanguages: ['en_US', 'zh_CN']
language: '2052' # zh_CN2
3
Launch at startup
electron/main.cts:
app.setLoginItemSettings({ openAtLogin: true });Expose a user-facing toggle; default off.
Release-day ritual
After tagging, spend 5 min on a manual smoke test: install → activate → import a sample map → draw a lane → export → uninstall. If those five steps work, your users can run it.