diff --git a/.pipelines/foundry-local-packaging.yml b/.pipelines/foundry-local-packaging.yml index c871cdf1..617ea587 100644 --- a/.pipelines/foundry-local-packaging.yml +++ b/.pipelines/foundry-local-packaging.yml @@ -70,9 +70,67 @@ extends: - repository: neutron-server - repository: test-data-shared stages: + # ── Compute Version ── + # A single version string is computed once and shared across all stages. + # This prevents timestamp drift between standard and WinML builds. + # Outputs three format variants: + # sdkVersion – semver for JS, C#, Rust (e.g. 1.0.0-dev.202604061234) + # pyVersion – PEP 440 for Python (e.g. 1.0.0.dev202604061234) + # flcVersion – NuGet/FLC style (e.g. 1.0.0-dev-202604061234-ab12cd34) + - stage: compute_version + displayName: 'Compute Version' + dependsOn: [] + jobs: + - job: version + displayName: 'Compute Version' + pool: + name: onnxruntime-Win-CPU-2022 + os: windows + templateContext: + outputs: + - output: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Build.ArtifactStagingDirectory)/version-info' + steps: + - checkout: none + - task: PowerShell@2 + displayName: 'Compute and write version files' + inputs: + targetType: inline + script: | + $base = "${{ parameters.version }}" + $preId = "${{ parameters.prereleaseId }}" + $ts = Get-Date -Format "yyyyMMddHHmm" + $commitId = "$(Build.SourceVersion)".Substring(0, 8) + + if ($preId -ne '' -and $preId -ne 'none') { + $sdkVersion = "$base-$preId" + $pyVersion = "$base$preId" + $flcVersion = "$base-$preId" + } elseif ("${{ parameters.isRelease }}" -ne "True") { + $sdkVersion = "$base-dev.$ts" + $pyVersion = "$base.dev$ts" + $flcVersion = "$base-dev-$ts-$commitId" + } else { + $sdkVersion = $base + $pyVersion = $base + $flcVersion = $base + } + + $outDir = "$(Build.ArtifactStagingDirectory)/version-info" + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + Set-Content -Path "$outDir/sdkVersion.txt" -Value $sdkVersion -NoNewline + Set-Content -Path "$outDir/pyVersion.txt" -Value $pyVersion -NoNewline + Set-Content -Path "$outDir/flcVersion.txt" -Value $flcVersion -NoNewline + + Write-Host "SDK version: $sdkVersion" + Write-Host "Python version: $pyVersion" + Write-Host "FLC version: $flcVersion" + # ── Build & Test FLC ── - stage: build_core displayName: 'Build & Test FLC' + dependsOn: compute_version jobs: - job: flc_win_x64 displayName: 'FLC win-x64' @@ -160,6 +218,10 @@ extends: name: onnxruntime-Win-CPU-2022 os: windows templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' outputs: - output: pipelineArtifact artifactName: 'flc-nuget' @@ -229,6 +291,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' @@ -261,6 +326,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' @@ -293,6 +361,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-wheels' targetPath: '$(Pipeline.Workspace)/flc-wheels' @@ -325,6 +396,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' @@ -467,7 +541,7 @@ extends: # ── Build & Test FLC (WinML) ── - stage: build_core_winml displayName: 'Build & Test FLC WinML' - dependsOn: [] + dependsOn: compute_version jobs: - job: flc_winml_win_x64 displayName: 'FLC win-x64 (WinML)' @@ -520,6 +594,10 @@ extends: name: onnxruntime-Win-CPU-2022 os: windows templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' outputs: - output: pipelineArtifact artifactName: 'flc-nuget-winml' @@ -575,6 +653,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget-winml' targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' @@ -608,6 +689,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget-winml' targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' @@ -640,6 +724,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-wheels-winml' targetPath: '$(Pipeline.Workspace)/flc-wheels-winml' @@ -673,6 +760,9 @@ extends: os: windows templateContext: inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' - input: pipelineArtifact artifactName: 'flc-nuget-winml' targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' diff --git a/.pipelines/templates/build-cs-steps.yml b/.pipelines/templates/build-cs-steps.yml index 978c2fff..38f5b8bf 100644 --- a/.pipelines/templates/build-cs-steps.yml +++ b/.pipelines/templates/build-cs-steps.yml @@ -38,22 +38,15 @@ steps: packageType: sdk version: '9.0.x' -# Compute package version +# Read version from the version-info artifact produced by compute_version stage. - task: PowerShell@2 displayName: 'Set package version' inputs: targetType: inline script: | - $v = "${{ parameters.version }}" - $preId = "${{ parameters.prereleaseId }}" - if ($preId -ne '' -and $preId -ne 'none') { - $v = "$v-$preId" - } elseif ("${{ parameters.isRelease }}" -ne "True") { - $ts = Get-Date -Format "yyyyMMddHHmm" - $v = "$v-dev.$ts" - } - Write-Host "##vso[task.setvariable variable=packageVersion]$v" + $v = (Get-Content "$(Pipeline.Workspace)/version-info/sdkVersion.txt" -Raw).Trim() Write-Host "Package version: $v" + Write-Host "##vso[task.setvariable variable=packageVersion]$v" # List downloaded artifact for debugging - task: PowerShell@2 diff --git a/.pipelines/templates/build-js-steps.yml b/.pipelines/templates/build-js-steps.yml index e288bbce..3aa2908d 100644 --- a/.pipelines/templates/build-js-steps.yml +++ b/.pipelines/templates/build-js-steps.yml @@ -45,20 +45,14 @@ steps: inputs: versionSpec: '20.x' -# Compute version +# Read version from the version-info artifact produced by compute_version stage. - task: PowerShell@2 displayName: 'Set package version' inputs: targetType: inline script: | - $v = "${{ parameters.version }}" - $preId = "${{ parameters.prereleaseId }}" - if ($preId -ne '' -and $preId -ne 'none') { - $v = "$v-$preId" - } elseif ("${{ parameters.isRelease }}" -ne "True") { - $ts = Get-Date -Format "yyyyMMddHHmm" - $v = "$v-dev.$ts" - } + $v = (Get-Content "$(Pipeline.Workspace)/version-info/sdkVersion.txt" -Raw).Trim() + Write-Host "Package version: $v" Write-Host "##vso[task.setvariable variable=packageVersion]$v" # Install dependencies including native binaries (FLC, ORT, GenAI) from NuGet feeds @@ -102,7 +96,8 @@ steps: Expand-Archive -Path $zip -DestinationPath $extractDir -Force # Overwrite FLC binary in the npm-installed location - $destDir = "$(repoRoot)/sdk/js/packages/@foundry-local-core/$platformKey" + $destDir = "$(repoRoot)/sdk/js/node_modules/@foundry-local-core/$platformKey" + New-Item -ItemType Directory -Path $destDir -Force | Out-Null $nativeDir = "$extractDir/runtimes/$rid/native" if (Test-Path $nativeDir) { Get-ChildItem $nativeDir -File | ForEach-Object { diff --git a/.pipelines/templates/build-python-steps.yml b/.pipelines/templates/build-python-steps.yml index f21d9508..f94aa712 100644 --- a/.pipelines/templates/build-python-steps.yml +++ b/.pipelines/templates/build-python-steps.yml @@ -47,22 +47,15 @@ steps: Write-Host "Contents of ${{ parameters.flcWheelsDir }}:" Get-ChildItem "${{ parameters.flcWheelsDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName } -# Compute package version +# Read version from the version-info artifact produced by compute_version stage. - task: PowerShell@2 displayName: 'Set package version' inputs: targetType: inline script: | - $v = "${{ parameters.version }}" - $preId = "${{ parameters.prereleaseId }}" - if ($preId -ne '' -and $preId -ne 'none') { - $v = "$v-$preId" - } elseif ("${{ parameters.isRelease }}" -ne "True") { - $ts = Get-Date -Format "yyyyMMddHHmm" - $v = "$v-dev.$ts" - } - Write-Host "##vso[task.setvariable variable=packageVersion]$v" + $v = (Get-Content "$(Pipeline.Workspace)/version-info/pyVersion.txt" -Raw).Trim() Write-Host "Package version: $v" + Write-Host "##vso[task.setvariable variable=packageVersion]$v" # Configure pip to use ORT-Nightly feed (plus PyPI as fallback) - task: PowerShell@2 diff --git a/.pipelines/templates/build-rust-steps.yml b/.pipelines/templates/build-rust-steps.yml index efccfaa4..ed3161e5 100644 --- a/.pipelines/templates/build-rust-steps.yml +++ b/.pipelines/templates/build-rust-steps.yml @@ -32,20 +32,13 @@ steps: Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" -# Compute package version and patch Cargo.toml +# Read version from the version-info artifact produced by compute_version stage. - task: PowerShell@2 displayName: 'Set crate version' inputs: targetType: inline script: | - $v = "${{ parameters.version }}" - $preId = "${{ parameters.prereleaseId }}" - if ($preId -ne '' -and $preId -ne 'none') { - $v = "$v-$preId" - } elseif ("${{ parameters.isRelease }}" -ne "True") { - $ts = Get-Date -Format "yyyyMMddHHmm" - $v = "$v-dev.$ts" - } + $v = (Get-Content "$(Pipeline.Workspace)/version-info/sdkVersion.txt" -Raw).Trim() Write-Host "Crate version: $v" # Patch Cargo.toml version field diff --git a/.pipelines/templates/package-core-steps.yml b/.pipelines/templates/package-core-steps.yml index 15b8fb54..01697085 100644 --- a/.pipelines/templates/package-core-steps.yml +++ b/.pipelines/templates/package-core-steps.yml @@ -74,23 +74,15 @@ steps: Copy-Item $license "$unifiedPath/LICENSE.txt" -Force } -# Compute version +# Read version from the version-info artifact produced by compute_version stage. - task: PowerShell@2 displayName: 'Set FLC package version' inputs: targetType: inline script: | - $v = "${{ parameters.version }}" - $preId = "${{ parameters.prereleaseId }}" - if ($preId -ne '' -and $preId -ne 'none') { - $v = "$v-$preId" - } elseif ("${{ parameters.isRelease }}" -ne "True") { - $ts = Get-Date -Format "yyyyMMddHHmm" - $commitId = "$(Build.SourceVersion)".Substring(0, 8) - $v = "$v-dev-$ts-$commitId" - } - Write-Host "##vso[task.setvariable variable=flcVersion]$v" + $v = (Get-Content "$(Pipeline.Workspace)/version-info/flcVersion.txt" -Raw).Trim() Write-Host "FLC version: $v" + Write-Host "##vso[task.setvariable variable=flcVersion]$v" # Pack NuGet - task: PowerShell@2 diff --git a/.pipelines/templates/test-js-steps.yml b/.pipelines/templates/test-js-steps.yml index 41ef7f62..1814626a 100644 --- a/.pipelines/templates/test-js-steps.yml +++ b/.pipelines/templates/test-js-steps.yml @@ -93,7 +93,8 @@ steps: Copy-Item $nupkg.FullName $zip -Force Expand-Archive -Path $zip -DestinationPath $extractDir -Force - $destDir = "$(repoRoot)/sdk/js/packages/@foundry-local-core/$platformKey" + $destDir = "$(repoRoot)/sdk/js/node_modules/@foundry-local-core/$platformKey" + New-Item -ItemType Directory -Path $destDir -Force | Out-Null $nativeDir = "$extractDir/runtimes/$rid/native" if (Test-Path $nativeDir) { Get-ChildItem $nativeDir -File | ForEach-Object { diff --git a/sdk/js/package.json b/sdk/js/package.json index 5830e3fe..6e4acf50 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -27,12 +27,6 @@ "koffi": "^2.9.0", "adm-zip": "^0.5.16" }, - "optionalDependencies": { - "@foundry-local-core/darwin-arm64": "file:packages/@foundry-local-core/darwin-arm64", - "@foundry-local-core/linux-x64": "file:packages/@foundry-local-core/linux-x64", - "@foundry-local-core/win32-arm64": "file:packages/@foundry-local-core/win32-arm64", - "@foundry-local-core/win32-x64": "file:packages/@foundry-local-core/win32-x64" - }, "devDependencies": { "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", diff --git a/sdk/js/script/install-utils.cjs b/sdk/js/script/install-utils.cjs index cc61f0db..090a25e3 100644 --- a/sdk/js/script/install-utils.cjs +++ b/sdk/js/script/install-utils.cjs @@ -19,7 +19,9 @@ const PLATFORM_MAP = { }; const platformKey = `${os.platform()}-${os.arch()}`; const RID = PLATFORM_MAP[platformKey]; -const BIN_DIR = path.join(__dirname, '..', 'packages', '@foundry-local-core', platformKey); +// Install binaries into node_modules/@foundry-local-core/ so they +// are shared across foundry-local-sdk and foundry-local-sdk-winml. +const BIN_DIR = path.join(__dirname, '..', 'node_modules', '@foundry-local-core', platformKey); const EXT = os.platform() === 'win32' ? '.dll' : os.platform() === 'darwin' ? '.dylib' : '.so'; const REQUIRED_FILES = [ @@ -104,7 +106,7 @@ async function getBaseAddress(feedUrl) { return baseAddress.endsWith('/') ? baseAddress : baseAddress + '/'; } -async function installPackage(artifact, tempDir) { +async function installPackage(artifact, tempDir, binDir) { const pkgName = artifact.name; const pkgVer = artifact.version; @@ -127,7 +129,7 @@ async function installPackage(artifact, tempDir) { if (entries.length > 0) { entries.forEach(entry => { - zip.extractEntryTo(entry, BIN_DIR, false, true); + zip.extractEntryTo(entry, binDir, false, true); console.log(` Extracted ${entry.name}`); }); } else { @@ -136,7 +138,7 @@ async function installPackage(artifact, tempDir) { // Update platform package.json version for Core packages if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) { - const pkgJsonPath = path.join(BIN_DIR, 'package.json'); + const pkgJsonPath = path.join(binDir, 'package.json'); if (fs.existsSync(pkgJsonPath)) { const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); pkgJson.version = pkgVer; @@ -145,24 +147,27 @@ async function installPackage(artifact, tempDir) { } } -async function runInstall(artifacts) { +async function runInstall(artifacts, options) { if (!RID) { console.warn(`[foundry-local] Unsupported platform: ${platformKey}. Skipping.`); return; } - if (fs.existsSync(BIN_DIR) && REQUIRED_FILES.every(f => fs.existsSync(path.join(BIN_DIR, f)))) { + const force = options && options.force; + const binDir = (options && options.binDir) || BIN_DIR; + + if (!force && fs.existsSync(binDir) && REQUIRED_FILES.every(f => fs.existsSync(path.join(binDir, f)))) { console.log(`[foundry-local] Native libraries already installed.`); return; } console.log(`[foundry-local] Installing native libraries for ${RID}...`); - fs.mkdirSync(BIN_DIR, { recursive: true }); + fs.mkdirSync(binDir, { recursive: true }); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foundry-install-')); try { for (const artifact of artifacts) { - await installPackage(artifact, tempDir); + await installPackage(artifact, tempDir, binDir); } console.log('[foundry-local] Installation complete.'); } finally { diff --git a/sdk/js/script/install-winml.cjs b/sdk/js/script/install-winml.cjs index e6fda732..e6fd554e 100644 --- a/sdk/js/script/install-winml.cjs +++ b/sdk/js/script/install-winml.cjs @@ -2,11 +2,22 @@ // Licensed under the MIT License. // Install script for foundry-local-sdk-winml variant. +// +// Overwrites the standard native binaries inside foundry-local-sdk's own +// directory tree with the WinML variants (Core.WinML, ORT, GenAI). +// After this runs, everything lives under foundry-local-sdk — users import +// from 'foundry-local-sdk' and get WinML binaries transparently. 'use strict'; +const path = require('path'); const { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall } = require('./install-utils.cjs'); +// Resolve foundry-local-sdk's binary directory +const sdkRoot = path.dirname(require.resolve('foundry-local-sdk/package.json')); +const platformKey = `${process.platform}-${process.arch}`; +const binDir = path.join(sdkRoot, 'node_modules', '@foundry-local-core', platformKey); + const ARTIFACTS = [ { name: 'Microsoft.AI.Foundry.Local.Core.WinML', version: '0.9.0-dev-202603310538-f6efa8d3', feed: ORT_NIGHTLY_FEED }, { name: 'Microsoft.ML.OnnxRuntime.Foundry', version: '1.23.2.3', feed: NUGET_FEED }, @@ -15,7 +26,8 @@ const ARTIFACTS = [ (async () => { try { - await runInstall(ARTIFACTS); + // Force override into foundry-local-sdk's binary directory + await runInstall(ARTIFACTS, { force: true, binDir }); } catch (err) { console.error('Failed to install WinML artifacts:', err); process.exit(1); diff --git a/sdk/js/script/pack.cjs b/sdk/js/script/pack.cjs index 32057c7e..79a00828 100644 --- a/sdk/js/script/pack.cjs +++ b/sdk/js/script/pack.cjs @@ -19,8 +19,16 @@ try { const pkg = JSON.parse(original); if (isWinML) { pkg.name = 'foundry-local-sdk-winml'; - pkg.scripts.install = 'node script/install-winml.cjs'; - pkg.files = ['dist', 'script/install-winml.cjs', 'script/install-utils.cjs', 'script/preinstall.cjs']; + pkg.description = 'Foundry Local JavaScript SDK – WinML variant'; + // The winml package is a thin wrapper: it depends on the standard SDK for all JS code + // and only overrides the native binaries at install time. + pkg.dependencies = { 'foundry-local-sdk': pkg.version }; + pkg.scripts = { install: 'node script/install-winml.cjs' }; + // No dist/ or preinstall needed — the standard SDK provides the JS code + pkg.files = ['script/install-winml.cjs', 'script/install-utils.cjs']; + delete pkg.main; + delete pkg.types; + delete pkg.optionalDependencies; } else { pkg.files = ['dist', 'script/install-standard.cjs', 'script/install-utils.cjs', 'script/preinstall.cjs']; } diff --git a/sdk/js/script/preinstall.cjs b/sdk/js/script/preinstall.cjs index 5590550b..8cd953d2 100644 --- a/sdk/js/script/preinstall.cjs +++ b/sdk/js/script/preinstall.cjs @@ -25,7 +25,7 @@ const ALL_PLATFORMS = Object.keys(optionalDependencies) }; }); -const packagesRoot = path.join(__dirname, '..', 'packages', '@foundry-local-core'); +const packagesRoot = path.join(__dirname, '..', 'node_modules', '@foundry-local-core'); for (const platform of ALL_PLATFORMS) { const dir = path.join(packagesRoot, platform.key); diff --git a/sdk/js/src/detail/coreInterop.ts b/sdk/js/src/detail/coreInterop.ts index 5af32421..6a0bc6b4 100644 --- a/sdk/js/src/detail/coreInterop.ts +++ b/sdk/js/src/detail/coreInterop.ts @@ -2,7 +2,6 @@ import koffi from 'koffi'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; import { Configuration } from '../configuration.js'; koffi.struct('RequestBuffer', { @@ -49,15 +48,14 @@ export class CoreInterop { } private static _resolveDefaultCorePath(config: Configuration): string | null { - const require = createRequire(import.meta.url); const platform = process.platform; const arch = process.arch; - // Matches names generated by preinstall.cjs - const packageName = `@foundry-local-core/${platform}-${arch}`; - - // Resolve the package path. - const packagePath = require.resolve(`${packageName}/package.json`); - const packageDir = path.dirname(packagePath); + const platformKey = `${platform}-${arch}`; + + // Resolve the platform package directory at node_modules/@foundry-local-core/, + // the shared location where install scripts place the native binaries. + const sdkRoot = path.resolve(__dirname, '..', '..'); + const packageDir = path.join(sdkRoot, 'node_modules', '@foundry-local-core', platformKey); const ext = CoreInterop._getLibraryExtension(); const corePath = path.join(packageDir, `Microsoft.AI.Foundry.Local.Core${ext}`);