fix(nix): package local speech worker (#1587)

* fix(nix): package local speech worker

* fix(nix): reuse sherpa package helper

* fix(nix): patch sherpa-onnx prebuilt binaries for NixOS libstdc++

The prebuilt sherpa-onnx-linux-x64 .so files link against libstdc++.so.6
which is not on the NixOS library path. Add autoPatchelfHook to fix ELF
RPATHs and stdenv.cc.cc.lib to provide the C++ runtime, resolving the
"Failed to load model because protobuf parsing failed" SIGABRT at
speech worker startup.
This commit is contained in:
yhori
2026-07-20 18:21:10 +09:00
committed by GitHub
parent 3d86c738ff
commit 0c68b26a8b
2 changed files with 31 additions and 14 deletions

View File

@@ -5,6 +5,7 @@
nodejs_22,
python3,
makeWrapper,
autoPatchelfHook,
# node-pty needs libuv headers on Linux
libuv,
# Exposed so downstream flakes that follow a different nixpkgs revision
@@ -59,10 +60,13 @@ buildNpmPackage rec {
nativeBuildInputs = [
python3 # for node-gyp (node-pty compilation)
makeWrapper
] ++ lib.optionals stdenv.hostPlatform.isLinux [
autoPatchelfHook
];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
libuv
stdenv.cc.cc.lib # libstdc++ for sherpa-onnx prebuilt binaries
];
# Don't use the default npm build hook — we need a custom build sequence
@@ -71,10 +75,9 @@ buildNpmPackage rec {
buildPhase = ''
runHook preBuild
# Rebuild only node-pty (native addon for terminal emulation).
# Speech-related native modules (sherpa-onnx, onnxruntime-node) are
# intentionally left unbuilt they're lazily loaded and gracefully
# degrade when unavailable.
# Rebuild only node-pty (native addon for terminal emulation). The sherpa
# speech runtime ships prebuilt platform packages and is copied into the
# daemon closure by scripts/trace-daemon.mjs.
npm rebuild node-pty
# Build all server packages in dependency order (defined in package.json)
@@ -89,7 +92,7 @@ buildNpmPackage rec {
# Compute the daemon's runtime closure by static module-graph tracing
# (@vercel/nft from supervisor-entrypoint.js, cli/dist/index.js, and the
# forked terminal-worker-process.js) plus an explicit list of non-JS
# forked terminal/speech worker processes) plus an explicit list of non-JS
# assets read at runtime. The trace script is the single source of
# truth for what the daemon needs at $out auditable in plain JS, no
# npm hoisting / .bin / workspace-symlink footguns.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Emit the set of files the daemon and CLI need at runtime, computed by
// static module-graph tracing (@vercel/nft) from the three entry points.
// static module-graph tracing (@vercel/nft) from the daemon entry points.
// Used by nix/package.nix's installPhase to materialize $out/lib/paseo
// with only the bytes the daemon actually loads — no Expo, RN, Metro,
// Electron, ML stacks, or other non-daemon workspace bloat.
@@ -15,16 +15,27 @@
import { nodeFileTrace } from "@vercel/nft";
import { glob } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const REPO_ROOT = path.resolve(import.meta.dirname, "..");
// Three entry points. The terminal worker is forked into its own Node
// process and has its own require tree (node-pty, etc.) — nft does not
// follow fork boundaries, so it must be traced separately.
const { sherpaPlatformPackageName } = await import(
pathToFileURL(
path.join(
REPO_ROOT,
"packages/server/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js",
),
).href
);
// Daemon entry points. Workers forked into their own Node processes have
// independent require trees; nft does not follow fork boundaries, so trace
// them separately.
const entries = [
"packages/cli/dist/index.js",
"packages/server/dist/scripts/supervisor-entrypoint.js",
"packages/server/dist/server/terminal/terminal-worker-process.js",
"packages/server/dist/server/server/speech/providers/local/worker-process.js",
];
// Files read at runtime via fs APIs rather than `require`. nft only
@@ -44,19 +55,22 @@ const additionalInputs = [
// the Nix derivation builds for one platform at a time and ships only
// its own binaries.
`node_modules/node-pty/prebuilds/${process.platform}-${process.arch}/**`,
// sherpa-onnx-node dynamically resolves a platform-specific native package.
// Copy the wrapper plus the host platform package explicitly.
"node_modules/sherpa-onnx-node/**",
`node_modules/${sherpaPlatformPackageName()}/**`,
];
// Trace.
const { fileList, warnings } = await nodeFileTrace(entries, {
base: REPO_ROOT,
// Tolerate the conditional / dynamic patterns we already audited:
// sherpa-onnx-${platform}-${arch} package resolution (sherpa is
// intentionally not built in the Nix sandbox; voice features degrade
// gracefully when unavailable), and a handful of test-only requires
// that get tree-shaken out by tsc.
// sherpa-onnx-${platform}-${arch} package resolution (the host package
// is copied explicitly above), and a handful of test-only requires that
// get tree-shaken out by tsc.
ignore: [
// Cross-platform native packages for the sherpa speech runtime;
// unsupported in the Nix build, lazily loaded when present.
// only the host platform package is needed at runtime.
"sherpa-onnx-*/**",
// Platform-specific clipboard variants; only the host's variant
// is needed at runtime, and the package's index.js resolver picks