Ohm installers#

Installer builds live here, one folder per platform, kept out of the app's npm workspaces so that packaging tools never end up in the app's dependency tree.

installers/
├─ shared/          branding artwork + the staging step both platforms use
│  ├─ branding.html the source of every image (icon, NSIS pages, DMG background)
│  ├─ assets/       the rendered images, committed so a build needs no design tools
│  ├─ config.mjs    product identity (appId, version, Electron version)
│  └─ stage.mjs     builds the app and lays it out the way it ships
├─ windows/         NSIS installer     → windows/dist
├─ macos/           DMG + zip          → macos/dist
├─ linux/           AppImage/deb/tar   → linux/dist
└─ build/           scratch space (staged app); safe to delete

Build#

cd installers
npm install
npm run assets          # once, or after changing branding.html

Windows (run on Windows):

npm run build:win                                  # NSIS installer, x64
npm run build:win -- --arch arm64
npm run build:win -- --targets nsis,portable,zip
npm run build:win -- --backends cpu,vulkan,cuda    # GPU llama.cpp, much larger
npm run build:win -- --no-build                    # reuse the last app build
npm run build:win -- --out windows/dist2           # build somewhere else

electron-builder deletes and recreates <out>/win-unpacked, so a build fails with EBUSY: resource busy or locked while anything is holding that folder — a shell whose current directory is inside it is enough, as is a running Ohm.exe. Close it, or use --out to build elsewhere.

macOS (run on macOS — hdiutil and codesign exist only there):

npm run build:mac                       # DMG + zip, arm64
npm run build:mac -- --arch x64
npm run build:mac -- --arch universal
npm run build:mac -- --validate         # any OS: check assets, staging and config

Linux (run on Linux for AppImage and deb):

npm run build:linux                           # AppImage + deb, x64
npm run build:linux -- --arch arm64
npm run build:linux -- --targets tar.gz       # works from any host
npm run build:linux -- --validate             # any OS: check assets, staging and config

AppImage and deb shell out to tooling that only exists on Linux (appimagetool, fakeroot/dpkg). tar.gz and dir are plain file copying, so they build from Windows or macOS too — useful for a smoke test, though the llama.cpp binaries for Linux are only installed when npm runs on Linux.

From the repository root, build.bat package (Windows) and ./build.sh package (macOS, Linux) do the install, the checks, the app build and the platform's packaging in one step.

All three scripts build the app first (npm run build in the repo root), so a fresh clone needs only npm install at the root and in installers/.

Testing the package#

npm run check runs against the source tree, where every file is on disk and every path resolves. Packaging is where a file stops being staged, a worker stops being bundled, or the compiled help never reaches resources — and the app still starts, so nobody notices until it is installed.

npm run smoke                                      # the default win-unpacked build
node scripts/smoke-package.cjs --app "C:\Program Files\Ohm\Ohm.exe"
node scripts/smoke-package.cjs --keep              # leave the throwaway profile behind

It drives the packaged app over CDP against a fresh scripts folder and a throwaway profile, so it never touches the Ohm you use, and checks eleven things that can only fail after packaging:

Check What it would catch
the window comes up the renderer or ohm-app:// not staged
the examples are seeded EXAMPLES not reaching a packaged first run
welcome.ohm runs end to end the Lua worker or its wasm missing from the package
LaTeX maths compiles and evaluates the compiler not bundled
a Volt program builds and runs the Volt parser or evaluator missing
Volt prices the run before starting it the plan line lost — the cost-before-run claim
the example app is found and running apps/ not seeded, or the scan not running
every capability lists its calls the host API table not reaching the packaged build
an undeclared capability is refused the capability check not wired in the real binary
the compiled help site is present npm run docs output not staged into resources
no errors on the console anything that throws on the way up

Run it after every buildinstaller.bat, before sending anyone the .exe.

What ends up in the package#

Part Where it comes from
desktop/dist/*.js esbuild bundles of the main process, preload and the Ohm worker
ui/dist/** the Vite build of the renderer, served through ohm-app://
node_modules/** only what is loaded at runtime: wasmoon, node-llama-cpp and their 89 transitive packages
@node-llama-cpp/* the prebuilt llama.cpp binary for the target only (CPU by default)

Two deliberate choices:

  • No asar. The app serves its own UI with net.fetch(file://…) and starts the Ohm interpreter in a worker thread; neither can see inside an asar archive.
  • CPU-only llama.cpp by default. The Vulkan and CUDA builds add ~600 MB. Pass --backends cpu,vulkan,cuda when you want them.

Roughly: a 120 MB Windows installer, 460 MB installed.

Windows installer behaviour#

  • A wizard (welcome → install for me/everyone → folder → progress → finish), branded with the sidebar and header from shared/assets.
  • Per-user by default, so no UAC prompt; "anyone who uses this computer" is offered on the install-mode page and elevates.
  • Desktop and Start Menu shortcuts, and an "Ohm version" entry in Apps & features.
  • HKCU\Software\Ohm records InstallLocation and Version.
  • Uninstalling removes the program, the shortcuts and those keys. It asks — defaulting to no — whether to also delete %APPDATA%\Ohm (settings, saved credentials, downloaded models). Documents\Ohm, where your scripts and projects live, is never touched.

Customise the wizard in windows/installer.nsh; electron-builder calls those macros at the matching point of its own NSIS script.

macOS disk image#

  • A DMG whose window shows the app, an arrow and the Applications folder on the drawn background, plus a .zip of the same .app.
  • Hardened runtime with entitlements for JIT (V8 and the Lua VM) and for loading the unsigned llama.cpp binary.

Signing#

Both builds work unsigned — useful locally, and what --validate assumes. For a build other people will install, set these in the environment before building:

Platform Variables
Windows CSC_LINK (.pfx path or base64), CSC_KEY_PASSWORD
macOS CSC_LINK, CSC_KEY_PASSWORD — and for notarization APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID

Unsigned Windows builds get a SmartScreen warning on first run; unsigned macOS builds get Gatekeeper's "unidentified developer".

Branding#

Every image comes from shared/branding.html, rendered by the repo's own Electron (npm run assets) into shared/assets:

File Used for
icon.png, icon-512.png source art, and the icon for any other target
icon.ico the Windows app, installer and uninstaller
icon.icns the macOS app and the DMG volume
installer-sidebar.bmp NSIS welcome and finish pages (164×314)
installer-header.bmp NSIS interior pages (150×57)
dmg-background.png, @2x the DMG window

Edit the HTML, re-run npm run assets, rebuild. The Ω is drawn as geometry, not type, so it renders the same on a machine with no fonts installed.