diff --git a/.github/workflows/build-rust-steps.yml b/.github/workflows/build-rust-steps.yml index f007b7ee..810b6c1e 100644 --- a/.github/workflows/build-rust-steps.yml +++ b/.github/workflows/build-rust-steps.yml @@ -58,6 +58,13 @@ jobs: Write-Host "Removed .cargo/config.toml crates-io redirect" } + # Copy deps_versions.json into the crate directory so cargo package + # can include it and build.rs can find it during verify. + - name: Copy deps_versions.json for crate packaging + shell: pwsh + working-directory: ${{ github.workspace }} + run: Copy-Item sdk/deps_versions.json sdk/rust/deps_versions.json + - name: Checkout test-data-shared from Azure DevOps if: ${{ inputs.run-integration-tests }} shell: pwsh diff --git a/.pipelines/foundry-local-packaging.yml b/.pipelines/foundry-local-packaging.yml index cb5766c0..d90a15e7 100644 --- a/.pipelines/foundry-local-packaging.yml +++ b/.pipelines/foundry-local-packaging.yml @@ -127,13 +127,13 @@ extends: Write-Host "Python version: $pyVersion" Write-Host "FLC version: $flcVersion" - # ── Build & Test FLC ── + # ── Build FLC ── - stage: build_core - displayName: 'Build & Test FLC' + displayName: 'Build Core' dependsOn: compute_version jobs: - job: flc_win_x64 - displayName: 'FLC win-x64' + displayName: 'Core win-x64' pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -153,7 +153,7 @@ extends: platform: x64 - job: flc_win_arm64 - displayName: 'FLC win-arm64' + displayName: 'Core win-arm64' pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -171,7 +171,7 @@ extends: platform: arm64 - job: flc_linux_x64 - displayName: 'FLC linux-x64' + displayName: 'Core linux-x64' pool: name: onnxruntime-Ubuntu2404-AMD-CPU os: linux @@ -189,7 +189,7 @@ extends: platform: x64 - job: flc_osx_arm64 - displayName: 'FLC osx-arm64' + displayName: 'Core osx-arm64' pool: name: Azure Pipelines vmImage: 'macOS-15' @@ -207,13 +207,13 @@ extends: flavor: osx-arm64 platform: arm64 - # ── Package FLC ── - - stage: package_core - displayName: 'Package FLC' - dependsOn: build_core - jobs: - job: package_flc - displayName: 'Package FLC' + displayName: 'Package Core' + dependsOn: + - flc_win_x64 + - flc_win_arm64 + - flc_linux_x64 + - flc_osx_arm64 pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -229,6 +229,9 @@ extends: - output: pipelineArtifact artifactName: 'flc-wheels' targetPath: '$(Build.ArtifactStagingDirectory)/flc-wheels' + - output: pipelineArtifact + artifactName: 'deps-versions-standard' + targetPath: '$(Build.ArtifactStagingDirectory)/deps-versions' steps: - checkout: neutron-server clean: true @@ -282,7 +285,8 @@ extends: # ── Build C# SDK ── - stage: build_cs displayName: 'Build C# SDK' - dependsOn: package_core + dependsOn: + - build_core jobs: - job: cs_sdk displayName: 'Build' @@ -297,6 +301,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' + - input: pipelineArtifact + artifactName: 'deps-versions-standard' + targetPath: '$(Pipeline.Workspace)/deps-versions-standard' outputs: - output: pipelineArtifact artifactName: 'cs-sdk' @@ -313,11 +320,13 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: false flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-standard' # ── Build JS SDK ── - stage: build_js displayName: 'Build JS SDK' - dependsOn: package_core + dependsOn: + - build_core jobs: - job: js_sdk displayName: 'Build' @@ -332,6 +341,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' + - input: pipelineArtifact + artifactName: 'deps-versions-standard' + targetPath: '$(Pipeline.Workspace)/deps-versions-standard' outputs: - output: pipelineArtifact artifactName: 'js-sdk' @@ -348,11 +360,13 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: false flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-standard' # ── Build Python SDK ── - stage: build_python displayName: 'Build Python SDK' - dependsOn: package_core + dependsOn: + - build_core jobs: - job: python_sdk displayName: 'Build' @@ -367,6 +381,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-wheels' targetPath: '$(Pipeline.Workspace)/flc-wheels' + - input: pipelineArtifact + artifactName: 'deps-versions-standard' + targetPath: '$(Pipeline.Workspace)/deps-versions-standard' outputs: - output: pipelineArtifact artifactName: 'python-sdk' @@ -383,11 +400,13 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: false flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-standard' # ── Build Rust SDK ── - stage: build_rust displayName: 'Build Rust SDK' - dependsOn: package_core + dependsOn: + - build_core jobs: - job: rust_sdk displayName: 'Build' @@ -402,6 +421,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-nuget' targetPath: '$(Pipeline.Workspace)/flc-nuget' + - input: pipelineArtifact + artifactName: 'deps-versions-standard' + targetPath: '$(Pipeline.Workspace)/deps-versions-standard' outputs: - output: pipelineArtifact artifactName: 'rust-sdk' @@ -418,133 +440,15 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: false flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-standard' - # ── Test C# SDK (win-x64) ── - - stage: test_cs - displayName: 'Test C# SDK' - dependsOn: build_cs - jobs: - - job: test_cs_win_x64 - displayName: 'Test C# (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget' - targetPath: '$(Pipeline.Workspace)/flc-nuget' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-cs-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: false - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' - - # TODO: Add macOS (osx-arm64) test job when a macOS ARM64 pool is available. - # TODO: Add Linux (linux-x64) test job when Linux onnxruntime dependency is stabilized. - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test JS SDK (win-x64) ── - - stage: test_js - displayName: 'Test JS SDK' - dependsOn: build_js - jobs: - - job: test_js_win_x64 - displayName: 'Test JS (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget' - targetPath: '$(Pipeline.Workspace)/flc-nuget' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-js-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: false - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' - - # TODO: Add macOS (osx-arm64) test job when a macOS ARM64 pool is available. - # TODO: Add Linux (linux-x64) test job when Linux onnxruntime dependency is stabilized. - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test Python SDK (win-x64) ── - - stage: test_python - displayName: 'Test Python SDK' - dependsOn: build_python - jobs: - - job: test_python_win_x64 - displayName: 'Test Python (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-wheels' - targetPath: '$(Pipeline.Workspace)/flc-wheels' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-python-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: false - flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels' - - # TODO: Add macOS (osx-arm64) test job when a macOS ARM64 pool is available. - # TODO: Add Linux (linux-x64) test job when Linux onnxruntime dependency is stabilized. - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test Rust SDK (win-x64) ── - - stage: test_rust - displayName: 'Test Rust SDK' - dependsOn: build_rust - jobs: - - job: test_rust_win_x64 - displayName: 'Test Rust (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget' - targetPath: '$(Pipeline.Workspace)/flc-nuget' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-rust-steps.yml@self - parameters: - isWinML: false - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget' - - # TODO: Add macOS (osx-arm64) test job when a macOS ARM64 pool is available. - # TODO: Add Linux (linux-x64) test job when Linux onnxruntime dependency is stabilized. - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Build & Test FLC (WinML) ── + # ── Build FLC (WinML) ── - stage: build_core_winml - displayName: 'Build & Test FLC WinML' + displayName: 'Build Core (WinML)' dependsOn: compute_version jobs: - job: flc_winml_win_x64 - displayName: 'FLC win-x64 (WinML)' + displayName: 'Core win-x64 (WinML)' pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -565,7 +469,7 @@ extends: isWinML: true - job: flc_winml_win_arm64 - displayName: 'FLC win-arm64 (WinML)' + displayName: 'Core win-arm64 (WinML)' pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -583,13 +487,11 @@ extends: platform: arm64 isWinML: true - # ── Package FLC (WinML) ── - - stage: package_core_winml - displayName: 'Package FLC WinML' - dependsOn: build_core_winml - jobs: - job: package_flc_winml - displayName: 'Package FLC (WinML)' + displayName: 'Package Core (WinML)' + dependsOn: + - flc_winml_win_x64 + - flc_winml_win_arm64 pool: name: onnxruntime-Win-CPU-2022 os: windows @@ -605,6 +507,9 @@ extends: - output: pipelineArtifact artifactName: 'flc-wheels-winml' targetPath: '$(Build.ArtifactStagingDirectory)/flc-wheels' + - output: pipelineArtifact + artifactName: 'deps-versions-winml' + targetPath: '$(Build.ArtifactStagingDirectory)/deps-versions' steps: - checkout: neutron-server clean: true @@ -643,8 +548,9 @@ extends: # ── Build C# SDK (WinML) ── - stage: build_cs_winml - displayName: 'Build C# SDK WinML' - dependsOn: package_core_winml + displayName: 'Build C# SDK (WinML)' + dependsOn: + - build_core_winml jobs: - job: cs_sdk_winml displayName: 'Build' @@ -659,6 +565,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-nuget-winml' targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' + - input: pipelineArtifact + artifactName: 'deps-versions-winml' + targetPath: '$(Pipeline.Workspace)/deps-versions-winml' outputs: - output: pipelineArtifact artifactName: 'cs-sdk-winml' @@ -675,12 +584,14 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: true flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-winml' outputDir: '$(Build.ArtifactStagingDirectory)/cs-sdk-winml' # ── Build JS SDK (WinML) ── - stage: build_js_winml - displayName: 'Build JS SDK WinML' - dependsOn: package_core_winml + displayName: 'Build JS SDK (WinML)' + dependsOn: + - build_core_winml jobs: - job: js_sdk_winml displayName: 'Build' @@ -695,6 +606,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-nuget-winml' targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' + - input: pipelineArtifact + artifactName: 'deps-versions-winml' + targetPath: '$(Pipeline.Workspace)/deps-versions-winml' outputs: - output: pipelineArtifact artifactName: 'js-sdk-winml' @@ -711,11 +625,13 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: true flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-winml' # ── Build Python SDK (WinML) ── - stage: build_python_winml - displayName: 'Build Python SDK WinML' - dependsOn: package_core_winml + displayName: 'Build Python SDK (WinML)' + dependsOn: + - build_core_winml jobs: - job: python_sdk_winml displayName: 'Build' @@ -730,6 +646,9 @@ extends: - input: pipelineArtifact artifactName: 'flc-wheels-winml' targetPath: '$(Pipeline.Workspace)/flc-wheels-winml' + - input: pipelineArtifact + artifactName: 'deps-versions-winml' + targetPath: '$(Pipeline.Workspace)/deps-versions-winml' outputs: - output: pipelineArtifact artifactName: 'python-sdk-winml' @@ -746,152 +665,49 @@ extends: prereleaseId: ${{ parameters.prereleaseId }} isWinML: true flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels-winml' + depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-winml' outputDir: '$(Build.ArtifactStagingDirectory)/python-sdk-winml' - # ── Build Rust SDK (WinML) ── - - stage: build_rust_winml - displayName: 'Build Rust SDK WinML' - dependsOn: package_core_winml - jobs: - - job: rust_sdk_winml - displayName: 'Build' - pool: - name: onnxruntime-Win-CPU-2022 - 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' - outputs: - - output: pipelineArtifact - artifactName: 'rust-sdk-winml' - targetPath: '$(Build.ArtifactStagingDirectory)/rust-sdk-winml' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/build-rust-steps.yml@self - parameters: - version: ${{ parameters.version }} - isRelease: ${{ parameters.isRelease }} - prereleaseId: ${{ parameters.prereleaseId }} - isWinML: true - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' - outputDir: '$(Build.ArtifactStagingDirectory)/rust-sdk-winml' - - # ── Test C# SDK WinML (win-x64) ── - - stage: test_cs_winml - displayName: 'Test C# SDK WinML' - dependsOn: build_cs_winml - jobs: - - job: test_cs_winml_win_x64 - displayName: 'Test C# WinML (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget-winml' - targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-cs-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: true - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' - - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test JS SDK WinML (win-x64) ── - - stage: test_js_winml - displayName: 'Test JS SDK WinML' - dependsOn: build_js_winml - jobs: - - job: test_js_winml_win_x64 - displayName: 'Test JS WinML (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget-winml' - targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-js-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: true - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' - - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test Python SDK WinML (win-x64) ── - - stage: test_python_winml - displayName: 'Test Python SDK WinML' - dependsOn: build_python_winml - jobs: - - job: test_python_winml_win_x64 - displayName: 'Test Python WinML (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-wheels-winml' - targetPath: '$(Pipeline.Workspace)/flc-wheels-winml' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-python-steps.yml@self - parameters: - version: ${{ parameters.version }} - isWinML: true - flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels-winml' - - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - - # ── Test Rust SDK WinML (win-x64) ── - - stage: test_rust_winml - displayName: 'Test Rust SDK WinML' - dependsOn: build_rust_winml - jobs: - - job: test_rust_winml_win_x64 - displayName: 'Test Rust WinML (win-x64)' - pool: - name: onnxruntime-Win-CPU-2022 - os: windows - templateContext: - inputs: - - input: pipelineArtifact - artifactName: 'flc-nuget-winml' - targetPath: '$(Pipeline.Workspace)/flc-nuget-winml' - steps: - - checkout: self - clean: true - - checkout: test-data-shared - lfs: true - - template: .pipelines/templates/test-rust-steps.yml@self - parameters: - isWinML: true - flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' - - # TODO: Add Windows ARM64 (win-arm64) test job when a Windows ARM64 pool is available. - + # Rust SDK has one package with different install options for standard vs WinML, + # so we only publish once under the standard stage and skip the WinML stage. Leaving + # it as a commented block incase we decide to publish separate Rust WinML package in the future. + # # ── Build Rust SDK (WinML) ── + # - stage: build_rust_winml + # displayName: 'Build Rust SDK (WinML)' + # dependsOn: + # - build_core_winml + # jobs: + # - job: rust_sdk_winml + # displayName: 'Build' + # pool: + # name: onnxruntime-Win-CPU-2022 + # 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' + # - input: pipelineArtifact + # artifactName: 'deps-versions-winml' + # targetPath: '$(Pipeline.Workspace)/deps-versions-winml' + # outputs: + # - output: pipelineArtifact + # artifactName: 'rust-sdk-winml' + # targetPath: '$(Build.ArtifactStagingDirectory)/rust-sdk-winml' + # steps: + # - checkout: self + # clean: true + # - checkout: test-data-shared + # lfs: true + # - template: .pipelines/templates/build-rust-steps.yml@self + # parameters: + # version: ${{ parameters.version }} + # isRelease: ${{ parameters.isRelease }} + # prereleaseId: ${{ parameters.prereleaseId }} + # isWinML: true + # flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml' + # depsVersionsDir: '$(Pipeline.Workspace)/deps-versions-winml' + # outputDir: '$(Build.ArtifactStagingDirectory)/rust-sdk-winml' \ No newline at end of file diff --git a/.pipelines/templates/build-cs-steps.yml b/.pipelines/templates/build-cs-steps.yml index 38f5b8bf..5d8f67c1 100644 --- a/.pipelines/templates/build-cs-steps.yml +++ b/.pipelines/templates/build-cs-steps.yml @@ -20,6 +20,10 @@ parameters: - name: prereleaseId type: string default: '' +- name: depsVersionsDir + type: string + default: '' + displayName: 'Path to deps-versions artifact directory' steps: # Set paths for multi-repo checkout - task: PowerShell@2 @@ -48,6 +52,13 @@ steps: Write-Host "Package version: $v" Write-Host "##vso[task.setvariable variable=packageVersion]$v" +# Load dependency versions from deps_versions.json +- template: update-deps-versions-steps.yml + parameters: + repoRoot: $(repoRoot) + artifactDir: ${{ parameters.depsVersionsDir }} + isWinML: ${{ parameters.isWinML }} + # List downloaded artifact for debugging - task: PowerShell@2 displayName: 'List downloaded FLC artifact' @@ -74,14 +85,9 @@ steps: "@ - # Determine the FLC version from the .nupkg filename + # Point the local NuGet feed at the directory that actually contains the .nupkg $nupkg = Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse -Filter "Microsoft.AI.Foundry.Local.Core*.nupkg" -Exclude "*.snupkg" | Select-Object -First 1 if (-not $nupkg) { throw "No FLC .nupkg found in ${{ parameters.flcNugetDir }}" } - $flcVer = $nupkg.BaseName -replace '^Microsoft\.AI\.Foundry\.Local\.Core(\.WinML)?\.', '' - Write-Host "##vso[task.setvariable variable=resolvedFlcVersion]$flcVer" - Write-Host "Resolved FLC version: $flcVer" - - # Point the local NuGet feed at the directory that actually contains the .nupkg $flcFeedDir = $nupkg.DirectoryName $nugetConfig = $nugetConfig -replace [regex]::Escape("${{ parameters.flcNugetDir }}"), $flcFeedDir $configPath = "$(Build.ArtifactStagingDirectory)/NuGet.config" @@ -182,3 +188,44 @@ steps: signConfigType: inlineSignParams inlineOperation: | [{"keyCode":"CP-401405","operationSetCode":"NuGetSign","parameters":[],"toolName":"sign","toolVersion":"6.2.9304.0"},{"keyCode":"CP-401405","operationSetCode":"NuGetVerify","parameters":[],"toolName":"sign","toolVersion":"6.2.9304.0"}] + +# ── Tests ── +- ${{ if eq(parameters.isWinML, true) }}: + - task: PowerShell@2 + displayName: 'Install Windows App SDK Runtime' + inputs: + targetType: 'inline' + script: | + $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" + $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath + & $installerPath --quiet --force + if ($LASTEXITCODE -ne 0) { throw "Windows App SDK Runtime install failed" } + errorActionPreference: 'stop' + +- task: PowerShell@2 + displayName: 'Restore & build tests' + inputs: + targetType: inline + script: | + dotnet restore "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` + --configfile "$(customNugetConfig)" ` + /p:UseWinML=${{ parameters.isWinML }} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + dotnet build "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` + --no-restore --configuration Release ` + /p:UseWinML=${{ parameters.isWinML }} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +- task: PowerShell@2 + displayName: 'Run SDK tests' + inputs: + targetType: inline + script: | + dotnet test "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` + --no-build --configuration Release ` + /p:UseWinML=${{ parameters.isWinML }} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + env: + TF_BUILD: 'true' diff --git a/.pipelines/templates/build-js-steps.yml b/.pipelines/templates/build-js-steps.yml index 3aa2908d..ca42fea1 100644 --- a/.pipelines/templates/build-js-steps.yml +++ b/.pipelines/templates/build-js-steps.yml @@ -17,6 +17,10 @@ parameters: - name: prereleaseId type: string default: '' +- name: depsVersionsDir + type: string + default: '' + displayName: 'Path to deps-versions artifact directory' steps: # Set paths for multi-repo checkout - task: PowerShell@2 @@ -45,7 +49,14 @@ steps: inputs: versionSpec: '20.x' -# Read version from the version-info artifact produced by compute_version stage. +# Load dependency versions from deps_versions.json +- template: update-deps-versions-steps.yml + parameters: + repoRoot: $(repoRoot) + artifactDir: ${{ parameters.depsVersionsDir }} + isWinML: ${{ parameters.isWinML }} + +# Compute version - task: PowerShell@2 displayName: 'Set package version' inputs: @@ -55,13 +66,66 @@ steps: Write-Host "Package version: $v" Write-Host "##vso[task.setvariable variable=packageVersion]$v" -# Install dependencies including native binaries (FLC, ORT, GenAI) from NuGet feeds +# Install JS dependencies. When a pipeline-built FLC artifact is provided, +# use --ignore-scripts to skip the native binary download (which would 404 +# on the unpublished FLC package), then extract FLC from the local artifact +# and run the install script manually to fetch ORT/GenAI from public feeds. - task: Npm@1 - displayName: 'npm install' + displayName: 'npm install (skip native downloads)' inputs: command: custom workingDir: $(repoRoot)/sdk/js - customCommand: 'install' + customCommand: 'install --ignore-scripts' + +- task: PowerShell@2 + displayName: 'Extract FLC from pipeline-built artifact' + inputs: + targetType: inline + script: | + $os = 'win32' + $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } + $platformKey = "$os-$arch" + $rid = if ($arch -eq 'arm64') { 'win-arm64' } else { 'win-x64' } + + if ($IsLinux) { + $os = 'linux' + $platformKey = "$os-$arch" + $rid = "linux-$arch" + } elseif ($IsMacOS) { + $os = 'darwin' + $platformKey = "$os-$arch" + $rid = "osx-$arch" + } + + $nupkg = Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse -Filter "Microsoft.AI.Foundry.Local.Core*.nupkg" -Exclude "*.snupkg" | Select-Object -First 1 + if (-not $nupkg) { throw "No FLC .nupkg found in ${{ parameters.flcNugetDir }}" } + + $extractDir = "$(Build.ArtifactStagingDirectory)/flc-extract" + $zip = [System.IO.Path]::ChangeExtension($nupkg.FullName, ".zip") + Copy-Item $nupkg.FullName $zip -Force + Expand-Archive -Path $zip -DestinationPath $extractDir -Force + + # Place FLC binary so the install script skips downloading it + $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 { + Copy-Item $_.FullName -Destination "$destDir/$($_.Name)" -Force + Write-Host "Placed $($_.Name) from pipeline artifact" + } + } else { + Write-Warning "No native binaries found at $nativeDir for RID $rid" + } + +- task: PowerShell@2 + displayName: 'Run native binary install (ORT + GenAI)' + inputs: + targetType: inline + script: | + Set-Location "$(repoRoot)/sdk/js" + node script/preinstall.cjs + node script/install-standard.cjs # Overwrite the FLC native binary with the one we just built - task: PowerShell@2 @@ -149,3 +213,26 @@ steps: $destDir = "$(Build.ArtifactStagingDirectory)/js-sdk" New-Item -ItemType Directory -Path $destDir -Force | Out-Null Copy-Item "$(repoRoot)/sdk/js/*.tgz" "$destDir/" + +# ── Tests ── +- ${{ if eq(parameters.isWinML, true) }}: + - task: PowerShell@2 + displayName: 'Install Windows App SDK Runtime' + inputs: + targetType: 'inline' + script: | + $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" + $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath + & $installerPath --quiet --force + if ($LASTEXITCODE -ne 0) { throw "Windows App SDK Runtime install failed" } + errorActionPreference: 'stop' + +- task: Npm@1 + displayName: 'npm test' + inputs: + command: custom + workingDir: $(repoRoot)/sdk/js + customCommand: 'test' + env: + TF_BUILD: 'true' diff --git a/.pipelines/templates/build-python-steps.yml b/.pipelines/templates/build-python-steps.yml index a8658772..5ada9cb6 100644 --- a/.pipelines/templates/build-python-steps.yml +++ b/.pipelines/templates/build-python-steps.yml @@ -20,6 +20,10 @@ parameters: - name: prereleaseId type: string default: '' +- name: depsVersionsDir + type: string + default: '' + displayName: 'Path to deps-versions artifact directory' steps: # Set paths for multi-repo checkout - task: PowerShell@2 @@ -37,6 +41,13 @@ steps: inputs: versionSpec: '3.12' +# Load dependency versions from deps_versions.json +- template: update-deps-versions-steps.yml + parameters: + repoRoot: $(repoRoot) + artifactDir: ${{ parameters.depsVersionsDir }} + isWinML: ${{ parameters.isWinML }} + # List downloaded FLC wheels for debugging - task: PowerShell@2 displayName: 'List downloaded FLC wheels' @@ -103,12 +114,19 @@ steps: Write-Warning "No FLC wheel found matching $filter in ${{ parameters.flcWheelsDir }}" } -- ${{ if eq(parameters.isWinML, true) }}: - - script: pip install onnxruntime-core==1.23.2.3 onnxruntime-genai-core==0.13.1 - displayName: 'Install ORT native packages (WinML)' -- ${{ else }}: - - script: pip install onnxruntime-core==1.24.4 onnxruntime-genai-core==0.13.1 - displayName: 'Install ORT native packages' +- task: PowerShell@2 + displayName: 'Install ORT native packages' + inputs: + targetType: inline + script: | + $isWinML = "${{ parameters.isWinML }}" -eq "True" + $fileName = if ($isWinML) { "deps_versions_winml.json" } else { "deps_versions.json" } + $deps = Get-Content "$(repoRoot)/sdk/$fileName" -Raw | ConvertFrom-Json + $ortVer = $deps.onnxruntime.version + $genaiVer = $deps.'onnxruntime-genai'.version + Write-Host "Installing onnxruntime-core==$ortVer onnxruntime-genai-core==$genaiVer" + pip install "onnxruntime-core==$ortVer" "onnxruntime-genai-core==$genaiVer" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - script: pip install "pydantic>=2.0.0" "requests>=2.32.4" "openai>=2.24.0" displayName: 'Install pure python dependencies' @@ -147,3 +165,26 @@ steps: Copy-Item "$(repoRoot)/sdk/python/dist/*" "$destDir/" Write-Host "Staged wheels:" Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" } + +# ── Tests ── +- ${{ if eq(parameters.isWinML, true) }}: + - task: PowerShell@2 + displayName: 'Install Windows App SDK Runtime' + inputs: + targetType: 'inline' + script: | + $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" + $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath + & $installerPath --quiet --force + if ($LASTEXITCODE -ne 0) { throw "Windows App SDK Runtime install failed" } + errorActionPreference: 'stop' + +- script: pip install coverage pytest>=7.0.0 pytest-timeout>=2.1.0 + displayName: 'Install test dependencies' + +- script: python -m pytest test/ -v + displayName: 'Run tests' + workingDirectory: $(repoRoot)/sdk/python + env: + TF_BUILD: 'true' diff --git a/.pipelines/templates/build-rust-steps.yml b/.pipelines/templates/build-rust-steps.yml index ed3161e5..c0489f4f 100644 --- a/.pipelines/templates/build-rust-steps.yml +++ b/.pipelines/templates/build-rust-steps.yml @@ -20,6 +20,10 @@ parameters: type: string default: '$(Build.ArtifactStagingDirectory)/rust-sdk' displayName: 'Path to directory for the packaged crate' +- name: depsVersionsDir + type: string + default: '' + displayName: 'Path to deps-versions artifact directory' steps: # Set paths for multi-repo checkout - task: PowerShell@2 @@ -57,6 +61,24 @@ steps: Write-Host "Contents of ${{ parameters.flcNugetDir }}:" Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName } +# Load dependency versions from deps_versions.json +- template: update-deps-versions-steps.yml + parameters: + repoRoot: $(repoRoot) + artifactDir: ${{ parameters.depsVersionsDir }} + isWinML: ${{ parameters.isWinML }} + +# Copy both deps_versions JSON files into the crate directory so cargo +# package includes them and build.rs can find the right one at build time +# since there is only 1 package for both rust artifacts. +- task: PowerShell@2 + displayName: 'Copy deps_versions for crate packaging' + inputs: + targetType: inline + script: | + Copy-Item "$(repoRoot)/sdk/deps_versions.json" "$(repoRoot)/sdk/rust/deps_versions.json" -Force + Copy-Item "$(repoRoot)/sdk/deps_versions_winml.json" "$(repoRoot)/sdk/rust/deps_versions_winml.json" -Force + # Extract FLC native binaries from the pipeline-built .nupkg so that # build.rs finds them already present and skips downloading from the feed. - task: PowerShell@2 @@ -90,7 +112,9 @@ steps: $flcNativeDir = "$(Build.ArtifactStagingDirectory)/flc-native-rust" New-Item -ItemType Directory -Path $flcNativeDir -Force | Out-Null Get-ChildItem $nativeDir -File | Copy-Item -Destination $flcNativeDir -Force - Write-Host "##vso[task.setvariable variable=flcNativeDir]$flcNativeDir" + # Set FOUNDRY_NATIVE_OVERRIDE_DIR so build.rs copies these into OUT_DIR + # instead of trying to download the unpublished FLC Core from the feed. + Write-Host "##vso[task.setvariable variable=FOUNDRY_NATIVE_OVERRIDE_DIR]$flcNativeDir" Write-Host "Extracted FLC native binaries to $flcNativeDir`:" Get-ChildItem $flcNativeDir | ForEach-Object { Write-Host " $($_.Name)" } @@ -154,28 +178,6 @@ steps: Invoke-Expression "cargo build $features" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } -# Overwrite the FLC core binary in cargo's OUT_DIR with the pipeline-built -# version so that integration tests use the freshly-built FLC. build.rs -# sets FOUNDRY_NATIVE_DIR to OUT_DIR, which the SDK checks at runtime. -- task: PowerShell@2 - displayName: 'Overwrite FLC binary with pipeline-built version' - inputs: - targetType: inline - script: | - # Find cargo's OUT_DIR for the foundry-local-sdk build script - $outDir = Get-ChildItem "$(repoRoot)/sdk/rust/target/debug/build" -Directory -Filter "foundry-local-sdk-*" -Recurse | - Where-Object { Test-Path "$($_.FullName)/out" } | - ForEach-Object { "$($_.FullName)/out" } | - Select-Object -First 1 - if (-not $outDir) { throw "Could not find cargo OUT_DIR for foundry-local-sdk" } - Write-Host "Cargo OUT_DIR: $outDir" - - # Copy pipeline-built FLC native binaries over the downloaded ones - Get-ChildItem "$(flcNativeDir)" -File -Filter "Microsoft.AI.Foundry.Local.Core.*" | ForEach-Object { - Copy-Item $_.FullName -Destination "$outDir/$($_.Name)" -Force - Write-Host "Overwrote $($_.Name) with pipeline-built version" - } - # --allow-dirty allows packaging with uncommitted changes (build.rs modifies generated files) - task: PowerShell@2 displayName: 'Package crate' @@ -198,3 +200,26 @@ steps: Copy-Item "$(repoRoot)/sdk/rust/target/package/*.crate" "$destDir/" Write-Host "Staged crates:" Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" } + +# ── Tests ── +- task: PowerShell@2 + displayName: 'Run unit tests' + inputs: + targetType: inline + script: | + Set-Location "$(repoRoot)/sdk/rust" + $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" } + Invoke-Expression "cargo test --lib $features" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +- task: PowerShell@2 + displayName: 'Run integration tests' + inputs: + targetType: inline + script: | + Set-Location "$(repoRoot)/sdk/rust" + $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" } + Invoke-Expression "cargo test --tests $features -- --include-ignored --test-threads=1 --nocapture" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + env: + TF_BUILD: 'true' diff --git a/.pipelines/templates/package-core-steps.yml b/.pipelines/templates/package-core-steps.yml index 01697085..fdd54c28 100644 --- a/.pipelines/templates/package-core-steps.yml +++ b/.pipelines/templates/package-core-steps.yml @@ -246,3 +246,50 @@ steps: Write-Host "`nAll wheels:" Get-ChildItem $stagingDir -Filter "*.whl" | ForEach-Object { Write-Host " $($_.Name)" } + +# Write partial deps_versions.json for this variant. The merge_deps_versions +# stage combines standard + WinML partials into a single complete artifact. +- task: PowerShell@2 + displayName: 'Write deps_versions.json artifact' + inputs: + targetType: inline + script: | + $nsRoot = "$(nsRoot)" + [xml]$propsXml = Get-Content "$nsRoot/Directory.Packages.props" + $pg = $propsXml.Project.PropertyGroup + + $isWinML = "${{ parameters.isWinML }}" -eq "True" + + # Compute PEP 440 version from the NuGet flcVersion + $parts = "$(flcVersion)" -split '-' + $pyVer = if ($parts.Count -ge 3 -and $parts[1] -eq 'dev') { "$($parts[0]).dev$($parts[2])" } + elseif ($parts.Count -eq 2) { "$($parts[0])$($parts[1])" } + else { $parts[0] } + + # Both standard and WinML write a deps_versions.json with identical key + # structure. The pipeline produces separate artifacts (deps-versions-standard + # / deps-versions-winml) so SDK stages pick the right one via isWinML. + if ($isWinML) { + $deps = @{ + 'foundry-local-core' = @{ nuget = "$(flcVersion)"; python = $pyVer } + onnxruntime = @{ version = [string]$pg.OnnxRuntimeFoundryVersionForWinML } + 'onnxruntime-genai' = @{ version = [string]$pg.OnnxRuntimeGenAIFoundryVersion } + } + } else { + $deps = @{ + 'foundry-local-core' = @{ nuget = "$(flcVersion)"; python = $pyVer } + onnxruntime = @{ version = [string]$pg.OnnxRuntimeFoundryVersion } + 'onnxruntime-genai' = @{ version = [string]$pg.OnnxRuntimeGenAIFoundryVersion } + } + } + + # WinML artifact is named deps_versions_winml.json to match repo convention. + $fileName = if ($isWinML) { "deps_versions_winml.json" } else { "deps_versions.json" } + $json = $deps | ConvertTo-Json -Depth 3 + Write-Host "${fileName}:" + Write-Host $json + + $outDir = "$(Build.ArtifactStagingDirectory)/deps-versions" + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + [System.IO.File]::WriteAllText("$outDir/$fileName", $json, [System.Text.UTF8Encoding]::new($false)) + Write-Host "Wrote $fileName to $outDir" diff --git a/.pipelines/templates/test-cs-steps.yml b/.pipelines/templates/test-cs-steps.yml deleted file mode 100644 index 92c9b6ee..00000000 --- a/.pipelines/templates/test-cs-steps.yml +++ /dev/null @@ -1,117 +0,0 @@ -# Lightweight test-only steps for the C# SDK. -# Builds from source and runs tests — no signing or NuGet packing. -parameters: -- name: version - type: string -- name: isWinML - type: boolean - default: false -- name: flcNugetDir - type: string - displayName: 'Path to directory containing the FLC .nupkg' - -steps: -- task: PowerShell@2 - displayName: 'Set source paths' - inputs: - targetType: inline - script: | - $repoRoot = "$(Build.SourcesDirectory)/Foundry-Local" - $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" - Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" - Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" - -- task: UseDotNet@2 - displayName: 'Use .NET 9 SDK' - inputs: - packageType: sdk - version: '9.0.x' - -- task: PowerShell@2 - displayName: 'List downloaded FLC artifact' - inputs: - targetType: inline - script: | - Write-Host "Contents of ${{ parameters.flcNugetDir }}:" - Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName } - -- ${{ if eq(parameters.isWinML, true) }}: - - task: PowerShell@2 - displayName: 'Install Windows App SDK Runtime' - inputs: - targetType: 'inline' - script: | - $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" - $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" - - Write-Host "Downloading Windows App SDK Runtime installer from $installerUrl..." - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath - - Write-Host "Installing Windows App SDK Runtime..." - & $installerPath --quiet --force - - if ($LASTEXITCODE -ne 0) { - Write-Error "Installation failed with exit code $LASTEXITCODE" - exit 1 - } - - Write-Host "Windows App SDK Runtime installed successfully." - errorActionPreference: 'stop' - -- task: PowerShell@2 - displayName: 'Create NuGet.config with local FLC feed' - inputs: - targetType: inline - script: | - $nugetConfig = @" - - - - - - - - - - "@ - $nupkg = Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse -Filter "Microsoft.AI.Foundry.Local.Core*.nupkg" -Exclude "*.snupkg" | Select-Object -First 1 - if (-not $nupkg) { throw "No FLC .nupkg found in ${{ parameters.flcNugetDir }}" } - $flcVer = $nupkg.BaseName -replace '^Microsoft\.AI\.Foundry\.Local\.Core(\.WinML)?\.', '' - Write-Host "##vso[task.setvariable variable=resolvedFlcVersion]$flcVer" - - $flcFeedDir = $nupkg.DirectoryName - $nugetConfig = $nugetConfig -replace [regex]::Escape("${{ parameters.flcNugetDir }}"), $flcFeedDir - $configPath = "$(Build.ArtifactStagingDirectory)/NuGet.config" - Set-Content -Path $configPath -Value $nugetConfig - Write-Host "##vso[task.setvariable variable=customNugetConfig]$configPath" - -- task: NuGetAuthenticate@1 - displayName: 'Authenticate NuGet feeds' - -- task: PowerShell@2 - displayName: 'Restore & build tests' - inputs: - targetType: inline - script: | - dotnet restore "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` - --configfile "$(customNugetConfig)" ` - /p:UseWinML=${{ parameters.isWinML }} ` - /p:FoundryLocalCoreVersion=$(resolvedFlcVersion) - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - dotnet build "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` - --no-restore --configuration Release ` - /p:UseWinML=${{ parameters.isWinML }} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -- task: PowerShell@2 - displayName: 'Run SDK tests' - inputs: - targetType: inline - script: | - dotnet test "$(repoRoot)/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" ` - --no-build --configuration Release ` - /p:UseWinML=${{ parameters.isWinML }} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - env: - TF_BUILD: 'true' diff --git a/.pipelines/templates/test-js-steps.yml b/.pipelines/templates/test-js-steps.yml deleted file mode 100644 index 1814626a..00000000 --- a/.pipelines/templates/test-js-steps.yml +++ /dev/null @@ -1,122 +0,0 @@ -# Lightweight test-only steps for the JS SDK. -# Builds from source and runs tests — no npm pack or artifact staging. -parameters: -- name: version - type: string -- name: isWinML - type: boolean - default: false -- name: flcNugetDir - type: string - displayName: 'Path to directory containing the FLC .nupkg' - -steps: -- task: PowerShell@2 - displayName: 'Set source paths' - inputs: - targetType: inline - script: | - $repoRoot = "$(Build.SourcesDirectory)/Foundry-Local" - $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" - Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" - Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" - -- ${{ if eq(parameters.isWinML, true) }}: - - task: PowerShell@2 - displayName: 'Install Windows App SDK Runtime' - inputs: - targetType: 'inline' - script: | - $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" - $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" - - Write-Host "Downloading Windows App SDK Runtime installer from $installerUrl..." - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath - - Write-Host "Installing Windows App SDK Runtime..." - & $installerPath --quiet --force - - if ($LASTEXITCODE -ne 0) { - Write-Error "Installation failed with exit code $LASTEXITCODE" - exit 1 - } - - Write-Host "Windows App SDK Runtime installed successfully." - errorActionPreference: 'stop' - -- task: PowerShell@2 - displayName: 'List downloaded FLC artifact' - inputs: - targetType: inline - script: | - Write-Host "Contents of ${{ parameters.flcNugetDir }}:" - Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName } - -- task: NodeTool@0 - displayName: 'Use Node.js 20' - inputs: - versionSpec: '20.x' - -- task: Npm@1 - displayName: 'npm install' - inputs: - command: custom - workingDir: $(repoRoot)/sdk/js - customCommand: 'install' - -# Overwrite the FLC native binary with the pipeline-built one -- task: PowerShell@2 - displayName: 'Overwrite FLC with pipeline-built binary' - inputs: - targetType: inline - script: | - $os = 'win32' - $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } - $platformKey = "$os-$arch" - $rid = if ($arch -eq 'arm64') { 'win-arm64' } else { 'win-x64' } - - if ($IsLinux) { - $os = 'linux' - $platformKey = "$os-$arch" - $rid = "linux-$arch" - } elseif ($IsMacOS) { - $os = 'darwin' - $platformKey = "$os-$arch" - $rid = "osx-$arch" - } - - $nupkg = Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse -Filter "Microsoft.AI.Foundry.Local.Core*.nupkg" -Exclude "*.snupkg" | Select-Object -First 1 - if (-not $nupkg) { throw "No FLC .nupkg found in ${{ parameters.flcNugetDir }}" } - - $extractDir = "$(Build.ArtifactStagingDirectory)/flc-extract" - $zip = [System.IO.Path]::ChangeExtension($nupkg.FullName, ".zip") - Copy-Item $nupkg.FullName $zip -Force - Expand-Archive -Path $zip -DestinationPath $extractDir -Force - - $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 { - Copy-Item $_.FullName -Destination "$destDir/$($_.Name)" -Force - Write-Host "Overwrote $($_.Name) with pipeline-built version" - } - } else { - Write-Warning "No native binaries found at $nativeDir for RID $rid" - } - -- task: Npm@1 - displayName: 'npm build' - inputs: - command: custom - workingDir: $(repoRoot)/sdk/js - customCommand: 'run build' - -- task: Npm@1 - displayName: 'npm test' - inputs: - command: custom - workingDir: $(repoRoot)/sdk/js - customCommand: 'test' - env: - TF_BUILD: 'true' diff --git a/.pipelines/templates/test-rust-steps.yml b/.pipelines/templates/test-rust-steps.yml deleted file mode 100644 index 31bfd75e..00000000 --- a/.pipelines/templates/test-rust-steps.yml +++ /dev/null @@ -1,159 +0,0 @@ -# Lightweight test-only steps for the Rust SDK. -# Builds from source and runs tests — no cargo package or artifact staging. -parameters: -- name: isWinML - type: boolean - default: false -- name: flcNugetDir - type: string - displayName: 'Path to directory containing the FLC .nupkg' - -steps: -- task: PowerShell@2 - displayName: 'Set source paths' - inputs: - targetType: inline - script: | - $repoRoot = "$(Build.SourcesDirectory)/Foundry-Local" - $testDataDir = "$(Build.SourcesDirectory)/test-data-shared" - Write-Host "##vso[task.setvariable variable=repoRoot]$repoRoot" - Write-Host "##vso[task.setvariable variable=testDataDir]$testDataDir" - -- ${{ if eq(parameters.isWinML, true) }}: - - task: PowerShell@2 - displayName: 'Install Windows App SDK Runtime' - inputs: - targetType: 'inline' - script: | - $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" - $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" - - Write-Host "Downloading Windows App SDK Runtime installer from $installerUrl..." - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath - - Write-Host "Installing Windows App SDK Runtime..." - & $installerPath --quiet --force - - if ($LASTEXITCODE -ne 0) { - Write-Error "Installation failed with exit code $LASTEXITCODE" - exit 1 - } - - Write-Host "Windows App SDK Runtime installed successfully." - errorActionPreference: 'stop' - -- task: PowerShell@2 - displayName: 'List downloaded FLC artifact' - inputs: - targetType: inline - script: | - Write-Host "Contents of ${{ parameters.flcNugetDir }}:" - Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName } - -# Extract FLC native binaries from the pipeline-built .nupkg -- task: PowerShell@2 - displayName: 'Extract FLC native binaries' - inputs: - targetType: inline - script: | - $nupkg = Get-ChildItem "${{ parameters.flcNugetDir }}" -Recurse -Filter "Microsoft.AI.Foundry.Local.Core*.nupkg" -Exclude "*.snupkg" | Select-Object -First 1 - if (-not $nupkg) { throw "No FLC .nupkg found in ${{ parameters.flcNugetDir }}" } - - $extractDir = "$(Build.ArtifactStagingDirectory)/flc-extract-rust" - $zip = [System.IO.Path]::ChangeExtension($nupkg.FullName, ".zip") - Copy-Item $nupkg.FullName $zip -Force - Expand-Archive -Path $zip -DestinationPath $extractDir -Force - - $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } - if ($IsLinux) { - $rid = "linux-$arch" - } elseif ($IsMacOS) { - $rid = "osx-$arch" - } else { - $rid = "win-$arch" - } - - $nativeDir = "$extractDir/runtimes/$rid/native" - if (-not (Test-Path $nativeDir)) { throw "No native binaries found at $nativeDir for RID $rid" } - - $flcNativeDir = "$(Build.ArtifactStagingDirectory)/flc-native-rust" - New-Item -ItemType Directory -Path $flcNativeDir -Force | Out-Null - Get-ChildItem $nativeDir -File | Copy-Item -Destination $flcNativeDir -Force - Write-Host "##vso[task.setvariable variable=flcNativeDir]$flcNativeDir" - Write-Host "Extracted FLC native binaries for $rid" - -- task: PowerShell@2 - displayName: 'Install Rust toolchain' - inputs: - targetType: inline - script: | - if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) { - Invoke-WebRequest -Uri https://win.rustup.rs/x86_64 -OutFile rustup-init.exe - .\rustup-init.exe -y --default-toolchain stable --profile minimal -c clippy,rustfmt - Remove-Item rustup-init.exe - $cargoPath = "$env:USERPROFILE\.cargo\bin" - } else { - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal -c clippy,rustfmt - $cargoPath = "$env:HOME/.cargo/bin" - } - Write-Host "##vso[task.prependpath]$cargoPath" - -- task: PowerShell@2 - displayName: 'Use crates.io directly' - inputs: - targetType: inline - script: | - $configPath = "$(repoRoot)/sdk/rust/.cargo/config.toml" - if (Test-Path $configPath) { - Remove-Item $configPath - Write-Host "Removed .cargo/config.toml crates-io redirect" - } - -- task: PowerShell@2 - displayName: 'Build' - inputs: - targetType: inline - script: | - Set-Location "$(repoRoot)/sdk/rust" - $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" } - Invoke-Expression "cargo build $features" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -# Overwrite FLC binary with pipeline-built version -- task: PowerShell@2 - displayName: 'Overwrite FLC binary with pipeline-built version' - inputs: - targetType: inline - script: | - $outDir = Get-ChildItem "$(repoRoot)/sdk/rust/target/debug/build" -Directory -Filter "foundry-local-sdk-*" -Recurse | - Where-Object { Test-Path "$($_.FullName)/out" } | - ForEach-Object { "$($_.FullName)/out" } | - Select-Object -First 1 - if (-not $outDir) { throw "Could not find cargo OUT_DIR for foundry-local-sdk" } - - Get-ChildItem "$(flcNativeDir)" -File -Filter "Microsoft.AI.Foundry.Local.Core.*" | ForEach-Object { - Copy-Item $_.FullName -Destination "$outDir/$($_.Name)" -Force - Write-Host "Overwrote $($_.Name) with pipeline-built version" - } - -- task: PowerShell@2 - displayName: 'Run unit tests' - inputs: - targetType: inline - script: | - Set-Location "$(repoRoot)/sdk/rust" - $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" } - Invoke-Expression "cargo test --lib $features" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -- task: PowerShell@2 - displayName: 'Run integration tests' - inputs: - targetType: inline - script: | - Set-Location "$(repoRoot)/sdk/rust" - $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" } - Invoke-Expression "cargo test --tests $features -- --include-ignored --test-threads=1 --nocapture" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - env: - TF_BUILD: 'true' diff --git a/.pipelines/templates/update-deps-versions-steps.yml b/.pipelines/templates/update-deps-versions-steps.yml new file mode 100644 index 00000000..9d489ab7 --- /dev/null +++ b/.pipelines/templates/update-deps-versions-steps.yml @@ -0,0 +1,41 @@ +# Shared template to update deps_versions.json / deps_versions_winml.json +# from pipeline artifacts. Both files use identical key structure — the +# isWinML parameter determines which file gets overwritten. +parameters: +- name: repoRoot + type: string + default: '$(repoRoot)' +- name: artifactDir + type: string + default: '' + displayName: 'Path to artifact directory containing pipeline-generated deps_versions JSON' +- name: isWinML + type: boolean + default: false + +steps: +- task: PowerShell@2 + displayName: 'Update deps_versions from pipeline artifact' + inputs: + targetType: inline + script: | + $isWinML = "${{ parameters.isWinML }}" -eq "True" + $fileName = if ($isWinML) { "deps_versions_winml.json" } else { "deps_versions.json" } + $repoJson = "${{ parameters.repoRoot }}/sdk/$fileName" + $artifactDir = "${{ parameters.artifactDir }}" + + if ($artifactDir -eq '' -or -not (Test-Path "$artifactDir/$fileName")) { + throw "Pipeline-built $fileName not found in artifact directory: $artifactDir" + } + + Copy-Item "$artifactDir/$fileName" $repoJson -Force + Write-Host "Updated repo $fileName from pipeline artifact at $artifactDir" + + $deps = Get-Content $repoJson -Raw | ConvertFrom-Json + + # Log resolved versions for debugging + Write-Host "Dependency versions from ${fileName}:" + Write-Host " FLC Core (NuGet): $($deps.'foundry-local-core'.nuget)" + Write-Host " FLC Core (Python): $($deps.'foundry-local-core'.python)" + Write-Host " OnnxRuntime: $($deps.onnxruntime.version)" + Write-Host " GenAI: $($deps.'onnxruntime-genai'.version)" diff --git a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj index 26d74ff6..df8fc2cf 100644 --- a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj +++ b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj @@ -98,10 +98,15 @@ - + + <_DepsVersionsPath Condition="'$(UseWinML)' == 'true'">$(MSBuildThisFileDirectory)..\..\deps_versions_winml.json + <_DepsVersionsPath Condition="'$(UseWinML)' != 'true'">$(MSBuildThisFileDirectory)..\..\deps_versions.json + <_DepsVersionsJson>$([System.IO.File]::ReadAllText('$(_DepsVersionsPath)')) $(FoundryLocalCoreVersion) - 0.9.0-dev-202603310538-f6efa8d3 - 0.9.0-dev-202603310538-f6efa8d3 + $([System.Text.RegularExpressions.Regex]::Match('$(_DepsVersionsJson)', '"nuget"\s*:\s*"([^"]+)"').Groups[1].Value) + $([System.Text.RegularExpressions.Regex]::Match('$(_DepsVersionsJson)', '"nuget"\s*:\s*"([^"]+)"').Groups[1].Value) + True diff --git a/sdk/deps_versions.json b/sdk/deps_versions.json new file mode 100644 index 00000000..aa0b3fa3 --- /dev/null +++ b/sdk/deps_versions.json @@ -0,0 +1,12 @@ +{ + "foundry-local-core": { + "nuget": "0.9.0-dev-202603310538-f6efa8d3", + "python": "0.9.0.dev20260327060216" + }, + "onnxruntime": { + "version": "1.24.3" + }, + "onnxruntime-genai": { + "version": "0.13.0" + } +} diff --git a/sdk/deps_versions_winml.json b/sdk/deps_versions_winml.json new file mode 100644 index 00000000..a4532421 --- /dev/null +++ b/sdk/deps_versions_winml.json @@ -0,0 +1,12 @@ +{ + "foundry-local-core": { + "nuget": "0.9.0-dev-202603310538-f6efa8d3", + "python": "0.9.0.dev20260331004032" + }, + "onnxruntime": { + "version": "1.23.2.3" + }, + "onnxruntime-genai": { + "version": "0.13.0" + } +} diff --git a/sdk/js/script/install-standard.cjs b/sdk/js/script/install-standard.cjs index 6901766d..cc86a96f 100644 --- a/sdk/js/script/install-standard.cjs +++ b/sdk/js/script/install-standard.cjs @@ -5,13 +5,21 @@ 'use strict'; +const fs = require('fs'); const os = require('os'); +const path = require('path'); const { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall } = require('./install-utils.cjs'); +// deps_versions.json lives at the package root when published, or at sdk/ in the repo. +const depsPath = fs.existsSync(path.resolve(__dirname, '..', 'deps_versions.json')) + ? path.resolve(__dirname, '..', 'deps_versions.json') + : path.resolve(__dirname, '..', '..', 'deps_versions.json'); +const deps = require(depsPath); + const ARTIFACTS = [ - { name: 'Microsoft.AI.Foundry.Local.Core', version: '0.9.0-dev-202603310538-f6efa8d3', feed: ORT_NIGHTLY_FEED }, - { name: os.platform() === 'linux' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry', version: '1.24.4', feed: NUGET_FEED }, - { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.13.1', feed: NUGET_FEED }, + { name: 'Microsoft.AI.Foundry.Local.Core', version: deps['foundry-local-core'].nuget, feed: ORT_NIGHTLY_FEED }, + { name: os.platform() === 'linux' ? 'Microsoft.ML.OnnxRuntime.Gpu.Linux' : 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version, feed: NUGET_FEED }, + { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai'].version, feed: ORT_NIGHTLY_FEED }, ]; (async () => { diff --git a/sdk/js/script/install-utils.cjs b/sdk/js/script/install-utils.cjs index 090a25e3..aa74f4d5 100644 --- a/sdk/js/script/install-utils.cjs +++ b/sdk/js/script/install-utils.cjs @@ -106,10 +106,29 @@ async function getBaseAddress(feedUrl) { return baseAddress.endsWith('/') ? baseAddress : baseAddress + '/'; } -async function installPackage(artifact, tempDir, binDir) { +async function installPackage(artifact, tempDir, binDir, skipIfPresent) { const pkgName = artifact.name; const pkgVer = artifact.version; + // Skip download if this package's main native binary is already present + // (e.g. pre-populated by CI from a locally-built artifact). + // Callers pass skipIfPresent=false when overriding (e.g. WinML over standard). + if (skipIfPresent) { + const prefix = os.platform() === 'win32' ? '' : 'lib'; + let expectedFile; + if (pkgName.includes('Foundry.Local.Core')) { + expectedFile = `Microsoft.AI.Foundry.Local.Core${EXT}`; + } else if (pkgName.includes('OnnxRuntimeGenAI')) { + expectedFile = `${prefix}onnxruntime-genai${EXT}`; + } else if (pkgName.includes('OnnxRuntime')) { + expectedFile = `${prefix}onnxruntime${EXT}`; + } + if (expectedFile && fs.existsSync(path.join(binDir, expectedFile))) { + console.log(` ${pkgName}: already present, skipping download.`); + return; + } + } + const baseAddress = await getBaseAddress(artifact.feed); const nameLower = pkgName.toLowerCase(); const verLower = pkgVer.toLowerCase(); @@ -136,14 +155,16 @@ async function installPackage(artifact, tempDir, binDir) { console.warn(` No files found for RID ${RID} in ${pkgName}.`); } - // Update platform package.json version for Core packages + // Overwrite FLC platform package.json so require.resolve can find the package if (pkgName.startsWith('Microsoft.AI.Foundry.Local.Core')) { const pkgJsonPath = path.join(binDir, 'package.json'); - if (fs.existsSync(pkgJsonPath)) { - const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); - pkgJson.version = pkgVer; - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2)); - } + const pkgContent = { + name: `@foundry-local-core/${platformKey}`, + version: pkgVer, + description: `Native binaries for Foundry Local SDK (${platformKey})`, + private: true + }; + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2)); } } @@ -153,13 +174,11 @@ async function runInstall(artifacts, options) { return; } - 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; - } + // When a custom binDir is provided (e.g. WinML overriding standard), + // don't skip packages whose output files already exist — we need to + // overwrite them with the variant's binaries. + const skipIfPresent = !(options && options.binDir); console.log(`[foundry-local] Installing native libraries for ${RID}...`); fs.mkdirSync(binDir, { recursive: true }); @@ -167,7 +186,7 @@ async function runInstall(artifacts, options) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foundry-install-')); try { for (const artifact of artifacts) { - await installPackage(artifact, tempDir, binDir); + await installPackage(artifact, tempDir, binDir, skipIfPresent); } console.log('[foundry-local] Installation complete.'); } finally { diff --git a/sdk/js/script/install-winml.cjs b/sdk/js/script/install-winml.cjs index efa2041c..22a2e97a 100644 --- a/sdk/js/script/install-winml.cjs +++ b/sdk/js/script/install-winml.cjs @@ -10,18 +10,26 @@ 'use strict'; +const fs = require('fs'); const path = require('path'); const { NUGET_FEED, ORT_NIGHTLY_FEED, runInstall } = require('./install-utils.cjs'); +// WinML uses its own deps_versions_winml.json with the same key structure +// as the standard deps_versions.json — no variant-specific keys needed. +// deps_versions_winml.json lives at the package root when published, or at sdk/ in the repo. +const depsPath = fs.existsSync(path.resolve(__dirname, '..', 'deps_versions_winml.json')) + ? path.resolve(__dirname, '..', 'deps_versions_winml.json') + : path.resolve(__dirname, '..', '..', 'deps_versions_winml.json'); +const deps = require(depsPath); // 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 }, - { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: '0.13.1', feed: NUGET_FEED }, + { name: 'Microsoft.AI.Foundry.Local.Core.WinML', version: deps['foundry-local-core']['nuget'], feed: ORT_NIGHTLY_FEED }, + { name: 'Microsoft.ML.OnnxRuntime.Foundry', version: deps.onnxruntime.version, feed: NUGET_FEED }, + { name: 'Microsoft.ML.OnnxRuntimeGenAI.Foundry', version: deps['onnxruntime-genai']['version'], feed: ORT_NIGHTLY_FEED }, ]; (async () => { diff --git a/sdk/js/script/pack.cjs b/sdk/js/script/pack.cjs index 79a00828..f550043e 100644 --- a/sdk/js/script/pack.cjs +++ b/sdk/js/script/pack.cjs @@ -15,6 +15,15 @@ const pkgPath = path.join(__dirname, '..', 'package.json'); const original = fs.readFileSync(pkgPath, 'utf8'); const isWinML = process.argv[2] === 'winml'; +// deps_versions.json lives in the parent sdk/ directory; copy it into the +// JS package root so that npm pack includes it in the tarball. +const pkgRoot = path.join(__dirname, '..'); +const depsSource = path.join(pkgRoot, '..', 'deps_versions.json'); +const depsDest = path.join(pkgRoot, 'deps_versions.json'); +const depsWinmlSource = path.join(pkgRoot, '..', 'deps_versions_winml.json'); +const depsWinmlDest = path.join(pkgRoot, 'deps_versions_winml.json'); +const copiedFiles = []; + try { const pkg = JSON.parse(original); if (isWinML) { @@ -25,16 +34,28 @@ try { 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']; + pkg.files = ['script/install-winml.cjs', 'script/install-utils.cjs', 'deps_versions_winml.json']; delete pkg.main; delete pkg.types; delete pkg.optionalDependencies; + if (fs.existsSync(depsWinmlSource) && !fs.existsSync(depsWinmlDest)) { + fs.copyFileSync(depsWinmlSource, depsWinmlDest); + copiedFiles.push(depsWinmlDest); + } } else { - pkg.files = ['dist', 'script/install-standard.cjs', 'script/install-utils.cjs', 'script/preinstall.cjs']; + pkg.files = ['dist', 'script/install-standard.cjs', 'script/install-utils.cjs', 'script/preinstall.cjs', 'deps_versions.json']; + if (fs.existsSync(depsSource) && !fs.existsSync(depsDest)) { + fs.copyFileSync(depsSource, depsDest); + copiedFiles.push(depsDest); + } } fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); - execSync('npm pack', { cwd: path.join(__dirname, '..'), stdio: 'inherit' }); + execSync('npm pack', { cwd: pkgRoot, stdio: 'inherit' }); } finally { // Always restore original package.json fs.writeFileSync(pkgPath, original); + // Clean up copied deps_versions files + for (const f of copiedFiles) { + if (fs.existsSync(f)) fs.unlinkSync(f); + } } diff --git a/sdk/js/script/preinstall.cjs b/sdk/js/script/preinstall.cjs index 8cd953d2..99e805d7 100644 --- a/sdk/js/script/preinstall.cjs +++ b/sdk/js/script/preinstall.cjs @@ -35,18 +35,16 @@ for (const platform of ALL_PLATFORMS) { } const pkgJsonPath = path.join(dir, 'package.json'); - if (!fs.existsSync(pkgJsonPath)) { - const pkgContent = { - name: `@foundry-local-core/${platform.key}`, - version: "0.0.0", // Placeholder version, will be replaced during script/install-utils.cjs (installPackage()) - description: `Native binaries for Foundry Local SDK (${platform.key})`, - os: [platform.os], - cpu: [platform.cpu], - private: true - }; - fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2)); - console.log(` Created skeleton for ${platform.key}`); - } + const pkgContent = { + name: `@foundry-local-core/${platform.key}`, + version: "0.0.0", + description: `Native binaries for Foundry Local SDK (${platform.key})`, + os: [platform.os], + cpu: [platform.cpu], + private: true + }; + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2)); + console.log(` Created skeleton for ${platform.key}`); } console.log('[foundry-local] Preinstall complete.'); diff --git a/sdk/python/build_backend.py b/sdk/python/build_backend.py index 1bdf6cbb..57e96286 100644 --- a/sdk/python/build_backend.py +++ b/sdk/python/build_backend.py @@ -30,8 +30,8 @@ from __future__ import annotations import contextlib +import json import os -import shutil from collections.abc import Generator from pathlib import Path @@ -44,13 +44,52 @@ _PROJECT_ROOT = Path(__file__).parent _PYPROJECT = _PROJECT_ROOT / "pyproject.toml" _REQUIREMENTS = _PROJECT_ROOT / "requirements.txt" -_REQUIREMENTS_WINML = _PROJECT_ROOT / "requirements-winml.txt" +_REQUIREMENTS_BASE = _PROJECT_ROOT / "requirements-base.txt" # The exact string in pyproject.toml to patch for the WinML variant. _STANDARD_NAME = 'name = "foundry-local-sdk"' _WINML_NAME = 'name = "foundry-local-sdk-winml"' +# --------------------------------------------------------------------------- +# Requirements generation from deps_versions.json +# --------------------------------------------------------------------------- + + +def _load_deps_versions(*, winml: bool) -> dict: + """Load the appropriate deps_versions JSON file. + + Standard and WinML each have their own file with identical key structure, + so callers never need variant-specific key names. + """ + filename = "deps_versions_winml.json" if winml else "deps_versions.json" + filepath = _PROJECT_ROOT.parent / filename + with open(filepath, encoding="utf-8-sig") as f: + return json.load(f) + + +def _generate_requirements(*, winml: bool) -> str: + """Generate requirements.txt content from base deps + deps_versions.json.""" + base = _REQUIREMENTS_BASE.read_text(encoding="utf-8").rstrip("\n") + deps = _load_deps_versions(winml=winml) + + if winml: + requirement_lines = [ + f"foundry-local-core-winml=={deps['foundry-local-core']['python']}", + f"onnxruntime-core=={deps['onnxruntime']['version']}", + f"onnxruntime-genai-core=={deps['onnxruntime-genai']['version']}", + ] + else: + requirement_lines = [ + f"foundry-local-core=={deps['foundry-local-core']['python']}", + f"""onnxruntime-gpu=={deps['onnxruntime']['version']}; platform_system == "Linux" """.rstrip(), + f"""onnxruntime-core=={deps['onnxruntime']['version']}; platform_system != "Linux" """.rstrip(), + f"""onnxruntime-genai-cuda=={deps['onnxruntime-genai']['version']}; platform_system == "Linux" """.rstrip(), + f"""onnxruntime-genai-core=={deps['onnxruntime-genai']['version']}; platform_system != "Linux" """.rstrip(), + ] + return f"{base}\n" + "\n".join(requirement_lines) + "\n" + + # --------------------------------------------------------------------------- # Variant detection # --------------------------------------------------------------------------- @@ -74,13 +113,12 @@ def _is_winml(config_settings: dict | None) -> bool: @contextlib.contextmanager def _patch_for_winml() -> Generator[None, None, None]: - """Temporarily patch ``pyproject.toml`` and ``requirements.txt`` for WinML. + """Temporarily patch ``pyproject.toml`` and generate ``requirements.txt`` for WinML. - Both files are restored to their original content in the ``finally`` - block, even if the build raises an exception. + ``pyproject.toml`` is restored in the ``finally`` block. + ``requirements.txt`` is left in place (generated from deps_versions.json). """ pyproject_original = _PYPROJECT.read_text(encoding="utf-8") - requirements_original = _REQUIREMENTS.read_text(encoding="utf-8") try: # Patch package name (simple string replacement — no TOML writer needed) patched_pyproject = pyproject_original.replace(_STANDARD_NAME, _WINML_NAME, 1) @@ -90,21 +128,24 @@ def _patch_for_winml() -> Generator[None, None, None]: "WinML name patch failed." ) _PYPROJECT.write_text(patched_pyproject, encoding="utf-8") - - # Swap requirements.txt with the WinML variant - shutil.copy2(_REQUIREMENTS_WINML, _REQUIREMENTS) - + _REQUIREMENTS.write_text(_generate_requirements(winml=True), encoding="utf-8") yield finally: _PYPROJECT.write_text(pyproject_original, encoding="utf-8") - _REQUIREMENTS.write_text(requirements_original, encoding="utf-8") + + +@contextlib.contextmanager +def _patch_standard_deps() -> Generator[None, None, None]: + """Generate ``requirements.txt`` from base deps + ``deps_versions.json``.""" + _REQUIREMENTS.write_text(_generate_requirements(winml=False), encoding="utf-8") + yield def _apply_patches(config_settings: dict | None): """Return a context manager that applies the appropriate patches.""" if _is_winml(config_settings): return _patch_for_winml() - return contextlib.nullcontext() + return _patch_standard_deps() # --------------------------------------------------------------------------- @@ -148,7 +189,5 @@ def get_requires_for_build_sdist(config_settings=None): def build_sdist(sdist_directory, config_settings=None): - if _is_winml(config_settings): - with _patch_for_winml(): - return _sb.build_sdist(sdist_directory, config_settings) - return _sb.build_sdist(sdist_directory, config_settings) + with _apply_patches(config_settings): + return _sb.build_sdist(sdist_directory, config_settings) diff --git a/sdk/python/requirements-base.txt b/sdk/python/requirements-base.txt new file mode 100644 index 00000000..dfc8d718 --- /dev/null +++ b/sdk/python/requirements-base.txt @@ -0,0 +1,3 @@ +pydantic>=2.0.0 +requests>=2.32.4 +openai>=2.24.0 diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 2a6292b7..af6a64f2 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundry-local-sdk" -version = "0.1.0" +version = "1.0.0" edition = "2021" license = "MIT" readme = "README.md" @@ -8,6 +8,7 @@ description = "Local AI model inference powered by the Foundry Local Core engine homepage = "https://www.foundrylocal.ai/" repository = "https://github.com/microsoft/Foundry-Local" documentation = "https://github.com/microsoft/Foundry-Local/blob/main/sdk/rust/docs/api.md" +include = ["src/**", "build.rs", "Cargo.toml", "README.md", "LICENSE", "deps_versions.json", "deps_versions_winml.json"] [features] default = [] diff --git a/sdk/rust/build.rs b/sdk/rust/build.rs index 999bca3d..7daf7a73 100644 --- a/sdk/rust/build.rs +++ b/sdk/rust/build.rs @@ -7,11 +7,64 @@ const NUGET_FEED: &str = "https://api.nuget.org/v3/index.json"; const ORT_NIGHTLY_FEED: &str = "https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/nuget/v3/index.json"; -const CORE_VERSION: &str = "0.9.0.8-rc3"; -const ORT_VERSION: &str = "1.24.4"; -const GENAI_VERSION: &str = "0.13.1"; +/// Versions loaded from deps_versions.json (or deps_versions_winml.json). +/// Both files share the same key structure — the build script picks the +/// right file based on the winml cargo feature. +struct DepsVersions { + core: String, + ort: String, + genai: String, +} + +fn load_deps_versions() -> DepsVersions { + let winml = env::var("CARGO_FEATURE_WINML").is_ok(); + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_default(); + let manifest_path = Path::new(&manifest_dir); + + // Standard and WinML each have their own file with identical key structure. + let filename = if winml { + "deps_versions_winml.json" + } else { + "deps_versions.json" + }; + + // Check manifest dir first (packaged crate), then parent (repo layout) + let json_path = if manifest_path.join(filename).exists() { + manifest_path.join(filename) + } else { + manifest_path.join("..").join(filename) + }; + + // Tell Cargo to rebuild if the versions file changes + println!( + "cargo:rerun-if-changed={}", + json_path + .canonicalize() + .unwrap_or(json_path.clone()) + .display() + ); -const WINML_ORT_VERSION: &str = "1.23.2.3"; + let content = fs::read_to_string(&json_path).expect("Failed to read deps_versions.json"); + // Strip UTF-8 BOM if present (PowerShell may write files with BOM) + let stripped_content = content.strip_prefix('\u{FEFF}').unwrap_or(&content); + let val: serde_json::Value = + serde_json::from_str(stripped_content).expect("Failed to parse deps_versions.json"); + + let s = |obj: &serde_json::Value, key: &str| -> String { + obj.get(key) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string() + }; + let flc = &val["foundry-local-core"]; + let ort = &val["onnxruntime"]; + let genai = &val["onnxruntime-genai"]; + DepsVersions { + core: s(flc, "nuget"), + ort: s(ort, "version"), + genai: s(genai, "version"), + } +} struct NuGetPackage { name: &'static str, @@ -43,6 +96,7 @@ fn native_lib_extension() -> &'static str { fn get_packages(rid: &str) -> Vec { let winml = env::var("CARGO_FEATURE_WINML").is_ok(); let is_linux = rid.starts_with("linux"); + let deps = load_deps_versions(); // Use pinned versions directly — dynamic resolution via resolve_latest_version // is unreliable (feed returns versions in unexpected order, and some old versions @@ -53,44 +107,44 @@ fn get_packages(rid: &str) -> Vec { if winml { packages.push(NuGetPackage { name: "Microsoft.AI.Foundry.Local.Core.WinML", - version: CORE_VERSION.to_string(), + version: deps.core.clone(), feed_url: ORT_NIGHTLY_FEED, }); packages.push(NuGetPackage { name: "Microsoft.ML.OnnxRuntime.Foundry", - version: WINML_ORT_VERSION.to_string(), + version: deps.ort.clone(), feed_url: NUGET_FEED, }); packages.push(NuGetPackage { name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", - version: GENAI_VERSION.to_string(), - feed_url: NUGET_FEED, + version: deps.genai.clone(), + feed_url: ORT_NIGHTLY_FEED, }); } else { packages.push(NuGetPackage { name: "Microsoft.AI.Foundry.Local.Core", - version: CORE_VERSION.to_string(), + version: deps.core.clone(), feed_url: ORT_NIGHTLY_FEED, }); if is_linux { packages.push(NuGetPackage { name: "Microsoft.ML.OnnxRuntime.Gpu.Linux", - version: ORT_VERSION.to_string(), + version: deps.ort.clone(), feed_url: NUGET_FEED, }); } else { packages.push(NuGetPackage { name: "Microsoft.ML.OnnxRuntime.Foundry", - version: ORT_VERSION.to_string(), + version: deps.ort.clone(), feed_url: NUGET_FEED, }); } packages.push(NuGetPackage { name: "Microsoft.ML.OnnxRuntimeGenAI.Foundry", - version: GENAI_VERSION.to_string(), - feed_url: NUGET_FEED, + version: deps.genai.clone(), + feed_url: ORT_NIGHTLY_FEED, }); } @@ -133,7 +187,33 @@ fn resolve_base_address(feed_url: &str) -> Result { } /// Download a .nupkg and extract native libraries for the given RID into `out_dir`. +/// Skips download if native files from this package are already present. fn download_and_extract(pkg: &NuGetPackage, rid: &str, out_dir: &Path) -> Result<(), String> { + // Skip if this package's main native library is already in out_dir + // (e.g. pre-populated from FOUNDRY_NATIVE_OVERRIDE_DIR). + let ext = native_lib_extension(); + let prefix = if env::consts::OS == "windows" { + "" + } else { + "lib" + }; + let expected_file = if pkg.name.contains("Foundry.Local.Core") { + format!("Microsoft.AI.Foundry.Local.Core.{ext}") + } else if pkg.name.contains("OnnxRuntimeGenAI") { + format!("{prefix}onnxruntime-genai.{ext}") + } else if pkg.name.contains("OnnxRuntime") { + format!("{prefix}onnxruntime.{ext}") + } else { + String::new() + }; + if !expected_file.is_empty() && out_dir.join(&expected_file).exists() { + println!( + "cargo:warning={} already present, skipping download.", + pkg.name + ); + return Ok(()); + } + let base_address = resolve_base_address(pkg.feed_url)?; let lower_name = pkg.name.to_lowercase(); let lower_version = pkg.version.to_lowercase(); @@ -212,19 +292,26 @@ fn download_and_extract(pkg: &NuGetPackage, rid: &str, out_dir: &Path) -> Result Ok(()) } -/// Check whether the core native library is already present in `out_dir`. +/// Check whether all required native libraries are already present in `out_dir`. fn libs_already_present(out_dir: &Path) -> bool { - let core_lib = match env::consts::OS { - "windows" => "Microsoft.AI.Foundry.Local.Core.dll", - "linux" => "Microsoft.AI.Foundry.Local.Core.so", - "macos" => "Microsoft.AI.Foundry.Local.Core.dylib", - _ => return false, + let ext = native_lib_extension(); + let prefix = if env::consts::OS == "windows" { + "" + } else { + "lib" }; - out_dir.join(core_lib).exists() + let required = [ + format!("Microsoft.AI.Foundry.Local.Core.{ext}"), + format!("{prefix}onnxruntime.{ext}"), + format!("{prefix}onnxruntime-genai.{ext}"), + ]; + required.iter().all(|f| out_dir.join(f).exists()) } fn main() { println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=FOUNDRY_NATIVE_OVERRIDE_DIR"); + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_WINML"); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); @@ -240,7 +327,29 @@ fn main() { } }; - // Skip download if libraries already exist + // If FOUNDRY_NATIVE_OVERRIDE_DIR is set (e.g. by CI), copy all native + // libraries from that directory into OUT_DIR. This pre-populates FLC Core + // binaries that aren't published to a feed yet. The download loop below + // will then only fetch packages whose files are still missing (ORT, GenAI). + if let Ok(override_dir) = env::var("FOUNDRY_NATIVE_OVERRIDE_DIR") { + let src = Path::new(&override_dir); + if src.is_dir() { + let ext = native_lib_extension(); + for entry in fs::read_dir(src).expect("Failed to read FOUNDRY_NATIVE_OVERRIDE_DIR") { + let path = entry.expect("Failed to read dir entry").path(); + if path.extension().and_then(|e| e.to_str()) == Some(ext) { + let dest = out_dir.join(path.file_name().unwrap()); + fs::copy(&path, &dest).expect("Failed to copy native lib from override dir"); + println!( + "cargo:warning=Copied {} from override dir", + path.file_name().unwrap().to_string_lossy() + ); + } + } + } + } + + // Skip all downloads if every required library is already present if libs_already_present(&out_dir) { println!("cargo:warning=Native libraries already present in OUT_DIR, skipping download."); println!("cargo:rustc-link-search=native={}", out_dir.display()); @@ -252,16 +361,22 @@ fn main() { let packages = get_packages(rid); + let mut download_failed = false; for pkg in &packages { if let Err(e) = download_and_extract(pkg, rid, &out_dir) { println!("cargo:warning=Error downloading {}: {e}", pkg.name); - println!("cargo:warning=Build will continue, but runtime loading may fail."); - println!( - "cargo:warning=You can manually place native libraries in the output directory." - ); + download_failed = true; } } + if download_failed && !libs_already_present(&out_dir) { + panic!( + "One or more native library downloads failed and required libraries are missing. \ + You can manually place native libraries in the output directory: {}", + out_dir.display() + ); + } + println!("cargo:rustc-link-search=native={}", out_dir.display()); println!("cargo:rustc-env=FOUNDRY_NATIVE_DIR={}", out_dir.display());