Packaging
How the app is built into something you can hand to someone else.
What ships
| Platform | Artifact | Architectures |
|---|---|---|
| Linux | .AppImage | x64 |
| macOS | .dmg | arm64, x64 |
| Windows | .exe (NSIS installer) | x64 |
Artifacts are named snowpea-ide-<version>-<os>-<arch>.<ext> and land in dist-electron/. There is no auto-update channel yet; releases are assets on a tag.
The bundled core
A packaged app carries its own core, so a person who installs it does not have to install anything else. The core is a PyInstaller onedir tree — never a onefile bundle — built from the public core repository and copied into the app's resources at resources/core/.
That tree has to exist for the target you are packaging before you package it, or the installer is built without a core and the app falls back to whatever snowpea-core it can find on the machine:
scripts/build-core.sh # linux-x64, darwin-arm64, darwin-x64
pwsh -File scripts/build-core.ps1 # win-x64PyInstaller cannot cross-compile, so each platform's core is built on that platform. The release pipeline has one job per platform for exactly this reason.
Building
npm run dist # every target this machine can produce
npm run dist:linux # AppImage
npm run dist:mac # dmg
npm run dist:win # NSIS installer
npm run dist:dir # unpacked tree, no installer — useful for testingnpm run dist runs the production build first, so it always packages what is in src/, not what was last left in out/.
The icon
The app icon is generated, never drawn by hand:
npm run iconsThat reads src/renderer/ui/mark.svg — the one file in this repo that holds the brand mark, and the same file the app inlines for its own wordmark — and writes build/icon.svg, build/icon.png (1024²) and build/icon-512.png. Change the mark, re-run it, commit what it wrote. Nothing else keeps the window's mark and its dock tile in step.
There is no .icns and no .ico in the repository, and none is generated: electron-builder derives both from a 512²-or-larger icon.png while it packages.
Inside the app the mark is flat and takes the theme's accent colour, so it is right in all eight themes. The icon tile is the one surface that carries the brand gradient, because it is the one surface that is only ever seen alone.
How the app finds a core
In order, first hit wins:
SNOWPEA_CORE_BIN, if it points at an executable- the bundled tree under
resources/core/ snowpea-coreonPATHuv run --project $SNOWPEA_REPO snowpea-core, the developer fallback
If every branch comes up empty the app shows the no-core screen with the install command rather than failing with a spawn error.
In development, 2 and 3 swap. !app.isPackaged, or an explicit SNOWPEA_REPO even in a packaged build, counts as "development": PATH/uv are tried before the bundle. A dev tree's resources/core/ is a leftover build artifact (from a previous scripts/build-core.sh run, or a stale electron-builder extraResources copy) far more often than it is the core a developer actually means to run against, so it no longer gets to win by default. SNOWPEA_CORE_BIN still wins outright over everything, and SNOWPEA_PREFER_BUNDLED=1 opts back into the packaged order for testing the bundle itself from a dev tree.
A stale bundle is skipped, not spawned. Before the bundled tree can win step 2, CoreManager reads resources/core/<platform>/snowpea-core-info.json next to the executable and refuses it — logging the reason to the main process console — when either is true:
- its
protocolVersionmajor/minor is older than the protocol this IDE build was generated against (@snowpea/sdk'sPROTOCOL_VERSION) - its
gitRefnames a tag older than the pinnedREF=default inscripts/build-core.sh(only checked in a dev tree that has the script next toresources/; a packaged app has no script to compare against)
A tree with no snowpea-core-info.json at all — an old bundle from before this check existed — is trusted rather than refused. If the stale bundle was the only candidate, resolution simply fails with the no-core screen instead of spawning a core that cannot answer half its RPCs.
A core that does start but turns out to share the IDE's protocol major while trailing its minor is not rejected either: start() connects it and flags protocolOutdated on the status instead, so the outdated-core banner shows ("core is older than this app: some screens will not work — update the core") while the rest of the app keeps working. Only a differing major blocks the app, with the full-screen incompatible-core view.
Two things that are easy to get wrong
node-pty must stay unpacked. It has a prebuilt native module that has to live on the real filesystem, not inside the asar archive. When it does not load, the Terminal falls back to a pipe-based shell.
The websocket library must not be bundled. ws stays a production dependency and out of the main bundle. Rolling it in produces an app that spawns the core, passes its health check, and then hangs forever while connecting — a failure with no error message at all.
macOS signing
The bundled core is an unsigned third-party binary tree, and a hardened runtime needs entitlements to execute it. Signing and notarisation are not part of v0.2, so a .dmg built here will be quarantined on someone else's Mac until they clear it by hand.