fix(server): harden sherpa native loading and centralize runtime env

This commit is contained in:
Mohamed Boudra
2026-02-16 02:59:12 +00:00
parent 4313e3623b
commit 5bd6cc11de
6 changed files with 278 additions and 30 deletions
@@ -3,6 +3,7 @@ import { closeSync, existsSync, openSync, readFileSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { loadConfig, resolvePaseoHome } from '@getpaseo/server'
import { applySherpaLoaderEnv } from './sherpa-env.js'
export interface DaemonStartOptions {
port?: string
@@ -280,6 +281,7 @@ export async function startLocalDaemonDetached(
}
const childEnv: NodeJS.ProcessEnv = { ...process.env }
applySherpaLoaderEnv(childEnv)
if (options.home) {
childEnv.PASEO_HOME = options.home
}
@@ -0,0 +1,53 @@
import { spawnSync } from 'node:child_process'
import {
applySherpaLoaderEnv as applySherpaLoaderEnvFromServer,
resolveSherpaLoaderEnv,
} from '@getpaseo/server'
const SHERPA_ENV_BOOTSTRAPPED = 'PASEO_SHERPA_ENV_BOOTSTRAPPED'
export function applySherpaLoaderEnv(env: NodeJS.ProcessEnv): {
changed: boolean
key: 'LD_LIBRARY_PATH' | 'DYLD_LIBRARY_PATH' | 'PATH' | null
libDir: string | null
} {
const result = applySherpaLoaderEnvFromServer(env)
return {
changed: result.changed,
key: result.key,
libDir: result.libDir,
}
}
export function reexecWithSherpaLoaderEnvIfNeeded(): never | void {
const resolved = resolveSherpaLoaderEnv()
if (!resolved) {
return
}
if (process.env[SHERPA_ENV_BOOTSTRAPPED] === '1') {
return
}
const envCopy: NodeJS.ProcessEnv = { ...process.env }
const applied = applySherpaLoaderEnvFromServer(envCopy)
const updatedValue = applied.key ? envCopy[applied.key] ?? '' : ''
if (applied.key && updatedValue === (process.env[applied.key] ?? '')) {
return
}
const env: NodeJS.ProcessEnv = {
...envCopy,
[SHERPA_ENV_BOOTSTRAPPED]: '1',
}
const result = spawnSync(process.execPath, process.argv.slice(1), {
env,
stdio: 'inherit',
})
if (result.error) {
throw result.error
}
process.exit(result.status ?? 1)
}
@@ -12,6 +12,7 @@ import {
startLocalDaemonDetached,
type DaemonStartOptions as StartOptions,
} from './local-daemon.js'
import { reexecWithSherpaLoaderEnvIfNeeded } from './sherpa-env.js'
import { getErrorMessage } from '../../utils/errors.js'
export type { DaemonStartOptions as StartOptions } from './local-daemon.js'
@@ -79,6 +80,8 @@ export async function runStart(options: StartOptions): Promise<void> {
return
}
reexecWithSherpaLoaderEnvIfNeeded()
if (options.home) {
process.env.PASEO_HOME = options.home
}
+9
View File
@@ -13,6 +13,15 @@ export {
type LocalSttModelId,
type LocalTtsModelId,
} from "./speech/providers/local/models.js";
export {
applySherpaLoaderEnv,
resolveSherpaLoaderEnv,
sherpaLoaderEnvKey,
sherpaPlatformArch,
sherpaPlatformPackageName,
type SherpaLoaderEnvKey,
type SherpaLoaderEnvResolution,
} from "./speech/providers/local/sherpa/sherpa-runtime-env.js";
// Agent SDK types for CLI commands
export type {
@@ -1,5 +1,12 @@
import { createRequire } from "node:module";
import path from "node:path";
import { existsSync } from "node:fs";
import { spawnSync } from "node:child_process";
import {
applySherpaLoaderEnv,
resolveSherpaLoaderEnv,
sherpaPlatformPackageName,
} from "./sherpa-runtime-env.js";
export type SherpaOnnxNodeModule = {
OfflineRecognizer: new (config: any) => any;
@@ -9,21 +16,77 @@ export type SherpaOnnxNodeModule = {
let cached: SherpaOnnxNodeModule | null = null;
function platformArch(): string {
const platform = process.platform === "win32" ? "win" : process.platform;
return `${platform}-${process.arch}`;
type LoadAttempt = {
target: string;
error: unknown;
};
function appendAttempt(attempts: LoadAttempt[], target: string, error: unknown): void {
attempts.push({ target, error });
}
function prependLibraryPath(
envKey: "DYLD_LIBRARY_PATH" | "LD_LIBRARY_PATH" | "PATH",
dir: string
): void {
const current = process.env[envKey] ?? "";
const parts = current.split(path.delimiter).filter(Boolean);
if (parts.includes(dir)) {
function formatError(error: unknown): string {
if (!error) {
return "unknown error";
}
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function maybePatchLinuxAddonRunpath(addonPath: string): void {
if (process.platform !== "linux") {
return;
}
process.env[envKey] = [dir, ...parts].join(path.delimiter);
const patchelfCheck = spawnSync("patchelf", ["--version"], {
stdio: "ignore",
});
if (patchelfCheck.status !== 0) {
return;
}
const currentRpath = spawnSync("patchelf", ["--print-rpath", addonPath], {
encoding: "utf8",
});
if (currentRpath.status !== 0) {
return;
}
const rpath = (currentRpath.stdout ?? "").trim();
if (rpath.includes("$ORIGIN")) {
return;
}
spawnSync("patchelf", ["--set-rpath", "$ORIGIN", addonPath], {
stdio: "ignore",
});
}
function loadWithRequire(
requireFn: NodeRequire,
target: string,
attempts: LoadAttempt[]
): SherpaOnnxNodeModule | null {
try {
return requireFn(target) as SherpaOnnxNodeModule;
} catch (error) {
appendAttempt(attempts, target, error);
return null;
}
}
function buildFailure(attempts: LoadAttempt[], pkgName: string): Error {
const details = attempts
.map((attempt) => `- ${attempt.target}: ${formatError(attempt.error)}`)
.join("\n");
const message = [
`Failed to load sherpa-onnx-node for ${process.platform}-${process.arch}.`,
`Node ${process.version} (ABI ${process.versions.modules}).`,
`Platform package: ${pkgName}.`,
"Load attempts:",
details || "- (no attempts made)",
].join("\n");
return new Error(message);
}
export function loadSherpaOnnxNode(): SherpaOnnxNodeModule {
@@ -32,26 +95,41 @@ export function loadSherpaOnnxNode(): SherpaOnnxNodeModule {
}
const require = createRequire(import.meta.url);
const attempts: LoadAttempt[] = [];
// sherpa-onnx-node depends on a platform-specific package (e.g. sherpa-onnx-darwin-arm64)
// that contains the native addon and shared libraries. Ensure the OS loader path includes it.
const arch = platformArch();
const pkgName = `sherpa-onnx-${arch}`;
try {
const pkgJson = require.resolve(`${pkgName}/package.json`);
const pkgDir = path.dirname(pkgJson);
if (process.platform === "darwin") {
prependLibraryPath("DYLD_LIBRARY_PATH", pkgDir);
} else if (process.platform === "linux") {
prependLibraryPath("LD_LIBRARY_PATH", pkgDir);
} else if (process.platform === "win32") {
prependLibraryPath("PATH", pkgDir);
}
} catch {
// Best effort - if the platform package isn't present, require() below will throw a useful error.
// Pass through upstream support matrix first.
const direct = loadWithRequire(require, "sherpa-onnx-node", attempts);
if (direct) {
cached = direct;
return cached;
}
cached = require("sherpa-onnx-node") as SherpaOnnxNodeModule;
return cached;
// sherpa-onnx-node depends on a platform-specific package (e.g. sherpa-onnx-darwin-arm64)
// that contains the native addon and shared libraries.
const pkgName = sherpaPlatformPackageName();
const resolvedEnv = resolveSherpaLoaderEnv();
const platformPkgDir = resolvedEnv?.libDir ?? null;
if (platformPkgDir) {
applySherpaLoaderEnv(process.env);
const addonPath = path.join(platformPkgDir, "sherpa-onnx.node");
if (existsSync(addonPath)) {
const byPath = loadWithRequire(require, addonPath, attempts);
if (byPath) {
cached = byPath;
return cached;
}
// Linux fallback for broken prebuilt RUNPATHs.
maybePatchLinuxAddonRunpath(addonPath);
const afterPatch = loadWithRequire(require, addonPath, attempts);
if (afterPatch) {
cached = afterPatch;
return cached;
}
}
}
throw buildFailure(attempts, pkgName);
}
@@ -0,0 +1,103 @@
import { createRequire } from "node:module";
import path from "node:path";
export type SherpaLoaderEnvKey = "LD_LIBRARY_PATH" | "DYLD_LIBRARY_PATH" | "PATH";
export type SherpaLoaderEnvResolution = {
key: SherpaLoaderEnvKey;
libDir: string;
packageName: string;
};
export function sherpaPlatformArch(
platform: NodeJS.Platform = process.platform,
arch: string = process.arch
): string {
const normalizedPlatform = platform === "win32" ? "win" : platform;
return `${normalizedPlatform}-${arch}`;
}
export function sherpaPlatformPackageName(
platform: NodeJS.Platform = process.platform,
arch: string = process.arch
): string {
return `sherpa-onnx-${sherpaPlatformArch(platform, arch)}`;
}
export function sherpaLoaderEnvKey(
platform: NodeJS.Platform = process.platform
): SherpaLoaderEnvKey | null {
if (platform === "linux") {
return "LD_LIBRARY_PATH";
}
if (platform === "darwin") {
return "DYLD_LIBRARY_PATH";
}
if (platform === "win32") {
return "PATH";
}
return null;
}
export function prependEnvPath(existing: string | undefined, value: string): string {
const parts = (existing ?? "").split(path.delimiter).filter(Boolean);
if (parts.includes(value)) {
return parts.join(path.delimiter);
}
return [value, ...parts].join(path.delimiter);
}
export function resolveSherpaLoaderEnv(
platform: NodeJS.Platform = process.platform,
arch: string = process.arch
): SherpaLoaderEnvResolution | null {
const key = sherpaLoaderEnvKey(platform);
if (!key) {
return null;
}
const packageName = sherpaPlatformPackageName(platform, arch);
const require = createRequire(import.meta.url);
try {
const pkgJson = require.resolve(`${packageName}/package.json`);
return {
key,
libDir: path.dirname(pkgJson),
packageName,
};
} catch {
return null;
}
}
export function applySherpaLoaderEnv(
env: NodeJS.ProcessEnv,
platform: NodeJS.Platform = process.platform,
arch: string = process.arch
): {
changed: boolean;
key: SherpaLoaderEnvKey | null;
libDir: string | null;
packageName: string | null;
} {
const resolved = resolveSherpaLoaderEnv(platform, arch);
if (!resolved) {
return {
changed: false,
key: null,
libDir: null,
packageName: null,
};
}
const next = prependEnvPath(env[resolved.key], resolved.libDir);
const changed = next !== (env[resolved.key] ?? "");
env[resolved.key] = next;
return {
changed,
key: resolved.key,
libDir: resolved.libDir,
packageName: resolved.packageName,
};
}