Skip to content

Commit ec94cba

Browse files
authored
Add ORT-Nightly as fallback for nuget package source at installation time (#664)
The JS and Rust SDK installers download three native NuGet packages (Microsoft.AI.Foundry.Local.Core, Microsoft.ML.OnnxRuntime.Foundry, Microsoft.ML.OnnxRuntimeGenAI.Foundry) from a single hard-coded feed ( api.nuget.org). Dev / pre-release versions are published to the public ORT-Nightly Azure DevOps feed before they reach nuget.org, so any build pinned to a dev version fails outright today. Change Both installers now try each feed in order and fall back to the next on any failure: 1. https://api.nuget.org/v3/index.json (primary) 2. https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json (fallback)
1 parent 99b091f commit ec94cba

4 files changed

Lines changed: 169 additions & 87 deletions

File tree

sdk/js/script/install-standard.cjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ if (fs.existsSync(winmlPkgJson)) {
1818
process.exit(0);
1919
}
2020

21-
const { NUGET_FEED, runInstall } = require('./install-utils.cjs');
21+
const { runInstall } = require('./install-utils.cjs');
2222

2323
// deps_versions.json lives at the package root when published, or at sdk/ in the repo.
2424
const depsPath = fs.existsSync(path.resolve(__dirname, '..', 'deps_versions.json'))
@@ -27,9 +27,9 @@ const depsPath = fs.existsSync(path.resolve(__dirname, '..', 'deps_versions.json
2727
const deps = require(depsPath);
2828

2929
const ARTIFACTS = [
30-
{ name: 'Microsoft.AI.Foundry.Local.Core', version: deps['foundry-local-core'].nuget, feed: NUGET_FEED },
31-
{ name: os.platform() === 'linux' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version, feed: NUGET_FEED },
32-
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai'].version, feed: NUGET_FEED },
30+
{ name: 'Microsoft.AI.Foundry.Local.Core', version: deps['foundry-local-core'].nuget },
31+
{ name: os.platform() === 'linux' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version },
32+
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai'].version },
3333
];
3434

3535
(async () => {

sdk/js/script/install-utils.cjs

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ const REQUIRED_FILES = [
2929
`${os.platform() === 'win32' ? '' : 'lib'}onnxruntime-genai${EXT}`,
3030
];
3131

32-
const NUGET_FEED = 'https://api.nuget.org/v3/index.json';
32+
// Feeds tried in order. Primary: nuget.org (stable releases). Fallback:
33+
// the public ORT-Nightly Azure DevOps NuGet feed (where dev / pre-release
34+
// builds of Foundry Local Core, ONNX Runtime and ONNX Runtime GenAI live
35+
// before they reach nuget.org). If a download from a feed fails for any
36+
// reason, the next feed is tried.
37+
const FEEDS = [
38+
'https://api.nuget.org/v3/index.json',
39+
'https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json',
40+
];
3341

3442
// --- Download helpers ---
3543

@@ -127,43 +135,60 @@ async function installPackage(artifact, tempDir, binDir, skipIfPresent) {
127135
}
128136
}
129137

130-
const baseAddress = await getBaseAddress(artifact.feed);
131-
const nameLower = pkgName.toLowerCase();
132-
const verLower = pkgVer.toLowerCase();
133-
const downloadUrl = `${baseAddress}${nameLower}/${verLower}/${nameLower}.${verLower}.nupkg`;
134-
135-
const nupkgPath = path.join(tempDir, `${pkgName}.${pkgVer}.nupkg`);
136-
console.log(` Downloading ${pkgName} ${pkgVer}...`);
137-
await downloadFile(downloadUrl, nupkgPath);
138-
139-
console.log(` Extracting...`);
140-
const zip = new AdmZip(nupkgPath);
141-
const targetPathPrefix = `runtimes/${RID}/native/`.toLowerCase();
142-
const entries = zip.getEntries().filter(e => {
143-
const p = e.entryName.toLowerCase();
144-
return p.includes(targetPathPrefix) && p.endsWith(EXT);
145-
});
146-
147-
if (entries.length > 0) {
148-
entries.forEach(entry => {
149-
zip.extractEntryTo(entry, binDir, false, true);
150-
console.log(` Extracted ${entry.name}`);
151-
});
152-
} else {
153-
console.warn(` No files found for RID ${RID} in ${pkgName}.`);
154-
}
138+
// Try each configured feed in order; on failure fall back to the next.
139+
let lastError;
140+
for (let i = 0; i < FEEDS.length; i++) {
141+
const feedUrl = FEEDS[i];
142+
const feedHost = new URL(feedUrl).host;
143+
try {
144+
const baseAddress = await getBaseAddress(feedUrl);
145+
const nameLower = pkgName.toLowerCase();
146+
const verLower = pkgVer.toLowerCase();
147+
const downloadUrl = `${baseAddress}${nameLower}/${verLower}/${nameLower}.${verLower}.nupkg`;
148+
149+
const nupkgPath = path.join(tempDir, `${pkgName}.${pkgVer}.nupkg`);
150+
console.log(` Downloading ${pkgName} ${pkgVer} from ${feedHost}...`);
151+
await downloadFile(downloadUrl, nupkgPath);
152+
153+
console.log(` Extracting...`);
154+
const zip = new AdmZip(nupkgPath);
155+
const targetPathPrefix = `runtimes/${RID}/native/`.toLowerCase();
156+
const entries = zip.getEntries().filter(e => {
157+
const p = e.entryName.toLowerCase();
158+
return p.includes(targetPathPrefix) && p.endsWith(EXT);
159+
});
155160

156-
// Write a metadata package.json with version info for diagnostics
157-
if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) {
158-
const pkgJsonPath = path.join(binDir, 'package.json');
159-
const pkgContent = {
160-
name: `@foundry-local-core/${platformKey}`,
161-
version: pkgVer,
162-
description: `Native binaries for Foundry Local SDK (${platformKey})`,
163-
private: true
164-
};
165-
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2));
161+
if (entries.length > 0) {
162+
entries.forEach(entry => {
163+
zip.extractEntryTo(entry, binDir, false, true);
164+
console.log(` Extracted ${entry.name}`);
165+
});
166+
} else {
167+
console.warn(` No files found for RID ${RID} in ${pkgName}.`);
168+
}
169+
170+
// Write a metadata package.json with version info for diagnostics
171+
if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) {
172+
const pkgJsonPath = path.join(binDir, 'package.json');
173+
const pkgContent = {
174+
name: `@foundry-local-core/${platformKey}`,
175+
version: pkgVer,
176+
description: `Native binaries for Foundry Local SDK (${platformKey})`,
177+
private: true
178+
};
179+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2));
180+
}
181+
return;
182+
} catch (err) {
183+
lastError = err;
184+
const isLast = i === FEEDS.length - 1;
185+
const reason = err instanceof Error ? err.message : String(err);
186+
if (!isLast) {
187+
console.warn(` ${pkgName} ${pkgVer}: download from ${feedHost} failed (${reason}); trying next feed...`);
188+
}
189+
}
166190
}
191+
throw new Error(`Failed to download ${pkgName} ${pkgVer} from any configured feed (${FEEDS.map(f => new URL(f).host).join(', ')}): ${lastError instanceof Error ? lastError.message : lastError}`);
167192
}
168193

169194
async function runInstall(artifacts, options) {
@@ -192,4 +217,4 @@ async function runInstall(artifacts, options) {
192217
}
193218
}
194219

195-
module.exports = { NUGET_FEED, runInstall };
220+
module.exports = { runInstall };

sdk/js/script/install-winml.cjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
const fs = require('fs');
1414
const path = require('path');
15-
const { NUGET_FEED, runInstall } = require('./install-utils.cjs');
15+
const { runInstall } = require('./install-utils.cjs');
1616

1717
// WinML uses its own deps_versions_winml.json with the same key structure
1818
// as the standard deps_versions.json — no variant-specific keys needed.
@@ -27,9 +27,9 @@ const platformKey = `${process.platform}-${process.arch}`;
2727
const binDir = path.join(sdkRoot, 'foundry-local-core', platformKey);
2828

2929
const ARTIFACTS = [
30-
{ name: 'Microsoft.AI.Foundry.Local.Core.WinML', version: deps['foundry-local-core']['nuget'], feed: NUGET_FEED },
31-
{ name: 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version, feed: NUGET_FEED },
32-
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai']['version'], feed: NUGET_FEED },
30+
{ name: 'Microsoft.AI.Foundry.Local.Core.WinML', version: deps['foundry-local-core']['nuget'] },
31+
{ name: 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version },
32+
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai']['version'] },
3333
];
3434

3535
(async () => {

sdk/rust/build.rs

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
use std::collections::HashMap;
12
use std::env;
23
use std::fs;
34
use std::io::{self, Read};
45
use std::path::{Path, PathBuf};
5-
6-
const NUGET_FEED: &str = "https://api.nuget.org/v3/index.json";
6+
use std::sync::Mutex;
7+
8+
/// Feeds tried in order. Primary: nuget.org (stable releases). Fallback:
9+
/// the public ORT-Nightly Azure DevOps NuGet feed (where dev / pre-release
10+
/// builds of Foundry Local Core, ONNX Runtime and ONNX Runtime GenAI live
11+
/// before they reach nuget.org). If a download from a feed fails for any
12+
/// reason, the next feed is tried.
13+
const FEEDS: &[&str] = &[
14+
"https://api.nuget.org/v3/index.json",
15+
"https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json",
16+
];
717

818
/// Versions loaded from deps_versions.json (or deps_versions_winml.json).
919
/// Both files share the same key structure — the build script picks the
@@ -67,7 +77,6 @@ fn load_deps_versions() -> DepsVersions {
6777
struct NuGetPackage {
6878
name: &'static str,
6979
version: String,
70-
feed_url: &'static str,
7180
}
7281

7382
fn get_rid() -> Option<&'static str> {
@@ -106,51 +115,56 @@ fn get_packages(rid: &str) -> Vec<NuGetPackage> {
106115
packages.push(NuGetPackage {
107116
name: "Microsoft.AI.Foundry.Local.Core.WinML",
108117
version: deps.core.clone(),
109-
feed_url: NUGET_FEED,
110118
});
111119
packages.push(NuGetPackage {
112120
name: "Microsoft.ML.OnnxRuntime.Foundry",
113121
version: deps.ort.clone(),
114-
feed_url: NUGET_FEED,
115122
});
116123
packages.push(NuGetPackage {
117124
name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry",
118125
version: deps.genai.clone(),
119-
feed_url: NUGET_FEED,
120126
});
121127
} else {
122128
packages.push(NuGetPackage {
123129
name: "Microsoft.AI.Foundry.Local.Core",
124130
version: deps.core.clone(),
125-
feed_url: NUGET_FEED,
126131
});
127132

128133
if is_linux {
129134
packages.push(NuGetPackage {
130135
name: "Microsoft.ML.OnnxRuntime.Gpu.Linux",
131136
version: deps.ort.clone(),
132-
feed_url: NUGET_FEED,
133137
});
134138
} else {
135139
packages.push(NuGetPackage {
136140
name: "Microsoft.ML.OnnxRuntime.Foundry",
137141
version: deps.ort.clone(),
138-
feed_url: NUGET_FEED,
139142
});
140143
}
141144

142145
packages.push(NuGetPackage {
143146
name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry",
144147
version: deps.genai.clone(),
145-
feed_url: NUGET_FEED,
146148
});
147149
}
148150

149151
packages
150152
}
151153

152-
/// Resolve the PackageBaseAddress from a NuGet v3 service index.
154+
/// Resolve the PackageBaseAddress from a NuGet v3 service index. The result
155+
/// is cached per feed URL so repeated calls within a single build (e.g. one
156+
/// per package, plus retries on fallback feeds) only hit the network once.
153157
fn resolve_base_address(feed_url: &str) -> Result<String, String> {
158+
static BASE_ADDRESS_CACHE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
159+
{
160+
let guard = BASE_ADDRESS_CACHE.lock().unwrap();
161+
if let Some(map) = guard.as_ref() {
162+
if let Some(cached) = map.get(feed_url) {
163+
return Ok(cached.clone());
164+
}
165+
}
166+
}
167+
154168
let body: String = ureq::get(feed_url)
155169
.call()
156170
.map_err(|e| format!("Failed to fetch NuGet feed index at {feed_url}: {e}"))?
@@ -174,6 +188,10 @@ fn resolve_base_address(feed_url: &str) -> Result<String, String> {
174188
} else {
175189
format!("{id}/")
176190
};
191+
let mut guard = BASE_ADDRESS_CACHE.lock().unwrap();
192+
guard
193+
.get_or_insert_with(HashMap::new)
194+
.insert(feed_url.to_string(), base.clone());
177195
return Ok(base);
178196
}
179197
}
@@ -184,49 +202,37 @@ fn resolve_base_address(feed_url: &str) -> Result<String, String> {
184202
))
185203
}
186204

187-
/// Download a .nupkg and extract native libraries for the given RID into `out_dir`.
188-
/// Skips download if native files from this package are already present.
189-
fn download_and_extract(pkg: &NuGetPackage, rid: &str, out_dir: &Path) -> Result<(), String> {
190-
// Skip if this package's main native library is already in out_dir
191-
// (e.g. pre-populated from FOUNDRY_NATIVE_OVERRIDE_DIR).
192-
let ext = native_lib_extension();
193-
let prefix = if env::consts::OS == "windows" {
194-
""
195-
} else {
196-
"lib"
197-
};
198-
let expected_file = if pkg.name.contains("Foundry.Local.Core") {
199-
format!("Microsoft.AI.Foundry.Local.Core.{ext}")
200-
} else if pkg.name.contains("OnnxRuntimeGenAI") {
201-
format!("{prefix}onnxruntime-genai.{ext}")
202-
} else if pkg.name.contains("OnnxRuntime") {
203-
format!("{prefix}onnxruntime.{ext}")
204-
} else {
205-
String::new()
206-
};
207-
if !expected_file.is_empty() && out_dir.join(&expected_file).exists() {
208-
println!(
209-
"cargo:warning={} already present, skipping download.",
210-
pkg.name
211-
);
212-
return Ok(());
213-
}
214-
215-
let base_address = resolve_base_address(pkg.feed_url)?;
205+
/// Try to download and extract a single package from a specific feed. Returns
206+
/// `Ok(())` on success, `Err(reason)` on any failure (network, HTTP error,
207+
/// zip parse error, etc.).
208+
fn try_download_from_feed(
209+
pkg: &NuGetPackage,
210+
rid: &str,
211+
out_dir: &Path,
212+
feed_url: &str,
213+
) -> Result<(), String> {
214+
let base_address = resolve_base_address(feed_url)?;
216215
let lower_name = pkg.name.to_lowercase();
217216
let lower_version = pkg.version.to_lowercase();
218217
let url =
219218
format!("{base_address}{lower_name}/{lower_version}/{lower_name}.{lower_version}.nupkg");
220219

220+
let feed_host = feed_url
221+
.split("://")
222+
.nth(1)
223+
.and_then(|s| s.split('/').next())
224+
.unwrap_or(feed_url);
225+
221226
println!(
222-
"cargo:warning=Downloading {name} {ver} from NuGet.org",
227+
"cargo:warning=Downloading {name} {ver} from {host}",
223228
name = pkg.name,
224229
ver = pkg.version,
230+
host = feed_host,
225231
);
226232

227233
let mut response = ureq::get(&url)
228234
.call()
229-
.map_err(|e| format!("Failed to download {}: {e}", pkg.name))?;
235+
.map_err(|e| format!("Failed to download {} from {feed_host}: {e}", pkg.name))?;
230236

231237
let mut bytes = Vec::new();
232238
response
@@ -285,6 +291,57 @@ fn download_and_extract(pkg: &NuGetPackage, rid: &str, out_dir: &Path) -> Result
285291
Ok(())
286292
}
287293

294+
/// Download a .nupkg and extract native libraries for the given RID into `out_dir`.
295+
/// Skips download if native files from this package are already present.
296+
/// Tries each configured feed in order; on failure falls back to the next.
297+
fn download_and_extract(pkg: &NuGetPackage, rid: &str, out_dir: &Path) -> Result<(), String> {
298+
// Skip if this package's main native library is already in out_dir
299+
// (e.g. pre-populated from FOUNDRY_NATIVE_OVERRIDE_DIR).
300+
let ext = native_lib_extension();
301+
let prefix = if env::consts::OS == "windows" {
302+
""
303+
} else {
304+
"lib"
305+
};
306+
let expected_file = if pkg.name.contains("Foundry.Local.Core") {
307+
format!("Microsoft.AI.Foundry.Local.Core.{ext}")
308+
} else if pkg.name.contains("OnnxRuntimeGenAI") {
309+
format!("{prefix}onnxruntime-genai.{ext}")
310+
} else if pkg.name.contains("OnnxRuntime") {
311+
format!("{prefix}onnxruntime.{ext}")
312+
} else {
313+
String::new()
314+
};
315+
if !expected_file.is_empty() && out_dir.join(&expected_file).exists() {
316+
println!(
317+
"cargo:warning={} already present, skipping download.",
318+
pkg.name
319+
);
320+
return Ok(());
321+
}
322+
323+
let mut last_error = String::new();
324+
for (i, feed_url) in FEEDS.iter().enumerate() {
325+
match try_download_from_feed(pkg, rid, out_dir, feed_url) {
326+
Ok(()) => return Ok(()),
327+
Err(e) => {
328+
let is_last = i == FEEDS.len() - 1;
329+
if !is_last {
330+
println!(
331+
"cargo:warning={} {}: {e}; trying next feed...",
332+
pkg.name, pkg.version
333+
);
334+
}
335+
last_error = e;
336+
}
337+
}
338+
}
339+
Err(format!(
340+
"Failed to download {} {} from any configured feed: {last_error}",
341+
pkg.name, pkg.version
342+
))
343+
}
344+
288345
/// Check whether all required native libraries are already present in `out_dir`.
289346
fn libs_already_present(out_dir: &Path) -> bool {
290347
let ext = native_lib_extension();

0 commit comments

Comments
 (0)