fix(server): harden sherpa native loading and centralize runtime env
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+108
-30
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user