Skip to content

Commit 0d7bab5

Browse files
prathikrPrathik RaoCopilot
authored
separates js sdk into foundry-local-sdk and foundry-local-sdk-winml packages (#555)
no longer need `npm install --winml` as `npm install` with the separate packages will fetch the appropriate binaries --------- Co-authored-by: Prathik Rao <prathikrao@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9434df7 commit 0d7bab5

8 files changed

Lines changed: 293 additions & 380 deletions

File tree

.github/workflows/build-js-steps.yml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,7 @@ jobs:
9292
run: |
9393
if (Test-Path .npmrc) { Remove-Item .npmrc -Force; Write-Host "Removed .npmrc" }
9494
95-
- name: npm install (WinML)
96-
if: ${{ inputs.useWinML == true }}
97-
working-directory: sdk/js
98-
run: npm install --winml
99-
100-
- name: npm install (Standard)
101-
if: ${{ inputs.useWinML == false }}
95+
- name: npm install
10296
working-directory: sdk/js
10397
run: npm install
10498

@@ -114,21 +108,15 @@ jobs:
114108
working-directory: sdk/js
115109
run: npm run build
116110

117-
- name: Pack npm package
111+
- name: Pack npm package (WinML)
112+
if: ${{ inputs.useWinML == true }}
118113
working-directory: sdk/js
119-
run: npm pack
114+
run: npm run pack:winml
120115

121-
- name: Rename WinML artifact
122-
if: ${{ inputs.useWinML == true }}
123-
shell: pwsh
116+
- name: Pack npm package (Standard)
117+
if: ${{ inputs.useWinML == false }}
124118
working-directory: sdk/js
125-
run: |
126-
$tgz = Get-ChildItem *.tgz | Select-Object -First 1
127-
if ($tgz) {
128-
$newName = $tgz.Name -replace '^foundry-local-sdk-', 'foundry-local-sdk-winml-'
129-
Rename-Item -Path $tgz.FullName -NewName $newName
130-
Write-Host "Renamed $($tgz.Name) to $newName"
131-
}
119+
run: npm run pack
132120

133121
- name: Upload npm packages
134122
uses: actions/upload-artifact@v4

sdk/js/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# @prathikrao/foundry-local-sdk
1+
# foundry-local-sdk
22

33
## Enumerations
44

sdk/js/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
"type": "module",
88
"files": [
99
"dist",
10-
"script"
10+
"script/install-standard.cjs",
11+
"script/install-winml.cjs",
12+
"script/install-utils.cjs",
13+
"script/pack.cjs",
14+
"script/preinstall.cjs"
1115
],
1216
"scripts": {
1317
"build": "tsc -p tsconfig.build.json",
1418
"docs": "typedoc",
1519
"example": "tsx examples/chat-completion.ts",
16-
"install": "node script/install.cjs",
20+
"install": "node script/install-standard.cjs",
21+
"pack": "node script/pack.cjs",
22+
"pack:winml": "node script/pack.cjs winml",
1723
"preinstall": "node script/preinstall.cjs",
1824
"test": "mocha --import=tsx test/**/*.test.ts"
1925
},
@@ -45,4 +51,4 @@
4551
},
4652
"author": "",
4753
"license": "ISC"
48-
}
54+
}

sdk/js/script/install-standard.cjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// Install script for foundry-local-sdk (standard variant).
5+
6+
'use strict';
7+
8+
const os = require('os');
9+
const { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall } = require('./install-utils.cjs');
10+
11+
const useNightly = process.env.npm_config_nightly === 'true';
12+
13+
const ARTIFACTS = [
14+
{ name: 'Microsoft.AI.Foundry.Local.Core', version: '0.9.0.8-rc3', feed: ORT_NIGHTLY_FEED, nightly: useNightly },
15+
{ name: os.platform() === 'linux' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry', version: '1.24.3', feed: NUGET_FEED, nightly: false },
16+
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.12.2', feed: NUGET_FEED, nightly: false },
17+
];
18+
19+
(async () => {
20+
try {
21+
await runInstall(ARTIFACTS);
22+
} catch (err) {
23+
console.error('[foundry-local] Installation failed:', err instanceof Error ? err.message : err);
24+
process.exit(1);
25+
}
26+
})();

sdk/js/script/install-utils.cjs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// Shared NuGet download and extraction utilities for install scripts.
5+
6+
'use strict';
7+
8+
const fs = require('fs');
9+
const path = require('path');
10+
const os = require('os');
11+
const https = require('https');
12+
const AdmZip = require('adm-zip');
13+
14+
const PLATFORM_MAP = {
15+
'win32-x64': 'win-x64',
16+
'win32-arm64': 'win-arm64',
17+
'linux-x64': 'linux-x64',
18+
'darwin-arm64': 'osx-arm64',
19+
};
20+
const platformKey = `${os.platform()}-${os.arch()}`;
21+
const RID = PLATFORM_MAP[platformKey];
22+
const BIN_DIR = path.join(__dirname, '..', 'packages', '@foundry-local-core', platformKey);
23+
const EXT = os.platform() === 'win32' ? '.dll' : os.platform() === 'darwin' ? '.dylib' : '.so';
24+
25+
const REQUIRED_FILES = [
26+
`Microsoft.AI.Foundry.Local.Core${EXT}`,
27+
`${os.platform() === 'win32' ? '' : 'lib'}onnxruntime${EXT}`,
28+
`${os.platform() === 'win32' ? '' : 'lib'}onnxruntime-genai${EXT}`,
29+
];
30+
31+
const NUGET_FEED = 'https://api.nuget.org/v3/index.json';
32+
const ORT_NIGHTLY_FEED = 'https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json';
33+
34+
// --- Download helpers ---
35+
36+
async function downloadWithRetryAndRedirects(url, destStream = null) {
37+
const maxRedirects = 5;
38+
let currentUrl = url;
39+
let redirects = 0;
40+
41+
while (redirects < maxRedirects) {
42+
const response = await new Promise((resolve, reject) => {
43+
https.get(currentUrl, (res) => resolve(res))
44+
.on('error', reject);
45+
});
46+
47+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
48+
currentUrl = response.headers.location;
49+
response.resume();
50+
redirects++;
51+
console.log(` Following redirect to ${new URL(currentUrl).host}...`);
52+
continue;
53+
}
54+
55+
if (response.statusCode !== 200) {
56+
throw new Error(`Download failed with status ${response.statusCode}: ${currentUrl}`);
57+
}
58+
59+
if (destStream) {
60+
response.pipe(destStream);
61+
return new Promise((resolve, reject) => {
62+
destStream.on('finish', resolve);
63+
destStream.on('error', reject);
64+
response.on('error', reject);
65+
});
66+
} else {
67+
let data = '';
68+
response.on('data', chunk => data += chunk);
69+
return new Promise((resolve, reject) => {
70+
response.on('end', () => resolve(data));
71+
response.on('error', reject);
72+
});
73+
}
74+
}
75+
throw new Error('Too many redirects');
76+
}
77+
78+
async function downloadJson(url) {
79+
return JSON.parse(await downloadWithRetryAndRedirects(url));
80+
}
81+
82+
async function downloadFile(url, dest) {
83+
const file = fs.createWriteStream(dest);
84+
try {
85+
await downloadWithRetryAndRedirects(url, file);
86+
file.close();
87+
} catch (e) {
88+
file.close();
89+
if (fs.existsSync(dest)) fs.unlinkSync(dest);
90+
throw e;
91+
}
92+
}
93+
94+
const serviceIndexCache = new Map();
95+
96+
async function getBaseAddress(feedUrl) {
97+
if (!serviceIndexCache.has(feedUrl)) {
98+
serviceIndexCache.set(feedUrl, await downloadJson(feedUrl));
99+
}
100+
const resources = serviceIndexCache.get(feedUrl).resources || [];
101+
const res = resources.find(r => r['@type'] && r['@type'].startsWith('PackageBaseAddress/3.0.0'));
102+
if (!res) throw new Error('Could not find PackageBaseAddress/3.0.0 in NuGet feed.');
103+
const baseAddress = res['@id'];
104+
return baseAddress.endsWith('/') ? baseAddress : baseAddress + '/';
105+
}
106+
107+
async function resolveLatestVersion(feedUrl, packageName) {
108+
const baseAddress = await getBaseAddress(feedUrl);
109+
const versionsUrl = `${baseAddress}${packageName.toLowerCase()}/index.json`;
110+
const versionData = await downloadJson(versionsUrl);
111+
const versions = versionData.versions || [];
112+
if (versions.length === 0) throw new Error(`No versions found for ${packageName}`);
113+
versions.sort((a, b) => b.localeCompare(a));
114+
console.log(`[foundry-local] Latest version of ${packageName}: ${versions[0]}`);
115+
return versions[0];
116+
}
117+
118+
async function installPackage(artifact, tempDir) {
119+
const pkgName = artifact.name;
120+
let pkgVer = artifact.version;
121+
if (artifact.nightly) {
122+
console.log(` Resolving latest version for ${pkgName}...`);
123+
pkgVer = await resolveLatestVersion(artifact.feed, pkgName);
124+
}
125+
126+
const baseAddress = await getBaseAddress(artifact.feed);
127+
const nameLower = pkgName.toLowerCase();
128+
const verLower = pkgVer.toLowerCase();
129+
const downloadUrl = `${baseAddress}${nameLower}/${verLower}/${nameLower}.${verLower}.nupkg`;
130+
131+
const nupkgPath = path.join(tempDir, `${pkgName}.${pkgVer}.nupkg`);
132+
console.log(` Downloading ${pkgName} ${pkgVer}...`);
133+
await downloadFile(downloadUrl, nupkgPath);
134+
135+
console.log(` Extracting...`);
136+
const zip = new AdmZip(nupkgPath);
137+
const targetPathPrefix = `runtimes/${RID}/native/`.toLowerCase();
138+
const entries = zip.getEntries().filter(e => {
139+
const p = e.entryName.toLowerCase();
140+
return p.includes(targetPathPrefix) && p.endsWith(EXT);
141+
});
142+
143+
if (entries.length > 0) {
144+
entries.forEach(entry => {
145+
zip.extractEntryTo(entry, BIN_DIR, false, true);
146+
console.log(` Extracted ${entry.name}`);
147+
});
148+
} else {
149+
console.warn(` No files found for RID ${RID} in ${pkgName}.`);
150+
}
151+
152+
// Update platform package.json version for Core packages
153+
if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) {
154+
const pkgJsonPath = path.join(BIN_DIR, 'package.json');
155+
if (fs.existsSync(pkgJsonPath)) {
156+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
157+
pkgJson.version = pkgVer;
158+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
159+
}
160+
}
161+
}
162+
163+
async function runInstall(artifacts) {
164+
if (!RID) {
165+
console.warn(`[foundry-local] Unsupported platform: ${platformKey}. Skipping.`);
166+
return;
167+
}
168+
169+
if (fs.existsSync(BIN_DIR) && REQUIRED_FILES.every(f => fs.existsSync(path.join(BIN_DIR, f)))) {
170+
if (process.env.npm_config_nightly === 'true') {
171+
console.log(`[foundry-local] Nightly requested. Forcing reinstall...`);
172+
fs.rmSync(BIN_DIR, { recursive: true, force: true });
173+
} else {
174+
console.log(`[foundry-local] Native libraries already installed.`);
175+
return;
176+
}
177+
}
178+
179+
console.log(`[foundry-local] Installing native libraries for ${RID}...`);
180+
fs.mkdirSync(BIN_DIR, { recursive: true });
181+
182+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foundry-install-'));
183+
try {
184+
for (const artifact of artifacts) {
185+
await installPackage(artifact, tempDir);
186+
}
187+
console.log('[foundry-local] Installation complete.');
188+
} finally {
189+
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch {}
190+
}
191+
}
192+
193+
module.exports = { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall };

sdk/js/script/install-winml.cjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
// Install script for foundry-local-sdk-winml variant.
5+
6+
'use strict';
7+
8+
const { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall } = require('./install-utils.cjs');
9+
10+
const useNightly = process.env.npm_config_nightly === 'true';
11+
12+
const ARTIFACTS = [
13+
{ name: 'Microsoft.AI.Foundry.Local.Core.WinML', version: '0.9.0.8-rc3', feed: ORT_NIGHTLY_FEED, nightly: useNightly },
14+
{ name: 'Microsoft.ML.OnnxRuntime.Foundry', version: '1.23.2.3', feed: NUGET_FEED, nightly: false },
15+
{ name: 'Microsoft.ML.OnnxRuntimeGenAI.WinML', version: '0.12.2', feed: NUGET_FEED, nightly: false },
16+
];
17+
18+
(async () => {
19+
try {
20+
await runInstall(ARTIFACTS);
21+
} catch (err) {
22+
console.error('Failed to install WinML artifacts:', err);
23+
process.exit(1);
24+
}
25+
})();

0 commit comments

Comments
 (0)