Skip to content

Commit 6121abf

Browse files
bmehta001Copilot
andcommitted
js: point GenAI at ORT via ORT_LIB_PATH; drop the unversioned dylib alias
onnxruntime-genai's InitApi() (built with dlopen on macOS-non-framework and Linux, per its CMakeLists) honors the ORT_LIB_PATH env var: it dlopens that exact file before its hardcoded unversioned libonnxruntime.{dylib,so} fallback. Set it in the JS addon loader to the ORT we ship, so the package no longer needs the unversioned alias purely for GenAI: - detail/native.ts: applyOrtLibPath() sets ORT_LIB_PATH (only if unset) to the resolved ORT in the prebuilds dir, or the configured libraryPath, before the addon -- and thus GenAI's lazy InitApi -- loads. No-op on Windows (ORT is linked directly, no dlopen). ortCandidateBasenames now prefers the versioned soname. - install-native.cjs: rename the extracted unversioned ORT to the versioned soname (libonnxruntime.1.dylib / .so.1) that libfoundry_local records, instead of symlinking and keeping both -- one file, no symlink. - copy-native.mjs: dev staging copies the versioned soname only, matching ship. macOS CI (js_test_osx_arm64) runs npm ci (install-native postinstall) + vitest with FOUNDRY_TEST_DATA_DIR set, so the real-model path that loads GenAI exercises this end to end. The C++ build keeps its own ORT symlink for the C++ test harness, which does not set ORT_LIB_PATH. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 11fd9e4 commit 6121abf

3 files changed

Lines changed: 63 additions & 43 deletions

File tree

sdk_v2/js/script/copy-native.mjs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,17 @@ const wanted = (() => {
5858
];
5959
}
6060
if (process.platform === "darwin") {
61-
// The ORT dylib is referenced under two fixed names: foundry_local loads it by
62-
// its soversion install_name (libonnxruntime.1.dylib), while GenAI's static
63-
// initializer dlopen()s the unversioned libonnxruntime.dylib. Both must sit
64-
// beside the addon. Shipped packages symlink one name to the other to avoid
65-
// duplicating the ~24MB binary; this dev-only staging just copies both, since
66-
// the duplicate never leaves the machine.
61+
// Copy only the versioned ORT soname (libonnxruntime.1.dylib) — the name
62+
// libfoundry_local records. GenAI is pointed at it via ORT_LIB_PATH (set by the
63+
// addon loader in detail/native.ts), so the unversioned alias isn't needed. The
64+
// C++ build stages the versioned name as a symlink; copyFileSync dereferences it.
6765
return [
6866
"libfoundry_local.dylib",
69-
"libonnxruntime.dylib",
7067
"libonnxruntime.1.dylib",
7168
"libonnxruntime-genai.dylib",
7269
];
7370
}
74-
return ["libfoundry_local.so", "libonnxruntime.so", "libonnxruntime.so.1", "libonnxruntime-genai.so"];
71+
return ["libfoundry_local.so", "libonnxruntime.so.1", "libonnxruntime-genai.so"];
7572
})();
7673

7774
let copied = 0;

sdk_v2/js/script/install-native.cjs

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -225,38 +225,32 @@ async function installPackage(artifact, tempDir, binDir) {
225225
);
226226
}
227227

228-
// Mirror the platform-specific post-build steps that sdk_v2/cpp/CMakeLists.txt
229-
// runs after building libfoundry_local. The Foundry ORT nupkg ships only the
230-
// unversioned `libonnxruntime.{so,dylib}`, but libfoundry_local records a
231-
// versioned SONAME/install_name dependency (libonnxruntime.so.1 /
232-
// libonnxruntime.1.dylib), so we add that soname symlink next to the shipped
233-
// file. GenAI continues to dlopen the unversioned name, which stays as-is.
234-
function applyOrtPlatformAliases(binDir, ortVersion) {
228+
// The Foundry ORT nupkg extracts the unversioned `libonnxruntime.{so,dylib}`, but
229+
// libfoundry_local records a versioned SONAME/install_name dependency
230+
// (libonnxruntime.so.1 / libonnxruntime.1.dylib). Rename the extracted file to that
231+
// versioned soname so foundry_local resolves it via rpath. GenAI is pointed at the
232+
// same file by the ORT_LIB_PATH env var the addon loader sets (onnxruntime-genai
233+
// honors it before its unversioned-name fallback), so no unversioned alias is
234+
// shipped. Windows uses onnxruntime.dll, which has no soname.
235+
function normalizeOrtLibName(binDir, ortVersion) {
236+
let unversioned;
237+
let versioned;
235238
if (os.platform() === 'linux') {
236-
const unv = path.join(binDir, 'libonnxruntime.so');
237-
const soname = path.join(binDir, 'libonnxruntime.so.1');
238-
if (fs.existsSync(unv) && !fs.existsSync(soname)) {
239-
try {
240-
fs.symlinkSync('libonnxruntime.so', soname);
241-
console.log(` Created symlink libonnxruntime.so.1 -> libonnxruntime.so`);
242-
} catch (err) {
243-
fs.copyFileSync(unv, soname);
244-
console.log(` Copied libonnxruntime.so -> libonnxruntime.so.1 (symlink failed: ${err.message})`);
245-
}
246-
}
239+
unversioned = path.join(binDir, 'libonnxruntime.so');
240+
versioned = path.join(binDir, 'libonnxruntime.so.1');
247241
} else if (os.platform() === 'darwin') {
248242
const major = ortVersion.split('.')[0];
249-
const unv = path.join(binDir, 'libonnxruntime.dylib');
250-
const soname = path.join(binDir, `libonnxruntime.${major}.dylib`);
251-
if (fs.existsSync(unv) && !fs.existsSync(soname)) {
252-
try {
253-
fs.symlinkSync('libonnxruntime.dylib', soname);
254-
console.log(` Created symlink libonnxruntime.${major}.dylib -> libonnxruntime.dylib`);
255-
} catch (err) {
256-
fs.copyFileSync(unv, soname);
257-
console.log(` Copied libonnxruntime.dylib -> libonnxruntime.${major}.dylib (symlink failed: ${err.message})`);
258-
}
259-
}
243+
unversioned = path.join(binDir, 'libonnxruntime.dylib');
244+
versioned = path.join(binDir, `libonnxruntime.${major}.dylib`);
245+
} else {
246+
return;
247+
}
248+
if (fs.existsSync(versioned)) {
249+
return;
250+
}
251+
if (fs.existsSync(unversioned)) {
252+
fs.renameSync(unversioned, versioned);
253+
console.log(` Renamed ${path.basename(unversioned)} -> ${path.basename(versioned)}`);
260254
}
261255
}
262256

@@ -269,7 +263,7 @@ function applyOrtPlatformAliases(binDir, ortVersion) {
269263
for (const artifact of ARTIFACTS) {
270264
await installPackage(artifact, tempDir, BIN_DIR);
271265
}
272-
applyOrtPlatformAliases(BIN_DIR, ortVersion);
266+
normalizeOrtLibName(BIN_DIR, ortVersion);
273267
console.log('[foundry-local] Native runtime install complete.');
274268
} catch (err) {
275269
console.error('[foundry-local] Installation failed:', err instanceof Error ? err.message : err);

sdk_v2/js/src/detail/native.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ function loadAddon(): NativeAddon {
258258
`Native addon not found at ${addonPath}.\nBuild it locally with:\n npm run copy-native:dev && npm run build:native\n(requires the C++ SDK to be built first via\n \`python sdk_v2/cpp/build.py --configure --build --config RelWithDebInfo\`).\nAlternatively, pass \`libraryPath\` in the FoundryLocalConfig (or call \`configureNativeLoader\`) to point at a directory containing the native library.`,
259259
);
260260
}
261+
// Point GenAI at the ORT bundled next to the addon before the addon loads.
262+
applyOrtLibPath(prebuildDir);
261263
const require = createRequire(import.meta.url);
262264
return require(addonPath) as NativeAddon;
263265
}
@@ -284,20 +286,44 @@ function nativeLibBasename(): string {
284286

285287
/**
286288
* Candidate basenames for an ORT-family library on the current platform. The loader tries them in order and
287-
* uses the first that exists on disk. The Foundry nupkg ships the unversioned `libonnxruntime.{so,dylib}`;
288-
* we add the versioned soname (`libonnxruntime.so.1` / `libonnxruntime.1.dylib`) beside it, which is the name
289-
* libfoundry_local actually records, so both are listed as fallbacks. GenAI has no versioned variant.
289+
* uses the first that exists on disk. We ship the versioned soname (`libonnxruntime.so.1` /
290+
* `libonnxruntime.1.dylib`) — the name libfoundry_local records — and point GenAI at it via ORT_LIB_PATH
291+
* (see applyOrtLibPath). The unversioned name is kept only as a tolerant fallback for layouts that still
292+
* stage it (e.g. the raw C++ build output). GenAI has no versioned variant of its own.
290293
*/
291294
function ortCandidateBasenames(name: "onnxruntime" | "onnxruntime-genai"): string[] {
292295
if (process.platform === "win32") return [`${name}.dll`];
293296
if (process.platform === "darwin") {
294-
if (name === "onnxruntime") return ["libonnxruntime.dylib", "libonnxruntime.1.dylib"];
297+
if (name === "onnxruntime") return ["libonnxruntime.1.dylib", "libonnxruntime.dylib"];
295298
return [`lib${name}.dylib`];
296299
}
297-
if (name === "onnxruntime") return ["libonnxruntime.so", "libonnxruntime.so.1"];
300+
if (name === "onnxruntime") return ["libonnxruntime.so.1", "libonnxruntime.so"];
298301
return [`lib${name}.so`];
299302
}
300303

304+
/**
305+
* Point onnxruntime-genai at the exact ORT we ship, via the `ORT_LIB_PATH` env var.
306+
*
307+
* On macOS/Linux the GenAI library (built with dlopen support) resolves ORT in its `InitApi()` by first
308+
* dlopen-ing `$ORT_LIB_PATH`, then falling back to the unversioned `libonnxruntime.{dylib,so}` leafname. We
309+
* ship only the versioned soname (the name libfoundry_local records), so setting `ORT_LIB_PATH` to it is what
310+
* lets the package omit the unversioned alias entirely. Windows links ORT directly (no dlopen), so this is a
311+
* no-op there.
312+
*
313+
* Must run before the addon loads (GenAI's `InitApi` is lazy, fired by the first model load). Never clobbers a
314+
* caller-provided `ORT_LIB_PATH`.
315+
*/
316+
function applyOrtLibPath(directory: string): void {
317+
if (process.platform === "win32" || process.env.ORT_LIB_PATH) return;
318+
for (const basename of ortCandidateBasenames("onnxruntime")) {
319+
const fullPath = resolve(directory, basename);
320+
if (existsSync(fullPath)) {
321+
process.env.ORT_LIB_PATH = fullPath;
322+
return;
323+
}
324+
}
325+
}
326+
301327
/**
302328
* Pre-load ORT and ORT-GenAI from `directory` by absolute path so that when foundry_local is loaded, the OS
303329
* loader resolves its NEEDED entries against the already-resident modules instead of doing a filesystem search
@@ -377,6 +403,9 @@ export function configureNativeLoader(opts: { libraryPath?: string }): void {
377403
// (.github/instructions/ort-loading-contract.instructions.md) — every binding must do this.
378404
preloadOrtIfPresent(libraryPath);
379405

406+
// Point GenAI at the ORT in this directory (takes precedence over the default prebuilds lookup).
407+
applyOrtLibPath(libraryPath);
408+
380409
try {
381410
getPreloadAddon().preloadLibrary(fullPath);
382411
} catch (err) {

0 commit comments

Comments
 (0)