diff --git a/.pipelines/foundry-local-packaging.yml b/.pipelines/foundry-local-packaging.yml index e67dc61c6..844348a78 100644 --- a/.pipelines/foundry-local-packaging.yml +++ b/.pipelines/foundry-local-packaging.yml @@ -54,17 +54,13 @@ parameters: variables: - group: FoundryLocal-ESRP-Signing # C++ SDK (sdk_v2/cpp) native dependency versions. Must match cmake defaults -# in sdk_v2/deps_versions.json and sdk_v2/deps_versions_winml.json. +# in sdk_v2/deps_versions.json. - name: cppOrtVersion value: '1.25.1' -- name: cppOrtVersionWinml - # Pinned to the WinML-aligned ORT line so foundry_local.dll's ORT ABI matches - # the WinML EP catalog plugins it loads. See FindOnnxRuntime.cmake. - value: '1.23.2.3' - name: cppGenaiVersion value: '0.13.2' - name: cppWinmlVersion - value: '1.8.2192' + value: '2.1.70' - name: cppBuildConfig value: 'RelWithDebInfo' @@ -269,6 +265,5 @@ extends: parameters: buildConfig: $(cppBuildConfig) ortVersion: $(cppOrtVersion) - ortVersionWinml: $(cppOrtVersionWinml) genaiVersion: $(cppGenaiVersion) winmlVersion: $(cppWinmlVersion) diff --git a/.pipelines/v2/sdk_v2-pipeline-plan.md b/.pipelines/v2/sdk_v2-pipeline-plan.md index 68e920d9f..c88e1af34 100644 --- a/.pipelines/v2/sdk_v2-pipeline-plan.md +++ b/.pipelines/v2/sdk_v2-pipeline-plan.md @@ -68,8 +68,7 @@ are gated separately via `.pipelines/v1/templates/stages-sdk-v1.yml`. `foundry-local-sdk-winml` when `FL_PYTHON_PACKAGE_NAME` is set in the environment. The same backend also handles ORT pin rewriting (decision 8). 8. **Single source of truth for ORT/GenAI versions.** ORT and GenAI versions - live in `sdk_v2/deps_versions.json` (standard) and - `sdk_v2/deps_versions_winml.json` (WinML). Both files have shape + live in `sdk_v2/deps_versions.json`. The file shape is `{ "onnxruntime": { "version": "..." }, "onnxruntime-genai": { "version": "..." } }`. Consumers: - **C++ build:** `sdk_v2/cpp/cmake/Find{OnnxRuntime,OnnxRuntimeGenAI}.cmake` @@ -175,15 +174,13 @@ compute_version * All build stages are independent (`dependsOn: [compute_version]`) and run in parallel. * Both pack stages run on every build (PR and `main`). -* The WinML build stages link against ORT 1.23.x (pinned via - `ortVersionWinml`) so the binary's ORT ABI matches the WinML EP catalog - plugins it loads. The base stages link against ORT 1.25.x. +* WinML and non-WinML build stages link against the same ORT version + (`ortVersion`, currently 1.25.x). WinML 2.x is reg-free and uses the + standard ORT package, so a separate WinML-aligned ORT pin is no longer + required. * Tests run on `cpp_build_win_x64`, `cpp_build_win_x64_winml`, `cpp_build_linux_x64`, and `cpp_build_osx_arm64`. The two ARM64 Windows - stages cross-compile from an x64 host and skip tests. The WinML x64 stage - runs the full suite — the C++ `VisionFixture` and the C# `VisionTests` - self-skip on the WinML-aligned ORT (older than the cataloged vision - models require), so the rest of the suite still exercises that configuration. + stages cross-compile from an x64 host and skip tests. ## Per-stage artifacts @@ -245,35 +242,31 @@ purposes: 1. **Version pinning** — the `KEY=PATH` pairs are passed via `--cmake_extra_defines` (`ORT_FETCH_URL`, `GENAI_FETCH_URL`, - `WINML_EP_CATALOG_FETCH_URL`, `WINAPPSDK_FOUNDATION_FETCH_URL`, - `ORT_GPU_LINUX_FETCH_URL`) so the cmake defaults in - `FindOnnxRuntime.cmake` / `FindOnnxRuntimeGenAI.cmake` are never - silently substituted. + `WINML_EP_CATALOG_FETCH_URL`, `ORT_GPU_LINUX_FETCH_URL`) so the cmake + defaults in `FindOnnxRuntime.cmake` / `FindOnnxRuntimeGenAI.cmake` are + never silently substituted. 2. **Stage isolation** — the build step no longer needs network access to the package feed once prefetch has completed. Versions are pipeline-level variables, currently: * `ortVersion` `1.25.1` (`Microsoft.ML.OnnxRuntime.Foundry`) -* `ortVersionWinml` `1.23.2.3` (WinML-aligned ORT line, used by the WinML build stages) * `genaiVersion` `0.13.2` (`Microsoft.ML.OnnxRuntimeGenAI.Foundry`) -* `winmlVersion` `1.8.2192` (`Microsoft.WindowsAppSDK.ML`) +* `winmlVersion` `2.1.70` (`Microsoft.Windows.AI.MachineLearning`, WinML 2.x reg-free) These must be kept in sync with the cmake defaults and with -`sdk_v2/deps_versions[_winml].json` (decision 8). When bumping, update -both places in the same PR. +`sdk_v2/deps_versions.json` (decision 8). When bumping, update both places +in the same PR. The shared download logic is in `steps-prefetch-nuget.yml` and exposes both a PowerShell (Windows) and a bash (Linux/macOS) implementation behind a `shell` parameter. It emits a single pipeline variable `cmakeFetchDefines` containing the quoted `KEY=PATH` pairs to splice into the build command. -WinML is special-cased: `Microsoft.WindowsAppSDK.ML` has a transitive -dependency on `Microsoft.WindowsAppSDK.Foundation` whose exact min version -is often unpublished, so the pwsh branch shells out to `nuget install -... -DependencyVersion Lowest` to let the resolver pick a satisfying -version, then emits fetch URLs for both `.nupkg`s. The bash branch fails -fast if `includeWinml=true` is ever passed — WinML is Windows-only. +WinML downloads `Microsoft.Windows.AI.MachineLearning` directly from +nuget.org as a single self-contained reg-free package — no transitive +WinAppSDK Foundation resolution needed. The bash branch fails fast if +`includeWinml=true` is ever passed — WinML is Windows-only. ## Test data diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index 295b9a934..09a75c33e 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -14,8 +14,6 @@ parameters: type: string - name: ortVersion type: string -- name: ortVersionWinml - type: string - name: genaiVersion type: string - name: winmlVersion @@ -181,7 +179,7 @@ stages: parameters: arch: x64 buildConfig: ${{ parameters.buildConfig }} - ortVersion: ${{ parameters.ortVersionWinml }} + ortVersion: ${{ parameters.ortVersion }} genaiVersion: ${{ parameters.genaiVersion }} winmlVersion: ${{ parameters.winmlVersion }} useWinml: true @@ -214,7 +212,7 @@ stages: parameters: arch: arm64 buildConfig: ${{ parameters.buildConfig }} - ortVersion: ${{ parameters.ortVersionWinml }} + ortVersion: ${{ parameters.ortVersion }} genaiVersion: ${{ parameters.genaiVersion }} winmlVersion: ${{ parameters.winmlVersion }} useWinml: true @@ -297,6 +295,6 @@ stages: steps: - template: steps-pack-nuget.yml parameters: - ortVersion: ${{ parameters.ortVersionWinml }} + ortVersion: ${{ parameters.ortVersion }} genaiVersion: ${{ parameters.genaiVersion }} variant: winml diff --git a/.pipelines/v2/templates/stages-sdk-v2.yml b/.pipelines/v2/templates/stages-sdk-v2.yml index c2cae47d1..5a1c59bab 100644 --- a/.pipelines/v2/templates/stages-sdk-v2.yml +++ b/.pipelines/v2/templates/stages-sdk-v2.yml @@ -16,8 +16,6 @@ parameters: default: 'RelWithDebInfo' - name: ortVersion type: string -- name: ortVersionWinml - type: string - name: genaiVersion type: string - name: winmlVersion @@ -30,7 +28,6 @@ stages: parameters: buildConfig: ${{ parameters.buildConfig }} ortVersion: ${{ parameters.ortVersion }} - ortVersionWinml: ${{ parameters.ortVersionWinml }} genaiVersion: ${{ parameters.genaiVersion }} winmlVersion: ${{ parameters.winmlVersion }} diff --git a/.pipelines/v2/templates/steps-build-windows.yml b/.pipelines/v2/templates/steps-build-windows.yml index ba2d82d79..0e17f07b6 100644 --- a/.pipelines/v2/templates/steps-build-windows.yml +++ b/.pipelines/v2/templates/steps-build-windows.yml @@ -9,9 +9,8 @@ # buildConfig – CMake config (Debug, Release, RelWithDebInfo, MinSizeRel) # ortVersion – Microsoft.ML.OnnxRuntime.Foundry version # genaiVersion – Microsoft.ML.OnnxRuntimeGenAI.Foundry version -# winmlVersion – Microsoft.WindowsAppSDK.ML version -# useWinml – Build the WinML variant (--use_winml). Caller is responsible -# for passing the WinML-aligned ortVersion (e.g. 1.23.2.3). +# winmlVersion – Microsoft.Windows.AI.MachineLearning version +# useWinml – Build the WinML variant (--use_winml). # runTests – Whether to run tests # stageHeaders – Whether to stage public headers as a separate artifact @@ -151,17 +150,12 @@ steps: # libcurl/libssl/zlib/brotli* into foundry_local.dll (see # sdk_v2/cpp/triplets/x64-windows.cmake), so the only runtime payload is # foundry_local.dll itself. -# - WinML build: foundry_local.dll picks up one extra static import — -# Microsoft.WindowsAppRuntime.Bootstrap.dll — which is the entry point -# that calls MddBootstrapInitialize2 to register the system-installed -# Windows App Runtime. Bootstrap.dll must travel with the wheel because -# end-user / CI machines don't have it on PATH otherwise. Everything -# downstream of Bootstrap (Microsoft.Windows.AI.MachineLearning.dll, -# DirectML, the WinML-flavored ORT, etc.) is supplied by the system -# App Runtime that Bootstrap registers — Microsoft.Windows.AI.MachineLearning -# is delay-loaded (see /DELAYLOAD in CMakeLists.txt) so it is NOT needed at -# foundry_local.dll load time. ORT/GenAI come from the onnxruntime-core / -# onnxruntime-genai-core pip deps. +# - WinML build: foundry_local.dll picks up one extra runtime dependency, +# Microsoft.Windows.AI.MachineLearning.dll, which must travel with the wheel. +# The WinML DLL is delay-loaded (see /DELAYLOAD in CMakeLists.txt) so it is +# NOT needed at foundry_local.dll load time, but the cmake post-build copy +# stages it next to foundry_local.dll for runtime EP discovery. ORT/GenAI +# come from the onnxruntime-core / onnxruntime-genai-core pip deps. - task: PowerShell@2 displayName: 'Stage native artifacts' inputs: @@ -179,7 +173,7 @@ steps: (Join-Path $linkDir 'foundry_local.lib') ) if ($${{ parameters.useWinml }}) { - $sources += (Join-Path $binDir 'Microsoft.WindowsAppRuntime.Bootstrap.dll') + $sources += (Join-Path $binDir 'Microsoft.Windows.AI.MachineLearning.dll') } foreach ($s in $sources) { diff --git a/.pipelines/v2/templates/steps-prefetch-nuget.yml b/.pipelines/v2/templates/steps-prefetch-nuget.yml index 70a83f7c2..661075294 100644 --- a/.pipelines/v2/templates/steps-prefetch-nuget.yml +++ b/.pipelines/v2/templates/steps-prefetch-nuget.yml @@ -7,7 +7,7 @@ # Parameters: # ortVersion – Microsoft.ML.OnnxRuntime.Foundry version # genaiVersion – Microsoft.ML.OnnxRuntimeGenAI.Foundry version -# winmlVersion – Microsoft.WindowsAppSDK.ML version (Windows only) +# winmlVersion – Microsoft.Windows.AI.MachineLearning version (Windows only) # 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) @@ -32,6 +32,39 @@ parameters: steps: +# Fail fast if the pipeline-pinned versions drift from the cmake source of +# truth in sdk_v2/deps_versions.json. +- task: PowerShell@2 + displayName: 'Validate pinned versions match deps_versions.json' + inputs: + targetType: inline + pwsh: true + script: | + $ErrorActionPreference = 'Stop' + $depsFile = Join-Path "$(Build.SourcesDirectory)" "sdk_v2/deps_versions.json" + if (-not (Test-Path $depsFile)) { throw "deps_versions.json not found at $depsFile" } + $deps = Get-Content $depsFile -Raw | ConvertFrom-Json + $expected = @{ + 'onnxruntime' = '${{ parameters.ortVersion }}' + 'onnxruntime-genai' = '${{ parameters.genaiVersion }}' + } + if ('${{ parameters.winmlVersion }}' -ne '') { + $expected['windows-ai-machinelearning'] = '${{ parameters.winmlVersion }}' + } + $errors = @() + foreach ($key in $expected.Keys) { + $actual = $deps.$key.version + if ($actual -ne $expected[$key]) { + $errors += " $key`: pipeline says '$($expected[$key])', deps_versions.json says '$actual'" + } + } + if ($errors.Count -gt 0) { + Write-Host "Version drift detected between pipeline literals and sdk_v2/deps_versions.json:" + $errors | ForEach-Object { Write-Host $_ } + throw "Bump the matching cpp*Version variable in .pipelines/foundry-local-packaging.yml" + } + Write-Host "Pinned versions agree with sdk_v2/deps_versions.json." + - ${{ if eq(parameters.shell, 'pwsh') }}: - task: PowerShell@2 displayName: 'Pre-download NuGet packages' @@ -74,34 +107,15 @@ steps: } if ($${{ parameters.includeWinml }}) { - # WinML's bootstrap dep (Microsoft.WindowsAppSDK.Foundation) is a transitive dep of - # Microsoft.WindowsAppSDK.ML, and nuget min-version semantics mean the exact min - # version often isn't published — only later patches are. Defer to `nuget install` - # so the real resolver picks a satisfying version. - $winmlDir = "$cacheDir/winml-resolved" - if (Test-Path $winmlDir) { Remove-Item -Recurse -Force $winmlDir } - New-Item -ItemType Directory -Force -Path $winmlDir | Out-Null - Write-Host "Resolving Microsoft.WindowsAppSDK.ML ${{ parameters.winmlVersion }} via nuget install" - nuget install Microsoft.WindowsAppSDK.ML ` - -Version '${{ parameters.winmlVersion }}' ` - -OutputDirectory $winmlDir ` - -Source 'https://api.nuget.org/v3/index.json' ` - -DependencyVersion Lowest ` - -PackageSaveMode nupkg ` - -DirectDownload ` - -Verbosity quiet - if ($LASTEXITCODE -ne 0) { throw "nuget install Microsoft.WindowsAppSDK.ML failed (exit $LASTEXITCODE)" } - - $mlNupkg = Get-ChildItem $winmlDir -Recurse -Filter 'Microsoft.WindowsAppSDK.ML.*.nupkg' | - Select-Object -First 1 - $foundationNupkg = Get-ChildItem $winmlDir -Recurse -Filter 'Microsoft.WindowsAppSDK.Foundation.*.nupkg' | - Select-Object -First 1 - if (-not $mlNupkg) { throw "Microsoft.WindowsAppSDK.ML .nupkg not found under $winmlDir after nuget install" } - if (-not $foundationNupkg) { throw "Microsoft.WindowsAppSDK.Foundation .nupkg not found under $winmlDir after nuget install" } - Write-Host " -> $($mlNupkg.FullName) ($($mlNupkg.Length) bytes)" - Write-Host " -> $($foundationNupkg.FullName) ($($foundationNupkg.Length) bytes)" - $defines += "WINML_EP_CATALOG_FETCH_URL=$($mlNupkg.FullName)" - $defines += "WINAPPSDK_FOUNDATION_FETCH_URL=$($foundationNupkg.FullName)" + # WinML 2.x (Microsoft.Windows.AI.MachineLearning) is reg-free — a single self-contained + # native package with no transitive Windows App SDK Foundation dependency to resolve. + $winmlUrl = "$feed/Microsoft.Windows.AI.MachineLearning/${{ parameters.winmlVersion }}" + $winmlOut = "$cacheDir/winml.nupkg" + Write-Host "Downloading Microsoft.Windows.AI.MachineLearning ${{ parameters.winmlVersion }}" + Invoke-WebRequest -Uri $winmlUrl -OutFile $winmlOut + if (-not (Test-Path $winmlOut)) { throw "WinML download failed" } + Write-Host " -> $winmlOut ($((Get-Item $winmlOut).Length) bytes)" + $defines += "WINML_EP_CATALOG_FETCH_URL=$winmlOut" } $joined = ($defines | ForEach-Object { "`"$_`"" }) -join ' ' @@ -126,8 +140,6 @@ steps: ) if [ "${{ parameters.includeWinml }}" = "True" ]; then # WinML is Windows-only; the bash branch should never receive includeWinml=true. - # If this fires, the prefetch needs to mirror the pwsh logic that resolves the - # Microsoft.WindowsAppSDK.Foundation version from the .ML nuspec dependency list. echo "ERROR: includeWinml=true is not supported on the bash prefetch branch (WinML is Windows-only)." >&2 exit 1 fi diff --git a/.pipelines/v2/templates/steps-test-cs.yml b/.pipelines/v2/templates/steps-test-cs.yml index 00b1902cd..c483857fc 100644 --- a/.pipelines/v2/templates/steps-test-cs.yml +++ b/.pipelines/v2/templates/steps-test-cs.yml @@ -76,20 +76,6 @@ steps: - task: NuGetAuthenticate@1 displayName: 'Authenticate NuGet feeds' -- ${{ if eq(parameters.isWinML, true) }}: - - task: PowerShell@2 - displayName: 'Install Windows App SDK Runtime' - inputs: - targetType: 'inline' - pwsh: true - script: | - $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" - $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath - & $installerPath --quiet --force - if ($LASTEXITCODE -ne 0) { throw "Windows App SDK Runtime install failed" } - errorActionPreference: 'stop' - # Per-job NuGet isolation to prevent "Central Directory corrupt" / file-locking # errors when multiple C# test jobs (regular + WinML) run concurrently on the # same reused agent. Keyed by $(System.JobId); cleaned on each run. @@ -168,10 +154,29 @@ steps: pwsh: true script: | $proj = "$(Build.SourcesDirectory)/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj" - dotnet test $proj ` - --no-build --configuration Release ` - /p:UseWinML=${{ parameters.isWinML }} - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Test TFMs: + # * net9.0 covers the .NET (Core) runtime test surface. + # * net462 (Windows only, non-WinML) exercises the netstandard2.0 surface + # + polyfills at runtime on .NET Framework. + # The csproj also targets net8.0, but only as build-time coverage — the .NET + # back-compat guarantee means a successful net9.0 test run validates the same + # assemblies for net8.0 consumers. + $frameworks = @('net9.0') + $isWin = $IsWindows -or ($PSVersionTable.Platform -eq $null) + $isWinML = '${{ parameters.isWinML }}' -eq 'True' + if ($isWin -and -not $isWinML) { + $frameworks += 'net462' + } + + foreach ($tfm in $frameworks) { + Write-Host "=== dotnet test --framework $tfm ===" + dotnet test $proj ` + --no-build --configuration Release ` + --framework $tfm ` + /p:UseWinML=${{ parameters.isWinML }} + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } env: TF_BUILD: 'true' 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 3b95b71fd..9396a7cce 100644 --- a/.pipelines/v2/templates/steps-test-python.yml +++ b/.pipelines/v2/templates/steps-test-python.yml @@ -30,20 +30,6 @@ steps: addToPath: true architecture: '${{ parameters.pythonArchitecture }}' -- ${{ if eq(parameters.isWinML, true) }}: - - task: PowerShell@2 - displayName: 'Install Windows App SDK Runtime' - inputs: - targetType: 'inline' - pwsh: true - script: | - $installerUrl = "https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe" - $installerPath = "$env:TEMP\windowsappruntimeinstall.exe" - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath - & $installerPath --quiet --force - if ($LASTEXITCODE -ne 0) { throw "Windows App SDK Runtime install failed" } - errorActionPreference: 'stop' - # Job-local venv so installs never pollute the agent's site-packages. - task: PowerShell@2 displayName: 'Create test venv' diff --git a/memories/repo/cs-local-packages.md b/memories/repo/cs-local-packages.md index 986b78267..2851a696e 100644 --- a/memories/repo/cs-local-packages.md +++ b/memories/repo/cs-local-packages.md @@ -17,7 +17,7 @@ dotnet pack src/Microsoft.AI.Foundry.Local.csproj -o ../../local-packages /p:IsP dotnet pack src/Microsoft.AI.Foundry.Local.csproj -o ../../local-packages /p:IsPacking=true /p:UseWinML=true /p:TreatWarningsAsErrors=false -c Release ``` - `IsPacking=true` auto-sets `Version=0.5.0-dev.local.` (see `Microsoft.AI.Foundry.Local.csproj`). -- `UseWinML=true` flips `PackageId`/`AssemblyName` to `Microsoft.AI.Foundry.Local.WinML`, single-targets `net9.0-windows10.0.26100.0`. +- `UseWinML=true` flips `PackageId`/`AssemblyName` to `Microsoft.AI.Foundry.Local.WinML`; both SKUs share the `net8.0;net9.0` TFM set (WinML SKU omits `netstandard2.0`). - Cross-platform + WinML are independent packages — most samples reference WinML on Windows and the cross-platform package elsewhere, so both must be packed. ## Full clean-rebuild diff --git a/samples/cs/audio-transcription-example/AudioTranscriptionExample.csproj b/samples/cs/audio-transcription-example/AudioTranscriptionExample.csproj index 4f048e152..ce8a65f04 100644 --- a/samples/cs/audio-transcription-example/AudioTranscriptionExample.csproj +++ b/samples/cs/audio-transcription-example/AudioTranscriptionExample.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/embeddings/Embeddings.csproj b/samples/cs/embeddings/Embeddings.csproj index 870c34acd..97cb8ef34 100644 --- a/samples/cs/embeddings/Embeddings.csproj +++ b/samples/cs/embeddings/Embeddings.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/foundry-local-web-server/FoundryLocalWebServer.csproj b/samples/cs/foundry-local-web-server/FoundryLocalWebServer.csproj index a7c1a3766..77fc929d6 100644 --- a/samples/cs/foundry-local-web-server/FoundryLocalWebServer.csproj +++ b/samples/cs/foundry-local-web-server/FoundryLocalWebServer.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/live-audio-transcription/LiveAudioTranscriptionExample.csproj b/samples/cs/live-audio-transcription/LiveAudioTranscriptionExample.csproj index 1a276b73d..4a0eed349 100644 --- a/samples/cs/live-audio-transcription/LiveAudioTranscriptionExample.csproj +++ b/samples/cs/live-audio-transcription/LiveAudioTranscriptionExample.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/model-management-example/ModelManagementExample.csproj b/samples/cs/model-management-example/ModelManagementExample.csproj index 870c34acd..97cb8ef34 100644 --- a/samples/cs/model-management-example/ModelManagementExample.csproj +++ b/samples/cs/model-management-example/ModelManagementExample.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/native-chat-completions/NativeChatCompletions.csproj b/samples/cs/native-chat-completions/NativeChatCompletions.csproj index 870c34acd..97cb8ef34 100644 --- a/samples/cs/native-chat-completions/NativeChatCompletions.csproj +++ b/samples/cs/native-chat-completions/NativeChatCompletions.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tool-calling-foundry-local-sdk/ToolCallingFoundryLocalSdk.csproj b/samples/cs/tool-calling-foundry-local-sdk/ToolCallingFoundryLocalSdk.csproj index 870c34acd..97cb8ef34 100644 --- a/samples/cs/tool-calling-foundry-local-sdk/ToolCallingFoundryLocalSdk.csproj +++ b/samples/cs/tool-calling-foundry-local-sdk/ToolCallingFoundryLocalSdk.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tool-calling-foundry-local-web-server/ToolCallingFoundryLocalWebServer.csproj b/samples/cs/tool-calling-foundry-local-web-server/ToolCallingFoundryLocalWebServer.csproj index a7c1a3766..77fc929d6 100644 --- a/samples/cs/tool-calling-foundry-local-web-server/ToolCallingFoundryLocalWebServer.csproj +++ b/samples/cs/tool-calling-foundry-local-web-server/ToolCallingFoundryLocalWebServer.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj b/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj index f07da7a75..fcc9257da 100644 --- a/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj +++ b/samples/cs/tutorial-chat-assistant/TutorialChatAssistant.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj b/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj index f07da7a75..fcc9257da 100644 --- a/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj +++ b/samples/cs/tutorial-document-summarizer/TutorialDocumentSummarizer.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj b/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj index f07da7a75..fcc9257da 100644 --- a/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj +++ b/samples/cs/tutorial-tool-calling/TutorialToolCalling.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj b/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj index f07da7a75..fcc9257da 100644 --- a/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj +++ b/samples/cs/tutorial-voice-to-text/TutorialVoiceToText.csproj @@ -4,18 +4,6 @@ Exe enable enable - - - - - net9.0-windows10.0.18362.0 - ARM64;x64 - None - false - - - - net9.0 diff --git a/samples/cs/verify-winml/VerifyWinML.csproj b/samples/cs/verify-winml/VerifyWinML.csproj index 151f655f3..860aa6740 100644 --- a/samples/cs/verify-winml/VerifyWinML.csproj +++ b/samples/cs/verify-winml/VerifyWinML.csproj @@ -4,16 +4,6 @@ Exe enable enable - - - - net9.0-windows10.0.18362.0 - x64;ARM64 - None - false - - - net9.0 diff --git a/sdk_v2/DEVELOPMENT.md b/sdk_v2/DEVELOPMENT.md index c9117dd13..bc8afb6c2 100644 --- a/sdk_v2/DEVELOPMENT.md +++ b/sdk_v2/DEVELOPMENT.md @@ -23,7 +23,7 @@ All four SDKs (C++, C#, Python, JS/TS) build on **Windows**, **Linux**, and | CMake | 3.20 | Driven by `sdk_v2/cpp/build.py`; do not invoke `cmake --build` directly. | | vcpkg | recent | Set `VCPKG_ROOT`, or use the copy bundled with Visual Studio (auto-detected). | | Python | 3.11–3.14, **64-bit** | Required by `build.py` and for the Python SDK. 32-bit Python will not work. | -| .NET SDK | 9.0 | C# projects target `net8.0;net9.0;netstandard2.0` (and `net462` on Windows from the .NET Framework Targeting Pack via VS); the WinML variant targets `net9.0-windows10.0.26100.0`. | +| .NET SDK | 9.0 | The SDK targets `net8.0;net9.0;netstandard2.0`; the test project additionally targets `net462` on Windows (via the .NET Framework Targeting Pack from VS); samples target `net9.0`. The WinML SKU shares the same SDK TFMs (minus `netstandard2.0`) — the Windows OS-version floor for WinML 2.x is enforced by the native runtime (`LoadLibraryW` + `RtlGetVersion` in `winml_ep_bootstrapper.cc`), not by a .NET TFM. | | Node.js | 20 LTS or newer | Brings `npm`. The JS SDK declares `"engines": { "node": ">=20" }`. | | PowerShell | 7+ (`pwsh`) | The one-shot script and `samples/js/test-v2.ps1` are written for PowerShell 7. | @@ -32,7 +32,7 @@ All four SDKs (C++, C#, Python, JS/TS) build on **Windows**, **Linux**, and | Tool | Version | Notes | | --------------------------- | ------------------------ | --------------------------------------------------------------------- | | Visual Studio 2026 (v18) | Enterprise / Professional / Community | Install the **Desktop development with C++** and **.NET desktop development** workloads. Provides MSVC, Windows SDK, and vcpkg. | -| Windows SDK | 10.0.26100 (VS workload) | Needed for the WinML target framework. | +| Windows SDK | 10.0 (VS workload) | Required by the C++ build for any `windows.h` consumer. The exact patch number is not pinned — whatever the VS C++ workload installs (currently 10.0.26100) works. | | .NET Framework 4.6.2 Targeting Pack | (VS workload) | One C# test target framework is `net462`. | Launch your dev shell with **x64** explicitly — `Enter-VsDevShell` defaults @@ -64,7 +64,7 @@ install per-SDK package dependencies on first run: | SDK | What runs | | ------ | -------------------------------------------------------------------------------------- | -| C++ | `python build.py [--config ...] [--use_winml] [--skip_tests]` — configure + build + ctest. vcpkg restores native deps; ORT/GenAI come from NuGet via FetchContent (versions from `sdk_v2/deps_versions.json` or `_winml.json`). | +| C++ | `python build.py [--config ...] [--use_winml] [--skip_tests]` — configure + build + ctest. vcpkg restores native deps; ORT/GenAI come from NuGet via FetchContent (versions from `sdk_v2/deps_versions.json`). | | C# | `dotnet test Microsoft.AI.Foundry.Local.SDK.sln -c Release [-p:UseWinML=true]` — restores NuGet packages on demand. | | Python | `python -m pip install -e .[dev]` (compiles the cffi extension; needs MSVC/Clang) → `python -m pytest test/`. | | JS | `npm install` (runs `node-gyp` against the C++ build output) → `npm run build` → `npm test` (vitest). | diff --git a/sdk_v2/build_and_test_all.ps1 b/sdk_v2/build_and_test_all.ps1 index fff54329a..e01561bdd 100644 --- a/sdk_v2/build_and_test_all.ps1 +++ b/sdk_v2/build_and_test_all.ps1 @@ -150,15 +150,32 @@ try { Invoke-Step 'cs' { Push-Location $csDir try { - $dotnetArgs = @( - 'test', - 'Microsoft.AI.Foundry.Local.SDK.sln', - '-c', $dotnetConfig, - '--nologo' - ) - if ($UseWinml) { $dotnetArgs += '-p:UseWinML=true' } - dotnet @dotnetArgs - if ($LASTEXITCODE -ne 0) { throw "dotnet test exit $LASTEXITCODE" } + $sln = 'Microsoft.AI.Foundry.Local.SDK.sln' + $buildArgs = @('build', $sln, '-c', $dotnetConfig, '--nologo') + if ($UseWinml) { $buildArgs += '-p:UseWinML=true' } + dotnet @buildArgs + if ($LASTEXITCODE -ne 0) { throw "dotnet build exit $LASTEXITCODE" } + + # The test csproj multi-targets net8.0/net9.0 (and net462 on Windows + # non-WinML) for build coverage; run the .NET (Core) test suite once + # on net9.0 (back-compat covers net8.0 consumers) plus net462 on + # Windows non-WinML to exercise the netstandard polyfills at runtime. + $frameworks = @('net9.0') + if ($IsWindows -and -not $UseWinml) { $frameworks += 'net462' } + + foreach ($tfm in $frameworks) { + Write-Host "=== dotnet test --framework $tfm ===" + $testArgs = @( + 'test', $sln, + '-c', $dotnetConfig, + '--no-build', + '--framework', $tfm, + '--nologo' + ) + if ($UseWinml) { $testArgs += '-p:UseWinML=true' } + dotnet @testArgs + if ($LASTEXITCODE -ne 0) { throw "dotnet test ($tfm) exit $LASTEXITCODE" } + } } finally { Pop-Location } diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index e9b046e76..ac2f814c5 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -44,7 +44,7 @@ option(FOUNDRY_LOCAL_BUILD_TESTS "Build unit tests" ON) option(FOUNDRY_LOCAL_BUILD_EXAMPLES "Build example programs" ON) option(FOUNDRY_LOCAL_BUILD_TOOLS "Build internal build-time tools (catalog_snapshot, ...)" ON) option(FOUNDRY_LOCAL_BUILD_SERVICE "Build web service support (requires oat++)" ON) -option(FOUNDRY_LOCAL_USE_WINML "Use WinML/WindowsAppSDK.ML for OnnxRuntime instead of standalone ORT" OFF) +option(FOUNDRY_LOCAL_USE_WINML "Enable the WinML 2.x EP catalog (Microsoft.Windows.AI.MachineLearning) for hardware EP discovery" OFF) option(FOUNDRY_LOCAL_ENABLE_ASAN "Enable AddressSanitizer + UndefinedBehaviorSanitizer (Linux only)" OFF) # Android: interactive examples and host tools don't run on device @@ -78,14 +78,16 @@ if(FOUNDRY_LOCAL_BUILD_TESTS) enable_testing() endif() -# ORT and ORT GenAI — acquired via FetchContent from nuget.org (or WinML SDK when FOUNDRY_LOCAL_USE_WINML=ON) +# ORT and ORT GenAI — acquired via FetchContent from nuget.org. list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(OnnxRuntimeGenAI REQUIRED) find_package(OnnxRuntime REQUIRED) -# WinML EP Catalog — Windows-only, for EP discovery and download. -# Not REQUIRED: builds without it fall back to CPU-only EP detection. -find_package(WinMLEpCatalog) +# WinML EP Catalog — Windows-only, for EP discovery and download. Only fetched +# when the WinML SKU is requested so non-WinML builds don't pull the catalog DLL. +if(FOUNDRY_LOCAL_USE_WINML) + find_package(WinMLEpCatalog) +endif() # -------------------------------------------------------------------------- # Library target @@ -101,7 +103,6 @@ set(FOUNDRY_LOCAL_PLATFORM_SOURCES) if(WIN32) list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES src/util/stacktrace_windows.cc - src/ep_detection/winml_ep_bootstrapper.cc src/platform/windows/path.cc ) else() @@ -111,6 +112,17 @@ else() ) endif() +# WinML EP bootstrapper is only built when the WinML EP catalog package is +# available — the source unconditionally references its headers/types. Other +# Windows builds (no WinML SKU) skip it entirely; manager.cc gates the call +# site on FOUNDRY_LOCAL_HAS_EP_CATALOG so DiscoverProviders is only invoked +# when this translation unit is linked in. +if(WinMLEpCatalog_FOUND) + list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES + src/ep_detection/winml_ep_bootstrapper.cc + ) +endif() + if(ANDROID) list(APPEND FOUNDRY_LOCAL_PLATFORM_SOURCES src/platform/android/ssl_cert_checker.cc) endif() @@ -177,7 +189,6 @@ set(FOUNDRY_LOCAL_SOURCES src/inferencing/predictive/inference_session.cc src/inferencing/predictive/tensor.cc src/manager.cc - src/winml_bootstrap.cc src/contracts/chat_completions.cc src/contracts/chat_completions_converter.cc src/contracts/audio_transcriptions.cc @@ -263,29 +274,12 @@ function(foundry_local_configure_target TARGET LINK_SCOPE) target_compile_definitions(${TARGET} ${LINK_SCOPE} FOUNDRY_LOCAL_HAS_WEB_SERVICE=1) endif() - if(FOUNDRY_LOCAL_USE_WINML) - target_compile_definitions(${TARGET} PUBLIC FOUNDRY_LOCAL_USE_WINML=1) - else() - target_compile_definitions(${TARGET} PUBLIC FOUNDRY_LOCAL_USE_WINML=0) - endif() - if(WinMLEpCatalog_FOUND) target_link_libraries(${TARGET} ${LINK_SCOPE} WinMLEpCatalog::WinMLEpCatalog) target_compile_definitions(${TARGET} ${LINK_SCOPE} FOUNDRY_LOCAL_HAS_EP_CATALOG=1) else() target_compile_definitions(${TARGET} ${LINK_SCOPE} FOUNDRY_LOCAL_HAS_EP_CATALOG=0) endif() - - # WinAppSDK Bootstrap (MddBootstrapInitialize2). Only linked in WinML builds, since - # WinML by definition depends on the WindowsAppSDK runtime; non-WinML builds skip it - # and the bootstrap call site in Manager::Create is compiled out via the same macro. - if(FOUNDRY_LOCAL_USE_WINML) - if(NOT TARGET WinAppSdkBootstrap::WinAppSdkBootstrap) - message(FATAL_ERROR "FOUNDRY_LOCAL_USE_WINML=ON but the WinAppSDK Bootstrap import " - "library was not found. See FindWinMLEpCatalog.cmake.") - endif() - target_link_libraries(${TARGET} ${LINK_SCOPE} WinAppSdkBootstrap::WinAppSdkBootstrap) - endif() endfunction() # -------------------------------------------------------------------------- @@ -420,16 +414,6 @@ if(TARGET OnnxRuntime::OnnxRuntime) $ ) endif() - - # Co-locate the WinAppSDK bootstrap DLL with foundry_local.dll. Required at runtime - # because it's an import-library dependency of the WinML build, not a delay-loaded one. - if(FOUNDRY_LOCAL_USE_WINML AND WINAPPSDK_BOOTSTRAP_DLL AND EXISTS "${WINAPPSDK_BOOTSTRAP_DLL}") - add_custom_command(TARGET foundry_local POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "${WINAPPSDK_BOOTSTRAP_DLL}" - $ - ) - endif() elseif(APPLE) # macOS: copy dylibs so consumers that only link libfoundry_local.dylib (e.g. cache_only_tests) find the correct # ORT version instead of any system-installed ORT, which would cause an Ort::InitApi() version mismatch. diff --git a/sdk_v2/cpp/build.py b/sdk_v2/cpp/build.py index f4e2a6a39..82acbff4f 100644 --- a/sdk_v2/cpp/build.py +++ b/sdk_v2/cpp/build.py @@ -162,13 +162,13 @@ class HelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescript ) parser.add_argument( "--use_winml", action="store_true", - help="Enable the WinML EP catalog (Microsoft.WindowsAppSDK.ML) for hardware EP " - "discovery. ORT itself still comes from Microsoft.ML.OnnxRuntime.Foundry; " + help="Enable the WinML EP catalog (Microsoft.Windows.AI.MachineLearning, WinML 2.x reg-free " + "runtime) for hardware EP discovery. ORT itself still comes from Microsoft.ML.OnnxRuntime.Foundry; " "this flag only adds the WinML EP catalog client.", ) parser.add_argument( "--winml_sdk_version", default=None, type=str, - help="Version of Microsoft.WindowsAppSDK.ML NuGet package (used for the WinML EP " + help="Version of Microsoft.Windows.AI.MachineLearning NuGet package (used for the WinML EP " "catalog when --use_winml is set; defaults to the version pinned in " "FindWinMLEpCatalog.cmake).", ) @@ -465,7 +465,7 @@ def configure(args: argparse.Namespace) -> None: if args.use_winml: command += ["-DFOUNDRY_LOCAL_USE_WINML=ON"] if args.winml_sdk_version: - command += [f"-DWINML_SDK_VERSION={args.winml_sdk_version}"] + command += [f"-DWINML_EP_CATALOG_VERSION={args.winml_sdk_version}"] else: # Pass explicitly so a re-configure without the flag clears any cached ON value. command += ["-DFOUNDRY_LOCAL_USE_WINML=OFF"] diff --git a/sdk_v2/cpp/cmake/FindOnnxRuntime.cmake b/sdk_v2/cpp/cmake/FindOnnxRuntime.cmake index 2ec92011f..197894e01 100644 --- a/sdk_v2/cpp/cmake/FindOnnxRuntime.cmake +++ b/sdk_v2/cpp/cmake/FindOnnxRuntime.cmake @@ -1,12 +1,11 @@ # Copyright (c) Microsoft. All rights reserved. # Find/acquire ONNX Runtime. # -# ORT is always sourced from Microsoft.ML.OnnxRuntime.Foundry (or -# Microsoft.ML.OnnxRuntime on Android) via FetchContent — nuget.org for releases, -# the ORT-Nightly ADO feed for -dev- versions. The FOUNDRY_LOCAL_USE_WINML flag -# does NOT change the ORT package source; it only: -# - selects a WinML-compatible ORT version (see version branch below), and -# - opts in to the WinML EP catalog (handled by FindWinMLEpCatalog.cmake). +# Sources ORT from Microsoft.ML.OnnxRuntime.Foundry (or Microsoft.ML.OnnxRuntime +# on Android) via FetchContent — nuget.org for releases, the ORT-Nightly ADO +# feed for -dev- versions. The version comes from sdk_v2/deps_versions.json and +# is shared by WinML and non-WinML builds; FOUNDRY_LOCAL_USE_WINML only gates +# the WinML EP catalog in FindWinMLEpCatalog.cmake. # # Creates an IMPORTED target: OnnxRuntime::OnnxRuntime @@ -80,14 +79,10 @@ else() # Standard path: FetchContent from nuget.org (releases) or ORT-Nightly ADO feed (dev builds) # ----------------------------------------------------------------------- if(NOT ORT_VERSION) - # Single source of truth: sdk_v2/deps_versions[_winml].json. The Python - # SDK build backend reads the same files so wheel deps and native ABI + # Single source of truth: sdk_v2/deps_versions.json. The Python SDK + # build backend reads the same file so wheel deps and native ABI # always agree. Override at the cmake command line with -DORT_VERSION=... - if(FOUNDRY_LOCAL_USE_WINML) - set(_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions_winml.json") - else() - set(_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions.json") - endif() + set(_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions.json") if(NOT EXISTS "${_DEPS_FILE}") message(FATAL_ERROR "Required versions file not found: ${_DEPS_FILE}") endif() @@ -245,8 +240,7 @@ else() set_target_properties(OnnxRuntime::OnnxRuntime PROPERTIES IMPORTED_IMPLIB "${_ORT_LIB_DIR}/onnxruntime.lib" ) - # On Windows the runtime DLL usually sits next to the import lib; WinML SDK is the - # exception (DLL lives under runtimes-framework/), so honour _ORT_DLL_DIR if set. + # On Windows, the runtime DLL sits next to the import lib for both flavors. if(NOT _ORT_DLL_DIR) set(_ORT_DLL_DIR "${_ORT_LIB_DIR}") endif() diff --git a/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake b/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake index 4906c254b..f83002d64 100644 --- a/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake +++ b/sdk_v2/cpp/cmake/FindOnnxRuntimeGenAI.cmake @@ -1,10 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. # Find/acquire ONNX Runtime GenAI. # -# Windows + FOUNDRY_LOCAL_USE_WINML=ON: Microsoft.ML.OnnxRuntimeGenAI.WinML -# Windows + FOUNDRY_LOCAL_USE_WINML=OFF: Microsoft.ML.OnnxRuntimeGenAI.Foundry -# Linux: Microsoft.ML.OnnxRuntimeGenAI.Foundry -# macOS: Microsoft.ML.OnnxRuntimeGenAI.Foundry +# All platforms / flavors: Microsoft.ML.OnnxRuntimeGenAI.Foundry # # When ORT_GENAI_HOME is set, uses the local ORT GenAI build instead of NuGet. # Otherwise uses FetchContent from nuget.org. @@ -104,21 +101,13 @@ else() message(FATAL_ERROR "Unsupported platform for OnnxRuntimeGenAI: ${CMAKE_GENERATOR_PLATFORM} on ${CMAKE_SYSTEM_NAME}") endif() -if(FOUNDRY_LOCAL_USE_WINML) - set(_GENAI_PACKAGE_NAME "Microsoft.ML.OnnxRuntimeGenAI.WinML") -else() - set(_GENAI_PACKAGE_NAME "Microsoft.ML.OnnxRuntimeGenAI.Foundry") -endif() +set(_GENAI_PACKAGE_NAME "Microsoft.ML.OnnxRuntimeGenAI.Foundry") if(NOT ORT_GENAI_VERSION) - # Single source of truth: sdk_v2/deps_versions[_winml].json. The Python - # SDK build backend reads the same files. Override at the cmake command - # line with -DORT_GENAI_VERSION=... - if(FOUNDRY_LOCAL_USE_WINML) - set(_GENAI_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions_winml.json") - else() - set(_GENAI_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions.json") - endif() + # Single source of truth: sdk_v2/deps_versions.json. The Python SDK build + # backend reads the same file. Override at the cmake command line with + # -DORT_GENAI_VERSION=... + set(_GENAI_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions.json") if(NOT EXISTS "${_GENAI_DEPS_FILE}") message(FATAL_ERROR "Required versions file not found: ${_GENAI_DEPS_FILE}") endif() diff --git a/sdk_v2/cpp/cmake/FindWinMLEpCatalog.cmake b/sdk_v2/cpp/cmake/FindWinMLEpCatalog.cmake index 2f83cf836..78793feeb 100644 --- a/sdk_v2/cpp/cmake/FindWinMLEpCatalog.cmake +++ b/sdk_v2/cpp/cmake/FindWinMLEpCatalog.cmake @@ -1,15 +1,18 @@ # Copyright (c) Microsoft. All rights reserved. -# Find/acquire the WinML EP Catalog C API from Microsoft.WindowsAppSDK.ML. +# Find/acquire the WinML EP Catalog C API from Microsoft.Windows.AI.MachineLearning. # -# Downloads the NuGet package if needed, then creates an IMPORTED target for -# the EP catalog library (Microsoft.Windows.AI.MachineLearning.dll/.lib). +# Downloads the NuGet package if needed, then loads the package's first-party +# CMake config (build/cmake/microsoft.windows.ai.machinelearning-config.cmake) +# for target discovery. The config defines WindowsML::Api (EP catalog) and +# WindowsML::OnnxRuntime; only WindowsML::Api is linked here — ORT comes from +# FindOnnxRuntime.cmake. # -# This is separate from ORT — we use the EP catalog to discover and download -# hardware-specific execution providers at runtime. ORT itself comes from -# FindOnnxRuntime.cmake (either WinML SDK or ORT-Nightly feed). +# Microsoft.Windows.AI.MachineLearning ships a self-contained native DLL that +# loads directly from any unpackaged app on Windows 10 19H1 (build 18362) or +# newer, with no Windows App SDK runtime bootstrap required. # -# Creates an IMPORTED target: WinMLEpCatalog::WinMLEpCatalog -# Sets: WINML_EP_CATALOG_HEADER_DIR, WINML_EP_CATALOG_DLL_DIR +# Exposes an ALIAS target: WinMLEpCatalog::WinMLEpCatalog -> WindowsML::Api +# Sets: WINML_EP_CATALOG_DLL_DIR (= WINML_BINARY_DIR) if(WinMLEpCatalog_FOUND) return() @@ -22,17 +25,39 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows") return() endif() -# Latest stable Microsoft.WindowsAppSDK.ML 1.8.x on nuget.org. Anything older -# than 1.8.2141 silently disables EP detection (no WinMLEpCatalog.h). -set(_WINML_EP_CATALOG_MIN_VERSION "1.8.2192") - -# WINML_EP_CATALOG_VERSION may be set explicitly; otherwise pick the minimum -# known-good version. We deliberately do NOT inherit WINML_SDK_VERSION here: -# the WinML SDK and the EP catalog package have independent compatibility -# requirements (the EP catalog ships only in newer WindowsAppSDK.ML packages, -# and our build no longer uses the WinML-bundled ORT regardless). +# Version comes from sdk_v2/deps_versions.json (single source of truth, same +# pattern as FindOnnxRuntime.cmake). The WinMLEpCatalog.h C ABI is stable +# across 2.0.x and 2.1.x. Override at the cmake command line with +# -DWINML_EP_CATALOG_VERSION=... if(NOT WINML_EP_CATALOG_VERSION) - set(WINML_EP_CATALOG_VERSION "${_WINML_EP_CATALOG_MIN_VERSION}") + set(_DEPS_FILE "${CMAKE_CURRENT_LIST_DIR}/../../deps_versions.json") + if(NOT EXISTS "${_DEPS_FILE}") + message(FATAL_ERROR "Required versions file not found: ${_DEPS_FILE}") + endif() + file(READ "${_DEPS_FILE}" _DEPS_JSON) + string(JSON WINML_EP_CATALOG_VERSION GET "${_DEPS_JSON}" "windows-ai-machinelearning" "version") + message(STATUS "WINML_EP_CATALOG_VERSION=${WINML_EP_CATALOG_VERSION} (from ${_DEPS_FILE})") +endif() + +# The package's own CMake config FATAL_ERRORs on architectures it doesn't ship +# binaries for (anything other than x64/ARM64). Pre-check so we degrade to a soft +# disable instead of halting configuration when someone builds e.g. ARM64EC with +# FOUNDRY_LOCAL_USE_WINML=ON. +if(CMAKE_GENERATOR_PLATFORM) + string(TOUPPER "${CMAKE_GENERATOR_PLATFORM}" _WINML_PLATFORM_UPPER) +elseif(CMAKE_VS_PLATFORM_NAME) + string(TOUPPER "${CMAKE_VS_PLATFORM_NAME}" _WINML_PLATFORM_UPPER) +elseif(CMAKE_SYSTEM_PROCESSOR) + string(TOUPPER "${CMAKE_SYSTEM_PROCESSOR}" _WINML_PLATFORM_UPPER) +else() + set(_WINML_PLATFORM_UPPER "X64") +endif() + +if(NOT _WINML_PLATFORM_UPPER MATCHES "^(AMD64|X64|X86_64|ARM64|AARCH64)$") + message(WARNING "WinML EP Catalog: unsupported architecture '${_WINML_PLATFORM_UPPER}'. " + "Package ships x64/ARM64 only. EP detection will be disabled.") + set(WinMLEpCatalog_FOUND FALSE) + return() endif() include(cmake/nuget.cmake) @@ -40,11 +65,6 @@ include(cmake/nuget.cmake) # WINML_EP_CATALOG_FETCH_URL can be set externally (e.g. for CI where nuget.org is blocked). set(WINML_EP_CATALOG_FETCH_URL "" CACHE STRING "Override URL or local path for the WinML EP Catalog NuGet package") -# Microsoft.WindowsAppSDK.Foundation ships the bootstrap (MddBootstrapInitialize2) ABI. When using -# FetchContent (offline CI) it must be supplied alongside the .ML package because nuget transitive -# resolution doesn't run on a raw .nupkg fetch. -set(WINAPPSDK_FOUNDATION_FETCH_URL "" CACHE STRING "Override URL or local path for the Microsoft.WindowsAppSDK.Foundation NuGet package") - if(WINML_EP_CATALOG_FETCH_URL) # Use FetchContent to download/extract the pre-downloaded package include(FetchContent) @@ -61,143 +81,43 @@ if(WINML_EP_CATALOG_FETCH_URL) set(_WINML_EP_ROOT "${winml_ep_catalog_SOURCE_DIR}") message(STATUS "WinML EP Catalog via FetchContent: ${_WINML_EP_ROOT}") else() - install_nuget_package(Microsoft.WindowsAppSDK.ML ${WINML_EP_CATALOG_VERSION} _WINML_EP_ROOT + install_nuget_package(Microsoft.Windows.AI.MachineLearning ${WINML_EP_CATALOG_VERSION} _WINML_EP_ROOT SOURCE https://api.nuget.org/v3/index.json) endif() -# Determine platform for lib/native path -if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "arm64") - set(_WINML_EP_ARCH "arm64") - set(_WINML_EP_RUNTIME_PLATFORM "win-arm64") -elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64EC" OR CMAKE_GENERATOR_PLATFORM STREQUAL "arm64EC") - set(_WINML_EP_ARCH "arm64ec") - set(_WINML_EP_RUNTIME_PLATFORM "win-arm64ec") -else() - set(_WINML_EP_ARCH "x64") - set(_WINML_EP_RUNTIME_PLATFORM "win-x64") -endif() - -set(_WINML_EP_HEADER_DIR "${_WINML_EP_ROOT}/include") -set(_WINML_EP_LIB_DIR "${_WINML_EP_ROOT}/lib/native/${_WINML_EP_ARCH}") -set(_WINML_EP_DLL_DIR "${_WINML_EP_ROOT}/runtimes-framework/${_WINML_EP_RUNTIME_PLATFORM}/native") - -# Validate headers -if(NOT EXISTS "${_WINML_EP_HEADER_DIR}/WinMLEpCatalog.h") - message(WARNING "WinML EP Catalog: WinMLEpCatalog.h not found at ${_WINML_EP_HEADER_DIR}. " - "EP detection will be disabled. Package version may be too old (need ≥1.8.2141).") +# Load the package's first-party CMake config for target discovery and layout +# resolution. The config lives at build/cmake/-config.cmake +# and defines WindowsML::Api / WindowsML::OnnxRuntime / WindowsML::DirectML. +set(_WINML_EP_CONFIG_DIR "${_WINML_EP_ROOT}/build/cmake") +if(NOT EXISTS "${_WINML_EP_CONFIG_DIR}/microsoft.windows.ai.machinelearning-config.cmake") + message(WARNING "WinML EP Catalog: package CMake config not found at ${_WINML_EP_CONFIG_DIR}. " + "Package version may be too old or layout has changed. EP detection will be disabled.") set(WinMLEpCatalog_FOUND FALSE) return() endif() -# Validate import lib -set(_WINML_EP_IMPORT_LIB "${_WINML_EP_LIB_DIR}/Microsoft.Windows.AI.MachineLearning.lib") -if(NOT EXISTS "${_WINML_EP_IMPORT_LIB}") - message(WARNING "WinML EP Catalog: import lib not found at ${_WINML_EP_IMPORT_LIB}. " +set(microsoft.windows.ai.machinelearning_DIR "${_WINML_EP_CONFIG_DIR}" CACHE PATH + "Path to the Microsoft.Windows.AI.MachineLearning CMake config" FORCE) +find_package(microsoft.windows.ai.machinelearning CONFIG REQUIRED) + +if(NOT TARGET WindowsML::Api) + message(WARNING "WinML EP Catalog: WindowsML::Api target not defined after find_package. " "EP detection will be disabled.") set(WinMLEpCatalog_FOUND FALSE) return() endif() -# Create imported target -add_library(WinMLEpCatalog::WinMLEpCatalog SHARED IMPORTED) -set_target_properties(WinMLEpCatalog::WinMLEpCatalog PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${_WINML_EP_HEADER_DIR}" - IMPORTED_IMPLIB "${_WINML_EP_IMPORT_LIB}" -) +# Promote WindowsML::Api to GLOBAL so the alias is visible in any subdirectory +# that links foundry_local transitively, then expose it as WinMLEpCatalog::WinMLEpCatalog. +set_target_properties(WindowsML::Api PROPERTIES IMPORTED_GLOBAL TRUE) +add_library(WinMLEpCatalog::WinMLEpCatalog ALIAS WindowsML::Api) -# Set the DLL location if it exists (for post-build copy) -set(_WINML_EP_DLL "${_WINML_EP_DLL_DIR}/Microsoft.Windows.AI.MachineLearning.dll") -if(EXISTS "${_WINML_EP_DLL}") - set_target_properties(WinMLEpCatalog::WinMLEpCatalog PROPERTIES - IMPORTED_LOCATION "${_WINML_EP_DLL}" - ) -endif() - -# Export paths for downstream use -set(WINML_EP_CATALOG_HEADER_DIR "${_WINML_EP_HEADER_DIR}" CACHE PATH "WinML EP Catalog include directory" FORCE) -set(WINML_EP_CATALOG_DLL_DIR "${_WINML_EP_DLL_DIR}" CACHE PATH "WinML EP Catalog native DLL directory" FORCE) +# Mirror the official config's WINML_BINARY_DIR as WINML_EP_CATALOG_DLL_DIR +# for the post-build DLL-copy step. Include paths flow through the +# WinMLEpCatalog::WinMLEpCatalog target's INTERFACE_INCLUDE_DIRECTORIES. +set(WINML_EP_CATALOG_DLL_DIR "${WINML_BINARY_DIR}" CACHE PATH "WinML EP Catalog native DLL directory" FORCE) set(WinMLEpCatalog_FOUND TRUE) message(STATUS "WinML EP Catalog: ${_WINML_EP_ROOT}") -message(STATUS " Headers: ${_WINML_EP_HEADER_DIR}") -message(STATUS " Import lib: ${_WINML_EP_IMPORT_LIB}") -message(STATUS " DLL dir: ${_WINML_EP_DLL_DIR}") - -# -------------------------------------------------------------------------- -# WinAppSDK Bootstrap (MddBootstrapInitialize2) -# -------------------------------------------------------------------------- -# The bootstrap C ABI ships in Microsoft.WindowsAppSDK.Foundation. There are two acquisition paths: -# 1. nuget install (default) — pulled in transitively as a dependency of Microsoft.WindowsAppSDK.ML. -# 2. FetchContent (offline CI) — caller must supply WINAPPSDK_FOUNDATION_FETCH_URL pointing at the -# Foundation .nupkg, since FetchContent doesn't resolve nuget transitive deps. -# -# Either path resolves to a directory layout matching the NuGet package and exposes: -# - WinAppSdkBootstrap::WinAppSdkBootstrap (IMPORTED SHARED) — link to get -# Microsoft.WindowsAppRuntime.Bootstrap.lib + the MddBootstrap.h include path. -# - WINAPPSDK_BOOTSTRAP_DLL — full path to Microsoft.WindowsAppRuntime.Bootstrap.dll -# for post-build co-location next to the consuming binary. -set(_WINAPPSDK_FOUNDATION_ROOT "") - -if(WINAPPSDK_FOUNDATION_FETCH_URL) - include(FetchContent) - string(REPLACE "\\" "/" WINAPPSDK_FOUNDATION_FETCH_URL "${WINAPPSDK_FOUNDATION_FETCH_URL}") - if(WINAPPSDK_FOUNDATION_FETCH_URL MATCHES "\\.nupkg$" AND NOT WINAPPSDK_FOUNDATION_FETCH_URL MATCHES "^https?://") - set(_WINAPPSDK_FOUNDATION_ZIP_PATH "${CMAKE_BINARY_DIR}/_deps/winappsdk_foundation-download/winappsdk_foundation.zip") - get_filename_component(_WINAPPSDK_FOUNDATION_ZIP_DIR "${_WINAPPSDK_FOUNDATION_ZIP_PATH}" DIRECTORY) - file(MAKE_DIRECTORY "${_WINAPPSDK_FOUNDATION_ZIP_DIR}") - file(COPY_FILE "${WINAPPSDK_FOUNDATION_FETCH_URL}" "${_WINAPPSDK_FOUNDATION_ZIP_PATH}") - set(WINAPPSDK_FOUNDATION_FETCH_URL "${_WINAPPSDK_FOUNDATION_ZIP_PATH}") - endif() - FetchContent_Declare(winappsdk_foundation URL ${WINAPPSDK_FOUNDATION_FETCH_URL} - DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_NAME winappsdk_foundation.zip) - FetchContent_MakeAvailable(winappsdk_foundation) - set(_WINAPPSDK_FOUNDATION_ROOT "${winappsdk_foundation_SOURCE_DIR}") - message(STATUS "WinAppSDK Foundation via FetchContent: ${_WINAPPSDK_FOUNDATION_ROOT}") -elseif(NOT WINML_EP_CATALOG_FETCH_URL) - if(NOT NUGET_PACKAGE_ROOT_PATH) - set(NUGET_PACKAGE_ROOT_PATH "${CMAKE_BINARY_DIR}/__nuget") - endif() - file(GLOB _WINAPPSDK_FOUNDATION_DIRS LIST_DIRECTORIES TRUE - "${NUGET_PACKAGE_ROOT_PATH}/Microsoft.WindowsAppSDK.Foundation.*") - if(_WINAPPSDK_FOUNDATION_DIRS) - list(SORT _WINAPPSDK_FOUNDATION_DIRS COMPARE NATURAL ORDER DESCENDING) - list(GET _WINAPPSDK_FOUNDATION_DIRS 0 _WINAPPSDK_FOUNDATION_ROOT) - endif() -endif() - -if(_WINAPPSDK_FOUNDATION_ROOT) - set(_WINAPPSDK_BOOTSTRAP_HEADER_DIR "${_WINAPPSDK_FOUNDATION_ROOT}/include") - set(_WINAPPSDK_BOOTSTRAP_LIB - "${_WINAPPSDK_FOUNDATION_ROOT}/lib/native/${_WINML_EP_ARCH}/Microsoft.WindowsAppRuntime.Bootstrap.lib") - set(_WINAPPSDK_BOOTSTRAP_DLL - "${_WINAPPSDK_FOUNDATION_ROOT}/runtimes/${_WINML_EP_RUNTIME_PLATFORM}/native/Microsoft.WindowsAppRuntime.Bootstrap.dll") - - if(EXISTS "${_WINAPPSDK_BOOTSTRAP_HEADER_DIR}/MddBootstrap.h" AND EXISTS "${_WINAPPSDK_BOOTSTRAP_LIB}") - add_library(WinAppSdkBootstrap::WinAppSdkBootstrap SHARED IMPORTED) - set_target_properties(WinAppSdkBootstrap::WinAppSdkBootstrap PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${_WINAPPSDK_BOOTSTRAP_HEADER_DIR}" - IMPORTED_IMPLIB "${_WINAPPSDK_BOOTSTRAP_LIB}" - ) - if(EXISTS "${_WINAPPSDK_BOOTSTRAP_DLL}") - set_target_properties(WinAppSdkBootstrap::WinAppSdkBootstrap PROPERTIES - IMPORTED_LOCATION "${_WINAPPSDK_BOOTSTRAP_DLL}" - ) - set(WINAPPSDK_BOOTSTRAP_DLL "${_WINAPPSDK_BOOTSTRAP_DLL}" - CACHE FILEPATH "Microsoft.WindowsAppRuntime.Bootstrap.dll for post-build copy" FORCE) - endif() - set(WinAppSdkBootstrap_FOUND TRUE) - message(STATUS "WinAppSDK Bootstrap: ${_WINAPPSDK_FOUNDATION_ROOT}") - message(STATUS " Import lib: ${_WINAPPSDK_BOOTSTRAP_LIB}") - message(STATUS " DLL: ${_WINAPPSDK_BOOTSTRAP_DLL}") - else() - message(WARNING "WinAppSDK Bootstrap: header or import lib missing under " - "${_WINAPPSDK_FOUNDATION_ROOT}. Bootstrap.Initialize() will be a no-op.") - endif() -elseif(WINML_EP_CATALOG_FETCH_URL) - message(WARNING "WinAppSDK Bootstrap: WINML_EP_CATALOG_FETCH_URL is set but " - "WINAPPSDK_FOUNDATION_FETCH_URL is not. WinML builds in this configuration " - "will fail to link the bootstrap.") -else() - message(WARNING "WinAppSDK Bootstrap: Microsoft.WindowsAppSDK.Foundation NuGet not found " - "under ${NUGET_PACKAGE_ROOT_PATH}. Bootstrap.Initialize() will be a no-op.") -endif() +message(STATUS " Target: WinMLEpCatalog::WinMLEpCatalog -> WindowsML::Api") +message(STATUS " DLL dir: ${WINML_EP_CATALOG_DLL_DIR}") diff --git a/sdk_v2/cpp/docs/CppPortGuide.md b/sdk_v2/cpp/docs/CppPortGuide.md index 8fc3772f7..5eaac27af 100644 --- a/sdk_v2/cpp/docs/CppPortGuide.md +++ b/sdk_v2/cpp/docs/CppPortGuide.md @@ -399,7 +399,7 @@ HuggingFace, NIM, and WCR providers were not ported. | `EpDetector` | `EpDetector` (implements `IEpDetector`) | Real detection via ORT `GetAvailableProviders()` + `GetEpDevices()` | | `IEpBootstrapper` | `IEpBootstrapper` (interface) | Bootstrapping interface for EP package download/registration | | `CudaEpBootstrapper` | `CudaEpBootstrapper` | Downloads CUDA EP zip, extracts, prepends to PATH, registers with ORT | -| `WinMLEpBootstrapper` | `WinMLEpBootstrapper` | Discovers WinML EPs via `Microsoft.Windows.AI.MachineLearning.dll` catalog API. Windows 11 24H2+ (build 26100). | +| `WinMLEpBootstrapper` | `WinMLEpBootstrapper` | Discovers WinML EPs via `Microsoft.Windows.AI.MachineLearning.dll` catalog API. WinML 2.x reg-free runtime, Windows 10 19H1+ (build 18362). | --- diff --git a/sdk_v2/cpp/docs/EpDetectionPlan.md b/sdk_v2/cpp/docs/EpDetectionPlan.md index 1dbd98857..935d3c53b 100644 --- a/sdk_v2/cpp/docs/EpDetectionPlan.md +++ b/sdk_v2/cpp/docs/EpDetectionPlan.md @@ -22,7 +22,7 @@ bootstrapping. C ABI (foundry_local_c.h) └── IEpDetector interface (expanded) ├── EpDetector (orchestrator — replaces stub) - │ ├── WinMLEpBootstrapper (Windows 11 24H2+, WinML package present) + │ ├── WinMLEpBootstrapper (Windows 10 19H1+ reg-free, WinML 2.x package present) │ ├── CudaEpBootstrapper (Windows x64 + NVIDIA, Linux x64) │ └── [future bootstrappers as needed] └── StubEpDetector (fallback — CPU only, used in tests) @@ -43,9 +43,13 @@ C ABI (foundry_local_c.h) ## Dependencies -### Microsoft.WindowsAppSDK.ML NuGet (via vcpkg) +### Microsoft.Windows.AI.MachineLearning NuGet (WinML 2.x) -**Package:** `Microsoft.WindowsAppSDK.ML` v1.8.2141+ +**Package:** `Microsoft.Windows.AI.MachineLearning` 2.1.70 (or newer GA) + +WinML 2.x is reg-free: the package ships a single self-contained native DLL +that loads directly on Windows 10 19H1 (build 18362) and later. There is no +Windows App SDK bootstrap step. **What we need from the package:** @@ -54,7 +58,7 @@ C ABI (foundry_local_c.h) | `WinMLEpCatalog.h` | `include/` | C API for EP catalog — enumerate, query, download, register | | `WinMLAsync.h` | `include/` | Async block + progress callback infrastructure | | `Microsoft.Windows.AI.MachineLearning.lib` | `lib/native/x64/` | Import library (delay-loaded) | -| `Microsoft.Windows.AI.MachineLearning.dll` | `runtimes-framework/win-x64/native/` | Runtime DLL (deploy alongside) | +| `Microsoft.Windows.AI.MachineLearning.dll` | `runtimes/win-x64/native/` | Runtime DLL (deploy alongside) | **What we do NOT use from the package:** @@ -73,7 +77,7 @@ because ORT's C API is ABI-stable. ### WinML EP Catalog C API Summary -The v1.8.2141 package provides a pure C API (no WinRT/C++/WinRT projection needed): +The `Microsoft.Windows.AI.MachineLearning` 2.1.70 package provides a pure C API (no WinRT/C++/WinRT projection needed): ```c // Catalog lifecycle @@ -119,31 +123,31 @@ STDAPI WinMLEpEnsureReadyAsync(WinMLEpHandle ep, WinMLAsyncBlock* async); > which can't be safely exercised via integration tests. Discovery and query paths are > tested through the full stack in `test/sdk_api/ep_detection_test.cc`. -### Phase 1: vcpkg Port for Microsoft.WindowsAppSDK.ML ✅ +### Phase 1: NuGet Acquisition for Microsoft.Windows.AI.MachineLearning ✅ **Goal:** Make the WinML headers and import lib available to CMake. -**Approach:** Custom vcpkg port that extracts from the NuGet package. +**Approach:** FetchContent-based download of the WinML 2.x NuGet package, no vcpkg port needed. -**What the port provides:** -- `WinMLEpCatalog.h`, `WinMLAsync.h` → vcpkg include path -- `Microsoft.Windows.AI.MachineLearning.lib` → vcpkg lib path (x64, arm64) +**What we use from the package:** +- `WinMLEpCatalog.h`, `WinMLAsync.h` → include path +- `Microsoft.Windows.AI.MachineLearning.lib` → import library (x64, arm64) - `Microsoft.Windows.AI.MachineLearning.dll` → copied to output dir for deployment -**What the port excludes:** -- All ORT files (`onnxruntime.dll`, `onnxruntime.lib`, etc.) +**What we exclude:** +- All ORT files (`onnxruntime.dll`, `onnxruntime.lib`, etc.) — Foundry-Local ships its own ORT - `DirectML.dll` (loaded by EPs themselves) - Auto-initializer `.cpp` files - Model catalog headers (we have our own) **CMake integration:** -- `find_package(WinMLEpCatalog)` or direct target -- Windows-only. Guarded by `if(WIN32)`. +- `find_package(WinMLEpCatalog)` — gated behind `FOUNDRY_LOCAL_USE_WINML` +- Windows-only. - Linked with `/DELAYLOAD:Microsoft.Windows.AI.MachineLearning.dll` — the DLL may not be present on older systems **Validation:** Write a minimal test that calls `WinMLEpCatalogCreate()` + -`WinMLEpCatalogEnumProviders()` and prints discovered EPs. Run on Windows 11 24H2+. +`WinMLEpCatalogEnumProviders()` and prints discovered EPs. Run on Windows 10 19H1+. --- @@ -220,20 +224,30 @@ New methods return empty/no-op. **Design:** - Each instance wraps a single `WinMLEpHandle` obtained from catalog enumeration. -- `Name()` → `WinMLEpGetName()` -- `IsRegistered()` → `WinMLEpGetReadyState() == WinMLEpReadyState_Ready` -- `DownloadAndRegister()` → `WinMLEpEnsureReadyAsync()` with `WinMLAsyncBlock` - progress callback, then synchronous wait via `WinMLAsyncGetStatus(async, TRUE)`. +- `Name()` returns the EP name captured during enumeration. +- `IsRegistered()` returns whether the EP has been successfully registered with ORT + (tracked via a `registered_` flag flipped on by `DownloadAndRegister`). Note this + is ORT-registration state, not WinML readyState — an EP can be `Ready` in the + WinML catalog but not yet registered with ORT. +- `DownloadAndRegister()` → `WinMLEpEnsureReadyAsync()` with a `WinMLAsyncBlock` + whose progress callback forwards WinML's progress value (a percentage in + 0–100, matching the WinRT `IAsyncOperationWithProgress<_, double>` projection) + straight through to the caller's percent callback after clamping, then + `WinMLAsyncGetStatus(async, TRUE)` for a synchronous wait. Caller-requested + cancellation goes through `WinMLAsyncCancel`. **Discovery (static factory):** ```cpp // Returns one bootstrapper per discovered WinML EP. Empty if unavailable. +// register_ep is plumbed through construction time so each bootstrapper can +// register its library with ORT inside DownloadAndRegister(). static std::vector> - DiscoverProviders(ILogger& logger); + DiscoverProviders(EpRegistrationCallback register_ep, ILogger& logger); ``` **Implementation:** -1. OS version check: `IsWindowsVersionOrGreater(10, 0, 26100)`. Return empty if not. +1. Query Windows build number via `RtlGetVersion` for diagnostics only — never gates + behavior. Gating happens at `WinMLEpCatalogCreate()` (DLL load failure → empty). 2. `WinMLEpCatalogCreate()` — if this fails (DLL not found / delay-load failure), log info, return empty. Not an error. 3. `WinMLEpCatalogEnumProviders()` — callback collects `WinMLEpHandle` + `WinMLEpInfo`. @@ -390,9 +404,7 @@ static const std::map kModelIdToRequiredEP = { | Risk | Mitigation | |------|------------| -| vcpkg port for `Microsoft.WindowsAppSDK.ML` doesn't exist | Write custom port extracting from NuGet | -| `Microsoft.Windows.AI.MachineLearning.dll` not on all Windows | Delay-load + graceful fallback + OS version check (26100+) | -| WinAppSDK bootstrapper init for unpackaged apps | Test if `MddBootstrapInitialize` is needed for EP catalog access | +| `Microsoft.Windows.AI.MachineLearning.dll` not present on every Windows install | `WinMLEpBootstrapper::DiscoverProviders` probes via `LoadLibraryW` and returns empty when the DLL is missing — no WinML bootstrappers join the discovery set | | CUDA download URLs / hashes change per release | Hardcode per release, mirroring C# pattern | | ORT `GetEpDevices()` not available in our ORT build | Check ORT version/API availability. Fallback to CPU-only device map | | Cross-platform CUDA EP registration | Windows: download from CDN. Linux: register from co-located `.so` | diff --git a/sdk_v2/cpp/docs/MigrationPlan_20260410.md b/sdk_v2/cpp/docs/MigrationPlan_20260410.md index ae7e904ad..9b78dd335 100644 --- a/sdk_v2/cpp/docs/MigrationPlan_20260410.md +++ b/sdk_v2/cpp/docs/MigrationPlan_20260410.md @@ -195,7 +195,8 @@ Refactored to use Azure SDK native features instead of a custom retry loop: Full implementation plan in [docs/EpDetectionPlan.md](EpDetectionPlan.md). Summary: - Replace stub `EpDetector` with real hardware detection via WinML EP catalog C API - (`WinMLEpCatalog.h` from `Microsoft.WindowsAppSDK.ML` NuGet, acquired via vcpkg). + (`WinMLEpCatalog.h` from `Microsoft.Windows.AI.MachineLearning` NuGet, fetched directly + from nuget.org — WinML 2.x is reg-free, no Windows App SDK bootstrap needed). - `WinMLEpBootstrapper` wraps the WinML C API for EP enumeration + download + registration. - `CudaEpBootstrapper` handles manual CUDA EP download from Azure CDN + registration. - `EpDetector` orchestrator manages bootstrappers, coordinates download, and invalidates diff --git a/sdk_v2/cpp/nuget/pack.py b/sdk_v2/cpp/nuget/pack.py index bcda45e8f..205d6d7fb 100644 --- a/sdk_v2/cpp/nuget/pack.py +++ b/sdk_v2/cpp/nuget/pack.py @@ -43,12 +43,12 @@ # primary library (plus, on Windows, .pdb and .lib companions consumed by the # Python wheel build). We copy that one file into runtimes//native/. # -# WinML builds add one extra non-delay-loaded sibling DLL — -# Microsoft.WindowsAppRuntime.Bootstrap.dll — which is the entry point used to -# register the system Windows App Runtime before any delay-loaded WinML symbol -# resolves. The pipeline only stages it for WinML builds, so we forward any -# entry from OPTIONAL_SIBLINGS that happens to be present in the upstream -# artifact dir; absent files are silently skipped. +# WinML builds add one extra sibling DLL — Microsoft.Windows.AI.MachineLearning.dll +# — that ships the reg-free WinML 2.x runtime. The pipeline only stages it for +# WinML builds (the cmake post-build copy in sdk_v2/cpp/CMakeLists.txt drops it +# next to foundry_local.dll), so we forward any entry from OPTIONAL_SIBLINGS that +# happens to be present in the upstream artifact dir; absent files are silently +# skipped. RIDS: dict[str, tuple[str, str]] = { "win_x64": ("win-x64", "foundry_local.dll"), "win_arm64": ("win-arm64", "foundry_local.dll"), @@ -57,10 +57,11 @@ } # Sibling files copied into runtimes//native/ when present in the upstream -# artifact. WinML builds drop Bootstrap.dll alongside foundry_local.dll; -# standard builds don't, and that's the only signal we need to differentiate. +# artifact. WinML builds drop Microsoft.Windows.AI.MachineLearning.dll alongside +# foundry_local.dll; standard builds don't, and that's the only signal we need +# to differentiate. OPTIONAL_SIBLINGS: tuple[str, ...] = ( - "Microsoft.WindowsAppRuntime.Bootstrap.dll", + "Microsoft.Windows.AI.MachineLearning.dll", ) log = logging.getLogger("pack") @@ -78,8 +79,8 @@ def _parse_args() -> argparse.Namespace: help="Minimum Microsoft.ML.OnnxRuntimeGenAI.Foundry version.") parser.add_argument("--package_id", default="Microsoft.AI.Foundry.Local.Runtime", help="NuGet package id. Use Microsoft.AI.Foundry.Local.Runtime.WinML " - "for the WinML variant (Windows-only RIDs, ORT linked against the " - "WinML-aligned 1.23.x line).") + "for the WinML variant (Windows-only RIDs, ships the WinML 2.x " + "reg-free runtime alongside foundry_local).") for arg_name, (rid, lib) in RIDS.items(): parser.add_argument(f"--{arg_name}", type=Path, default=None, @@ -153,7 +154,7 @@ def stage(args: argparse.Namespace, staging: Path) -> int: # The upstream artifact for each RID is the primary library plus, on # Windows, .pdb / .lib companions used by the Python wheel build, and - # — for the WinML variant — Microsoft.WindowsAppRuntime.Bootstrap.dll. + # — for the WinML variant — Microsoft.Windows.AI.MachineLearning.dll. # We forward the primary library plus any present OPTIONAL_SIBLINGS; # everything else (.pdb, .lib) stays out of the NuGet runtime payload. shutil.copy2(lib_path, native_dir) diff --git a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc index 6f50de93d..c9c89d298 100644 --- a/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/cuda_ep_bootstrapper.cc @@ -28,35 +28,19 @@ constexpr const char* kLockFileName = "cuda-ep.lock"; constexpr const char* kUserAgent = "FoundryLocal"; constexpr int kMaxInstallAttempts = 5; -// CUDA EP package is built against the ONNX Runtime version we link against, so -// WinML and non-WinML builds need separate downloads. Hashes mirror the C# core -// (see neutron.main/src/Service/Providers/Detector/CudaEpBootstrapper.cs). -// WinML build -> ORT 1.23.2 (cuda-ep-20260501-182408.zip) -// Non-WinML -> ORT 1.25.1 (cuda-ep-20260501-062935.zip) -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML -constexpr const char* kDownloadUrl = - "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/cuda-ep-20260501-182408.zip"; -#else +// CUDA EP package is built against the ONNX Runtime version we link against. constexpr const char* kDownloadUrl = "https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/cuda-ep-20260501-062935.zip"; -#endif struct ExpectedBinary { const char* filename; const char* sha256; }; -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML -constexpr ExpectedBinary kExpectedBinaries[] = { - {"onnxruntime_providers_cuda.dll", "4CEF18654878CEFCFCF8488E9C3A705EB5327AA9B5556155C319C9CBB2D98FCF"}, - {"onnxruntime-genai-cuda.dll", "BC953F8E2AAFC6219B2D723B65AB8F1A9426A6B7724D6A01ED756FAE8C3DE6AE"}, -}; -#else constexpr ExpectedBinary kExpectedBinaries[] = { {"onnxruntime_providers_cuda.dll", "DD540FCFECFBC68B4675C9ADF09C2858CF6B054563859D79598AA2524406A76F"}, {"onnxruntime-genai-cuda.dll", "BC953F8E2AAFC6219B2D723B65AB8F1A9426A6B7724D6A01ED756FAE8C3DE6AE"}, }; -#endif constexpr const char* kRegistrationName = "Foundry.CUDA"; constexpr const char* kCudaProviderDll = "onnxruntime_providers_cuda.dll"; diff --git a/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.cc index 4172ab395..8a1ca5848 100644 --- a/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.cc @@ -1,75 +1,132 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// +// This translation unit is only compiled when the WinML EP catalog NuGet +// package was resolved at CMake time (WinMLEpCatalog_FOUND). See +// sdk_v2/cpp/CMakeLists.txt for the source-list gating. The corresponding +// header (and this file) unconditionally reference WinML 2.x catalog APIs. #include "ep_detection/winml_ep_bootstrapper.h" #include "logger.h" #include +#include #include #include -#ifdef _WIN32 - // WinML EP Catalog C API — delay-loaded via Microsoft.Windows.AI.MachineLearning.dll -#if FOUNDRY_LOCAL_HAS_EP_CATALOG +#include #include -#endif #define WIN32_LEAN_AND_MEAN #include +namespace fl { + namespace { -/// Checks for Windows 10 build 26100+ (Windows 11 24H2), the minimum OS -/// version that ships the WinML EP catalog. -/// -/// Uses RtlGetVersion (ntdll) because VerifyVersionInfoW lies without a -/// compatibility manifest — it caps the reported version at 6.2 (Win 8.1) -/// unless the app declares support for newer Windows versions. -bool IsWindows11_24H2OrLater() { - // RtlGetVersion is always available and always returns the true OS version. +/// Look up the running Windows build number via ``ntdll!RtlGetVersion``, +/// resolved through ``GetProcAddress`` to avoid pulling ```` and +/// its macro pollution. ``RtlGetVersion`` accepts an ``OSVERSIONINFOW`` +/// (typedef-aliased to ``RTL_OSVERSIONINFOW`` in ````). +/// Returns 0 on failure; the value is purely diagnostic and never gates behavior. +DWORD QueryWindowsBuild() { using RtlGetVersionFn = LONG(WINAPI*)(OSVERSIONINFOW*); - auto* ntdll = GetModuleHandleW(L"ntdll.dll"); + HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll"); if (!ntdll) { - return false; + return 0; } - auto rtl_get_version = reinterpret_cast( - GetProcAddress(ntdll, "RtlGetVersion")); + ::GetProcAddress(ntdll, "RtlGetVersion")); if (!rtl_get_version) { - return false; + return 0; + } + OSVERSIONINFOW info{}; + info.dwOSVersionInfoSize = sizeof(info); + if (rtl_get_version(&info) != 0) { + return 0; } + return info.dwBuildNumber; +} - OSVERSIONINFOW osvi = {}; - osvi.dwOSVersionInfoSize = sizeof(osvi); - if (rtl_get_version(&osvi) != 0) { - return false; +/// Try to load ``Microsoft.Windows.AI.MachineLearning.dll`` from the directory +/// that hosts ``foundry_local.dll`` (i.e., the module containing this code). +/// Returns ``NULL`` if the module path cannot be resolved or the sibling file +/// is absent — callers should fall back to a bare-name ``LoadLibraryW`` so a +/// system-installed copy on the default search path still wins. +/// +/// Rationale: bindings that load ``foundry_local.dll`` from a non-default +/// directory (e.g., the JS prebuilds folder) don't push that directory onto +/// the process search path, so a bare ``LoadLibraryW("Microsoft.Windows.AI.MachineLearning.dll")`` +/// silently misses the sibling DLL and EP discovery returns zero providers. +/// Resolving the path explicitly removes that dependency on caller-side +/// PATH / ``os.add_dll_directory`` setup. +HMODULE LoadWinMLDllFromOwnDirectory() { + HMODULE own_module = nullptr; + if (!::GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&LoadWinMLDllFromOwnDirectory), + &own_module) || + own_module == nullptr) { + return nullptr; } - // Windows 11 24H2 = build 26100+ - if (osvi.dwMajorVersion > 10) { - return true; + wchar_t module_path[MAX_PATH]; + DWORD len = ::GetModuleFileNameW(own_module, module_path, MAX_PATH); + if (len == 0 || len >= MAX_PATH) { + return nullptr; } - if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0) { - return osvi.dwBuildNumber >= 26100; + + std::wstring path(module_path, len); + const size_t slash = path.find_last_of(L"\\/"); + if (slash == std::wstring::npos) { + return nullptr; } + path.resize(slash + 1); + path += L"Microsoft.Windows.AI.MachineLearning.dll"; - return false; + return ::LoadLibraryExW(path.c_str(), nullptr, + LOAD_WITH_ALTERED_SEARCH_PATH); } -} // anonymous namespace +/// Context handed to ``WinMLAsyncBlock`` so the progress thunk can forward +/// fractional progress to the caller-supplied percentage callback and signal +/// cancellation back through ``WinMLAsyncCancel``. +struct EnsureReadyAsyncCtx { + const std::string* name = nullptr; + const fl::IEpBootstrapper::ProgressCallback* progress_cb = nullptr; + std::atomic cancel_requested{false}; +}; + +/// Forwards WinML's download progress to our percentage callback. WinML +/// reports a percent value (0-100) directly, matching how the WinRT +/// projection's IAsyncOperationWithProgress<_, double> reports progress. +void CALLBACK EnsureReadyProgressThunk(WinMLAsyncBlock* async, double progress) { + auto* ctx = static_cast(async->context); + if (!ctx || !ctx->progress_cb || !*ctx->progress_cb) { + return; + } -namespace fl { + double pct = progress; + if (pct < 0.0) pct = 0.0; + if (pct > 100.0) pct = 100.0; + + if (!(*ctx->progress_cb)(*ctx->name, static_cast(pct))) { + ctx->cancel_requested.store(true, std::memory_order_release); + WinMLAsyncCancel(async); + } +} + +} // namespace -#if FOUNDRY_LOCAL_HAS_EP_CATALOG WinMLEpBootstrapper::WinMLEpBootstrapper(std::string name, EpRegistrationCallback register_ep, std::shared_ptr catalog_ref, WinMLEpHandle ep_handle) : name_(std::move(name)), register_ep_(std::move(register_ep)), catalog_ref_(std::move(catalog_ref)), ep_handle_(ep_handle) {} -#endif const std::string& WinMLEpBootstrapper::Name() const { return name_; @@ -89,48 +146,67 @@ bool WinMLEpBootstrapper::DownloadAndRegister(bool force, return true; } -#if !FOUNDRY_LOCAL_HAS_EP_CATALOG - logger.Log(LogLevel::Warning, - fmt::format("WinML EP {}: EP catalog not available at compile time", name_)); - return false; -#else - // Ask the OS to download/prepare the EP if needed. - HRESULT hr = WinMLEpEnsureReady(ep_handle_); + // Ask the OS to download/prepare the EP if needed. We use the async variant + // so we can forward fractional download progress to the caller and respect + // cancellation; WinMLAsyncGetStatus(..., TRUE) gives us a synchronous wait. + EnsureReadyAsyncCtx ctx; + ctx.name = &name_; + ctx.progress_cb = &progress_cb; + + WinMLAsyncBlock block{}; + block.context = &ctx; + block.callback = nullptr; + block.progress = (progress_cb ? &EnsureReadyProgressThunk : nullptr); + + HRESULT hr = WinMLEpEnsureReadyAsync(ep_handle_, &block); + if (SUCCEEDED(hr)) { + hr = WinMLAsyncGetStatus(&block, TRUE); + } + WinMLAsyncClose(&block); if (FAILED(hr)) { - logger.Log(LogLevel::Warning, - fmt::format("WinML EP {}: EnsureReady failed (hr=0x{:08X})", name_, static_cast(hr))); + if (ctx.cancel_requested.load(std::memory_order_acquire)) { + logger.Log(LogLevel::Information, + fmt::format("WinML EP {}: cancelled by caller", name_)); + } else { + logger.Log(LogLevel::Warning, + fmt::format("WinML EP {}: EnsureReady failed (hr=0x{:08X})", name_, + static_cast(hr))); + } return false; } - // Retrieve the library path from the EP handle. - size_t path_size = 0; - hr = WinMLEpGetLibraryPathSize(ep_handle_, &path_size); + // Reuse the path captured during enumeration when the EP was already Ready; + // otherwise fetch it now that EnsureReady has populated it. + if (library_path_.empty()) { + size_t path_size = 0; + hr = WinMLEpGetLibraryPathSize(ep_handle_, &path_size); - if (FAILED(hr) || path_size == 0) { - logger.Log(LogLevel::Warning, fmt::format("WinML EP {}: failed to get library path size (hr=0x{:08X})", name_, - static_cast(hr))); - return false; - } + if (FAILED(hr) || path_size == 0) { + logger.Log(LogLevel::Warning, fmt::format("WinML EP {}: failed to get library path size (hr=0x{:08X})", name_, + static_cast(hr))); + return false; + } - std::string path(path_size, '\0'); - hr = WinMLEpGetLibraryPath(ep_handle_, path.size(), path.data(), nullptr); + std::string path(path_size, '\0'); + hr = WinMLEpGetLibraryPath(ep_handle_, path.size(), path.data(), nullptr); - if (FAILED(hr)) { - logger.Log(LogLevel::Warning, fmt::format("WinML EP {}: failed to get library path (hr=0x{:08X})", name_, - static_cast(hr))); - return false; - } + if (FAILED(hr)) { + logger.Log(LogLevel::Warning, fmt::format("WinML EP {}: failed to get library path (hr=0x{:08X})", name_, + static_cast(hr))); + return false; + } - // The API may include a trailing null in the reported size. - if (!path.empty() && path.back() == '\0') { - path.pop_back(); - } + // The API may include a trailing null in the reported size. + if (!path.empty() && path.back() == '\0') { + path.pop_back(); + } - library_path_ = path; + library_path_ = std::move(path); + } // Register with ORT via the callback. - if (!register_ep_(name_, std::filesystem::path(path))) { + if (!register_ep_(name_, std::filesystem::path(library_path_))) { logger.Log(LogLevel::Warning, fmt::format("WinML EP {}: ORT registration failed", name_)); return false; } @@ -144,31 +220,34 @@ bool WinMLEpBootstrapper::DownloadAndRegister(bool force, // Library path + version are logged by the central register_ep callback; // no extra bootstrapper-side line needed. return true; -#endif } std::vector> WinMLEpBootstrapper::DiscoverProviders( EpRegistrationCallback register_ep, ILogger& logger) { -#if !FOUNDRY_LOCAL_HAS_EP_CATALOG - (void)register_ep; - (void)logger; - return {}; -#else - if (!IsWindows11_24H2OrLater()) { - logger.Log(LogLevel::Information, - "WinML EP catalog: requires Windows 11 24H2+ (build 26100)"); - return {}; - } - // Pre-check that the WinML DLL is loadable. The DLL is delay-loaded, so // calling WinML functions without it present would cause a structured // exception. Loading it explicitly is cleaner than SEH. - HMODULE winml_dll = LoadLibraryW(L"Microsoft.Windows.AI.MachineLearning.dll"); + // + // Try the sibling-of-foundry_local.dll location first so bindings that + // distribute the WinML DLL alongside foundry_local.dll (JS prebuilds, + // Python wheels, C# packed runtimes) don't need to also push that + // directory onto the process PATH. Fall back to the bare-name lookup + // so a system-installed copy on the default search path still wins. + HMODULE winml_dll = LoadWinMLDllFromOwnDirectory(); + if (!winml_dll) { + winml_dll = LoadLibraryW(L"Microsoft.Windows.AI.MachineLearning.dll"); + } if (!winml_dll) { + // Microsoft.Windows.AI.MachineLearning.dll only ships on Windows 10 19H1 + // (build 18362) and newer. Log GetLastError() and the OS build for diagnostics. + DWORD load_err = ::GetLastError(); + DWORD os_build = QueryWindowsBuild(); logger.Log(LogLevel::Information, - "WinML EP catalog: DLL not available — EP discovery disabled"); + fmt::format("WinML EP catalog: DLL not available — EP discovery disabled " + "(LoadLibrary err={}, Windows build {})", + load_err, os_build)); return {}; } // Keep the DLL loaded — the delay-load stubs will resolve against it. @@ -237,9 +316,6 @@ std::vector> WinMLEpBootstrapper::DiscoverP fmt::format("WinML EP catalog: discovered {} provider(s)", ctx.bootstrappers.size())); return std::move(ctx.bootstrappers); -#endif } } // namespace fl - -#endif // _WIN32 diff --git a/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.h b/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.h index 96e9625a6..7dd2933a3 100644 --- a/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.h +++ b/sdk_v2/cpp/src/ep_detection/winml_ep_bootstrapper.h @@ -1,15 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// +// This translation unit is only compiled when the WinML EP catalog NuGet +// package was resolved at CMake time (WinMLEpCatalog_FOUND, which also sets +// the C++ macro FOUNDRY_LOCAL_HAS_EP_CATALOG=1). Source-list gating happens +// in sdk_v2/cpp/CMakeLists.txt; all C++ callers must guard references on +// FOUNDRY_LOCAL_HAS_EP_CATALOG. #pragma once -#ifdef _WIN32 - #include "ep_detection/ep_bootstrapper.h" #include "ep_detection/ep_types.h" -#if FOUNDRY_LOCAL_HAS_EP_CATALOG #include -#endif #include #include @@ -21,7 +23,11 @@ class ILogger; /// Bootstrapper for a single WinML-based execution provider. /// Each instance wraps one WinMLEpHandle from the WinML EP catalog. -/// Windows 11 24H2+ (build 26100) only. +/// +/// Code path works on Windows 10 19H1+ (build 18362) — the minimum OS +/// for the bundled WinML 2.x redist DLL (Microsoft.Windows.AI.MachineLearning). +/// Actual EP discovery returns providers only on Windows 11 24H2+ (build 26100), +/// where the OS-delivered EP catalog is populated via Windows Update / Store. class WinMLEpBootstrapper : public IEpBootstrapper { public: ~WinMLEpBootstrapper() override = default; @@ -41,7 +47,7 @@ class WinMLEpBootstrapper : public IEpBootstrapper { ILogger& logger) override; /// Discovers all WinML EPs available on this system. - /// Returns empty on non-Windows, unsupported OS version, or missing WinML DLL. + /// Returns empty on unsupported OS version or missing WinML DLL. /// @param register_ep Callback called after EnsureReady to register the EP with ORT. /// @param logger Logger for diagnostic output. static std::vector> DiscoverProviders( @@ -54,17 +60,16 @@ class WinMLEpBootstrapper : public IEpBootstrapper { bool registered_ = false; EpRegistrationCallback register_ep_; -#if FOUNDRY_LOCAL_HAS_EP_CATALOG // Shared across all bootstrappers from the same DiscoverProviders() call // to keep the catalog alive until the last bootstrapper is destroyed. std::shared_ptr catalog_ref_; + // Owned by the catalog (no per-EP release in the WinML C API), so this + // raw pointer is valid as long as catalog_ref_ outlives it. Per-EP + // cleanup is intentionally absent — do not add Release-like calls here. WinMLEpHandle ep_handle_ = nullptr; WinMLEpBootstrapper(std::string name, EpRegistrationCallback register_ep, std::shared_ptr catalog_ref, WinMLEpHandle ep_handle); -#endif }; } // namespace fl - -#endif // _WIN32 diff --git a/sdk_v2/cpp/src/manager.cc b/sdk_v2/cpp/src/manager.cc index 6ec8dbed5..3702bd000 100644 --- a/sdk_v2/cpp/src/manager.cc +++ b/sdk_v2/cpp/src/manager.cc @@ -26,12 +26,8 @@ #include "telemetry/telemetry_action_tracker.h" #include "telemetry/telemetry_logger.h" #include "utils.h" -#include "winml_bootstrap.h" -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include +#if FOUNDRY_LOCAL_HAS_EP_CATALOG #include "ep_detection/winml_ep_bootstrapper.h" #endif @@ -238,8 +234,10 @@ Manager::Manager(const Configuration& config) // Discover bootstrappers from available EP sources std::vector> bootstrappers; -#ifdef _WIN32 - // WinML EPs — enumerate from the OS EP catalog (Windows 11 24H2+) +#if FOUNDRY_LOCAL_HAS_EP_CATALOG + // WinML EPs — enumerate from the OS EP catalog (Windows 10 19H1+ reg-free runtime). + // Only present when the WinML EP catalog NuGet package was successfully resolved + // at CMake time (gated on WinMLEpCatalog_FOUND in sdk_v2/cpp/CMakeLists.txt). auto winml_providers = WinMLEpBootstrapper::DiscoverProviders(register_ep, *logger_); for (auto& p : winml_providers) { bootstrappers.push_back(std::move(p)); @@ -256,8 +254,6 @@ Manager::Manager(const Configuration& config) } // WebGPU EP — always available (no hardware detection needed). - // TODO(@bmehta001): When WinML 2.0 adds WebGPU support, add a WinML-aware - // WebGPU path here that can coexist with the WinML EPs discovered above. const auto webgpu_ep_dir = cache_dir / "webgpu-ep"; bootstrappers.push_back(std::make_unique(webgpu_ep_dir.string(), register_ep)); } @@ -355,30 +351,6 @@ Manager& Manager::Create(const Configuration& config) { "Manager already created. Call Destroy() first."); } - // Optional Windows App SDK bootstrap. When the caller passes Bootstrap=true in - // additional_options we initialize the WinAppSDK framework package for this process. This - // must run before the Manager constructor so that WinML EP discovery (inside - // Manager::Manager) can resolve Microsoft.Windows.AI.MachineLearning.dll. We use a - // temporary stderr logger here because the Manager-owned logger doesn't exist yet; - // bootstrap output is low-volume (one line on success, one warning on failure). Mirrors - // the C# FoundryLocalCore IS_WINML path. Only meaningful in WinML builds; outside that - // configuration TryInitializeWindowsAppSdk is a no-op stub. -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML - { - auto it = config.additional_options.find("Bootstrap"); - constexpr std::string_view kTrue = "true"; - if (it != config.additional_options.end() && it->second.size() == kTrue.size() && - std::equal(it->second.begin(), it->second.end(), kTrue.begin(), - [](char a, char b) { - return std::tolower(static_cast(a)) == - std::tolower(static_cast(b)); - })) { - StderrLogger bootstrap_logger; - TryInitializeWindowsAppSdk(bootstrap_logger); - } - } -#endif - // Construct into a local unique_ptr so a throw between construction and the post-init // telemetry/log calls cleans up the partially-initialized Manager instead of leaking it. // The constructor validates and resolves defaults; if it throws, no Manager exists. @@ -414,14 +386,6 @@ Manager& Manager::Instance() { void Manager::Destroy() { std::lock_guard lock(s_mutex_); s_instance_.reset(); - - // Pair WinAppSDK bootstrap shutdown with Manager teardown. No-op if the bootstrap was - // never initialized for this process. Use a temporary logger for the same reason as in - // Create — the Manager-owned logger has been destroyed by this point. -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML - StderrLogger bootstrap_logger; - ShutdownWindowsAppSdk(bootstrap_logger); -#endif } ICatalog& Manager::GetCatalog() { diff --git a/sdk_v2/cpp/src/winml_bootstrap.cc b/sdk_v2/cpp/src/winml_bootstrap.cc deleted file mode 100644 index 5999a6d54..000000000 --- a/sdk_v2/cpp/src/winml_bootstrap.cc +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -#include "winml_bootstrap.h" - -// Entire translation unit is empty outside WinML builds — see winml_bootstrap.h. Callers -// guard their use sites with the same FOUNDRY_LOCAL_USE_WINML macro, so there are no -// undefined references to no-op stubs. -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML - -#include "logger.h" - -#define WIN32_LEAN_AND_MEAN -#include -#include - -#include -#include -#include - -namespace { - -// Windows App SDK 1.8 — matches the C# FoundryLocalCore reference (majorMinorVersion= -// 0x00010008, minVersion={1,8,1,0}). Update in lockstep with the WinML EP catalog NuGet. -constexpr UINT32 kMajorMinorVersion = 0x00010008; -constexpr UINT16 kMinMajor = 1; -constexpr UINT16 kMinMinor = 8; -constexpr UINT16 kMinBuild = 1; -constexpr UINT16 kMinRevision = 0; - -std::atomic g_initialized{false}; - -} // namespace - -namespace fl { - -bool TryInitializeWindowsAppSdk(ILogger& logger) { - if (g_initialized.load(std::memory_order_acquire)) { - return true; - } - - PACKAGE_VERSION min_version{}; - min_version.Major = kMinMajor; - min_version.Minor = kMinMinor; - min_version.Build = kMinBuild; - min_version.Revision = kMinRevision; - - HRESULT hr = ::MddBootstrapInitialize2( - kMajorMinorVersion, nullptr, min_version, - MddBootstrapInitializeOptions_OnNoMatch_ShowUI); - if (FAILED(hr)) { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%08lX", static_cast(hr)); - logger.Log(LogLevel::Warning, - std::string("WindowsAppSdk bootstrap: MddBootstrapInitialize2 failed (HRESULT=0x") + - buf + "). WinML EP discovery may find no providers."); - return false; - } - - g_initialized.store(true, std::memory_order_release); - logger.Log(LogLevel::Information, - "WindowsAppSdk bootstrap: initialized successfully (WinAppSDK >= 1.8.1.0)."); - return true; -} - -void ShutdownWindowsAppSdk(ILogger& logger) { - if (!g_initialized.exchange(false, std::memory_order_acq_rel)) { - return; - } - - ::MddBootstrapShutdown(); - logger.Log(LogLevel::Information, "WindowsAppSdk bootstrap: shutdown complete."); -} - -} // namespace fl - -#endif // FOUNDRY_LOCAL_USE_WINML diff --git a/sdk_v2/cpp/src/winml_bootstrap.h b/sdk_v2/cpp/src/winml_bootstrap.h deleted file mode 100644 index 4a068c83a..000000000 --- a/sdk_v2/cpp/src/winml_bootstrap.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Optional Windows App SDK bootstrap helper for non-packaged consumer processes (e.g. the -// JavaScript binding loaded into Node, where the host process has no built-in WinAppSDK -// activation). When opted in via additional_options["Bootstrap"]="true", initializes the -// Windows App Runtime framework package so that APIs depending on it — notably the WinML EP -// catalog DLL `Microsoft.Windows.AI.MachineLearning.dll` consumed by `WinMLEpBootstrapper` — -// can resolve at runtime. Defaults to off; matches the C# FoundryLocalCore behavior. -// -// Compiled only in WinML builds (FOUNDRY_LOCAL_USE_WINML=ON), which already take a hard -// dependency on the WindowsAppSDK NuGet. Outside that configuration the header is empty and -// callers must guard their use sites with the same FOUNDRY_LOCAL_USE_WINML macro. -#pragma once - -#if defined(FOUNDRY_LOCAL_USE_WINML) && FOUNDRY_LOCAL_USE_WINML - -namespace fl { - -class ILogger; - -/// Initialize the Windows App Runtime framework package for this process by calling -/// MddBootstrapInitialize2 with WinAppSDK 1.8 minimum. Idempotent — subsequent calls are -/// no-ops once initialized. -/// -/// Returns true if bootstrap succeeded (or was already initialized). Returns false on -/// failure; the reason is logged but the process continues — WinML EP discovery will simply -/// find no providers. -bool TryInitializeWindowsAppSdk(ILogger& logger); - -/// Reverse the effects of TryInitializeWindowsAppSdk(). Safe to call even if init never -/// succeeded. After this call the WinAppSDK framework package should not be used. -void ShutdownWindowsAppSdk(ILogger& logger); - -} // namespace fl - -#endif // FOUNDRY_LOCAL_USE_WINML diff --git a/sdk_v2/cpp/test/CMakeLists.txt b/sdk_v2/cpp/test/CMakeLists.txt index e14a9fc4d..343b3ca25 100644 --- a/sdk_v2/cpp/test/CMakeLists.txt +++ b/sdk_v2/cpp/test/CMakeLists.txt @@ -116,9 +116,14 @@ else() endif() include(GoogleTest) +# DISCOVERY_TIMEOUT defaults to 5s, which is not enough for the WinML build: +# foundry_local.dll links against Microsoft.Windows.AI.MachineLearning.dll +# (delay-loaded) plus standalone ORT, and process startup + gtest enumeration +# can exceed 5s on cold caches or slow CI agents. Bump to 60s for headroom. gtest_discover_tests(foundry_local_tests WORKING_DIRECTORY $ DISCOVERY_MODE PRE_TEST + DISCOVERY_TIMEOUT 60 ) # ========================================================================== @@ -262,4 +267,5 @@ endif() gtest_discover_tests(cache_only_tests WORKING_DIRECTORY $ DISCOVERY_MODE PRE_TEST + DISCOVERY_TIMEOUT 60 ) diff --git a/sdk_v2/cpp/test/sdk_api/ep_detection_test.cc b/sdk_v2/cpp/test/sdk_api/ep_detection_test.cc index dee373a04..c386051a5 100644 --- a/sdk_v2/cpp/test/sdk_api/ep_detection_test.cc +++ b/sdk_v2/cpp/test/sdk_api/ep_detection_test.cc @@ -75,7 +75,7 @@ TEST_F(EpDetectionApiTest, IsEpDownloadInProgress_ConsistentAcrossCalls) { } #ifdef _WIN32 -// On Windows 11 24H2+, WinML should discover at least one EP. +// WinML 2.x reg-free runtime supports Windows 10 19H1 (build 18362) and later. // This test verifies the full Manager → EpDetector → WinMLEpBootstrapper chain. TEST_F(EpDetectionApiTest, GetDiscoverableEps_WindowsHasWinMLProviders) { auto eps = manager().GetDiscoverableEps(); @@ -92,4 +92,46 @@ TEST_F(EpDetectionApiTest, GetDiscoverableEps_WindowsHasWinMLProviders) { } } } + +// Exercises the WinML 2.x download/register path end-to-end: +// WinMLEpEnsureReady → WinMLEpGetLibraryPath → ORT RegisterExecutionProvider. +// Picks the first discoverable EP that is not already registered. SharedTestEnv +// pre-registers Foundry's CUDA + WebGPU, so the unregistered candidate is always +// served by the OS WinML catalog. +// Skipped on minimal images where the OS exposes no WinML EPs. +TEST_F(EpDetectionApiTest, DownloadAndRegister_WinMLEp_RegistersFromOsCatalog) { + auto eps_before = manager().GetDiscoverableEps(); + + std::string target; + for (const auto& ep : eps_before) { + if (!ep.is_registered) { + target = ep.name; + break; + } + } + + if (target.empty()) { + GTEST_SKIP() << "No unregistered WinML-catalog EP available on this machine"; + } + + std::cout << "[ INFO ] Registering WinML EP via 2.x catalog: " << target << std::endl; + + ASSERT_NO_THROW(manager().DownloadAndRegisterEps( + {target}, [](std::string_view name, float pct) { + std::cout << " " << name << ": " << static_cast(pct) << "%" << std::endl; + return true; + })) << "DownloadAndRegisterEps threw for " << target; + + // Verify the post-register state surfaces through the public API. + auto eps_after = manager().GetDiscoverableEps(); + bool found_registered = false; + for (const auto& ep : eps_after) { + if (ep.name == target) { + EXPECT_TRUE(ep.is_registered) << target << " should be marked registered after EnsureReady"; + found_registered = true; + break; + } + } + EXPECT_TRUE(found_registered) << target << " disappeared from discoverable list"; +} #endif diff --git a/sdk_v2/cs/src/FoundryLocalManager.cs b/sdk_v2/cs/src/FoundryLocalManager.cs index 4453c44f1..54863556f 100644 --- a/sdk_v2/cs/src/FoundryLocalManager.cs +++ b/sdk_v2/cs/src/FoundryLocalManager.cs @@ -240,7 +240,7 @@ await Task.Run(() => } } - // Merge AdditionalSettings with build-flavor-specific defaults (e.g. WinML Bootstrap). + // Merge AdditionalSettings with user-supplied entries. // Done as a local dict so we don't mutate the user-supplied AdditionalSettings. var additionalSettings = new Dictionary(StringComparer.Ordinal); if (_config.AdditionalSettings != null) @@ -255,15 +255,6 @@ await Task.Run(() => } } -#if IS_WINML - // WinML build needs the native side to bootstrap the Windows App Runtime. - // Caller can override by setting "Bootstrap" explicitly in AdditionalSettings. - if (!additionalSettings.ContainsKey("Bootstrap")) - { - additionalSettings["Bootstrap"] = "true"; - } -#endif - if (additionalSettings.Count > 0) { // Create a KeyValuePairs from additional settings diff --git a/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj index 8114eadaf..cd4c1c6de 100644 --- a/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj +++ b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj @@ -88,14 +88,16 @@ Microsoft Foundry Local SDK for WinML Microsoft.AI.Foundry.Local.WinML Microsoft.AI.Foundry.Local.WinML - $(DefineConstants);IS_WINML - net9.0-windows10.0.26100.0 + + net8.0;net9.0 win-x64;win-arm64 - - 10.0.17763.0 - - - $(NoWarn);CsWinRT1028 diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj b/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj index 1196a1b6c..77611864c 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj +++ b/sdk_v2/cs/test/FoundryLocal.Tests/Microsoft.AI.Foundry.Local.Tests.csproj @@ -1,19 +1,23 @@  - - net462;net8.0 - net8.0 + false + true + + + net8.0;net9.0 + net462;net8.0;net9.0 + enable enable latest false - true - false false @@ -47,17 +51,6 @@ - - net9.0-windows10.0.26100.0 - 10.0.17763.0 - None - - $(DefineConstants);USE_WINML - - diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs b/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs index ba8d6b975..e684e1e5d 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs +++ b/sdk_v2/cs/test/FoundryLocal.Tests/Utils.cs @@ -558,6 +558,8 @@ private static List BuildTestCatalog(bool includeCuda = true) private static string GetSourceFilePath([CallerFilePath] string path = "") => path; // Gets the root directory of the foundry-local-sdk repository by finding the .git directory. + // In a regular checkout `.git` is a directory; in a git worktree it's a file pointing to + // the main repo's worktrees/ entry — accept either form. private static string GetRepoRoot() { var sourceFile = GetSourceFilePath(); @@ -565,7 +567,8 @@ private static string GetRepoRoot() while (dir != null) { - if (Directory.Exists(Path.Combine(dir.FullName, ".git"))) + var gitPath = Path.Combine(dir.FullName, ".git"); + if (Directory.Exists(gitPath) || File.Exists(gitPath)) return dir.FullName; dir = dir.Parent; diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/VisionTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/VisionTests.cs index 03399b88f..07be22307 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/VisionTests.cs +++ b/sdk_v2/cs/test/FoundryLocal.Tests/VisionTests.cs @@ -17,23 +17,13 @@ namespace Microsoft.AI.Foundry.Local.Tests; [SkipUnlessIntegration] internal sealed class VisionTests { -#pragma warning disable CS0649 // assigned in non-WinML Setup; under USE_WINML the field is never written private static IModel? model; -#pragma warning restore CS0649 private static string TestImagePath => Utils.TestDataPath("Taittinger.jpg"); [Before(Class)] public static async Task Setup() { -#if USE_WINML - // The WinML variant pins an older ORT than the base SDK. Cataloged vision - // models use ops (e.g. com.microsoft:CausalConvWithState) that aren't - // registered in that ORT, so no vision model can load. Skip the whole suite. - Console.WriteLine("VisionTests: skipped on WinML build (ORT version lacks required ops)"); - await Task.CompletedTask; - return; -#else try { var manager = FoundryLocalManager.Instance; @@ -86,7 +76,6 @@ public static async Task Setup() Console.WriteLine($"VisionTests setup failed: {ex}"); throw; } -#endif } [Test] diff --git a/sdk_v2/deps_versions.json b/sdk_v2/deps_versions.json index 204f90a7c..a668dfc68 100644 --- a/sdk_v2/deps_versions.json +++ b/sdk_v2/deps_versions.json @@ -1,5 +1,6 @@ { - "_comment": "Single source of truth for ORT and ORT-GenAI versions in sdk_v2. Read by sdk_v2/cpp/cmake/FindOnnxRuntime*.cmake and sdk_v2/python/_build_backend/__init__.py.", - "onnxruntime": { "version": "1.25.1" }, - "onnxruntime-genai": { "version": "0.13.2" } + "_comment": "Single source of truth for native dependency versions in sdk_v2. Read by sdk_v2/cpp/cmake/Find*.cmake and sdk_v2/python/_build_backend/__init__.py. The .pipelines/foundry-local-packaging.yml literals must match; the 'Validate pinned versions' step fails the build on drift.", + "onnxruntime": { "version": "1.25.1" }, + "onnxruntime-genai": { "version": "0.13.2" }, + "windows-ai-machinelearning": { "version": "2.1.70" } } diff --git a/sdk_v2/deps_versions_winml.json b/sdk_v2/deps_versions_winml.json deleted file mode 100644 index c2f43bfb5..000000000 --- a/sdk_v2/deps_versions_winml.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "_comment": "WinML variant of sdk_v2/deps_versions.json. Selected by FOUNDRY_LOCAL_USE_WINML=ON (cmake) and FL_PYTHON_PACKAGE_NAME=foundry-local-sdk-winml (python build backend).", - "onnxruntime": { "version": "1.23.2.3" }, - "onnxruntime-genai": { "version": "0.13.2" } -} diff --git a/sdk_v2/js/script/copy-native.mjs b/sdk_v2/js/script/copy-native.mjs index 8fcb05340..694b41716 100644 --- a/sdk_v2/js/script/copy-native.mjs +++ b/sdk_v2/js/script/copy-native.mjs @@ -1,5 +1,5 @@ -// Dev-time only. Copies foundry_local.{dll,so,dylib} AND its ORT/GenAI/WinML/ -// WindowsAppRuntime siblings from sdk_v2/cpp/build///bin/ +// Dev-time only. Copies foundry_local.{dll,so,dylib} AND its ORT/GenAI/WinML +// siblings from sdk_v2/cpp/build///bin/ // / into sdk_v2/js/prebuilds/-/ so // `npm test` works locally without the developer having to configure // Configuration.libraryPath or set env vars. @@ -43,7 +43,7 @@ const destDir = resolve(pkgRoot, "prebuilds", `${process.platform}-${process.arc mkdirSync(destDir, { recursive: true }); // Files to copy: the foundry_local shared lib + its sibling runtime deps -// (ORT, ORT-GenAI, and on Windows the WinML/AppRuntime bootstraps). The +// (ORT, ORT-GenAI, and on Windows the WinML 2.x reg-free runtime DLL). The // addon's process must be able to resolve foundry_local's load-time deps // via the OS default search (which on Windows includes the addon's own // directory, on POSIX is satisfied by our rpath of $ORIGIN/@loader_path). @@ -55,7 +55,6 @@ const wanted = (() => { "onnxruntime-genai.dll", "onnxruntime_providers_shared.dll", "Microsoft.Windows.AI.MachineLearning.dll", - "Microsoft.WindowsAppRuntime.Bootstrap.dll", ]; } if (process.platform === "darwin") { diff --git a/sdk_v2/js/script/pack-prebuilds.mjs b/sdk_v2/js/script/pack-prebuilds.mjs index 8416cc785..b8129effd 100644 --- a/sdk_v2/js/script/pack-prebuilds.mjs +++ b/sdk_v2/js/script/pack-prebuilds.mjs @@ -1,5 +1,5 @@ // CI-only. Stages the foundry_local shared library into prebuilds/ for npm -// publish. ORT / ORT-GenAI / WinML / WindowsAppRuntime are NOT bundled — the +// publish. ORT / ORT-GenAI / WinML are NOT bundled — the // install-native.cjs postinstall hook fetches them from NuGet at the user's // machine (see ort-loading-contract.instructions.md). The .node addon itself // is already produced into prebuilds/-/ by `node-gyp rebuild`. diff --git a/sdk_v2/js/src/detail/native.ts b/sdk_v2/js/src/detail/native.ts index c9f8a7356..683afe86f 100644 --- a/sdk_v2/js/src/detail/native.ts +++ b/sdk_v2/js/src/detail/native.ts @@ -390,13 +390,3 @@ export function configureNativeLoader(opts: { libraryPath?: string }): void { export function getPreloadedLibraryPath(): string | undefined { return preloaded; } - -/** - * Returns the directory the native foundry_local shared library is resolved from for the given config — - * either the caller's explicit `libraryPath` or the prebuild directory the addon itself lives in (which is - * where `copy-native:dev` and CI prebuild populate `foundry_local.{dll,so,dylib}`). - */ -export function getResolvedLibraryDir(libraryPath?: string): string { - if (libraryPath !== undefined && libraryPath !== "") return libraryPath; - return prebuildDir; -} diff --git a/sdk_v2/js/src/foundryLocalManager.ts b/sdk_v2/js/src/foundryLocalManager.ts index 3830c00da..e3499462c 100644 --- a/sdk_v2/js/src/foundryLocalManager.ts +++ b/sdk_v2/js/src/foundryLocalManager.ts @@ -3,9 +3,6 @@ // `FoundryLocalError` from the native addon. `create()` and `createAsync()` are factory wrappers around the // constructor — the native layer is the source of truth for instance identity. -import { existsSync } from "node:fs"; -import { resolve } from "node:path"; - import { type Catalog, wrapNativeCatalog } from "./catalog.js"; import { FOUNDRY_LOCAL_CONFIG_KEYS, type FoundryLocalConfig } from "./configuration.js"; import { @@ -13,38 +10,9 @@ import { configureNativeLoader, getAddon, getPreloadedLibraryPath, - getResolvedLibraryDir, } from "./detail/native.js"; import type { EpDownloadResult, EpInfo } from "./types.js"; -/** - * On Windows WinML builds, the native side needs the Windows App Runtime bootstrapped (the build co-locates - * `Microsoft.WindowsAppRuntime.Bootstrap.dll` next to `foundry_local.dll`). Detect that layout at runtime and - * inject `additionalSettings.Bootstrap = "true"` unless the caller already set it. Returns a shallow-copied - * config so the caller's `additionalSettings` is never mutated. No-op on non-Windows. - * - * Mirrors the `IS_WINML`-gated block in C# `FoundryLocalManager` (sdk_v2/cs/src/FoundryLocalManager.cs), but - * driven by a filesystem probe instead of a compile-time flag. - */ - -export function applyBootstrapAutoDetect(config: FoundryLocalConfig, libraryDir: string): FoundryLocalConfig { - if (process.platform !== "win32") return config; - const existing = config.additionalSettings; - // Defer to native validation when additionalSettings is present but not a plain object — spreading a string, - // array, or other non-record would silently produce a "valid" object and mask the TypeError the native layer - // would otherwise throw. - if (existing !== undefined && (existing === null || typeof existing !== "object" || Array.isArray(existing))) { - return config; - } - if (existing?.Bootstrap !== undefined) return config; - const bootstrap = resolve(libraryDir, "Microsoft.WindowsAppRuntime.Bootstrap.dll"); - if (!existsSync(bootstrap)) return config; - return { - ...config, - additionalSettings: { ...(existing ?? {}), Bootstrap: "true" }, - }; -} - export class FoundryLocalManager { readonly #native: NativeManager; #catalog: Catalog | undefined; @@ -89,8 +57,7 @@ export class FoundryLocalManager { } } } - const merged = applyBootstrapAutoDetect(config, getResolvedLibraryDir(config.libraryPath)); - this.#native = new (getAddon().Manager)(merged); + this.#native = new (getAddon().Manager)(config); } /** diff --git a/sdk_v2/js/test/bootstrap-autodetect.test.ts b/sdk_v2/js/test/bootstrap-autodetect.test.ts deleted file mode 100644 index 9295e2ccb..000000000 --- a/sdk_v2/js/test/bootstrap-autodetect.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Tests for `applyBootstrapAutoDetect`. Exercised directly (no addon load required) — that's the whole point -// of extracting it as a pure helper. -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { applyBootstrapAutoDetect } from "../src/foundryLocalManager.js"; - -// Platform-conditional registration (rather than `describe.skip`) so the inactive -// suite is never collected — keeps vitest's skipped count at 0 on both platforms, -// so a non-zero skip count is always a real signal worth investigating. -const isWindows = process.platform === "win32"; - -if (isWindows) describe("applyBootstrapAutoDetect (Windows)", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = mkdtempSync(join(tmpdir(), "fl-js-v2-bootstrap-")); - }); - - afterEach(() => { - rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("injects Bootstrap=true when the bootstrap DLL is co-located", () => { - writeFileSync(join(tmpDir, "Microsoft.WindowsAppRuntime.Bootstrap.dll"), "dummy"); - const result = applyBootstrapAutoDetect({ appName: "test" }, tmpDir); - expect(result.additionalSettings).toEqual({ Bootstrap: "true" }); - }); - - it("preserves existing additionalSettings while injecting Bootstrap", () => { - writeFileSync(join(tmpDir, "Microsoft.WindowsAppRuntime.Bootstrap.dll"), "dummy"); - const result = applyBootstrapAutoDetect({ appName: "test", additionalSettings: { foo: "bar" } }, tmpDir); - expect(result.additionalSettings).toEqual({ foo: "bar", Bootstrap: "true" }); - }); - - it("does not overwrite a caller-supplied Bootstrap value", () => { - writeFileSync(join(tmpDir, "Microsoft.WindowsAppRuntime.Bootstrap.dll"), "dummy"); - const result = applyBootstrapAutoDetect({ appName: "test", additionalSettings: { Bootstrap: "false" } }, tmpDir); - expect(result.additionalSettings).toEqual({ Bootstrap: "false" }); - }); - - it("does not mutate the caller's config or additionalSettings object", () => { - writeFileSync(join(tmpDir, "Microsoft.WindowsAppRuntime.Bootstrap.dll"), "dummy"); - const original = { appName: "test", additionalSettings: { foo: "bar" } }; - const originalSettings = original.additionalSettings; - const result = applyBootstrapAutoDetect(original, tmpDir); - expect(original.additionalSettings).toBe(originalSettings); - expect(original.additionalSettings).toEqual({ foo: "bar" }); - expect(result).not.toBe(original); - expect(result.additionalSettings).not.toBe(originalSettings); - }); - - it("returns the config unchanged when the bootstrap DLL is absent", () => { - const config = { appName: "test", additionalSettings: { foo: "bar" } }; - const result = applyBootstrapAutoDetect(config, tmpDir); - expect(result).toBe(config); - }); -}); - -if (!isWindows) describe("applyBootstrapAutoDetect (non-Windows)", () => { - it("is a no-op regardless of files present in the directory", () => { - const tmp = mkdtempSync(join(tmpdir(), "fl-js-v2-bootstrap-")); - try { - writeFileSync(join(tmp, "Microsoft.WindowsAppRuntime.Bootstrap.dll"), "dummy"); - const config = { appName: "test" }; - const result = applyBootstrapAutoDetect(config, tmp); - expect(result).toBe(config); - } finally { - rmSync(tmp, { recursive: true, force: true }); - } - }); -}); diff --git a/sdk_v2/python/README.md b/sdk_v2/python/README.md index a8291525b..3d7b1bbc3 100644 --- a/sdk_v2/python/README.md +++ b/sdk_v2/python/README.md @@ -145,7 +145,6 @@ config = Configuration( app_name="MyApp", model_cache_dir="/path/to/cache", # optional log_level=LogLevel.INFORMATION, # optional (default: Warning) - additional_settings={"Bootstrap": "false"}, # optional. winml only ) FoundryLocalManager.initialize(config) manager = FoundryLocalManager.instance diff --git a/sdk_v2/python/_build_backend/__init__.py b/sdk_v2/python/_build_backend/__init__.py index d3bb42dd3..a7a90014b 100644 --- a/sdk_v2/python/_build_backend/__init__.py +++ b/sdk_v2/python/_build_backend/__init__.py @@ -50,11 +50,9 @@ _ENV_VAR = "FL_PYTHON_PACKAGE_NAME" -_WINML_PKG_NAME = "foundry-local-sdk-winml" _PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml" _SDK_V2_ROOT = _PYPROJECT.resolve().parent.parent _DEPS_JSON_STD = _SDK_V2_ROOT / "deps_versions.json" -_DEPS_JSON_WINML = _SDK_V2_ROOT / "deps_versions_winml.json" # Match ``name = "..."`` only inside the [project] table. The regex is # anchored to the first ``name = "foundry-local-sdk"`` occurrence which @@ -108,16 +106,16 @@ def _patch_pyproject_text(original: str, *, override_name: str | None, deps_file def _maybe_patch_name() -> Generator[None, None, None]: """Context manager that rewrites pyproject.toml during PEP 517 hook execution. - Always rewrites ORT/GenAI version pins from the appropriate deps JSON - (single source of truth). Conditionally rewrites the project name when - ``FL_PYTHON_PACKAGE_NAME`` selects the WinML variant. + Always rewrites ORT/GenAI version pins from deps_versions.json (single + source of truth). Conditionally rewrites the project name when + ``FL_PYTHON_PACKAGE_NAME`` selects the WinML variant. The WinML and + standard flavors share the same ORT/GenAI versions; only the + distribution name differs. """ override = os.environ.get(_ENV_VAR, "").strip() or None - is_winml = override == _WINML_PKG_NAME - deps_file = _DEPS_JSON_WINML if is_winml else _DEPS_JSON_STD original = _PYPROJECT.read_text(encoding="utf-8") - patched = _patch_pyproject_text(original, override_name=override, deps_file=deps_file) + patched = _patch_pyproject_text(original, override_name=override, deps_file=_DEPS_JSON_STD) if patched == original: # Nothing to rewrite (e.g. JSON already matches and no name override). diff --git a/sdk_v2/python/pyproject.toml b/sdk_v2/python/pyproject.toml index d1bc1d71c..7b6e7478b 100644 --- a/sdk_v2/python/pyproject.toml +++ b/sdk_v2/python/pyproject.toml @@ -38,10 +38,10 @@ classifiers = [ ] # ORT/GenAI version pins below use sentinel ``0.0.0``. The real versions -# come from sdk_v2/deps_versions.json (or deps_versions_winml.json for the -# WinML variant) — the _build_backend rewrites these pins from JSON at -# wheel-build time. JSON is the single source of truth; bumping ORT -# versions is a one-file edit. +# come from sdk_v2/deps_versions.json — the _build_backend rewrites these +# pins from JSON at wheel-build time. JSON is the single source of truth; +# bumping ORT versions is a one-file edit. The WinML wheel variant shares +# the same ORT/GenAI versions and only differs in the distribution name. # # If a wheel ever ships with ``==0.0.0`` it means the backend wasn't # invoked (e.g. raw setuptools bypass) — pip install will fail loudly diff --git a/sdk_v2/python/test/integration/test_configuration_native.py b/sdk_v2/python/test/integration/test_configuration_native.py index 1b8e99960..1874e57d0 100644 --- a/sdk_v2/python/test/integration/test_configuration_native.py +++ b/sdk_v2/python/test/integration/test_configuration_native.py @@ -71,7 +71,7 @@ def test_catalog_urls_accepted(self): def test_additional_settings_accepted(self): c = Configuration( app_name="BuildNativeTest", - additional_settings={"Bootstrap": "false", "K2": "v2"}, + additional_settings={"K1": "v1", "K2": "v2"}, ) with _native_config(c) as ptr: assert ptr is not None