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,cudawhen 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\OhmrecordsInstallLocationandVersion.- 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
.zipof 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.