Skip to content

Packaging ​

How the app is built into something you can hand to someone else.

What ships ​

PlatformArtifactArchitectures
Linux.AppImagex64
macOS.dmgarm64, 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:

bash
scripts/build-core.sh              # linux-x64, darwin-arm64, darwin-x64
pwsh -File scripts/build-core.ps1  # win-x64

PyInstaller 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 ​

bash
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 testing

npm 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:

bash
npm run icons

That 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:

  1. SNOWPEA_CORE_BIN, if it points at an executable
  2. the bundled tree under resources/core/
  3. snowpea-core on PATH
  4. uv 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 protocolVersion major/minor is older than the protocol this IDE build was generated against (@snowpea/sdk's PROTOCOL_VERSION)
  • its gitRef names a tag older than the pinned REF= default in scripts/build-core.sh (only checked in a dev tree that has the script next to resources/; 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.

MIT licensed. The manual is generated from the product repositories — see the banner on each page.