diff --git a/.github/workflows/build-cs-steps.yml b/.github/workflows/build-cs-steps.yml
index dcfed979..cf680d49 100644
--- a/.github/workflows/build-cs-steps.yml
+++ b/.github/workflows/build-cs-steps.yml
@@ -41,19 +41,41 @@ jobs:
env:
NUGET_AUTH_TOKEN: ${{ secrets.AZURE_DEVOPS_PAT }}
+ - name: Generate temporary NuGet.config
+ run: |
+ # The repo-level NuGet.config cleared all sources and only included ORT-Nightly.
+ # We generate a temporary one with both nuget.org and ORT-Nightly.
+ # We provide credentials to allow the ORT-Nightly feed to pull from its upstreams.
+ $xml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "@
+ Set-Content -Path sdk/cs/NuGet.temp.config -Value $xml
+ shell: pwsh
+
# TODO: once the nightly packaging is fixed, add back the commented out lines with /p:FoundryLocalCoreVersion="*-*"
# /p:FoundryLocalCoreVersion="*-*" to always use nightly version of Foundry Local Core
- - name: Authenticate to Azure Artifacts NuGet feed
- run: dotnet nuget update source ORT-Nightly --username az --password ${{ secrets.AZURE_DEVOPS_PAT }} --store-password-in-clear-text --configfile sdk/cs/NuGet.config
-
- name: Restore dependencies
run: |
- # dotnet restore sdk/cs/src/Microsoft.AI.Foundry.Local.csproj /p:UseWinML=${{ inputs.useWinML }} /p:FoundryLocalCoreVersion="*-*" --configfile sdk/cs/NuGet.config
- dotnet restore sdk/cs/src/Microsoft.AI.Foundry.Local.csproj /p:UseWinML=${{ inputs.useWinML }} --configfile sdk/cs/NuGet.config
+ # Clear the local NuGet cache to avoid bad metadata or corrupted package states.
+ dotnet nuget locals all --clear
+ # Restore using the temporary config file with credentials.
+ dotnet restore sdk/cs/src/Microsoft.AI.Foundry.Local.csproj /p:UseWinML=${{ inputs.useWinML }} --configfile sdk/cs/NuGet.temp.config
- name: Build solution
run: |
- # dotnet build sdk/cs/src/Microsoft.AI.Foundry.Local.csproj --no-restore --configuration ${{ inputs.buildConfiguration }} /p:UseWinML=${{ inputs.useWinML }} /p:FoundryLocalCoreVersion="*-*"
dotnet build sdk/cs/src/Microsoft.AI.Foundry.Local.csproj --no-restore --configuration ${{ inputs.buildConfiguration }} /p:UseWinML=${{ inputs.useWinML }}
# need to use direct git commands to clone from Azure DevOps instead of actions/checkout
@@ -89,6 +111,7 @@ jobs:
- name: Run Foundry Local Core tests
run: |
# dotnet test sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj --verbosity normal /p:UseWinML=${{ inputs.useWinML }} /p:FoundryLocalCoreVersion="*-*"
+ # Use the temporary config file for test restore as well.
dotnet test sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj --verbosity normal /p:UseWinML=${{ inputs.useWinML }}
- name: Pack NuGet package
diff --git a/.github/workflows/foundry-local-sdk-build.yml b/.github/workflows/foundry-local-sdk-build.yml
index 13eddf6d..07ae4d68 100644
--- a/.github/workflows/foundry-local-sdk-build.yml
+++ b/.github/workflows/foundry-local-sdk-build.yml
@@ -17,60 +17,8 @@ permissions:
contents: read
jobs:
- build-cs-windows:
- uses: ./.github/workflows/build-cs-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- secrets: inherit
- build-js-windows:
- uses: ./.github/workflows/build-js-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- secrets: inherit
- build-python-windows:
- uses: ./.github/workflows/build-python-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- secrets: inherit
- build-rust-windows:
- uses: ./.github/workflows/build-rust-steps.yml
- with:
- platform: 'windows'
- run-integration-tests: true
- secrets: inherit
-
- build-cs-windows-WinML:
- uses: ./.github/workflows/build-cs-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- useWinML: true
- secrets: inherit
- build-js-windows-WinML:
- uses: ./.github/workflows/build-js-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- useWinML: true
- secrets: inherit
- build-python-windows-WinML:
- uses: ./.github/workflows/build-python-steps.yml
- with:
- version: '0.9.0.${{ github.run_number }}'
- platform: 'windows'
- useWinML: true
- secrets: inherit
- build-rust-windows-WinML:
- uses: ./.github/workflows/build-rust-steps.yml
- with:
- platform: 'windows'
- useWinML: true
- run-integration-tests: true
- secrets: inherit
-
+ # Windows build/test moved to .pipelines/foundry-local-packaging.yml and runs in ADO
+ # MacOS ARM64 not supported in ADO, need to use GitHub Actions
build-cs-macos:
uses: ./.github/workflows/build-cs-steps.yml
with:
diff --git a/.pipelines/foundry-local-packaging.yml b/.pipelines/foundry-local-packaging.yml
index b87eb70e..2cb9ee2a 100644
--- a/.pipelines/foundry-local-packaging.yml
+++ b/.pipelines/foundry-local-packaging.yml
@@ -1,9 +1,807 @@
-# Foundry Local SDK Packaging Pipeline (placeholder)
-trigger: none
+# Foundry Local Packaging Pipeline
+#
+# Builds Foundry Local Core from neutron-server (windows.ai.toolkit project),
+# then packages the C# and JS SDKs from this repo using the built Core.
+#
+# Produces artifacts: flc-nuget, flc-nuget-winml, flc-wheels, flc-wheels-winml,
+# cs-sdk, cs-sdk-winml, js-sdk, js-sdk-winml, python-sdk, python-sdk-winml,
+# rust-sdk, rust-sdk-winml
-pool:
- vmImage: 'windows-latest'
+pr:
+- main
+- releases/*
+
+name: $(Date:yyyyMMdd).$(Rev:r)
+
+parameters:
+- name: version
+ displayName: 'Package version'
+ type: string
+ default: '0.9.0'
+- name: prereleaseId
+ displayName: 'Pre-release identifier (e.g. rc1, beta).'
+ type: string
+ default: 'none'
+- name: isRelease
+ displayName: 'Release build'
+ type: boolean
+ default: false
+- name: neutronServerBranch
+ displayName: 'Foundry Local Core branch (windows.ai.toolkit/neutron-server)'
+ type: string
+ default: 'dev/FoundryLocalCore/main'
+
+variables:
+- group: FoundryLocal-ESRP-Signing
+
+resources:
+ repositories:
+ - repository: neutron-server
+ type: git
+ name: windows.ai.toolkit/neutron-server
+ endpoint: AIFoundryLocal-WindowsAIToolkit-SC
+ ref: refs/heads/${{ parameters.neutronServerBranch }}
+ - repository: test-data-shared
+ type: git
+ name: windows.ai.toolkit/test-data-shared
+ endpoint: AIFoundryLocal-WindowsAIToolkit-SC
+ lfs: true
+ ref: refs/heads/main
+ - repository: 1ESPipelineTemplates
+ type: git
+ name: 1ESPipelineTemplates/1ESPipelineTemplates
+ ref: refs/tags/release
+
+extends:
+ template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
+ parameters:
+ settings:
+ networkIsolationPolicy: Permissive
+ pool:
+ # default all windows jobs, individual jobs override
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ sdl:
+ binskim:
+ break: false
+ scanOutputDirectoryOnly: true
+ sourceRepositoriesToScan:
+ include:
+ - repository: neutron-server
+ - repository: test-data-shared
+ stages:
+ # ── Build & Test FLC ──
+ - stage: build_core
+ displayName: 'Build & Test FLC'
+ jobs:
+ - job: flc_win_x64
+ displayName: 'FLC win-x64'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-win-x64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: win-x64
+ platform: x64
+
+ - job: flc_win_arm64
+ displayName: 'FLC win-arm64'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-win-arm64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: win-arm64
+ platform: arm64
+
+ - job: flc_linux_x64
+ displayName: 'FLC linux-x64'
+ pool:
+ name: onnxruntime-Ubuntu2404-AMD-CPU
+ os: linux
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-linux-x64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: linux-x64
+ platform: x64
+
+ - job: flc_osx_arm64
+ displayName: 'FLC osx-arm64'
+ pool:
+ name: Azure Pipelines
+ vmImage: 'macOS-14'
+ os: macOS
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-osx-arm64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: osx-arm64
+ platform: arm64
+
+ # ── Package FLC ──
+ - stage: package_core
+ displayName: 'Package FLC'
+ dependsOn: build_core
+ jobs:
+ - job: package_flc
+ displayName: 'Package FLC'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-nuget'
+ targetPath: '$(Build.ArtifactStagingDirectory)/flc-nuget'
+ - output: pipelineArtifact
+ artifactName: 'flc-wheels'
+ targetPath: '$(Build.ArtifactStagingDirectory)/flc-wheels'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-win-x64'
+ targetPath: '$(Pipeline.Workspace)/flc-win-x64'
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-win-arm64'
+ targetPath: '$(Pipeline.Workspace)/flc-win-arm64'
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-linux-x64'
+ targetPath: '$(Pipeline.Workspace)/flc-linux-x64'
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-osx-arm64'
+ targetPath: '$(Pipeline.Workspace)/flc-osx-arm64'
+ - task: PowerShell@2
+ displayName: 'List downloaded platform artifacts'
+ inputs:
+ targetType: inline
+ script: |
+ foreach ($name in @('flc-win-x64','flc-win-arm64','flc-linux-x64','flc-osx-arm64')) {
+ $dir = "$(Pipeline.Workspace)/$name"
+ Write-Host "Contents of ${dir}:"
+ if (Test-Path $dir) { Get-ChildItem $dir -Recurse | ForEach-Object { Write-Host $_.FullName } }
+ else { Write-Host " (directory not found)" }
+ }
+ - template: .pipelines/templates/package-core-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: false
+ platforms:
+ - name: win-x64
+ artifactName: flc-win-x64
+ - name: win-arm64
+ artifactName: flc-win-arm64
+ - name: linux-x64
+ artifactName: flc-linux-x64
+ - name: osx-arm64
+ artifactName: flc-osx-arm64
+
+ # ── Build C# SDK ──
+ - stage: build_cs
+ displayName: 'Build C# SDK'
+ dependsOn: package_core
+ jobs:
+ - job: cs_sdk
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-nuget'
+ targetPath: '$(Pipeline.Workspace)/flc-nuget'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'cs-sdk'
+ targetPath: '$(Build.ArtifactStagingDirectory)/cs-sdk'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-cs-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: false
+ flcNugetDir: '$(Pipeline.Workspace)/flc-nuget'
+
+ # ── Build JS SDK ──
+ - stage: build_js
+ displayName: 'Build JS SDK'
+ dependsOn: package_core
+ jobs:
+ - job: js_sdk
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-nuget'
+ targetPath: '$(Pipeline.Workspace)/flc-nuget'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'js-sdk'
+ targetPath: '$(Build.ArtifactStagingDirectory)/js-sdk'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-js-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: false
+ flcNugetDir: '$(Pipeline.Workspace)/flc-nuget'
+
+ # ── Build Python SDK ──
+ - stage: build_python
+ displayName: 'Build Python SDK'
+ dependsOn: package_core
+ jobs:
+ - job: python_sdk
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-wheels'
+ targetPath: '$(Pipeline.Workspace)/flc-wheels'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'python-sdk'
+ targetPath: '$(Build.ArtifactStagingDirectory)/python-sdk'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-python-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: false
+ flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels'
+
+ # ── Build Rust SDK ──
+ - stage: build_rust
+ displayName: 'Build Rust SDK'
+ dependsOn: package_core
+ jobs:
+ - job: rust_sdk
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-nuget'
+ targetPath: '$(Pipeline.Workspace)/flc-nuget'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'rust-sdk'
+ targetPath: '$(Build.ArtifactStagingDirectory)/rust-sdk'
+ 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: false
+ flcNugetDir: '$(Pipeline.Workspace)/flc-nuget'
+
+ # ── 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) ──
+ - stage: build_core_winml
+ displayName: 'Build & Test FLC WinML'
+ dependsOn: []
+ jobs:
+ - job: flc_winml_win_x64
+ displayName: 'FLC win-x64 (WinML)'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-winml-win-x64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: win-x64
+ platform: x64
+ isWinML: true
+
+ - job: flc_winml_win_arm64
+ displayName: 'FLC win-arm64 (WinML)'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-winml-win-arm64'
+ targetPath: '$(Build.ArtifactStagingDirectory)/native'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - template: .pipelines/templates/build-core-steps.yml@self
+ parameters:
+ flavor: win-arm64
+ 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)'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'flc-nuget-winml'
+ targetPath: '$(Build.ArtifactStagingDirectory)/flc-nuget'
+ - output: pipelineArtifact
+ artifactName: 'flc-wheels-winml'
+ targetPath: '$(Build.ArtifactStagingDirectory)/flc-wheels'
+ steps:
+ - checkout: neutron-server
+ clean: true
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-winml-win-x64'
+ targetPath: '$(Pipeline.Workspace)/flc-winml-win-x64'
+ - task: DownloadPipelineArtifact@2
+ inputs:
+ buildType: current
+ artifactName: 'flc-winml-win-arm64'
+ targetPath: '$(Pipeline.Workspace)/flc-winml-win-arm64'
+ - task: PowerShell@2
+ displayName: 'List downloaded WinML platform artifacts'
+ inputs:
+ targetType: inline
+ script: |
+ foreach ($name in @('flc-winml-win-x64','flc-winml-win-arm64')) {
+ $dir = "$(Pipeline.Workspace)/$name"
+ Write-Host "Contents of ${dir}:"
+ if (Test-Path $dir) { Get-ChildItem $dir -Recurse | ForEach-Object { Write-Host $_.FullName } }
+ else { Write-Host " (directory not found)" }
+ }
+ - template: .pipelines/templates/package-core-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: true
+ platforms:
+ - name: win-x64
+ artifactName: flc-winml-win-x64
+ - name: win-arm64
+ artifactName: flc-winml-win-arm64
+
+ # ── Build C# SDK (WinML) ──
+ - stage: build_cs_winml
+ displayName: 'Build C# SDK WinML'
+ dependsOn: package_core_winml
+ jobs:
+ - job: cs_sdk_winml
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-nuget-winml'
+ targetPath: '$(Pipeline.Workspace)/flc-nuget-winml'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'cs-sdk-winml'
+ targetPath: '$(Build.ArtifactStagingDirectory)/cs-sdk-winml'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-cs-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: true
+ flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml'
+ outputDir: '$(Build.ArtifactStagingDirectory)/cs-sdk-winml'
+
+ # ── Build JS SDK (WinML) ──
+ - stage: build_js_winml
+ displayName: 'Build JS SDK WinML'
+ dependsOn: package_core_winml
+ jobs:
+ - job: js_sdk_winml
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-nuget-winml'
+ targetPath: '$(Pipeline.Workspace)/flc-nuget-winml'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'js-sdk-winml'
+ targetPath: '$(Build.ArtifactStagingDirectory)/js-sdk'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-js-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: true
+ flcNugetDir: '$(Pipeline.Workspace)/flc-nuget-winml'
+
+ # ── Build Python SDK (WinML) ──
+ - stage: build_python_winml
+ displayName: 'Build Python SDK WinML'
+ dependsOn: package_core_winml
+ jobs:
+ - job: python_sdk_winml
+ displayName: 'Build'
+ pool:
+ name: onnxruntime-Win-CPU-2022
+ os: windows
+ templateContext:
+ inputs:
+ - input: pipelineArtifact
+ artifactName: 'flc-wheels-winml'
+ targetPath: '$(Pipeline.Workspace)/flc-wheels-winml'
+ outputs:
+ - output: pipelineArtifact
+ artifactName: 'python-sdk-winml'
+ targetPath: '$(Build.ArtifactStagingDirectory)/python-sdk-winml'
+ steps:
+ - checkout: self
+ clean: true
+ - checkout: test-data-shared
+ lfs: true
+ - template: .pipelines/templates/build-python-steps.yml@self
+ parameters:
+ version: ${{ parameters.version }}
+ isRelease: ${{ parameters.isRelease }}
+ prereleaseId: ${{ parameters.prereleaseId }}
+ isWinML: true
+ flcWheelsDir: '$(Pipeline.Workspace)/flc-wheels-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: '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.
-steps:
-- script: echo "Foundry Local packaging pipeline - placeholder"
- displayName: 'Placeholder'
\ No newline at end of file
diff --git a/.pipelines/templates/build-core-steps.yml b/.pipelines/templates/build-core-steps.yml
new file mode 100644
index 00000000..9f024c42
--- /dev/null
+++ b/.pipelines/templates/build-core-steps.yml
@@ -0,0 +1,194 @@
+# Steps to build a single Foundry Local Core native AOT binary.
+# Parameterized by flavor (RID) and platform (arch).
+# The parent job must checkout 'neutron-server'.
+parameters:
+- name: flavor
+ type: string # e.g. win-x64, linux-x64, osx-arm64
+- name: platform
+ type: string # e.g. x64, arm64
+- name: isWinML
+ type: boolean
+ default: false
+
+steps:
+- task: PowerShell@2
+ displayName: 'Set source paths'
+ inputs:
+ targetType: inline
+ script: |
+ # Multi-checkout places repos in subdirectories; single checkout places contents at root
+ $multiCheckout = "$(Build.SourcesDirectory)/neutron-server"
+ if (Test-Path $multiCheckout) {
+ $nsRoot = $multiCheckout
+ } else {
+ $nsRoot = "$(Build.SourcesDirectory)"
+ }
+ Write-Host "##vso[task.setvariable variable=nsRoot]$nsRoot"
+ Write-Host "neutron-server root: $nsRoot"
+
+- task: UseDotNet@2
+ displayName: 'Use .NET SDK from global.json'
+ inputs:
+ packageType: sdk
+ useGlobalJson: true
+ workingDirectory: '$(nsRoot)'
+
+- task: PowerShell@2
+ displayName: 'Override nuget.config'
+ inputs:
+ targetType: inline
+ script: |
+ $nugetConfig = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "@
+ Set-Content -Path "$(nsRoot)/nuget.config" -Value $nugetConfig
+ Write-Host "Updated nuget.config to use nuget.org, ORT-Nightly, and Neutron with mappings"
+
+- ${{ if eq(parameters.isWinML, true) }}:
+ - task: DotNetCoreCLI@2
+ displayName: 'Restore FLC Core ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: restore
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ restoreArguments: '-r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:NetTargetFramework=net9.0-windows10.0.26100.0 /p:UseWinML=true'
+ feedsToUse: config
+ nugetConfigPath: '$(nsRoot)/nuget.config'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Build FLC Core ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: build
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ arguments: '--no-restore -r ${{ parameters.flavor }} -f net9.0-windows10.0.26100.0 /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:NetTargetFramework=net9.0-windows10.0.26100.0 /p:UseWinML=true'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Publish FLC AOT ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: publish
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ arguments: '--no-restore --no-build -r ${{ parameters.flavor }} -f net9.0-windows10.0.26100.0 /p:Platform=${{ parameters.platform }} /p:Configuration=Release /p:PublishAot=true /p:NetTargetFramework=net9.0-windows10.0.26100.0 /p:UseWinML=true'
+ publishWebProjects: false
+ zipAfterPublish: false
+
+ - ${{ if eq(parameters.flavor, 'win-x64') }}:
+ - task: DotNetCoreCLI@2
+ displayName: 'Restore FLC Tests ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: restore
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ restoreArguments: '-r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:NetTargetFramework=net9.0-windows10.0.26100.0 /p:UseWinML=true'
+ feedsToUse: config
+ nugetConfigPath: '$(nsRoot)/nuget.config'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Build FLC Tests ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: build
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ arguments: '--no-restore -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:NetTargetFramework=net9.0-windows10.0.26100.0 /p:UseWinML=true'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Test FLC ${{ parameters.flavor }} (WinML)'
+ inputs:
+ command: test
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ arguments: '--no-build --configuration Release -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }}'
+
+- ${{ if eq(parameters.isWinML, false) }}:
+ - task: DotNetCoreCLI@2
+ displayName: 'Restore FLC Core ${{ parameters.flavor }}'
+ inputs:
+ command: restore
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ restoreArguments: '-r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:TargetFramework=net9.0'
+ feedsToUse: config
+ nugetConfigPath: '$(nsRoot)/nuget.config'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Build FLC Core ${{ parameters.flavor }}'
+ inputs:
+ command: build
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ arguments: '--no-restore -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release'
+
+ - ${{ if eq(parameters.flavor, 'win-x64') }}:
+ - task: DotNetCoreCLI@2
+ displayName: 'Restore FLC Tests ${{ parameters.flavor }}'
+ inputs:
+ command: restore
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ restoreArguments: '-r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release /p:TargetFramework=net9.0'
+ feedsToUse: config
+ nugetConfigPath: '$(nsRoot)/nuget.config'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Build FLC Tests ${{ parameters.flavor }}'
+ inputs:
+ command: build
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ arguments: '--no-restore -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:IncludeWebService=true /p:Configuration=Release'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Test FLC ${{ parameters.flavor }}'
+ inputs:
+ command: test
+ projects: '$(nsRoot)/test/FoundryLocalCore/Core/FoundryLocalCore.Tests.csproj'
+ arguments: '--no-build --configuration Release -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }}'
+
+ - task: DotNetCoreCLI@2
+ displayName: 'Publish FLC AOT ${{ parameters.flavor }}'
+ inputs:
+ command: publish
+ projects: '$(nsRoot)/src/FoundryLocalCore/Core/Core.csproj'
+ arguments: '--no-restore --no-build -r ${{ parameters.flavor }} /p:Platform=${{ parameters.platform }} /p:Configuration=Release /p:PublishAot=true /p:TargetFramework=net9.0'
+ publishWebProjects: false
+ zipAfterPublish: false
+
+# Cleanup non-binary files
+- task: PowerShell@2
+ displayName: 'Cleanup publish artifacts'
+ inputs:
+ targetType: inline
+ script: |
+ Get-ChildItem "$(nsRoot)/artifacts/publish" -Recurse -Include "*.json", "*.xml" |
+ Remove-Item -Force
+
+# Stage the native binary for the artifact
+- task: PowerShell@2
+ displayName: 'Stage ${{ parameters.flavor }} binary'
+ inputs:
+ targetType: inline
+ script: |
+ $destDir = "$(Build.ArtifactStagingDirectory)/native"
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ # WinML publishes additional files (e.g. WindowsAppRuntime Bootstrapper DLLs)
+ # beyond Microsoft.AI.Foundry.Local.Core.*.
+ $isWinML = "${{ parameters.isWinML }}" -eq "True"
+ if ($isWinML) {
+ Get-ChildItem "$(nsRoot)/artifacts/publish" -Recurse -File |
+ Where-Object { $_.Name -like "Microsoft.AI.Foundry.Local.Core.*" -or $_.Name -eq "Microsoft.WindowsAppRuntime.Bootstrap.dll" } |
+ Copy-Item -Destination $destDir -Force
+ } else {
+ Get-ChildItem "$(nsRoot)/artifacts/publish" -Recurse -File |
+ Where-Object { $_.Name -like "Microsoft.AI.Foundry.Local.Core.*" } |
+ Copy-Item -Destination $destDir -Force
+ }
+ Write-Host "Staged binaries:"
+ Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" }
+
diff --git a/.pipelines/templates/build-cs-steps.yml b/.pipelines/templates/build-cs-steps.yml
new file mode 100644
index 00000000..978c2fff
--- /dev/null
+++ b/.pipelines/templates/build-cs-steps.yml
@@ -0,0 +1,191 @@
+# Steps to build, sign, and pack the C# SDK NuGet package.
+# When test-data-shared is checked out alongside self, ADO places repos under
+# $(Build.SourcesDirectory)/. The self repo is 'Foundry-Local'.
+parameters:
+- name: version
+ type: string
+- name: isRelease
+ type: boolean
+ default: false
+- name: isWinML
+ type: boolean
+ default: false
+- name: flcNugetDir
+ type: string
+ displayName: 'Path to directory containing the FLC .nupkg'
+- name: outputDir
+ type: string
+ default: '$(Build.ArtifactStagingDirectory)/cs-sdk'
+ displayName: 'Path to directory for the packed SDK'
+- name: prereleaseId
+ type: string
+ default: ''
+steps:
+# Set paths for multi-repo checkout
+- 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'
+
+# Compute package version
+- 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"
+ Write-Host "Package version: $v"
+
+# List downloaded artifact for debugging
+- 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 }
+
+# Create a temporary NuGet.config that includes the local FLC feed
+- task: PowerShell@2
+ displayName: 'Create NuGet.config with local FLC feed'
+ inputs:
+ targetType: inline
+ script: |
+ $nugetConfig = @"
+
+
+
+
+
+
+
+
+
+ "@
+ # Determine the FLC version from the .nupkg filename
+ $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"
+ Set-Content -Path $configPath -Value $nugetConfig
+ Write-Host "##vso[task.setvariable variable=customNugetConfig]$configPath"
+ Write-Host "Local FLC feed directory: $flcFeedDir"
+
+- task: NuGetAuthenticate@1
+ displayName: 'Authenticate NuGet feeds'
+
+- task: PowerShell@2
+ displayName: 'Restore SDK'
+ inputs:
+ targetType: inline
+ script: |
+ $proj = "$(repoRoot)/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj"
+ if (-not (Test-Path $proj)) { throw "Project not found: $proj" }
+ dotnet restore $proj `
+ --configfile "$(customNugetConfig)" `
+ /p:UseWinML=${{ parameters.isWinML }}
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+- task: PowerShell@2
+ displayName: 'Build SDK'
+ inputs:
+ targetType: inline
+ script: |
+ dotnet build "$(repoRoot)/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj" `
+ --no-restore --configuration Release `
+ /p:UseWinML=${{ parameters.isWinML }}
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+# Discover target framework directory
+- task: PowerShell@2
+ displayName: 'Find target framework'
+ inputs:
+ targetType: inline
+ script: |
+ $base = "$(repoRoot)/sdk/cs/src/bin/Release"
+ # The SDK targets net9.0 (standard) or net9.0-windows10.0.26100.0 (WinML).
+ # Find whichever TFM directory was produced by the build.
+ $tfmDir = Get-ChildItem $base -Directory | Select-Object -First 1
+ if (-not $tfmDir) { throw "No target framework directory found under $base" }
+ Write-Host "##vso[task.setvariable variable=TargetFramework]$($tfmDir.Name)"
+ Write-Host "Target framework: $($tfmDir.Name)"
+
+# Sign DLLs
+- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5
+ displayName: 'Sign SDK DLLs'
+ inputs:
+ ConnectedServiceName: 'OnnxrunTimeCodeSign_20240611'
+ UseMSIAuthentication: true
+ AppRegistrationClientId: '$(esrpClientId)'
+ AppRegistrationTenantId: '$(esrpTenantId)'
+ EsrpClientId: '$(esrpClientId)'
+ AuthAKVName: '$(esrpAkvName)'
+ AuthSignCertName: '$(esrpSignCertName)'
+ FolderPath: '$(repoRoot)/sdk/cs/src/bin/Release/$(TargetFramework)'
+ Pattern: '*.dll'
+ SessionTimeout: 90
+ ServiceEndpointUrl: 'https://api.esrp.microsoft.com/api/v2'
+ MaxConcurrency: 25
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [{"keyCode":"CP-230012","operationSetCode":"SigntoolSign","parameters":[{"parameterName":"OpusName","parameterValue":"Microsoft"},{"parameterName":"OpusInfo","parameterValue":"http://www.microsoft.com"},{"parameterName":"PageHash","parameterValue":"/NPH"},{"parameterName":"FileDigest","parameterValue":"/fd sha256"},{"parameterName":"TimeStamp","parameterValue":"/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"}],"toolName":"signtool.exe","toolVersion":"6.2.9304.0"}]
+
+# Pack NuGet
+- task: PowerShell@2
+ displayName: 'Pack NuGet'
+ inputs:
+ targetType: inline
+ script: |
+ dotnet pack "$(repoRoot)/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj" `
+ --no-build --no-restore --configuration Release `
+ --output "${{ parameters.outputDir }}" `
+ /p:PackageVersion=$(packageVersion) `
+ /p:UseWinML=${{ parameters.isWinML }} `
+ /p:IncludeSymbols=true `
+ /p:SymbolPackageFormat=snupkg
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+# Sign NuGet package
+- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5
+ displayName: 'Sign SDK NuGet package'
+ inputs:
+ ConnectedServiceName: 'OnnxrunTimeCodeSign_20240611'
+ UseMSIAuthentication: true
+ AppRegistrationClientId: '$(esrpClientId)'
+ AppRegistrationTenantId: '$(esrpTenantId)'
+ EsrpClientId: '$(esrpClientId)'
+ AuthAKVName: '$(esrpAkvName)'
+ AuthSignCertName: '$(esrpSignCertName)'
+ FolderPath: '${{ parameters.outputDir }}'
+ Pattern: '*.nupkg'
+ SessionTimeout: 90
+ ServiceEndpointUrl: 'https://api.esrp.microsoft.com/api/v2'
+ MaxConcurrency: 25
+ 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"}]
diff --git a/.pipelines/templates/build-js-steps.yml b/.pipelines/templates/build-js-steps.yml
new file mode 100644
index 00000000..e288bbce
--- /dev/null
+++ b/.pipelines/templates/build-js-steps.yml
@@ -0,0 +1,156 @@
+# Steps to build and pack the JS SDK.
+# When test-data-shared is checked out alongside self, ADO places repos under
+# $(Build.SourcesDirectory)/. The self repo is 'Foundry-Local'.
+parameters:
+- name: version
+ type: string
+- name: isRelease
+ type: boolean
+ default: false
+- name: isWinML
+ type: boolean
+ default: false
+- name: flcNugetDir
+ type: string
+ default: ''
+ displayName: 'Path to directory containing the FLC .nupkg (for tests)'
+- name: prereleaseId
+ type: string
+ default: ''
+steps:
+# Set paths for multi-repo checkout
+- 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"
+ Write-Host "Repo root: $repoRoot"
+ Write-Host "Test data: $testDataDir"
+
+- task: PowerShell@2
+ displayName: 'List downloaded FLC artifact'
+ condition: and(succeeded(), ne('${{ parameters.flcNugetDir }}', ''))
+ 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'
+
+# Compute version
+- 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"
+
+# Install dependencies including native binaries (FLC, ORT, GenAI) from NuGet feeds
+- task: Npm@1
+ displayName: 'npm install'
+ inputs:
+ command: custom
+ workingDir: $(repoRoot)/sdk/js
+ customCommand: 'install'
+
+# Overwrite the FLC native binary with the one we just built
+- task: PowerShell@2
+ displayName: 'Overwrite FLC with pipeline-built binary'
+ condition: and(succeeded(), ne('${{ parameters.flcNugetDir }}', ''))
+ 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' }
+
+ # Detect macOS/Linux
+ 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 }}" }
+
+ # Extract the NuGet package (it's a zip)
+ $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
+
+ # Overwrite FLC binary in the npm-installed location
+ $destDir = "$(repoRoot)/sdk/js/packages/@foundry-local-core/$platformKey"
+ $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"
+ }
+
+ Write-Host "Final binaries in $destDir`:"
+ Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" }
+
+- task: Npm@1
+ displayName: 'npm version'
+ inputs:
+ command: custom
+ workingDir: $(repoRoot)/sdk/js
+ customCommand: 'version $(packageVersion) --no-git-tag-version --allow-same-version'
+
+- task: Npm@1
+ displayName: 'npm build'
+ inputs:
+ command: custom
+ workingDir: $(repoRoot)/sdk/js
+ customCommand: 'run build'
+
+- ${{ if eq(parameters.isWinML, true) }}:
+ - task: Npm@1
+ displayName: 'npm run pack:winml'
+ inputs:
+ command: custom
+ workingDir: $(repoRoot)/sdk/js
+ customCommand: 'run pack:winml'
+
+- ${{ else }}:
+ - task: Npm@1
+ displayName: 'npm run pack'
+ inputs:
+ command: custom
+ workingDir: $(repoRoot)/sdk/js
+ customCommand: 'run pack'
+
+- task: PowerShell@2
+ displayName: 'Stage artifact'
+ inputs:
+ targetType: inline
+ script: |
+ $destDir = "$(Build.ArtifactStagingDirectory)/js-sdk"
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ Copy-Item "$(repoRoot)/sdk/js/*.tgz" "$destDir/"
diff --git a/.pipelines/templates/build-python-steps.yml b/.pipelines/templates/build-python-steps.yml
new file mode 100644
index 00000000..6fd0cd34
--- /dev/null
+++ b/.pipelines/templates/build-python-steps.yml
@@ -0,0 +1,146 @@
+# Steps to build and pack the Python SDK wheel.
+# When test-data-shared is checked out alongside self, ADO places repos under
+# $(Build.SourcesDirectory)/. The self repo is 'Foundry-Local'.
+parameters:
+- name: version
+ type: string
+- name: isRelease
+ type: boolean
+ default: false
+- name: isWinML
+ type: boolean
+ default: false
+- name: flcWheelsDir
+ type: string
+ default: ''
+ displayName: 'Path to directory containing the FLC wheels (for overriding foundry-local-core)'
+- name: outputDir
+ type: string
+ default: '$(Build.ArtifactStagingDirectory)/python-sdk'
+ displayName: 'Path to directory for the built wheel'
+- name: prereleaseId
+ type: string
+ default: ''
+steps:
+# Set paths for multi-repo checkout
+- 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: UsePythonVersion@0
+ displayName: 'Use Python 3.12'
+ inputs:
+ versionSpec: '3.12'
+
+# List downloaded FLC wheels for debugging
+- task: PowerShell@2
+ displayName: 'List downloaded FLC wheels'
+ condition: and(succeeded(), ne('${{ parameters.flcWheelsDir }}', ''))
+ inputs:
+ targetType: inline
+ script: |
+ Write-Host "Contents of ${{ parameters.flcWheelsDir }}:"
+ Get-ChildItem "${{ parameters.flcWheelsDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName }
+
+# Compute package version
+- 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"
+ Write-Host "Package version: $v"
+
+# Configure pip to use ORT-Nightly feed (plus PyPI as fallback)
+- task: PowerShell@2
+ displayName: 'Configure pip for Azure Artifacts'
+ inputs:
+ targetType: inline
+ script: |
+ pip config set global.index-url https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/pypi/simple/
+ pip config set global.extra-index-url https://pypi.org/simple/
+ pip config set global.pre true
+
+# Install the build tool
+- script: python -m pip install build
+ displayName: 'Install build tool'
+
+# Write version file
+- task: PowerShell@2
+ displayName: 'Set SDK version'
+ inputs:
+ targetType: inline
+ script: |
+ Set-Content -Path "$(repoRoot)/sdk/python/src/version.py" -Value '__version__ = "$(packageVersion)"'
+
+# Install the FLC wheels from the pipeline if provided, so the build
+# backend picks up the freshly-built foundry-local-core instead of
+# pulling a stale one from the feed.
+- task: PowerShell@2
+ displayName: 'Pre-install pipeline-built FLC wheel'
+ condition: and(succeeded(), ne('${{ parameters.flcWheelsDir }}', ''))
+ inputs:
+ targetType: inline
+ script: |
+ # Determine platform wheel tag for the current machine
+ $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'amd64' }
+ if ($IsLinux) { $platTag = "manylinux*x86_64" }
+ elseif ($IsMacOS) { $platTag = "macosx*$arch" }
+ else { $platTag = "win_$arch" }
+
+ $filter = if ("${{ parameters.isWinML }}" -eq "True") { "foundry_local_core_winml*$platTag.whl" } else { "foundry_local_core-*$platTag.whl" }
+ $wheel = Get-ChildItem "${{ parameters.flcWheelsDir }}" -Recurse -Filter $filter | Select-Object -First 1
+ if ($wheel) {
+ Write-Host "Installing pipeline-built FLC wheel: $($wheel.FullName)"
+ pip install $($wheel.FullName)
+ } else {
+ Write-Warning "No FLC wheel found matching $filter in ${{ parameters.flcWheelsDir }}"
+ }
+
+# Build wheel — standard or WinML variant
+# skip-native-deps=true omits foundry-local-core/onnxruntime pinned versions
+# from the wheel metadata, since the pipeline pre-installs its own builds.
+- ${{ if eq(parameters.isWinML, true) }}:
+ - script: python -m build --wheel -C winml=true -C skip-native-deps=true --outdir dist/
+ displayName: 'Build wheel (WinML)'
+ workingDirectory: $(repoRoot)/sdk/python
+
+- ${{ else }}:
+ - script: python -m build --wheel -C skip-native-deps=true --outdir dist/
+ displayName: 'Build wheel'
+ workingDirectory: $(repoRoot)/sdk/python
+
+# Install the built wheel
+- task: PowerShell@2
+ displayName: 'Install built wheel'
+ inputs:
+ targetType: inline
+ script: |
+ $wheel = (Get-ChildItem "$(repoRoot)/sdk/python/dist/*.whl" | Select-Object -First 1).FullName
+ pip install $wheel
+
+# Stage output
+- task: PowerShell@2
+ displayName: 'Stage wheel artifact'
+ inputs:
+ targetType: inline
+ script: |
+ $destDir = "${{ parameters.outputDir }}"
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ Copy-Item "$(repoRoot)/sdk/python/dist/*" "$destDir/"
+ Write-Host "Staged wheels:"
+ Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" }
diff --git a/.pipelines/templates/build-rust-steps.yml b/.pipelines/templates/build-rust-steps.yml
new file mode 100644
index 00000000..efccfaa4
--- /dev/null
+++ b/.pipelines/templates/build-rust-steps.yml
@@ -0,0 +1,207 @@
+# Steps to build and package the Rust SDK crate.
+# When test-data-shared is checked out alongside self, ADO places repos under
+# $(Build.SourcesDirectory)/. The self repo is 'Foundry-Local'.
+parameters:
+- name: version
+ type: string
+- name: isRelease
+ type: boolean
+ default: false
+- name: prereleaseId
+ type: string
+ default: ''
+- name: isWinML
+ type: boolean
+ default: false
+- name: flcNugetDir
+ type: string
+ displayName: 'Path to directory containing the FLC .nupkg'
+- name: outputDir
+ type: string
+ default: '$(Build.ArtifactStagingDirectory)/rust-sdk'
+ displayName: 'Path to directory for the packaged crate'
+steps:
+# Set paths for multi-repo checkout
+- 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"
+
+# Compute package version and patch Cargo.toml
+- 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"
+ }
+ Write-Host "Crate version: $v"
+
+ # Patch Cargo.toml version field
+ $cargoPath = "$(repoRoot)/sdk/rust/Cargo.toml"
+ $content = Get-Content $cargoPath -Raw
+ $content = $content -replace '(?m)^version\s*=\s*"[^"]+"', "version = `"$v`""
+ Set-Content -Path $cargoPath -Value $content
+ Write-Host "Patched Cargo.toml with version $v"
+
+# List downloaded FLC artifact for debugging
+- 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 so that
+# build.rs finds them already present and skips downloading from the feed.
+- task: PowerShell@2
+ displayName: 'Extract FLC native binaries for Rust build'
+ 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 }}" }
+ Write-Host "Found NuGet package: $($nupkg.FullName)"
+
+ $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
+
+ # Determine RID for this agent
+ $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" }
+
+ # Stage them where build.rs can discover them
+ $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 to $flcNativeDir`:"
+ Get-ChildItem $flcNativeDir | ForEach-Object { Write-Host " $($_.Name)" }
+
+# Install Rust toolchain
+- 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"
+
+# The .cargo/config.toml redirects crates-io to an Azure Artifacts feed
+# for CFS compliance. Remove the redirect in CI so cargo can fetch from
+# crates.io directly without Azure DevOps auth.
+- 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: 'Check formatting'
+ inputs:
+ targetType: inline
+ script: |
+ Set-Location "$(repoRoot)/sdk/rust"
+ cargo fmt --all -- --check
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+- task: PowerShell@2
+ displayName: 'Run clippy'
+ inputs:
+ targetType: inline
+ script: |
+ Set-Location "$(repoRoot)/sdk/rust"
+ $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" }
+ Invoke-Expression "cargo clippy --all-targets $features -- -D warnings"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+- 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 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'
+ inputs:
+ targetType: inline
+ script: |
+ Set-Location "$(repoRoot)/sdk/rust"
+ $features = if ("${{ parameters.isWinML }}" -eq "True") { "--features winml" } else { "" }
+ Invoke-Expression "cargo package $features --allow-dirty"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+# Stage output
+- task: PowerShell@2
+ displayName: 'Stage crate artifact'
+ inputs:
+ targetType: inline
+ script: |
+ $destDir = "${{ parameters.outputDir }}"
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ Copy-Item "$(repoRoot)/sdk/rust/target/package/*.crate" "$destDir/"
+ Write-Host "Staged crates:"
+ Get-ChildItem $destDir | ForEach-Object { Write-Host " $($_.Name)" }
diff --git a/.pipelines/templates/package-core-steps.yml b/.pipelines/templates/package-core-steps.yml
new file mode 100644
index 00000000..e5755a21
--- /dev/null
+++ b/.pipelines/templates/package-core-steps.yml
@@ -0,0 +1,256 @@
+# Steps to collect per-platform FLC native binaries, organize into NuGet layout,
+# pack + sign the NuGet package, and build Python wheels (wheel package name and
+# platforms depend on the isWinML parameter). The parent job must download all
+# platform artifacts and checkout neutron-server.
+parameters:
+- name: version
+ type: string
+- name: isRelease
+ type: boolean
+ default: false
+- name: isWinML
+ type: boolean
+ default: false
+- name: prereleaseId
+ type: string
+ default: ''
+- name: platforms
+ type: object # list of { name, artifactName }
+
+steps:
+- task: PowerShell@2
+ displayName: 'Set source paths'
+ inputs:
+ targetType: inline
+ script: |
+ $nsRoot = "$(Build.SourcesDirectory)"
+ Write-Host "##vso[task.setvariable variable=nsRoot]$nsRoot"
+
+- task: PowerShell@2
+ displayName: 'Organize native binaries'
+ inputs:
+ targetType: inline
+ script: |
+ $unifiedPath = "$(Build.ArtifactStagingDirectory)/unified"
+ New-Item -ItemType Directory -Path $unifiedPath -Force | Out-Null
+
+ $platformsJson = @'
+ ${{ convertToJson(parameters.platforms) }}
+ '@
+ $platforms = $platformsJson | ConvertFrom-Json
+
+ foreach ($p in $platforms) {
+ $srcDir = "$(Pipeline.Workspace)/$($p.artifactName)"
+ Write-Host "Looking for artifacts at: $srcDir"
+ if (-not (Test-Path $srcDir)) {
+ throw "Artifact directory $srcDir does not exist. All platform artifacts must be present to produce a complete NuGet package."
+ }
+ $destDir = "$unifiedPath/runtimes/$($p.name)/native"
+ New-Item -ItemType Directory -Path $destDir -Force | Out-Null
+ # WinML artifacts include WindowsAppRuntime Bootstrapper DLLs in addition
+ # to Microsoft.AI.Foundry.Local.Core.*.
+ $isWinML = "${{ parameters.isWinML }}" -eq "True"
+ if ($isWinML) {
+ Get-ChildItem $srcDir -File |
+ Where-Object { $_.Name -like "Microsoft.AI.Foundry.Local.Core.*" -or $_.Name -eq "Microsoft.WindowsAppRuntime.Bootstrap.dll" } |
+ Copy-Item -Destination $destDir -Force
+ } else {
+ Get-ChildItem $srcDir -File | Where-Object { $_.Name -like "Microsoft.AI.Foundry.Local.Core.*" } |
+ Copy-Item -Destination $destDir -Force
+ }
+ Write-Host "Copied $($p.name) binaries to $destDir"
+ }
+
+ # Copy build integration files from neutron-server
+ $nsRoot = "$(nsRoot)"
+ foreach ($dir in @("build", "buildTransitive")) {
+ $src = "$nsRoot/src/FoundryLocalCore/Core/$dir"
+ if (Test-Path $src) {
+ Copy-Item -Path $src -Destination "$unifiedPath/$dir" -Recurse -Force
+ }
+ }
+ $license = "$nsRoot/src/FoundryLocalCore/Core/LICENSE.txt"
+ if (Test-Path $license) {
+ Copy-Item $license "$unifiedPath/LICENSE.txt" -Force
+ }
+
+# Compute version
+- 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"
+ Write-Host "FLC version: $v"
+
+# Pack NuGet
+- task: PowerShell@2
+ displayName: 'Pack FLC NuGet'
+ inputs:
+ targetType: inline
+ script: |
+ $nsRoot = "$(nsRoot)"
+ [xml]$propsXml = Get-Content "$nsRoot/Directory.Packages.props"
+ $pg = $propsXml.Project.PropertyGroup
+
+ $outDir = "$(Build.ArtifactStagingDirectory)/flc-nuget"
+ New-Item -ItemType Directory -Path $outDir -Force | Out-Null
+
+ if ("${{ parameters.isWinML }}" -eq "True") {
+ $nuspec = "$nsRoot/src/FoundryLocalCore/Core/WinMLNuget.nuspec"
+ $id = "Microsoft.AI.Foundry.Local.Core.WinML"
+ $ortVer = $pg.OnnxRuntimeFoundryVersionForWinML
+ $genaiVer = $pg.OnnxRuntimeGenAIWinML
+ $winAppSdkVer = $pg.WinAppSdkVersion
+ $props = "id=$id;version=$(flcVersion);commitId=$(Build.SourceVersion);OnnxRuntimeFoundryVersion=$ortVer;OnnxRuntimeGenAIWinML=$genaiVer;WinAppSdkVersion=$winAppSdkVer"
+ } else {
+ $nuspec = "$nsRoot/src/FoundryLocalCore/Core/NativeNuget.nuspec"
+ $id = "Microsoft.AI.Foundry.Local.Core"
+ $ortVer = $pg.OnnxRuntimeFoundryVersion
+ $genaiVer = $pg.OnnxRuntimeGenAIFoundryVersion
+ $props = "id=$id;version=$(flcVersion);commitId=$(Build.SourceVersion);OnnxRuntimeFoundryVersion=$ortVer;OnnxRuntimeGenAIFoundryVersion=$genaiVer"
+ }
+
+ $nugetArgs = @(
+ 'pack', $nuspec,
+ '-OutputDirectory', $outDir,
+ '-BasePath', "$(Build.ArtifactStagingDirectory)/unified",
+ '-Properties', $props,
+ '-Symbols', '-SymbolPackageFormat', 'snupkg'
+ )
+ Write-Host "Running: nuget $($nugetArgs -join ' ')"
+ & nuget $nugetArgs
+ if ($LASTEXITCODE -ne 0) { throw "NuGet pack failed" }
+
+# Sign NuGet package
+- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5
+ displayName: 'Sign FLC NuGet package'
+ inputs:
+ ConnectedServiceName: 'OnnxrunTimeCodeSign_20240611'
+ UseMSIAuthentication: true
+ AppRegistrationClientId: '$(esrpClientId)'
+ AppRegistrationTenantId: '$(esrpTenantId)'
+ EsrpClientId: '$(esrpClientId)'
+ AuthAKVName: '$(esrpAkvName)'
+ AuthSignCertName: '$(esrpSignCertName)'
+ FolderPath: '$(Build.ArtifactStagingDirectory)/flc-nuget'
+ Pattern: '*.nupkg'
+ SessionTimeout: 90
+ ServiceEndpointUrl: 'https://api.esrp.microsoft.com/api/v2'
+ MaxConcurrency: 25
+ 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"}]
+
+# Build Python wheels from the NuGet package
+- task: PowerShell@2
+ displayName: 'Build foundry_local_core Python Wheels'
+ inputs:
+ targetType: inline
+ script: |
+ $stagingDir = "$(Build.ArtifactStagingDirectory)/flc-wheels"
+ New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
+
+ $isWinML = "${{ parameters.isWinML }}" -eq "True"
+
+ # Find and extract the NuGet package (.nupkg is a zip archive)
+ $nupkgFilter = if ($isWinML) { "Microsoft.AI.Foundry.Local.Core.WinML*.nupkg" } else { "Microsoft.AI.Foundry.Local.Core*.nupkg" }
+ $nupkg = Get-ChildItem "$(Build.ArtifactStagingDirectory)/flc-nuget" -Filter $nupkgFilter | Where-Object { $_.Name -notlike "*.snupkg" } | Select-Object -First 1
+ if (-not $nupkg) { throw "No FLC .nupkg found matching $nupkgFilter" }
+ Write-Host "Found NuGet package: $($nupkg.Name)"
+
+ $extractDir = "$(Build.ArtifactStagingDirectory)/flc-extracted"
+ $nupkgZip = [System.IO.Path]::ChangeExtension($nupkg.FullName, ".zip")
+ Copy-Item -Path $nupkg.FullName -Destination $nupkgZip -Force
+ Expand-Archive -Path $nupkgZip -DestinationPath $extractDir -Force
+
+ # Convert NuGet version to PEP 440
+ # NuGet: 0.9.0-dev-202603271723-bb400310 → PEP 440: 0.9.0.dev202603271723
+ # The commit hash is dropped because .devN requires N to be a pure integer.
+ $nupkgVersion = $nupkg.BaseName -replace '^Microsoft\.AI\.Foundry\.Local\.Core(\.WinML)?\.', ''
+ $parts = $nupkgVersion -split '-'
+ $pyVersion = 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] }
+ Write-Host "Python package version: $pyVersion"
+
+ $packageName = if ($isWinML) { "foundry_local_core_winml" } else { "foundry_local_core" }
+
+ if ($isWinML) {
+ $platforms = @(
+ @{rid="win-x64"; pyKey="bin"; tag="win_amd64"},
+ @{rid="win-arm64"; pyKey="bin"; tag="win_arm64"}
+ )
+ } else {
+ $platforms = @(
+ @{rid="win-x64"; pyKey="bin"; tag="win_amd64"},
+ @{rid="win-arm64"; pyKey="bin"; tag="win_arm64"},
+ @{rid="linux-x64"; pyKey="bin"; tag="manylinux_2_28_x86_64"},
+ @{rid="osx-arm64"; pyKey="bin"; tag="macosx_11_0_arm64"}
+ )
+ }
+
+ foreach ($p in $platforms) {
+ $nativeSrc = "$extractDir/runtimes/$($p.rid)/native"
+ if (-not (Test-Path $nativeSrc)) {
+ Write-Warning "No native binaries found for $($p.rid) — skipping."
+ continue
+ }
+
+ $wheelRoot = "$(Build.ArtifactStagingDirectory)/wheels-build/flc_wheel_$($p.tag)"
+ $pkgDir = "$wheelRoot/$packageName"
+ New-Item -ItemType Directory -Path "$pkgDir/$($p.pyKey)" -Force | Out-Null
+ "" | Set-Content -Encoding ascii "$pkgDir/__init__.py"
+ Get-ChildItem $nativeSrc -File | Copy-Item -Destination "$pkgDir/$($p.pyKey)"
+
+ $normalizedName = $packageName.Replace('_', '-')
+ $wheelTag = "py3-none-$($p.tag)"
+ $distInfoName = "$packageName-$pyVersion"
+ $wheelName = "$distInfoName-$wheelTag.whl"
+ $distInfoDir = "$wheelRoot/$distInfoName.dist-info"
+ New-Item -ItemType Directory -Path $distInfoDir -Force | Out-Null
+
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+
+ [System.IO.File]::WriteAllText("$distInfoDir/WHEEL",
+ "Wheel-Version: 1.0`nGenerator: custom`nRoot-Is-Purelib: false`nTag: $wheelTag`n", $utf8NoBom)
+
+ [System.IO.File]::WriteAllText("$distInfoDir/METADATA",
+ "Metadata-Version: 2.1`nName: $normalizedName`nVersion: $pyVersion`n", $utf8NoBom)
+
+ $recordLines = Get-ChildItem $wheelRoot -Recurse -File | ForEach-Object {
+ $rel = $_.FullName.Substring($wheelRoot.Length + 1).Replace('\', '/')
+ $raw = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
+ $bytes = [byte[]]::new($raw.Length / 2)
+ for ($i = 0; $i -lt $raw.Length; $i += 2) { $bytes[$i/2] = [Convert]::ToByte($raw.Substring($i, 2), 16) }
+ $b64 = [Convert]::ToBase64String($bytes) -replace '\+','-' -replace '/','_' -replace '=',''
+ "$rel,sha256=$b64,$($_.Length)"
+ }
+ $recordContent = ($recordLines + "$distInfoName.dist-info/RECORD,,") -join "`n"
+ [System.IO.File]::WriteAllText("$distInfoDir/RECORD", $recordContent, $utf8NoBom)
+
+ $wheelPath = "$stagingDir/$wheelName"
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::Open($wheelPath, 'Create')
+ try {
+ Get-ChildItem $wheelRoot -Recurse -File | ForEach-Object {
+ $rel = $_.FullName.Substring($wheelRoot.Length + 1).Replace('\', '/')
+ [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, $rel) | Out-Null
+ }
+ } finally {
+ $zip.Dispose()
+ }
+ Write-Host "Created wheel: $wheelName"
+ }
+
+ Write-Host "`nAll wheels:"
+ Get-ChildItem $stagingDir -Filter "*.whl" | ForEach-Object { Write-Host " $($_.Name)" }
diff --git a/.pipelines/templates/test-cs-steps.yml b/.pipelines/templates/test-cs-steps.yml
new file mode 100644
index 00000000..f7dc1aff
--- /dev/null
+++ b/.pipelines/templates/test-cs-steps.yml
@@ -0,0 +1,116 @@
+# 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
new file mode 100644
index 00000000..41ef7f62
--- /dev/null
+++ b/.pipelines/templates/test-js-steps.yml
@@ -0,0 +1,121 @@
+# 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/packages/@foundry-local-core/$platformKey"
+ $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-python-steps.yml b/.pipelines/templates/test-python-steps.yml
new file mode 100644
index 00000000..f54a9464
--- /dev/null
+++ b/.pipelines/templates/test-python-steps.yml
@@ -0,0 +1,133 @@
+# Lightweight test-only steps for the Python SDK.
+# Builds from source and runs tests — no artifact staging.
+parameters:
+- name: version
+ type: string
+- name: isWinML
+ type: boolean
+ default: false
+- name: flcWheelsDir
+ type: string
+ default: ''
+ displayName: 'Path to directory containing the FLC wheels'
+
+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: UsePythonVersion@0
+ displayName: 'Use Python 3.12'
+ inputs:
+ versionSpec: '3.12'
+
+- task: PowerShell@2
+ displayName: 'List downloaded FLC wheels'
+ condition: and(succeeded(), ne('${{ parameters.flcWheelsDir }}', ''))
+ inputs:
+ targetType: inline
+ script: |
+ Write-Host "Contents of ${{ parameters.flcWheelsDir }}:"
+ Get-ChildItem "${{ parameters.flcWheelsDir }}" -Recurse | ForEach-Object { Write-Host $_.FullName }
+
+- task: PowerShell@2
+ displayName: 'Configure pip for Azure Artifacts'
+ inputs:
+ targetType: inline
+ script: |
+ pip config set global.index-url https://pkgs.dev.azure.com/aiinfra/PublicPackages/_packaging/ORT-Nightly/pypi/simple/
+ pip config set global.extra-index-url https://pypi.org/simple/
+ pip config set global.pre true
+
+- script: python -m pip install build
+ displayName: 'Install build tool'
+
+- task: PowerShell@2
+ displayName: 'Set SDK version'
+ inputs:
+ targetType: inline
+ script: |
+ Set-Content -Path "$(repoRoot)/sdk/python/src/version.py" -Value '__version__ = "${{ parameters.version }}"'
+
+- task: PowerShell@2
+ displayName: 'Pre-install pipeline-built FLC wheel'
+ condition: and(succeeded(), ne('${{ parameters.flcWheelsDir }}', ''))
+ inputs:
+ targetType: inline
+ script: |
+ # Determine platform wheel tag for the current machine
+ $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'amd64' }
+ if ($IsLinux) { $platTag = "manylinux*x86_64" }
+ elseif ($IsMacOS) { $platTag = "macosx*$arch" }
+ else { $platTag = "win_$arch" }
+
+ $filter = if ("${{ parameters.isWinML }}" -eq "True") { "foundry_local_core_winml*$platTag.whl" } else { "foundry_local_core-*$platTag.whl" }
+ $wheel = Get-ChildItem "${{ parameters.flcWheelsDir }}" -Recurse -Filter $filter | Select-Object -First 1
+ if ($wheel) {
+ Write-Host "Installing pipeline-built FLC wheel: $($wheel.FullName)"
+ pip install $($wheel.FullName)
+ } else {
+ Write-Warning "No FLC wheel found matching $filter"
+ }
+
+# Install ORT native packages from the ORT-Nightly feed.
+# skip-native-deps strips these from the SDK wheel metadata, so they
+# must be installed explicitly for tests to locate the native binaries.
+- script: pip install onnxruntime-core onnxruntime-genai-core
+ displayName: 'Install ORT native packages'
+
+- ${{ if not(parameters.isWinML) }}:
+ - script: python -m build --wheel -C skip-native-deps=true --outdir dist/
+ displayName: 'Build wheel'
+ workingDirectory: $(repoRoot)/sdk/python
+
+- ${{ if parameters.isWinML }}:
+ - script: python -m build --wheel -C winml=true -C skip-native-deps=true --outdir dist/
+ displayName: 'Build wheel (WinML)'
+ workingDirectory: $(repoRoot)/sdk/python
+
+- task: PowerShell@2
+ displayName: 'Install built wheel'
+ inputs:
+ targetType: inline
+ script: |
+ $wheel = (Get-ChildItem "$(repoRoot)/sdk/python/dist/*.whl" | Select-Object -First 1).FullName
+ pip install $wheel
+
+- 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/test-rust-steps.yml b/.pipelines/templates/test-rust-steps.yml
new file mode 100644
index 00000000..31bfd75e
--- /dev/null
+++ b/.pipelines/templates/test-rust-steps.yml
@@ -0,0 +1,159 @@
+# 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/sdk/cs/README.md b/sdk/cs/README.md
index f58e41e0..2b574325 100644
--- a/sdk/cs/README.md
+++ b/sdk/cs/README.md
@@ -48,7 +48,7 @@ dotnet build src/Microsoft.AI.Foundry.Local.csproj /p:UseWinML=true
### Triggering EP download
-EP download can be time-consuming. Call `EnsureEpsDownloadedAsync` early (after initialization) to separate the download step from catalog access:
+EP download can be time-consuming. Call `DownloadAndRegisterEpsAsync` early (after initialization) to separate the download step from catalog access:
```csharp
// Initialize the manager first (see Quick Start)
@@ -56,7 +56,7 @@ await FoundryLocalManager.CreateAsync(
new Configuration { AppName = "my-app" },
NullLogger.Instance);
-await FoundryLocalManager.Instance.EnsureEpsDownloadedAsync();
+await FoundryLocalManager.Instance.DownloadAndRegisterEpsAsync();
// Now catalog access won't trigger an EP download
var catalog = await FoundryLocalManager.Instance.GetCatalogAsync();
diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md
index 93f162b7..9e5be8aa 100644
--- a/sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md
+++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md
@@ -98,7 +98,7 @@ The model catalog.
The catalog is populated on first use.
If you are using a WinML build this will trigger a one-off execution provider download if not already done.
- It is recommended to call [FoundryLocalManager.EnsureEpsDownloadedAsync(Nullable<CancellationToken>)](./microsoft.ai.foundry.local.foundrylocalmanager.md#ensureepsdownloadedasyncnullablecancellationtoken) first to separate out the two steps.
+ It is recommended to call [FoundryLocalManager.DownloadAndRegisterEpsAsync(Nullable<CancellationToken>)](./microsoft.ai.foundry.local.foundrylocalmanager.md#downloadandregisterepsasyncnullablecancellationtoken) first to separate out the two steps.
### **StartWebServiceAsync(Nullable<CancellationToken>)**
@@ -141,9 +141,9 @@ Optional cancellation token.
[Task](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task)
Task stopping the web service.
-### **EnsureEpsDownloadedAsync(Nullable<CancellationToken>)**
+### **DownloadAndRegisterEpsAsync(Nullable<CancellationToken>)**
-Ensure execution providers are downloaded and registered.
+Download and register execution providers.
Only relevant when using WinML.
Execution provider download can be time consuming due to the size of the packages.
@@ -151,7 +151,7 @@ Ensure execution providers are downloaded and registered.
on subsequent calls.
```csharp
-public Task EnsureEpsDownloadedAsync(Nullable ct)
+public Task DownloadAndRegisterEpsAsync(Nullable ct)
```
#### Parameters
diff --git a/sdk/cs/src/Detail/CoreInterop.cs b/sdk/cs/src/Detail/CoreInterop.cs
index 8411473b..d7867cad 100644
--- a/sdk/cs/src/Detail/CoreInterop.cs
+++ b/sdk/cs/src/Detail/CoreInterop.cs
@@ -124,6 +124,15 @@ internal CoreInterop(Configuration config, ILogger logger)
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var request = new CoreInteropRequest { Params = config.AsDictionary() };
+
+#if IS_WINML
+ // WinML builds require bootstrapping the Windows App Runtime
+ if (!request.Params.ContainsKey("Bootstrap"))
+ {
+ request.Params["Bootstrap"] = "true";
+ }
+#endif
+
var response = ExecuteCommand("initialize", request);
if (response.Error != null)
diff --git a/sdk/cs/src/FoundryLocalManager.cs b/sdk/cs/src/FoundryLocalManager.cs
index 639be3a2..d3e4fb79 100644
--- a/sdk/cs/src/FoundryLocalManager.cs
+++ b/sdk/cs/src/FoundryLocalManager.cs
@@ -99,7 +99,7 @@ public static async Task CreateAsync(Configuration configuration, ILogger logger
///
/// The catalog is populated on first use.
/// If you are using a WinML build this will trigger a one-off execution provider download if not already done.
- /// It is recommended to call first to separate out the two steps.
+ /// It is recommended to call first to separate out the two steps.
///
public async Task GetCatalogAsync(CancellationToken? ct = null)
{
@@ -135,7 +135,7 @@ await Utils.CallWithExceptionHandling(() => StopWebServiceImplAsync(ct),
}
///
- /// Ensure execution providers are downloaded and registered.
+ /// Download and register execution providers.
/// Only relevant when using WinML.
///
/// Execution provider download can be time consuming due to the size of the packages.
@@ -143,10 +143,10 @@ await Utils.CallWithExceptionHandling(() => StopWebServiceImplAsync(ct),
/// on subsequent calls.
///
/// Optional cancellation token.
- public async Task EnsureEpsDownloadedAsync(CancellationToken? ct = null)
+ public async Task DownloadAndRegisterEpsAsync(CancellationToken? ct = null)
{
- await Utils.CallWithExceptionHandling(() => EnsureEpsDownloadedImplAsync(ct),
- "Error ensuring execution providers downloaded.", _logger)
+ await Utils.CallWithExceptionHandling(() => DownloadAndRegisterEpsImplAsync(ct),
+ "Error downloading and registering execution providers.", _logger)
.ConfigureAwait(false);
}
@@ -259,16 +259,16 @@ private async Task StopWebServiceImplAsync(CancellationToken? ct = null)
Urls = null;
}
- private async Task EnsureEpsDownloadedImplAsync(CancellationToken? ct = null)
+ private async Task DownloadAndRegisterEpsImplAsync(CancellationToken? ct = null)
{
using var disposable = await asyncLock.LockAsync().ConfigureAwait(false);
CoreInteropRequest? input = null;
- var result = await _coreInterop!.ExecuteCommandAsync("ensure_eps_downloaded", input, ct);
+ var result = await _coreInterop!.ExecuteCommandAsync("download_and_register_eps", input, ct).ConfigureAwait(false);
if (result.Error != null)
{
- throw new FoundryLocalException($"Error ensuring execution providers downloaded: {result.Error}", _logger);
+ throw new FoundryLocalException($"Error downloading and registering execution providers: {result.Error}", _logger);
}
}
diff --git a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj
index 905f9652..936f3a93 100644
--- a/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj
+++ b/sdk/cs/src/Microsoft.AI.Foundry.Local.csproj
@@ -13,7 +13,7 @@
https://github.com/microsoft/Foundry-Local
git
- net8.0
+ net9.0
win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64
true
@@ -87,7 +87,8 @@
Microsoft Foundry Local SDK for WinML
Microsoft.AI.Foundry.Local.WinML
Microsoft.AI.Foundry.Local.WinML
- net8.0-windows10.0.26100.0
+ $(DefineConstants);IS_WINML
+ net9.0-windows10.0.26100.0
win-x64;win-arm64
10.0.17763.0
diff --git a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
index 5f0c7cf2..fe0dfcd2 100644
--- a/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
+++ b/sdk/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj
@@ -1,7 +1,7 @@
- net10.0
+ net9.0
enable
enable
false
@@ -19,10 +19,9 @@
- net10.0-windows10.0.26100.0
+ net9.0-windows10.0.26100.0
10.0.17763.0
None
- true
diff --git a/sdk/js/docs/classes/FoundryLocalManager.md b/sdk/js/docs/classes/FoundryLocalManager.md
index 63bb2dd1..dc4908a6 100644
--- a/sdk/js/docs/classes/FoundryLocalManager.md
+++ b/sdk/js/docs/classes/FoundryLocalManager.md
@@ -87,6 +87,29 @@ Error - If the web service is not running.
***
+### downloadAndRegisterEps()
+
+```ts
+downloadAndRegisterEps(): void;
+```
+
+Download and register execution providers.
+Only relevant when using the WinML variant. On non-WinML builds this is a no-op.
+
+Call this after initialization to trigger EP download before accessing the catalog,
+so that hardware-accelerated execution providers (e.g. QNN for NPU) are available
+when listing and loading models.
+
+#### Returns
+
+`void`
+
+#### Throws
+
+Error - If execution provider download or registration fails.
+
+***
+
### startWebService()
```ts
diff --git a/sdk/js/src/foundryLocalManager.ts b/sdk/js/src/foundryLocalManager.ts
index bc408f78..6da0bcc7 100644
--- a/sdk/js/src/foundryLocalManager.ts
+++ b/sdk/js/src/foundryLocalManager.ts
@@ -61,6 +61,24 @@ export class FoundryLocalManager {
return this._urls;
}
+ /**
+ * Download and register execution providers.
+ * Only relevant when using the WinML variant. On non-WinML builds this is a no-op.
+ *
+ * Call this after initialization to trigger EP download before accessing the catalog,
+ * so that hardware-accelerated execution providers (e.g. QNN for NPU) are available
+ * when listing and loading models.
+ *
+ * @throws Error - If execution provider download or registration fails.
+ */
+ public downloadAndRegisterEps(): void {
+ try {
+ this.coreInterop.executeCommand("download_and_register_eps");
+ } catch (error) {
+ throw new Error(`Error downloading and registering execution providers: ${error}`);
+ }
+ }
+
/**
* Starts the local web service.
* Use the `urls` property to retrieve the bound addresses after the service has started.
diff --git a/sdk/python/build_backend.py b/sdk/python/build_backend.py
index b4b91a1b..3789501b 100644
--- a/sdk/python/build_backend.py
+++ b/sdk/python/build_backend.py
@@ -18,9 +18,14 @@
python -m build --wheel -C winml=true
+Skip native deps (use pre-installed foundry-local-core / ORT / GenAI)::
+
+ python -m build --wheel -C skip-native-deps=true
+
Environment variable fallback (useful in CI pipelines)::
FOUNDRY_VARIANT=winml python -m build --wheel
+ FOUNDRY_SKIP_NATIVE_DEPS=1 python -m build --wheel
"""
from __future__ import annotations
@@ -46,6 +51,13 @@
_STANDARD_NAME = 'name = "foundry-local-sdk"'
_WINML_NAME = 'name = "foundry-local-sdk-winml"'
+# Native binary package prefixes to strip when skip-native-deps is active.
+_NATIVE_DEP_PREFIXES = (
+ "foundry-local-core",
+ "onnxruntime-core",
+ "onnxruntime-genai-core",
+)
+
# ---------------------------------------------------------------------------
# Variant detection
@@ -63,6 +75,23 @@ def _is_winml(config_settings: dict | None) -> bool:
return os.environ.get("FOUNDRY_VARIANT", "").lower() == "winml"
+def _is_skip_native_deps(config_settings: dict | None) -> bool:
+ """Return True when native binary dependencies should be omitted.
+
+ When set, ``foundry-local-core``, ``onnxruntime-core``, and
+ ``onnxruntime-genai-core`` are stripped from requirements.txt so the
+ wheel is built against whatever versions are already installed.
+ Useful in CI pipelines that pre-install pipeline-built native wheels.
+
+ Checks ``config_settings["skip-native-deps"]`` first
+ (set via ``-C skip-native-deps=true``), then falls back to the
+ ``FOUNDRY_SKIP_NATIVE_DEPS`` environment variable.
+ """
+ if config_settings and str(config_settings.get("skip-native-deps", "")).lower() == "true":
+ return True
+ return os.environ.get("FOUNDRY_SKIP_NATIVE_DEPS", "").lower() in ("1", "true")
+
+
# ---------------------------------------------------------------------------
# In-place patching context manager
# ---------------------------------------------------------------------------
@@ -96,58 +125,88 @@ def _patch_for_winml() -> Generator[None, None, None]:
_REQUIREMENTS.write_text(requirements_original, encoding="utf-8")
+@contextlib.contextmanager
+def _strip_native_deps() -> Generator[None, None, None]:
+ """Temporarily remove native binary deps from requirements.txt.
+
+ Lines starting with any prefix in ``_NATIVE_DEP_PREFIXES`` (case-
+ insensitive) are removed. The file is restored in the ``finally``
+ block.
+ """
+ requirements_original = _REQUIREMENTS.read_text(encoding="utf-8")
+ try:
+ filtered = [
+ line for line in requirements_original.splitlines(keepends=True)
+ if not any(line.lstrip().lower().startswith(p) for p in _NATIVE_DEP_PREFIXES)
+ ]
+ _REQUIREMENTS.write_text("".join(filtered), encoding="utf-8")
+ yield
+ finally:
+ _REQUIREMENTS.write_text(requirements_original, encoding="utf-8")
+
+
+def _apply_patches(config_settings: dict | None):
+ """Return a context manager that applies the appropriate patches."""
+ winml = _is_winml(config_settings)
+ skip_native = _is_skip_native_deps(config_settings)
+
+ @contextlib.contextmanager
+ def _combined():
+ # Stack contexts: WinML swaps requirements first, then strip_native
+ # removes native deps from whatever requirements are active.
+ if winml and skip_native:
+ with _patch_for_winml(), _strip_native_deps():
+ yield
+ elif winml:
+ with _patch_for_winml():
+ yield
+ elif skip_native:
+ with _strip_native_deps():
+ yield
+ else:
+ yield
+
+ return _combined()
+
+
# ---------------------------------------------------------------------------
# PEP 517 hook delegation
# ---------------------------------------------------------------------------
def get_requires_for_build_wheel(config_settings=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.get_requires_for_build_wheel(config_settings)
- return _sb.get_requires_for_build_wheel(config_settings)
+ with _apply_patches(config_settings):
+ return _sb.get_requires_for_build_wheel(config_settings)
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
- return _sb.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
+ with _apply_patches(config_settings):
+ return _sb.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.build_wheel(wheel_directory, config_settings, metadata_directory)
- return _sb.build_wheel(wheel_directory, config_settings, metadata_directory)
+ with _apply_patches(config_settings):
+ return _sb.build_wheel(wheel_directory, config_settings, metadata_directory)
def get_requires_for_build_editable(config_settings=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.get_requires_for_build_editable(config_settings)
- return _sb.get_requires_for_build_editable(config_settings)
+ with _apply_patches(config_settings):
+ return _sb.get_requires_for_build_editable(config_settings)
def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.prepare_metadata_for_build_editable(metadata_directory, config_settings)
- return _sb.prepare_metadata_for_build_editable(metadata_directory, config_settings)
+ with _apply_patches(config_settings):
+ return _sb.prepare_metadata_for_build_editable(metadata_directory, config_settings)
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.build_editable(wheel_directory, config_settings, metadata_directory)
- return _sb.build_editable(wheel_directory, config_settings, metadata_directory)
+ with _apply_patches(config_settings):
+ return _sb.build_editable(wheel_directory, config_settings, metadata_directory)
def get_requires_for_build_sdist(config_settings=None):
- if _is_winml(config_settings):
- with _patch_for_winml():
- return _sb.get_requires_for_build_sdist(config_settings)
- return _sb.get_requires_for_build_sdist(config_settings)
+ with _apply_patches(config_settings):
+ return _sb.get_requires_for_build_sdist(config_settings)
def build_sdist(sdist_directory, config_settings=None):
diff --git a/sdk/python/src/detail/core_interop.py b/sdk/python/src/detail/core_interop.py
index 7a6bb08c..4f4ddb67 100644
--- a/sdk/python/src/detail/core_interop.py
+++ b/sdk/python/src/detail/core_interop.py
@@ -205,6 +205,9 @@ def __init__(self, config: Configuration):
if sys.platform.startswith("win"):
bootstrap_dll = paths.core_dir / "Microsoft.WindowsAppRuntime.Bootstrap.dll"
if bootstrap_dll.exists():
+ # Pre-load so the DLL is already in the process when
+ # C# P/Invoke resolves it during Bootstrap.Initialize().
+ ctypes.CDLL(str(bootstrap_dll))
if config.additional_settings is None:
config.additional_settings = {}
if "Bootstrap" not in config.additional_settings:
diff --git a/sdk/python/src/foundry_local_manager.py b/sdk/python/src/foundry_local_manager.py
index 4486eaf1..4c02a127 100644
--- a/sdk/python/src/foundry_local_manager.py
+++ b/sdk/python/src/foundry_local_manager.py
@@ -71,17 +71,17 @@ def _initialize(self):
self._model_load_manager = ModelLoadManager(self._core_interop, external_service_url)
self.catalog = Catalog(self._model_load_manager, self._core_interop)
- def ensure_eps_downloaded(self) -> None:
- """Ensure execution providers are downloaded and registered (synchronous).
+ def download_and_register_eps(self) -> None:
+ """Download and register execution providers.
Only relevant when using WinML.
Raises:
- FoundryLocalException: If execution provider download fails.
+ FoundryLocalException: If execution provider download or registration fails.
"""
- result = self._core_interop.execute_command("ensure_eps_downloaded")
+ result = self._core_interop.execute_command("download_and_register_eps")
if result.error is not None:
- raise FoundryLocalException(f"Error ensuring execution providers downloaded: {result.error}")
+ raise FoundryLocalException(f"Error downloading and registering execution providers: {result.error}")
def start_web_service(self):
"""Start the optional web service.
diff --git a/sdk/rust/src/foundry_local_manager.rs b/sdk/rust/src/foundry_local_manager.rs
index f80a7176..9cf2477f 100644
--- a/sdk/rust/src/foundry_local_manager.rs
+++ b/sdk/rust/src/foundry_local_manager.rs
@@ -133,4 +133,18 @@ impl FoundryLocalManager {
.clear();
Ok(())
}
+
+ /// Download and register execution providers.
+ ///
+ /// Only relevant when using the WinML variant. On non-WinML builds this
+ /// is a no-op. Call this after initialisation to trigger EP download
+ /// before accessing the catalog, so that hardware-accelerated execution
+ /// providers (e.g. QNN for NPU) are available when listing and loading
+ /// models.
+ pub async fn download_and_register_eps(&self) -> Result<()> {
+ self.core
+ .execute_command_async("download_and_register_eps".into(), None)
+ .await?;
+ Ok(())
+ }
}