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(()) + } }