fix(nix): natively compile and correctly stage node-pty for desktop app

- Add ELECTRON_SKIP_BINARY_DOWNLOAD=1 to nix/lib.nix to prevent offline download failures.
- Manually trigger native compilation of node-pty via npm rebuild --build-from-source in buildPhase.
- Run stage-native-deps.cjs to copy the natively compiled binary into build/native-deps.
- Flatten native-deps and install-stamp.json to the root of the output derivation in installPhase, matching electron-builder's extraResources behavior so main.cjs can find it at process.resourcesPath + '/native-deps/node-pty'.
- Add doCheck=true and a strict checkPhase to fail fast if the staged native binary is missing.
This commit is contained in:
ethernet 2026-06-11 16:46:03 -04:00
parent 046f444ddc
commit 906bee9cf7
4 changed files with 89 additions and 49 deletions

View file

@ -95,6 +95,7 @@ try {
nodePty = require(nodePtyDir)
}
} catch {
console.log(`[terminal] failed to load node-pty from path ${nodePtyDir}`)
nodePty = null
nodePtyDir = null
}

View file

@ -18,7 +18,8 @@
"profile:main": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
"profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
"start": "npm run build && electron .",
"build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build && node scripts/assert-dist-built.cjs",
"build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build && npm run postbuild",
"postbuild": "node scripts/assert-dist-built.cjs",
"builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 electron-builder",
"pack": "npm run build && npm run builder -- --dir",
"dist": "npm run build && npm run builder",

View file

@ -6,63 +6,99 @@
# `HERMES_DESKTOP_HERMES` override env var, so the desktop's resolver
# uses our fully wrapped binary at step 4 ("existing Hermes CLI").
# No reimplementation of the agent resolution in this wrapper.
{ pkgs, lib, stdenv, makeWrapper, hermesNpmLib, electron, hermesAgent, ... }:
{
pkgs,
lib,
stdenv,
makeWrapper,
hermesNpmLib,
electron,
hermesAgent,
...
}:
let
npm = hermesNpmLib.mkNpmPassthru { folder = "apps/desktop"; attr = "desktop"; pname = "hermes-desktop"; };
npm = hermesNpmLib.mkNpmPassthru {
folder = "apps/desktop";
attr = "desktop";
pname = "hermes-desktop";
};
packageJson = builtins.fromJSON (builtins.readFile (npm.src + "/apps/desktop/package.json"));
version = packageJson.version;
# Build the renderer (dist/ + electron/ + package.json).
renderer = pkgs.buildNpmPackage (npm // {
pname = "hermes-desktop-renderer";
inherit version;
renderer = pkgs.buildNpmPackage (
npm
// {
pname = "hermes-desktop-renderer";
inherit version;
doCheck = true;
doCheck = false;
# The workspace lockfile resolves all peer deps
# correctly so --legacy-peer-deps is not needed.
# --ignore-scripts comes from mkNpmPassthru (shared).
makeCacheWritable = true;
buildPhase = ''
runHook preBuild
buildPhase = ''
runHook preBuild
# write-build-stamp.cjs replacement. Packaged Electron reads this
# at first-launch to pin the install.ps1 git ref; informational in
# nix builds (the backend comes from the derivation directly).
mkdir -p apps/desktop/build
echo '{"schemaVersion":1,"commit":"nix","branch":"nix","dirty":false,"source":"nix"}' > apps/desktop/build/install-stamp.json
# write-build-stamp.cjs replacement. Packaged Electron reads this
# at first-launch to pin the install.ps1 git ref; informational in
# nix builds (the backend comes from the derivation directly).
mkdir -p apps/desktop/build
echo '{"schemaVersion":1,"commit":"nix","branch":"nix","dirty":false,"source":"nix"}' > apps/desktop/build/install-stamp.json
# patch shebangs in node_modules/.bin so npm exec can find the
# nix-store equivalents of /usr/bin/env (which doesn't exist in the sandbox)
patchShebangs .
# Build from apps/desktop/ so vite.config.ts resolves correctly.
# The workspace root's node_modules/ is accessible as ../../node_modules/.
cd apps/desktop
pushd apps/desktop
# stage node-pty native binaries into build/native-deps for the final nix output
npm rebuild node-pty --build-from-source
node scripts/stage-native-deps.cjs
npm exec tsc -b
npm exec vite build
popd
# vite handles TS transpilation via esbuild — no type-checking.
# We skip `tsc -b` to avoid type errors in test files that don't
# ship in the bundle (real upstream peer-dep version mismatches
# in @testing-library/react v16 — not blocking the build).
# Call vite directly from root node_modules to avoid npx resolving
# through unpatched workspace symlinks.
node ../../node_modules/vite/bin/vite.js build --outDir dist
runHook postBuild
'';
# Return to source root so installPhase paths are correct.
cd ../..
checkPhase = ''
runHook preCheck
runHook postBuild
'';
pushd apps/desktop
installPhase = ''
runHook preInstall
mkdir -p $out
# vite writes to apps/desktop/dist/ (we cd'd there in buildPhase).
# apps/desktop/build was created before the cd. electron/ is source.
cp -r apps/desktop/dist $out/
cp -r apps/desktop/electron $out/
cp -r apps/desktop/build $out/
cp apps/desktop/package.json $out/
runHook postInstall
'';
});
npm run postbuild
# validate staged node-pty native binary is present
STAGED_PTY_NODE="./build/native-deps/node-pty/build/Release/pty.node"
if [ ! -f "$STAGED_PTY_NODE" ]; then
echo "FATAL: Missing staged node-pty native binary at $STAGED_PTY_NODE"
echo "node-pty must be compiled natively"
exit 1
fi
popd
runHook postCheck
'';
installPhase = ''
runHook preInstall
mkdir -p $out
# vite writes to apps/desktop/dist/ (we cd'd there in buildPhase).
# apps/desktop/build was created before the cd. electron/ is source.
cp -rn apps/desktop/dist $out/
cp -rn apps/desktop/electron $out/
# flatten native-deps and install-stamp.json to the root level, exactly like
# electron-builder's extraResources does ("from": "build/native-deps", "to": "native-deps")
# so main.cjs can find it at process.resourcesPath + '/native-deps/node-pty'
cp -rn apps/desktop/build/native-deps $out/
cp -n apps/desktop/build/install-stamp.json $out/
cp -n apps/desktop/package.json $out/
runHook postInstall
'';
}
);
in
# Electron wrapper: nixpkgs' electron binary pointed at the renderer dir.
@ -81,6 +117,12 @@ stdenv.mkDerivation {
mkdir -p $out/share/hermes-desktop $out/bin
cp -r ${renderer}/* $out/share/hermes-desktop/
# Standard nixpkgs pattern for electron-builder apps: patch process.resourcesPath
# to point to the app's directory. In Nix, unpackaged electron defaults this
# to the electron distribution's resources path, breaking extraResources lookups.
substituteInPlace $out/share/hermes-desktop/electron/main.cjs \
--replace-fail "process.resourcesPath" "'$out/share/hermes-desktop'"
# Wrap the nixpkgs electron binary to launch our app. Set
# HERMES_DESKTOP_HERMES to the absolute path of the nix-built `hermes`
# binary so the desktop's resolver step 4 ("existing Hermes CLI on

View file

@ -65,11 +65,7 @@ in
npmRoot = ".";
npmDepsFetcherVersion = 2;
# --ignore-scripts: the workspace includes electron (apps/desktop)
# which has a postinstall that tries to download from github.com.
# nix builds are offline, so all scripts must be skipped. Each
# package sets up its own build commands in buildPhase instead.
npmFlags = [ "--ignore-scripts" ];
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
patchPhase = ''
runHook prePatch