diff --git a/.pipelines/templates/fetch-test-data-from-blob.yml b/.pipelines/templates/fetch-test-data-from-blob.yml index 2ca3682b0..2e45017b9 100644 --- a/.pipelines/templates/fetch-test-data-from-blob.yml +++ b/.pipelines/templates/fetch-test-data-from-blob.yml @@ -5,13 +5,63 @@ parameters: - name: destinationPath type: string default: '$(Build.SourcesDirectory)/test-data-shared' +- name: scriptType + type: string + default: pscore + values: ['pscore', 'ps'] +- name: installAzCli + type: boolean + default: false steps: +- ${{ if eq(parameters.installAzCli, true) }}: + - task: PowerShell@2 + displayName: 'Ensure Azure CLI is installed (Windows)' + condition: eq(variables['Agent.OS'], 'Windows_NT') + inputs: + targetType: inline + pwsh: false + script: | + $ErrorActionPreference = 'Stop' + + if (Get-Command az -ErrorAction SilentlyContinue) { + $azDir = Split-Path -Path (Get-Command az).Source -Parent + Write-Host "##vso[task.prependpath]$azDir" + az --version | Select-Object -First 1 | Write-Host + Write-Host 'Azure CLI already installed.' + exit 0 + } + + try { + $installerPath = Join-Path $env:TEMP 'azure-cli-installer.msi' + $installerUrl = 'https://aka.ms/installazurecliwindows' + + Write-Host "Downloading Azure CLI installer from $installerUrl" + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing + + Write-Host 'Installing Azure CLI via MSI...' + $process = Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i `"$installerPath`" /qn /norestart" -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "Azure CLI MSI install failed with exit code $($process.ExitCode)." + } + } + catch { + throw "Azure CLI install failed: $($_.Exception.Message)" + } + + $env:PATH = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User') + $azCommand = Get-Command az -ErrorAction SilentlyContinue + if (-not $azCommand) { + throw 'Azure CLI install was attempted but az is still not on PATH.' + } + Write-Host "##vso[task.prependpath]$(Split-Path -Path $azCommand.Source -Parent)" + az version | Write-Host + - task: AzureCLI@2 displayName: 'Fetch test data from blob' inputs: azureSubscription: 'ortcibuild_readonly_mi2-ONNX Runtime-AIFoundryLocal' - scriptType: pscore + scriptType: ${{ parameters.scriptType }} scriptLocation: inlineScript inlineScript: | $ErrorActionPreference = 'Stop' @@ -21,7 +71,17 @@ steps: $container = 'models' $prefix = 'foundrylocal/models' - if (-not (Get-Command azcopy -ErrorAction SilentlyContinue)) { + $azcopyAvailable = $false + try { + azcopy --version | Write-Host + if ($LASTEXITCODE -eq 0) { + $azcopyAvailable = $true + } + } + catch { + } + + if (-not $azcopyAvailable) { if ($IsMacOS) { Write-Host 'azcopy not found on macOS agent. Installing via Homebrew...' if (-not (Get-Command brew -ErrorAction SilentlyContinue)) { @@ -30,6 +90,20 @@ steps: brew update --quiet brew install --quiet azcopy + } elseif ($env:AGENT_OS -eq 'Windows_NT' -and $env:AGENT_OSARCHITECTURE -eq 'ARM64') { + Write-Host 'azcopy not found on Windows agent. Installing from official zip...' + $zipPath = Join-Path $env:TEMP 'azcopy_windows.zip' + $extractRoot = Join-Path $env:TEMP 'azcopy' + Invoke-WebRequest -Uri 'https://aka.ms/downloadazcopy-v10-windows' -OutFile $zipPath -UseBasicParsing + if (Test-Path $extractRoot) { + Remove-Item -Path $extractRoot -Recurse -Force + } + Expand-Archive -Path $zipPath -DestinationPath $extractRoot -Force + $azcopyExe = Get-ChildItem -Path $extractRoot -Filter 'azcopy.exe' -Recurse | Select-Object -First 1 + if (-not $azcopyExe) { + throw 'azcopy install completed but azcopy.exe was not found.' + } + $env:PATH = "$($azcopyExe.Directory.FullName);$env:PATH" } else { throw 'azcopy is not installed on this agent.' } diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index f46777676..c9f313997 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -56,7 +56,7 @@ stages: stageHeaders: true # ==================================================================== - # Windows ARM64 — cross-compile only (no tests on x64 host) + # Windows ARM64 — build + test # ==================================================================== - stage: cpp_build_win_arm64 displayName: 'C++ Native: Windows ARM64' @@ -65,8 +65,9 @@ stages: jobs: - job: build pool: - name: onnxruntime-Win-CPU-2022 + name: onnxruntime-native-arm64-16core os: windows + hostArchitecture: arm64 templateContext: inputs: - input: pipelineArtifact @@ -84,8 +85,10 @@ stages: ortVersion: ${{ parameters.ortVersion }} genaiVersion: ${{ parameters.genaiVersion }} winmlVersion: ${{ parameters.winmlVersion }} - runTests: false + runTests: true stageHeaders: false + pythonArchitecture: arm64 + usePwsh: false # ==================================================================== # Linux x64 — build + test diff --git a/.pipelines/v2/templates/stages-cs.yml b/.pipelines/v2/templates/stages-cs.yml index b83f48b35..654f39994 100644 --- a/.pipelines/v2/templates/stages-cs.yml +++ b/.pipelines/v2/templates/stages-cs.yml @@ -68,10 +68,45 @@ stages: - checkout: self clean: true - template: ../../templates/fetch-test-data-from-blob.yml@self + parameters: + scriptType: ps + installAzCli: true + - template: steps-test-cs.yml + parameters: + flNugetDir: '$(Pipeline.Workspace)/cpp-nuget' + testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' + usePwsh: false + +- stage: cs_test_win_arm64 + displayName: 'C# SDK: Test Windows ARM64' + dependsOn: + - cs_build + jobs: + - job: test + pool: + name: onnxruntime-native-arm64-16core + os: windows + hostArchitecture: arm64 + templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'version-info' + targetPath: '$(Pipeline.Workspace)/version-info' + - input: pipelineArtifact + artifactName: 'cpp-nuget' + targetPath: '$(Pipeline.Workspace)/cpp-nuget' + steps: + - checkout: self + clean: true + - template: ../../templates/fetch-test-data-from-blob.yml@self + parameters: + scriptType: ps + installAzCli: true - template: steps-test-cs.yml parameters: flNugetDir: '$(Pipeline.Workspace)/cpp-nuget' testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' + usePwsh: false # ==================================================================== # Test — Linux x64 / macOS ARM64 diff --git a/.pipelines/v2/templates/stages-js.yml b/.pipelines/v2/templates/stages-js.yml index f11ed23bb..5103ea37f 100644 --- a/.pipelines/v2/templates/stages-js.yml +++ b/.pipelines/v2/templates/stages-js.yml @@ -13,8 +13,7 @@ # * cpp_build_win_x64 (the cpp-native-include artifact, which is # stage-headers-only on the win-x64 build) # -# Tests run on the same platform as the build, except win-arm64 (no -# Windows-ARM64 pool; build-only, matching the C# + Python matrices). +# Tests run on the same platform as the build. stages: @@ -69,8 +68,9 @@ stages: jobs: - job: build pool: - name: onnxruntime-Win-CPU-2022 + name: onnxruntime-native-arm64-16core os: windows + hostArchitecture: arm64 variables: - group: FoundryLocal-ESRP-Signing templateContext: @@ -97,7 +97,6 @@ stages: nativeArtifactDir: '$(Pipeline.Workspace)/cpp-native-win-arm64' includeArtifactDir: '$(Pipeline.Workspace)/cpp-native-include' outputDir: '$(Build.ArtifactStagingDirectory)/js-prebuild' - targetArch: 'arm64' signWindows: true - stage: js_build_linux_x64 @@ -177,9 +176,9 @@ stages: outputDir: '$(Build.ArtifactStagingDirectory)/js-prebuild' signMac: true -# ===================================================================== -# Test stages — same-platform only. win-arm64 is build-only. -# ===================================================================== +# ================================================================ +# Test stages — same-platform only (including native win-arm64). +# ================================================================ - stage: js_test_win_x64 displayName: 'JS SDK: Test Windows x64' @@ -205,6 +204,35 @@ stages: prebuildArtifactDir: '$(Pipeline.Workspace)/js-prebuild-win-x64' testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' +- stage: js_test_win_arm64 + displayName: 'JS SDK: Test Windows ARM64' + dependsOn: + - js_build_win_arm64 + jobs: + - job: test + pool: + name: onnxruntime-native-arm64-16core + os: windows + hostArchitecture: arm64 + templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'js-prebuild-win-arm64' + targetPath: '$(Pipeline.Workspace)/js-prebuild-win-arm64' + steps: + - checkout: self + clean: true + - template: ../../templates/fetch-test-data-from-blob.yml@self + parameters: + scriptType: ps + installAzCli: true + - template: steps-test-js.yml + parameters: + rid: 'win-arm64' + prebuildArtifactDir: '$(Pipeline.Workspace)/js-prebuild-win-arm64' + testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' + usePwsh: false + - stage: js_test_linux_x64 displayName: 'JS SDK: Test Linux x64' dependsOn: diff --git a/.pipelines/v2/templates/stages-python.yml b/.pipelines/v2/templates/stages-python.yml index c67002f9b..40a553b05 100644 --- a/.pipelines/v2/templates/stages-python.yml +++ b/.pipelines/v2/templates/stages-python.yml @@ -1,7 +1,7 @@ # Build + test stages for the sdk_v2 Python SDK (foundry-local-sdk). # -# Builds win-x64 + win-arm64 + linux-x64 + osx-arm64; tests win-x64 + linux-x64 -# + osx-arm64 (win-arm64 is build-only, matching the C# matrix). +# Builds win-x64 + win-arm64 + linux-x64 + osx-arm64; tests win-x64 + +# win-arm64 + linux-x64 + osx-arm64. # # Each build stage depends on the matching native build stage from # stages-build-native.yml and consumes the `cpp-native-` pipeline artifact @@ -55,8 +55,9 @@ stages: jobs: - job: build pool: - name: onnxruntime-Win-CPU-2022 + name: onnxruntime-native-arm64-16core os: windows + hostArchitecture: arm64 templateContext: inputs: - input: pipelineArtifact @@ -76,7 +77,7 @@ stages: parameters: nativeArtifactDir: '$(Pipeline.Workspace)/cpp-native-win-arm64' rid: 'win-arm64' - targetArch: 'arm64' + pythonArchitecture: 'arm64' outputDir: '$(Build.ArtifactStagingDirectory)/python-sdk' - stage: python_build_linux_x64 @@ -144,9 +145,9 @@ stages: pythonArchitecture: 'arm64' outputDir: '$(Build.ArtifactStagingDirectory)/python-sdk' -# ================================================================= -# Tests — win-x64, linux-x64, osx-arm64. win-arm64 is build-only. -# ================================================================= +# ====================================================== +# Tests — win-x64, win-arm64, linux-x64, osx-arm64. +# ====================================================== - stage: python_test_win_x64 displayName: 'Python SDK: Test Windows x64' dependsOn: @@ -172,6 +173,35 @@ stages: wheelDir: '$(Pipeline.Workspace)/python-sdk-win-x64' testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' +- stage: python_test_win_arm64 + displayName: 'Python SDK: Test Windows ARM64' + dependsOn: + - python_build_win_arm64 + jobs: + - job: test + pool: + name: onnxruntime-native-arm64-16core + os: windows + hostArchitecture: arm64 + templateContext: + inputs: + - input: pipelineArtifact + artifactName: 'python-sdk-win-arm64' + targetPath: '$(Pipeline.Workspace)/python-sdk-win-arm64' + steps: + - checkout: self + clean: true + - template: ../../templates/fetch-test-data-from-blob.yml@self + parameters: + scriptType: ps + installAzCli: true + - template: steps-test-python.yml + parameters: + wheelDir: '$(Pipeline.Workspace)/python-sdk-win-arm64' + testDataSharedDir: '$(Build.SourcesDirectory)/test-data-shared' + pythonArchitecture: 'arm64' + usePwsh: false + - stage: python_test_linux_x64 displayName: 'Python SDK: Test Linux x64' dependsOn: diff --git a/.pipelines/v2/templates/steps-build-js.yml b/.pipelines/v2/templates/steps-build-js.yml index 657704bd9..88a71bc20 100644 --- a/.pipelines/v2/templates/steps-build-js.yml +++ b/.pipelines/v2/templates/steps-build-js.yml @@ -34,7 +34,7 @@ parameters: type: string default: 'native' values: ['native', 'arm64'] - displayName: 'Cross-compile target arch for node-gyp on Windows x64 host. "native" = no cross.' + displayName: 'Optional node-gyp target arch override on Windows. "native" = host architecture.' - name: signWindows type: boolean default: false @@ -51,17 +51,72 @@ steps: inputs: versionSpec: '20.x' +- ${{ if eq(parameters.rid, 'win-x64') }}: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12 (x64)' + inputs: + versionSpec: '3.12' + architecture: 'x64' + +- ${{ if eq(parameters.rid, 'win-arm64') }}: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12 (arm64)' + inputs: + versionSpec: '3.12' + architecture: 'arm64' + +- ${{ if eq(parameters.rid, 'win-arm64') }}: + - task: PowerShell@2 + displayName: 'Ensure Visual Studio Build Tools for node-gyp (win-arm64)' + inputs: + targetType: inline + pwsh: false + script: | + $installRoot = 'C:\BuildTools' + $vcvars = Join-Path $installRoot 'VC\Auxiliary\Build\vcvarsall.bat' + + if (-not (Test-Path $vcvars)) { + Write-Host 'Visual Studio Build Tools not fully present. Installing...' + $bootstrapper = Join-Path $env:TEMP 'vs_BuildTools.exe' + Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $bootstrapper -UseBasicParsing + + $arguments = @( + '--quiet', + '--wait', + '--norestart', + '--nocache', + '--installPath', $installRoot, + '--add', 'Microsoft.VisualStudio.Workload.VCTools', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22621', + '--includeRecommended' + ) + + $process = Start-Process -FilePath $bootstrapper -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "Visual Studio Build Tools install failed with exit code $($process.ExitCode)." + } + } + + if (-not (Test-Path $vcvars)) { + throw "vcvarsall.bat was not found at $vcvars after Build Tools install." + } + + Write-Host "vcvarsall.bat: $vcvars" + Write-Host "##vso[task.setvariable variable=vcvarsAllPath]$vcvars" + # Map ADO RID -> Node-style platform-arch key used inside the npm tarball # (prebuilds/-/) so downstream consumers find the addon under # the directory matching `${process.platform}-${process.arch}` at runtime. - ${{ if eq(parameters.rid, 'win-x64') }}: - - pwsh: | + - powershell: | Write-Host "##vso[task.setvariable variable=prebuildDir]win32-x64" Write-Host "##vso[task.setvariable variable=nativeLib]foundry_local.dll" displayName: 'Set prebuild directory (win32-x64)' - ${{ if eq(parameters.rid, 'win-arm64') }}: - - pwsh: | + - powershell: | Write-Host "##vso[task.setvariable variable=prebuildDir]win32-arm64" Write-Host "##vso[task.setvariable variable=nativeLib]foundry_local.dll" displayName: 'Set prebuild directory (win32-arm64)' @@ -94,15 +149,26 @@ steps: # we point INCLUDE_DIR at that artifact root, and the public include path # is added separately in binding.gyp via `../cpp/include`. - ${{ if or(eq(parameters.rid, 'win-x64'), eq(parameters.rid, 'win-arm64')) }}: - - pwsh: | + - powershell: | + python --version $arch = '${{ parameters.targetArch }}' $args = if ($arch -eq 'native') { 'rebuild' } else { "rebuild --arch=$arch" } Write-Host "node-gyp $args" Set-Location "$(Build.SourcesDirectory)/sdk_v2/js" - npx --no-install node-gyp $args.Split(' ') + if ('${{ parameters.rid }}' -eq 'win-arm64') { + $vcvars = "$(vcvarsAllPath)" + if (-not $vcvars) { throw "vcvarsAllPath pipeline variable is empty" } + $cmdLine = ('"{0}" arm64 && npx --no-install node-gyp {1}' -f $vcvars, $args) + Write-Host "cmd /c $cmdLine" + cmd /c $cmdLine + } else { + npx --no-install node-gyp $args.Split(' ') + } if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } displayName: 'Build Node-API addon' env: + PYTHON: '$(pythonLocation)\python.exe' + npm_config_python: '$(pythonLocation)\python.exe' FOUNDRY_LOCAL_LIB_DIR: '${{ parameters.nativeArtifactDir }}' FOUNDRY_LOCAL_INCLUDE_DIR: '${{ parameters.includeArtifactDir }}' # Force the copy_addon_to_prebuilds destination to the target-arch dir. @@ -128,7 +194,7 @@ steps: # into sdk_v2/js/prebuilds/-/; we copy from there into the artifact # dir alongside the shared library. - ${{ if or(eq(parameters.rid, 'win-x64'), eq(parameters.rid, 'win-arm64')) }}: - - pwsh: | + - powershell: | $dst = "${{ parameters.outputDir }}/prebuilds/$(prebuildDir)" if (Test-Path $dst) { Remove-Item -Recurse -Force $dst } New-Item -ItemType Directory -Force -Path $dst | Out-Null diff --git a/.pipelines/v2/templates/steps-build-python.yml b/.pipelines/v2/templates/steps-build-python.yml index 50f86c9da..37370e211 100644 --- a/.pipelines/v2/templates/steps-build-python.yml +++ b/.pipelines/v2/templates/steps-build-python.yml @@ -25,7 +25,7 @@ parameters: type: string default: 'native' values: ['native', 'arm64'] - displayName: 'Cross-compile target arch on a Windows x64 host. Use "native" for no cross-compile.' + displayName: 'Optional Windows wheel target arch override. Use "native" for host-native builds.' - name: pythonArchitecture type: string default: 'x64' @@ -45,7 +45,7 @@ steps: displayName: 'Set package version from pyVersion.txt' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | $versionFile = "$(Pipeline.Workspace)/version-info/pyVersion.txt" if (-not (Test-Path $versionFile)) { throw "Missing $versionFile" } @@ -57,7 +57,7 @@ steps: displayName: 'Stamp version into pyproject.toml' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | $pyproject = "$(Build.SourcesDirectory)/sdk_v2/python/pyproject.toml" $content = Get-Content $pyproject -Raw @@ -78,7 +78,7 @@ steps: displayName: 'List downloaded native artifact' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | Write-Host "Contents of ${{ parameters.nativeArtifactDir }}:" Get-ChildItem "${{ parameters.nativeArtifactDir }}" -Recurse -File | @@ -88,7 +88,7 @@ steps: displayName: 'Stage native lib into _native/${{ parameters.rid }}/' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | $dest = "$(Build.SourcesDirectory)/sdk_v2/python/src/foundry_local_sdk/_native/${{ parameters.rid }}" if (Test-Path $dest) { Remove-Item -Recurse -Force $dest } @@ -127,7 +127,7 @@ steps: displayName: 'Clean stale build artifacts' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | $py = "$(Build.SourcesDirectory)/sdk_v2/python" # Remove stale, untagged cffi extension from prior dev compiles. @@ -153,18 +153,59 @@ steps: displayName: 'Install build tooling' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | python -m pip install --upgrade pip python -m pip install --upgrade build setuptools wheel "cffi>=1.16" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +- ${{ if eq(parameters.rid, 'win-arm64') }}: + - task: PowerShell@2 + displayName: 'Ensure Visual Studio Build Tools for Windows ARM64 Python build' + inputs: + targetType: inline + pwsh: false + script: | + $installRoot = 'C:\BuildTools' + $vcvars = Join-Path $installRoot 'VC\Auxiliary\Build\vcvarsall.bat' + + if (-not (Test-Path $vcvars)) { + Write-Host 'Visual Studio Build Tools not fully present. Installing...' + $bootstrapper = Join-Path $env:TEMP 'vs_BuildTools.exe' + Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $bootstrapper -UseBasicParsing + + $arguments = @( + '--quiet', + '--wait', + '--norestart', + '--nocache', + '--installPath', $installRoot, + '--add', 'Microsoft.VisualStudio.Workload.VCTools', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22621', + '--includeRecommended' + ) + + $process = Start-Process -FilePath $bootstrapper -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "Visual Studio Build Tools install failed with exit code $($process.ExitCode)." + } + } + + if (-not (Test-Path $vcvars)) { + throw "vcvarsall.bat was not found at $vcvars after Build Tools install." + } + + Write-Host "vcvarsall.bat: $vcvars" + Write-Host "##vso[task.setvariable variable=vcvarsAllPath]$vcvars" + - ${{ if eq(parameters.targetArch, 'arm64') }}: - task: PowerShell@2 displayName: 'Locate MSVC arm64 cross-tools (vswhere)' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | # Resolve vcvarsall.bat once and stash the path in a pipeline variable. The actual # invocation happens inside the build step's child cmd.exe so its env is inherited @@ -196,7 +237,7 @@ steps: condition: and(succeeded(), ne(variables.PYTHON_ARM64_CACHE_HIT, 'true')) inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | # We need arm64 Python's headers + python3.lib to link the cffi extension # for win-arm64 from an x64 host. We do NOT need to *run* the arm64 @@ -240,7 +281,7 @@ steps: displayName: 'Capture arm64 Python paths for cross-link' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | # Path is the same whether we just installed via NuGet above or restored # the directory from cache — both produce $stageRoot/pythonarm64/tools/. @@ -263,7 +304,7 @@ steps: displayName: 'Build wheel' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | $outDir = "${{ parameters.outputDir }}" New-Item -ItemType Directory -Force -Path $outDir | Out-Null @@ -314,6 +355,15 @@ steps: Write-Host "cmd /v:on /c $cmdLine" cmd /v:on /c $cmdLine if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } elseif ('${{ parameters.rid }}' -eq 'win-arm64') { + $vcvars = "$(vcvarsAllPath)" + if (-not $vcvars) { throw "vcvarsAllPath pipeline variable is empty" } + $buildArgs = @('-m', 'build', '--wheel', '--outdir', $outDir) + $extraArgs + $quoted = ($buildArgs | ForEach-Object { '"' + $_ + '"' }) -join ' ' + $cmdLine = ('"{0}" arm64 && set DISTUTILS_USE_SDK=1 && set MSSdk=1 && python {1}' -f $vcvars, $quoted) + Write-Host "cmd /c $cmdLine" + cmd /c $cmdLine + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } else { python -m build --wheel --outdir $outDir @extraArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -326,7 +376,7 @@ steps: displayName: 'List produced wheel' inputs: targetType: inline - pwsh: true + pwsh: ${{ ne(parameters.rid, 'win-arm64') }} script: | Get-ChildItem "${{ parameters.outputDir }}" -Filter '*.whl' | ForEach-Object { Write-Host ("{0,12} {1}" -f $_.Length, $_.Name) } diff --git a/.pipelines/v2/templates/steps-build-windows.yml b/.pipelines/v2/templates/steps-build-windows.yml index 5f1d0f761..4735a1af0 100644 --- a/.pipelines/v2/templates/steps-build-windows.yml +++ b/.pipelines/v2/templates/steps-build-windows.yml @@ -5,13 +5,15 @@ # templateContext.outputs. # # Parameters: -# arch – 'x64' or 'arm64' (arm64 is cross-compiled, no tests) +# arch – 'x64' or 'arm64' # buildConfig – CMake config (Debug, Release, RelWithDebInfo, MinSizeRel) # ortVersion – Microsoft.ML.OnnxRuntime.Foundry version # genaiVersion – Microsoft.ML.OnnxRuntimeGenAI.Foundry version # winmlVersion – Microsoft.Windows.AI.MachineLearning version # runTests – Whether to run tests # stageHeaders – Whether to stage public headers as a separate artifact +# pythonArchitecture – Python architecture installed by UsePythonVersion +# usePwsh – Whether PowerShell tasks should use PowerShell Core parameters: - name: arch @@ -31,18 +33,24 @@ parameters: - name: stageHeaders type: boolean default: false +- name: pythonArchitecture + type: string + default: 'x64' + values: ['x64', 'arm64'] +- name: usePwsh + type: boolean + default: true steps: # Windows hosted agents don't have python on PATH by default — the launcher # stub redirects to the Microsoft Store. UsePythonVersion installs and adds it -# to PATH for the rest of the job. arm64 builds cross-compile on the x64 -# Windows host, so x64 Python is correct for both archs. +# to PATH for the rest of the job. - task: UsePythonVersion@0 displayName: 'Use Python 3.12' inputs: versionSpec: '3.12' - architecture: 'x64' + architecture: '${{ parameters.pythonArchitecture }}' - script: | git clone https://github.com/microsoft/vcpkg.git $(Build.BinariesDirectory)\vcpkg @@ -58,6 +66,64 @@ steps: winmlVersion: ${{ parameters.winmlVersion }} includeWinml: true shell: pwsh + usePwsh: ${{ parameters.usePwsh }} + +- ${{ if eq(parameters.arch, 'arm64') }}: + - task: PowerShell@2 + displayName: 'Ensure Visual Studio Build Tools and CMake for ARM64' + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $installRoot = 'C:\BuildTools' + $vcvars = Join-Path $installRoot 'VC\Auxiliary\Build\vcvarsall.bat' + $cmakeDir = Join-Path $installRoot 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin' + $cmakePath = Join-Path $cmakeDir 'cmake.exe' + $ctestPath = Join-Path $cmakeDir 'ctest.exe' + + if (-not (Test-Path $vcvars) -or -not (Test-Path $cmakePath) -or -not (Test-Path $ctestPath)) { + Write-Host 'Visual Studio Build Tools not fully present. Installing...' + $bootstrapper = Join-Path $env:TEMP 'vs_BuildTools.exe' + Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $bootstrapper -UseBasicParsing + + $arguments = @( + '--quiet', + '--wait', + '--norestart', + '--nocache', + '--installPath', $installRoot, + '--add', 'Microsoft.VisualStudio.Workload.VCTools', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '--add', 'Microsoft.VisualStudio.Component.VC.CMake.Project', + '--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22621', + '--includeRecommended' + ) + + $process = Start-Process -FilePath $bootstrapper -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "Visual Studio Build Tools install failed with exit code $($process.ExitCode)." + } + } + + if (-not (Test-Path $vcvars)) { + throw "vcvarsall.bat was not found at $vcvars after Build Tools install." + } + if (-not (Test-Path $cmakePath)) { + throw "cmake.exe was not found at $cmakePath after Build Tools install." + } + if (-not (Test-Path $ctestPath)) { + throw "ctest.exe was not found at $ctestPath after Build Tools install." + } + + Write-Host "##vso[task.prependpath]$cmakeDir" + Write-Host "vcvarsall.bat: $vcvars" + Write-Host "cmake.exe: $cmakePath" + Write-Host "ctest.exe: $ctestPath" + Write-Host "##vso[task.setvariable variable=vcvarsAllPath]$vcvars" + Write-Host "##vso[task.setvariable variable=cmakePath]$cmakePath" + Write-Host "##vso[task.setvariable variable=ctestPath]$ctestPath" + # Bake the pipeline-computed version into the binary so FoundryLocalGetVersionString() # matches the .nupkg version (e.g. 0.1.0-dev.202605111234) instead of the cmake default. @@ -65,7 +131,7 @@ steps: displayName: 'Append version define' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $version = (Get-Content "$(Pipeline.Workspace)/version-info/sdkVersion.txt" -Raw).Trim() $defines = '$(cmakeFetchDefines)' + " `"FOUNDRY_LOCAL_VERSION_STRING=$version`"" @@ -77,6 +143,8 @@ steps: - template: ../../templates/fetch-test-data-from-blob.yml@self parameters: destinationPath: '$(Agent.BuildDirectory)/test-data-shared' + scriptType: ps + installAzCli: true - ${{ if eq(parameters.arch, 'x64') }}: - script: >- @@ -94,25 +162,42 @@ steps: - ${{ if eq(parameters.arch, 'arm64') }}: - script: >- + call "$(vcvarsAllPath)" arm64 && python build.py --configure --build --arm64 --config ${{ parameters.buildConfig }} --cmake_generator "Visual Studio 17 2022" --winml_sdk_version ${{ parameters.winmlVersion }} + --cmake_path "$(cmakePath)" + --ctest_path "$(ctestPath)" --cmake_extra_defines $(cmakeFetchDefines) - displayName: 'Configure and build (arm64 cross-compile)' + displayName: 'Configure and build (arm64)' workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp env: VCPKG_ROOT: $(Build.BinariesDirectory)\vcpkg PKG_CONFIG: $(Build.BinariesDirectory)\tools\pkg-config.bat -- ${{ if and(eq(parameters.runTests, true), eq(parameters.arch, 'x64')) }}: - - script: python build.py --test --config ${{ parameters.buildConfig }} - displayName: 'Run tests' - workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp - env: - VCPKG_ROOT: $(Build.BinariesDirectory)\vcpkg - FOUNDRY_TEST_DATA_DIR: $(Agent.BuildDirectory)\test-data-shared +- ${{ if eq(parameters.runTests, true) }}: + - ${{ if eq(parameters.arch, 'x64') }}: + - script: python build.py --test --config ${{ parameters.buildConfig }} + displayName: 'Run tests' + workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp + env: + VCPKG_ROOT: $(Build.BinariesDirectory)\vcpkg + FOUNDRY_TEST_DATA_DIR: $(Agent.BuildDirectory)\test-data-shared + + - ${{ if eq(parameters.arch, 'arm64') }}: + - script: >- + python build.py + --test + --config ${{ parameters.buildConfig }} + --cmake_path "$(cmakePath)" + --ctest_path "$(ctestPath)" + displayName: 'Run tests' + workingDirectory: $(Build.SourcesDirectory)/sdk_v2/cpp + env: + VCPKG_ROOT: $(Build.BinariesDirectory)\vcpkg + FOUNDRY_TEST_DATA_DIR: $(Agent.BuildDirectory)\test-data-shared # Stage the redistributable native artifacts. vcpkg statically links # ORT/GenAI/azure-*/spdlog/fmt/libcurl/libssl/zlib/brotli* into foundry_local.dll @@ -127,7 +212,7 @@ steps: displayName: 'Stage native artifacts' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $binDir = '$(Build.SourcesDirectory)/sdk_v2/cpp/build/Windows/${{ parameters.buildConfig }}/bin/${{ parameters.buildConfig }}' $linkDir = '$(Build.SourcesDirectory)/sdk_v2/cpp/build/Windows/${{ parameters.buildConfig }}/${{ parameters.buildConfig }}' @@ -150,6 +235,7 @@ steps: Write-Host " staged $(Split-Path -Leaf $s)" } + - ${{ if eq(parameters.stageHeaders, true) }}: - task: CopyFiles@2 displayName: 'Stage public headers' diff --git a/.pipelines/v2/templates/steps-prefetch-nuget.yml b/.pipelines/v2/templates/steps-prefetch-nuget.yml index 661075294..2c6b9f715 100644 --- a/.pipelines/v2/templates/steps-prefetch-nuget.yml +++ b/.pipelines/v2/templates/steps-prefetch-nuget.yml @@ -11,6 +11,7 @@ # includeWinml – Download WinML and emit WINML_EP_CATALOG_FETCH_URL # includeOrtGpuLinux – Also download Microsoft.ML.OnnxRuntime.Gpu.Linux (Linux only) # shell – 'pwsh' (Windows/macOS) or 'bash' (Linux) +# usePwsh – Whether PowerShell tasks should use PowerShell Core parameters: - name: ortVersion @@ -29,6 +30,9 @@ parameters: - name: shell type: string values: ['pwsh', 'bash'] +- name: usePwsh + type: boolean + default: true steps: @@ -38,7 +42,7 @@ steps: displayName: 'Validate pinned versions match deps_versions.json' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $ErrorActionPreference = 'Stop' $depsFile = Join-Path "$(Build.SourcesDirectory)" "sdk_v2/deps_versions.json" @@ -70,7 +74,7 @@ steps: displayName: 'Pre-download NuGet packages' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $ErrorActionPreference = 'Stop' $cacheDir = "$(Build.BinariesDirectory)/nuget_packages" diff --git a/.pipelines/v2/templates/steps-test-cs.yml b/.pipelines/v2/templates/steps-test-cs.yml index adbb10644..f510d754e 100644 --- a/.pipelines/v2/templates/steps-test-cs.yml +++ b/.pipelines/v2/templates/steps-test-cs.yml @@ -15,6 +15,10 @@ parameters: type: string default: '' displayName: 'Optional extra dotnet test arguments' +- name: usePwsh + type: boolean + default: true + displayName: 'Use PowerShell Core (pwsh) for PowerShell tasks' steps: @@ -36,11 +40,50 @@ steps: packageType: runtime version: '8.0.x' +- task: PowerShell@2 + displayName: 'Bootstrap VC++ ARM64 runtime' + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['Agent.OSArchitecture'], 'ARM64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Stop' + $url = 'https://aka.ms/vs/17/release/vc_redist.arm64.exe' + $installer = Join-Path $env:TEMP 'vc_redist.arm64.exe' + $logPath = Join-Path $env:TEMP 'vc_redist_arm64_install.log' + + Write-Host "Downloading ARM64 VC++ runtime from: $url" + Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing + + $arguments = @('/install', '/quiet', '/norestart', '/log', $logPath) + $proc = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru + if ($proc.ExitCode -notin @(0, 3010, 1638)) { + throw "VC++ ARM64 runtime installer failed with exit code $($proc.ExitCode). Log: $logPath" + } + + if ($proc.ExitCode -eq 3010) { + Write-Host 'VC++ ARM64 runtime installed; reboot is required by installer but deferred in CI.' + } elseif ($proc.ExitCode -eq 1638) { + Write-Host 'A newer/same VC++ ARM64 runtime is already installed.' + } else { + Write-Host 'VC++ ARM64 runtime installation completed successfully.' + } + + $runtimeDlls = @('vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll') + foreach ($dll in $runtimeDlls) { + $path = Join-Path "$env:WINDIR\System32" $dll + if (-not (Test-Path $path)) { + throw "Expected ARM64 VC runtime DLL not found: $path" + } + $ver = (Get-Item $path).VersionInfo.FileVersion + Write-Host " $dll => $path (version: $ver)" + } + - task: PowerShell@2 displayName: 'Set package version' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $v = (Get-Content "$(Pipeline.Workspace)/version-info/sdkVersion.txt" -Raw).Trim() Write-Host "Package version: $v" @@ -50,7 +93,7 @@ steps: displayName: 'Create NuGet.config with local Foundry Local Runtime feed' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $pkgPattern = 'Microsoft.AI.Foundry.Local.Runtime*.nupkg' $nupkg = Get-ChildItem "${{ parameters.flNugetDir }}" -Recurse -Filter $pkgPattern | @@ -84,7 +127,7 @@ steps: displayName: 'Set isolated NuGet packages path' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $buildDir = "$(Build.BinariesDirectory)/nuget-isolated-$(System.JobId)" if (Test-Path $buildDir) { @@ -105,7 +148,7 @@ steps: displayName: 'Restore & build tests' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $proj = "$(Build.SourcesDirectory)/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" $rid = dotnet msbuild $proj -getProperty:NETCoreSdkRuntimeIdentifier @@ -130,7 +173,7 @@ steps: displayName: 'Purge stale native redirect files' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | # Defense in depth: in CI we resolve foundry_local exclusively via the # Microsoft.AI.Foundry.Local.Runtime NuGet package (runtimes//native/). @@ -146,11 +189,84 @@ steps: Write-Host "No stale foundry_local.native.cfg files found." } +- task: PowerShell@2 + displayName: 'ARM64 native loader diagnostics (C#)' + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['Agent.OSArchitecture'], 'ARM64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Continue' + Write-Host "Agent.OS=$env:AGENT_OS" + Write-Host "Agent.OSArchitecture=$env:AGENT_OSARCHITECTURE" + Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE" + + $dumpbin = (Get-Command dumpbin.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + $linkExe = (Get-Command link.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + + function Show-DllDiagnostics([string]$dllPath) { + if (-not (Test-Path $dllPath)) { + Write-Warning "Missing target DLL: $dllPath" + return + } + + Write-Host "" + Write-Host "=== Diagnostics for $dllPath ===" + $output = @() + if ($dumpbin) { + $output = & $dumpbin /dependents $dllPath 2>&1 + } elseif ($linkExe) { + $output = & $linkExe /dump /dependents $dllPath 2>&1 + } else { + Write-Warning 'Neither dumpbin.exe nor link.exe found on PATH.' + } + + if ($output.Count -gt 0) { + $output | ForEach-Object { Write-Host $_ } + $deps = @($output | + ForEach-Object { + $line = $_.ToString().Trim() + if ($line -match '^[A-Za-z0-9._-]+\\.dll$') { $line.ToLowerInvariant() } + } | + Where-Object { $_ } | + Select-Object -Unique) + + $probeDirs = @( + (Split-Path -Parent $dllPath), + "$env:WINDIR\\System32", + "$env:WINDIR\\SysWOW64" + ) + foreach ($dep in $deps) { + $resolved = $null + foreach ($d in $probeDirs) { + $p = Join-Path $d $dep + if (Test-Path $p) { $resolved = $p; break } + } + if ($resolved) { + Write-Host " resolved: $dep -> $resolved" + } else { + Write-Warning " unresolved: $dep" + } + } + } + } + + $nativeCandidates = Get-ChildItem -Path "$(Build.SourcesDirectory)/sdk_v2/cs" -Recurse -Filter 'foundry_local.dll' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match 'runtimes\\win-arm64\\native' } + + if (-not $nativeCandidates) { + Write-Warning 'No runtimes/win-arm64/native/foundry_local.dll found under sdk_v2/cs output.' + } + + foreach ($dll in $nativeCandidates) { + Show-DllDiagnostics $dll.FullName + } + - task: PowerShell@2 displayName: 'Run SDK tests' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $proj = "$(Build.SourcesDirectory)/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" diff --git a/.pipelines/v2/templates/steps-test-js.yml b/.pipelines/v2/templates/steps-test-js.yml index bf7e7b74c..3d4f5b545 100644 --- a/.pipelines/v2/templates/steps-test-js.yml +++ b/.pipelines/v2/templates/steps-test-js.yml @@ -7,7 +7,7 @@ parameters: - name: rid type: string - values: ['win-x64', 'linux-x64', 'osx-arm64'] + values: ['win-x64', 'win-arm64', 'linux-x64', 'osx-arm64'] displayName: 'ADO RID for the platform being tested (no cross-arch testing)' - name: prebuildArtifactDir type: string @@ -15,6 +15,10 @@ parameters: - name: testDataSharedDir type: string displayName: 'Path to the model cache directory used by tests' +- name: usePwsh + type: boolean + default: true + displayName: 'Use PowerShell Core (pwsh) for Windows PowerShell tasks' steps: @@ -23,16 +27,20 @@ steps: inputs: versionSpec: '20.x' -- ${{ if eq(parameters.rid, 'win-x64') }}: - - pwsh: | - $src = "${{ parameters.prebuildArtifactDir }}/prebuilds" - $dst = "$(Build.SourcesDirectory)/sdk_v2/js/prebuilds" - if (-not (Test-Path $src)) { throw "Prebuild artifact missing: $src" } - if (Test-Path $dst) { Remove-Item -Recurse -Force $dst } - New-Item -ItemType Directory -Force -Path $dst | Out-Null - Copy-Item "$src/*" -Destination $dst -Recurse -Force - Get-ChildItem -Recurse $dst | ForEach-Object { Write-Host " $($_.FullName)" } +- ${{ if or(eq(parameters.rid, 'win-x64'), eq(parameters.rid, 'win-arm64')) }}: + - task: PowerShell@2 displayName: 'Drop prebuild into sdk_v2/js/prebuilds' + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $src = "${{ parameters.prebuildArtifactDir }}/prebuilds" + $dst = "$(Build.SourcesDirectory)/sdk_v2/js/prebuilds" + if (-not (Test-Path $src)) { throw "Prebuild artifact missing: $src" } + if (Test-Path $dst) { Remove-Item -Recurse -Force $dst } + New-Item -ItemType Directory -Force -Path $dst | Out-Null + Copy-Item "$src/*" -Destination $dst -Recurse -Force + Get-ChildItem -Recurse $dst | ForEach-Object { Write-Host " $($_.FullName)" } - ${{ if or(eq(parameters.rid, 'linux-x64'), eq(parameters.rid, 'osx-arm64')) }}: - bash: | @@ -46,7 +54,46 @@ steps: find "$dst" -maxdepth 3 -print displayName: 'Drop prebuild into sdk_v2/js/prebuilds' -- ${{ if eq(parameters.rid, 'win-x64') }}: +- ${{ if or(eq(parameters.rid, 'win-x64'), eq(parameters.rid, 'win-arm64')) }}: + - task: PowerShell@2 + displayName: 'Bootstrap VC++ ARM64 runtime' + condition: and(succeeded(), eq('${{ parameters.rid }}', 'win-arm64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Stop' + $url = 'https://aka.ms/vs/17/release/vc_redist.arm64.exe' + $installer = Join-Path $env:TEMP 'vc_redist.arm64.exe' + $logPath = Join-Path $env:TEMP 'vc_redist_arm64_install.log' + + Write-Host "Downloading ARM64 VC++ runtime from: $url" + Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing + + $arguments = @('/install', '/quiet', '/norestart', '/log', $logPath) + $proc = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru + if ($proc.ExitCode -notin @(0, 3010, 1638)) { + throw "VC++ ARM64 runtime installer failed with exit code $($proc.ExitCode). Log: $logPath" + } + + if ($proc.ExitCode -eq 3010) { + Write-Host 'VC++ ARM64 runtime installed; reboot is required by installer but deferred in CI.' + } elseif ($proc.ExitCode -eq 1638) { + Write-Host 'A newer/same VC++ ARM64 runtime is already installed.' + } else { + Write-Host 'VC++ ARM64 runtime installation completed successfully.' + } + + $runtimeDlls = @('vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll') + foreach ($dll in $runtimeDlls) { + $path = Join-Path "$env:WINDIR\System32" $dll + if (-not (Test-Path $path)) { + throw "Expected ARM64 VC runtime DLL not found: $path" + } + $ver = (Get-Item $path).VersionInfo.FileVersion + Write-Host " $dll => $path (version: $ver)" + } + - task: Npm@1 displayName: 'npm ci (runs install-native postinstall)' inputs: @@ -54,20 +101,97 @@ steps: workingDir: '$(Build.SourcesDirectory)/sdk_v2/js' customCommand: 'ci --no-audit --no-fund' - - pwsh: | - Set-Location "$(Build.SourcesDirectory)/sdk_v2/js" - npm run build:ts - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - task: PowerShell@2 displayName: 'Build TypeScript' + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + Set-Location "$(Build.SourcesDirectory)/sdk_v2/js" + npm run build:ts + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - pwsh: | - Set-Location "$(Build.SourcesDirectory)/sdk_v2/js" - Write-Host "FOUNDRY_TEST_DATA_DIR=$env:FOUNDRY_TEST_DATA_DIR" - if (-not (Test-Path $env:FOUNDRY_TEST_DATA_DIR)) { throw "FOUNDRY_TEST_DATA_DIR does not exist: $env:FOUNDRY_TEST_DATA_DIR" } - Get-ChildItem -Path $env:FOUNDRY_TEST_DATA_DIR -Force | ForEach-Object { Write-Host " $($_.FullName)" } - npx --no-install vitest run --reporter=verbose - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - task: PowerShell@2 + displayName: 'ARM64 native loader diagnostics (JS)' + condition: and(succeeded(), eq('${{ parameters.rid }}', 'win-arm64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Continue' + $prebuildDir = "$(Build.SourcesDirectory)/sdk_v2/js/prebuilds/win32-arm64" + Write-Host "Diagnostic prebuild dir: $prebuildDir" + Write-Host "Agent.OS=$env:AGENT_OS" + Write-Host "Agent.OSArchitecture=$env:AGENT_OSARCHITECTURE" + Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE" + Get-ChildItem -Path $prebuildDir -Force | ForEach-Object { Write-Host " $($_.Name)" } + + $dumpbin = (Get-Command dumpbin.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + $linkExe = (Get-Command link.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + + function Show-DllDiagnostics([string]$dllPath) { + if (-not (Test-Path $dllPath)) { + Write-Warning "Missing target DLL: $dllPath" + return + } + + Write-Host "" + Write-Host "=== Diagnostics for $dllPath ===" + $output = @() + if ($dumpbin) { + $output = & $dumpbin /dependents $dllPath 2>&1 + } elseif ($linkExe) { + $output = & $linkExe /dump /dependents $dllPath 2>&1 + } else { + Write-Warning 'Neither dumpbin.exe nor link.exe found on PATH.' + } + + if ($output.Count -gt 0) { + $output | ForEach-Object { Write-Host $_ } + $deps = @($output | + ForEach-Object { + $line = $_.ToString().Trim() + if ($line -match '^[A-Za-z0-9._-]+\\.dll$') { $line.ToLowerInvariant() } + } | + Where-Object { $_ } | + Select-Object -Unique) + + $probeDirs = @( + (Split-Path -Parent $dllPath), + "$env:WINDIR\\System32", + "$env:WINDIR\\SysWOW64" + ) + foreach ($dep in $deps) { + $resolved = $null + foreach ($d in $probeDirs) { + $p = Join-Path $d $dep + if (Test-Path $p) { $resolved = $p; break } + } + if ($resolved) { + Write-Host " resolved: $dep -> $resolved" + } else { + Write-Warning " unresolved: $dep" + } + } + } + } + + Show-DllDiagnostics (Join-Path $prebuildDir 'foundry_local.dll') + Show-DllDiagnostics (Join-Path $prebuildDir 'onnxruntime.dll') + Show-DllDiagnostics (Join-Path $prebuildDir 'onnxruntime-genai.dll') + + - task: PowerShell@2 displayName: 'Run vitest' + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + Set-Location "$(Build.SourcesDirectory)/sdk_v2/js" + Write-Host "FOUNDRY_TEST_DATA_DIR=$env:FOUNDRY_TEST_DATA_DIR" + if (-not (Test-Path $env:FOUNDRY_TEST_DATA_DIR)) { throw "FOUNDRY_TEST_DATA_DIR does not exist: $env:FOUNDRY_TEST_DATA_DIR" } + Get-ChildItem -Path $env:FOUNDRY_TEST_DATA_DIR -Force | ForEach-Object { Write-Host " $($_.FullName)" } + npx --no-install vitest run --reporter=verbose + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } env: FOUNDRY_TEST_DATA_DIR: '${{ parameters.testDataSharedDir }}' diff --git a/.pipelines/v2/templates/steps-test-python.yml b/.pipelines/v2/templates/steps-test-python.yml index 223ef527f..a3ade3a06 100644 --- a/.pipelines/v2/templates/steps-test-python.yml +++ b/.pipelines/v2/templates/steps-test-python.yml @@ -17,6 +17,10 @@ parameters: default: 'x64' values: ['x64', 'arm64'] displayName: 'Architecture of the host Python interpreter (matches the agent OS arch).' +- name: usePwsh + type: boolean + default: true + displayName: 'Use PowerShell Core (pwsh). Set false to use Windows PowerShell.' steps: @@ -27,12 +31,51 @@ steps: addToPath: true architecture: '${{ parameters.pythonArchitecture }}' +- task: PowerShell@2 + displayName: 'Bootstrap VC++ ARM64 runtime' + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['Agent.OSArchitecture'], 'ARM64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Stop' + $url = 'https://aka.ms/vs/17/release/vc_redist.arm64.exe' + $installer = Join-Path $env:TEMP 'vc_redist.arm64.exe' + $logPath = Join-Path $env:TEMP 'vc_redist_arm64_install.log' + + Write-Host "Downloading ARM64 VC++ runtime from: $url" + Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing + + $arguments = @('/install', '/quiet', '/norestart', '/log', $logPath) + $proc = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru + if ($proc.ExitCode -notin @(0, 3010, 1638)) { + throw "VC++ ARM64 runtime installer failed with exit code $($proc.ExitCode). Log: $logPath" + } + + if ($proc.ExitCode -eq 3010) { + Write-Host 'VC++ ARM64 runtime installed; reboot is required by installer but deferred in CI.' + } elseif ($proc.ExitCode -eq 1638) { + Write-Host 'A newer/same VC++ ARM64 runtime is already installed.' + } else { + Write-Host 'VC++ ARM64 runtime installation completed successfully.' + } + + $runtimeDlls = @('vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll') + foreach ($dll in $runtimeDlls) { + $path = Join-Path "$env:WINDIR\System32" $dll + if (-not (Test-Path $path)) { + throw "Expected ARM64 VC runtime DLL not found: $path" + } + $ver = (Get-Item $path).VersionInfo.FileVersion + Write-Host " $dll => $path (version: $ver)" + } + # Job-local venv so installs never pollute the agent's site-packages. - task: PowerShell@2 displayName: 'Create test venv' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | $venvDir = "$(Agent.TempDirectory)/fl-py-venv-$(System.JobId)" if (Test-Path $venvDir) { Remove-Item -Recurse -Force $venvDir } @@ -51,7 +94,7 @@ steps: displayName: 'Install built wheel + test deps' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | # Locate the wheel. Each job's wheel lands in its own artifact, so a # single match is expected. @@ -71,11 +114,87 @@ steps: & "$(venvPy)" -m pip install pytest pytest-cov if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +- task: PowerShell@2 + displayName: 'ARM64 native loader diagnostics (Python)' + condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['Agent.OSArchitecture'], 'ARM64')) + inputs: + targetType: inline + pwsh: ${{ parameters.usePwsh }} + script: | + $ErrorActionPreference = 'Continue' + Write-Host "Agent.OS=$env:AGENT_OS" + Write-Host "Agent.OSArchitecture=$env:AGENT_OSARCHITECTURE" + Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE" + + $dumpbin = (Get-Command dumpbin.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + $linkExe = (Get-Command link.exe -ErrorAction SilentlyContinue | Select-Object -First 1).Source + + function Show-DllDiagnostics([string]$dllPath) { + if (-not (Test-Path $dllPath)) { + Write-Warning "Missing target DLL: $dllPath" + return + } + + Write-Host "" + Write-Host "=== Diagnostics for $dllPath ===" + $output = @() + if ($dumpbin) { + $output = & $dumpbin /dependents $dllPath 2>&1 + } elseif ($linkExe) { + $output = & $linkExe /dump /dependents $dllPath 2>&1 + } else { + Write-Warning 'Neither dumpbin.exe nor link.exe found on PATH.' + } + + if ($output.Count -gt 0) { + $output | ForEach-Object { Write-Host $_ } + $deps = @($output | + ForEach-Object { + $line = $_.ToString().Trim() + if ($line -match '^[A-Za-z0-9._-]+\\.dll$') { $line.ToLowerInvariant() } + } | + Where-Object { $_ } | + Select-Object -Unique) + + $probeDirs = @( + (Split-Path -Parent $dllPath), + "$env:WINDIR\\System32", + "$env:WINDIR\\SysWOW64" + ) + foreach ($dep in $deps) { + $resolved = $null + foreach ($d in $probeDirs) { + $p = Join-Path $d $dep + if (Test-Path $p) { $resolved = $p; break } + } + if ($resolved) { + Write-Host " resolved: $dep -> $resolved" + } else { + Write-Warning " unresolved: $dep" + } + } + } + } + + $wheelNativeDir = & "$(venvPy)" -c "import pathlib,foundry_local_sdk._native.lib_loader as l; print((pathlib.Path(l.__file__).resolve().parent / 'win-arm64'))" + $wheelNativeDir = $wheelNativeDir.Trim() + Write-Host "Wheel native dir: $wheelNativeDir" + if (Test-Path $wheelNativeDir) { + Get-ChildItem -Path $wheelNativeDir -Force | ForEach-Object { Write-Host " $($_.Name)" } + } + + $ortDll = & "$(venvPy)" -c "import importlib.util,pathlib; s=importlib.util.find_spec('onnxruntime_core'); print((pathlib.Path(s.origin).resolve().parent / 'bin' / 'onnxruntime.dll') if s else '')" + $genaiDll = & "$(venvPy)" -c "import importlib.util,pathlib; s=importlib.util.find_spec('onnxruntime_genai_core'); print((pathlib.Path(s.origin).resolve().parent / 'bin' / 'onnxruntime-genai.dll') if s else '')" + + Show-DllDiagnostics (Join-Path $wheelNativeDir 'foundry_local.dll') + if ($ortDll) { Show-DllDiagnostics $ortDll.Trim() } + if ($genaiDll) { Show-DllDiagnostics $genaiDll.Trim() } + - task: PowerShell@2 displayName: 'Run SDK tests' inputs: targetType: inline - pwsh: true + pwsh: ${{ parameters.usePwsh }} script: | Push-Location "$(Build.SourcesDirectory)/sdk_v2/python" try { diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs index e74f15715..a74ac6133 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs +++ b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs @@ -17,6 +17,14 @@ internal sealed class OpenAIAudioClientTests { private static IModel? model; + private const string ExpectedTranscriptionX64 = " And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."; + private const string ExpectedTranscriptionArm64 = " And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from a live concert behind the scenes photo gallery and album to purchase like these next few links."; + + private static string ExpectedTranscription => + System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64 + ? ExpectedTranscriptionArm64 + : ExpectedTranscriptionX64; + [Before(Class)] public static async Task Setup() { @@ -68,7 +76,7 @@ public async Task AudioTranscription_NoStreaming_Succeeds() await Assert.That(response).IsNotNull(); await Assert.That(response.Text).IsNotNull().And.IsNotEmpty(); var content = response.Text; - await Assert.That(content).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + await Assert.That(content).IsEqualTo(ExpectedTranscription); Console.WriteLine($"Response: {content}"); } @@ -93,7 +101,7 @@ public async Task AudioTranscription_NoStreaming_Succeeds_WithTemperature() await Assert.That(response).IsNotNull(); await Assert.That(response.Text).IsNotNull().And.IsNotEmpty(); var content = response.Text; - await Assert.That(content).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + await Assert.That(content).IsEqualTo(ExpectedTranscription); Console.WriteLine($"Response: {content}"); } @@ -157,7 +165,7 @@ public async Task AudioTranscription_Streaming_Succeeds() var fullResponse = responseMessage.ToString(); Console.WriteLine(fullResponse); - await Assert.That(fullResponse).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + await Assert.That(fullResponse).IsEqualTo(ExpectedTranscription); } @@ -191,7 +199,7 @@ public async Task AudioTranscription_Streaming_Succeeds_WithTemperature() var fullResponse = responseMessage.ToString(); Console.WriteLine(fullResponse); - await Assert.That(fullResponse).IsEqualTo(" And lots of times you need to give people more than one link at a time. You a band could give their fans a couple new videos from the live concert behind the scenes photo gallery and album to purchase like these next few links."); + await Assert.That(fullResponse).IsEqualTo(ExpectedTranscription); } diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/AudioSessionTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/AudioSessionTests.cs index 103ad8256..9fb6e9d7a 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/AudioSessionTests.cs +++ b/sdk_v2/cs/test/FoundryLocal.Tests/AudioSessionTests.cs @@ -17,11 +17,21 @@ internal sealed class AudioSessionTests { private static IModel? model; - private const string ExpectedTranscription = + private const string ExpectedTranscriptionX64 = " And lots of times you need to give people more than one link at a time." + " You a band could give their fans a couple new videos from the live concert" + " behind the scenes photo gallery and album to purchase like these next few links."; + private const string ExpectedTranscriptionArm64 = + " And lots of times you need to give people more than one link at a time." + + " You a band could give their fans a couple new videos from a live concert" + + " behind the scenes photo gallery and album to purchase like these next few links."; + + private static string ExpectedTranscription => + System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64 + ? ExpectedTranscriptionArm64 + : ExpectedTranscriptionX64; + [Before(Class)] public static async Task Setup() { @@ -103,7 +113,7 @@ public async Task Transcribe_NoStreaming_Succeeds() } await Assert.That(text).IsNotNull().And.IsNotEmpty(); - await Assert.That(text!).IsEqualTo(ExpectedTranscription); + await Assert.That(text!).IsEqualTo(ExpectedTranscription); await Assert.That(segmentCount).IsGreaterThan(0); Console.WriteLine($"Response: {text} ({segmentCount} segments)"); } @@ -146,7 +156,7 @@ public async Task Transcribe_Streaming_Succeeds() var fullResponse = sb.ToString(); Console.WriteLine($"Streaming response ({callbackCount} callbacks): {fullResponse}"); await Assert.That(callbackCount).IsGreaterThan(0); - await Assert.That(fullResponse).IsEqualTo(ExpectedTranscription); + await Assert.That(fullResponse).IsEqualTo(ExpectedTranscription); } [Test] @@ -206,7 +216,7 @@ public async Task Transcribe_Streaming_FinalResponse_AggregatesTranscript() } await Assert.That(aggregated).IsNotNull(); - await Assert.That(aggregated!).IsEqualTo(ExpectedTranscription); + await Assert.That(aggregated!).IsEqualTo(ExpectedTranscription); } [Test] diff --git a/sdk_v2/js/package-lock.json b/sdk_v2/js/package-lock.json index c9d8a4fbd..4bf6916ce 100644 --- a/sdk_v2/js/package-lock.json +++ b/sdk_v2/js/package-lock.json @@ -445,9 +445,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -465,9 +462,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -485,9 +479,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -505,9 +496,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -525,9 +513,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -545,9 +530,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1209,9 +1191,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1233,9 +1212,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1257,9 +1233,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1281,9 +1254,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/sdk_v2/python/src/foundry_local_sdk/_native/lib_loader.py b/sdk_v2/python/src/foundry_local_sdk/_native/lib_loader.py index 1369759c8..a1a7bddb6 100644 --- a/sdk_v2/python/src/foundry_local_sdk/_native/lib_loader.py +++ b/sdk_v2/python/src/foundry_local_sdk/_native/lib_loader.py @@ -29,6 +29,9 @@ def _lib_name() -> str: def _platform_subdir() -> str: if sys.platform == "win32": + machine = platform.machine().lower() + if machine in {"arm64", "aarch64"}: + return "win-arm64" return "win-x64" if sys.platform == "darwin": return "osx-arm64" if platform.machine() == "arm64" else "osx-x64"