From e351aeae51990961448d582210169843f2bab89d Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Wed, 15 Apr 2026 15:46:53 -0700
Subject: [PATCH 01/41] Add NuGet packaging for WebGPU plugin EP
Add Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package project (netstandard2.0) with a C# helper class for cross-platform native library resolution, a test console app, a reusable pack_nuget.ps1 script, and a CI pipeline stage for building, signing, and testing the package.
New files:
- plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/ (.csproj, WebGpuEp.cs, README.md)
- plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/ (test app, nuget.config, mul.onnx, model generator)
- plugin-ep-webgpu/csharp/pack_nuget.ps1 (reusable pack script for local and CI)
- plugin-ep-webgpu/csharp/README.md (development readme)
- tools/ci_build/.../stages/plugin-webgpu-nuget-packaging-stage.yml
Modified: plugin-webgpu-packaging-stage.yml (wire in NuGet stage)
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 72 +++++++
.../README.md | 45 +++++
.../WebGpuEp.cs | 100 ++++++++++
plugin-ep-webgpu/csharp/README.md | 162 ++++++++++++++++
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 175 ++++++++++++++++++
.../csharp/test/WebGpuEpNuGetTest/Program.cs | 81 ++++++++
.../WebGpuEpNuGetTest.csproj | 27 +++
.../WebGpuEpNuGetTest/generate_mul_model.py | 26 +++
.../csharp/test/WebGpuEpNuGetTest/mul.onnx | 17 ++
.../test/WebGpuEpNuGetTest/nuget.config | 8 +
.../plugin-webgpu-nuget-packaging-stage.yml | 167 +++++++++++++++++
.../stages/plugin-webgpu-packaging-stage.yml | 12 ++
12 files changed, 892 insertions(+)
create mode 100644 plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
create mode 100644 plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
create mode 100644 plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
create mode 100644 plugin-ep-webgpu/csharp/README.md
create mode 100644 plugin-ep-webgpu/csharp/pack_nuget.ps1
create mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
create mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
create mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
create mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
create mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
create mode 100644 tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
new file mode 100644
index 0000000000000..6c44b9bf96176
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -0,0 +1,72 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+
+
+ Microsoft.ML.OnnxRuntime.EP.WebGpu
+ 1.0.0
+ Microsoft
+ Microsoft
+ WebGPU plugin Execution Provider for ONNX Runtime. Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
+ README.md
+ ONNX;ONNX Runtime;Machine Learning;AI;Deep Learning;WebGPU
+
+
+ MIT
+ https://github.com/microsoft/onnxruntime
+ git
+ © Microsoft Corporation. All rights reserved.
+
+
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
new file mode 100644
index 0000000000000..8f4836f86cf35
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -0,0 +1,45 @@
+## Microsoft.ML.OnnxRuntime.EP.WebGpu
+
+WebGPU plugin Execution Provider for [ONNX Runtime](https://github.com/microsoft/onnxruntime).
+Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
+
+### Usage
+
+```csharp
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.EP.WebGpu;
+
+// Register the WebGPU EP plugin library
+var env = OrtEnv.Instance();
+env.RegisterExecutionProviderLibrary("webgpu_ep", WebGpuEp.GetLibraryPath());
+
+// Find the WebGPU EP device
+OrtEpDevice? webGpuDevice = null;
+foreach (var device in env.GetEpDevices())
+{
+ if (string.Equals(WebGpuEp.GetEpName(), device.EpName, StringComparison.Ordinal))
+ {
+ webGpuDevice = device;
+ break;
+ }
+}
+
+// Create a session with the WebGPU EP
+using var sessionOptions = new SessionOptions();
+sessionOptions.AppendExecutionProvider(env, new[] { webGpuDevice }, new Dictionary());
+
+using var session = new InferenceSession("model.onnx", sessionOptions);
+```
+
+### Supported Platforms
+
+| Runtime Identifier | Native Library |
+|---|---|
+| win-x64 | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` |
+| win-arm64 | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` |
+| linux-x64 | `libonnxruntime_providers_webgpu.so` |
+| osx-arm64 | `libonnxruntime_providers_webgpu.dylib` |
+
+### Requirements
+
+- [Microsoft.ML.OnnxRuntime](https://www.nuget.org/packages/Microsoft.ML.OnnxRuntime) >= 1.24.4
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
new file mode 100644
index 0000000000000..f2501ba6f5816
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.ML.OnnxRuntime.EP.WebGpu
+{
+ ///
+ /// Provides helper methods to locate the WebGPU plugin EP native library
+ /// and retrieve the EP name for registration with ONNX Runtime.
+ ///
+ public static class WebGpuEp
+ {
+ ///
+ /// Returns the path to the WebGPU plugin EP native library contained by this package.
+ /// Can be passed to OrtEnv.RegisterExecutionProviderLibrary().
+ ///
+ /// Full path to the EP native library.
+ /// If the native library file does not exist at the expected path.
+ public static string GetLibraryPath()
+ {
+ string rootDir = GetNativeDirectory();
+ string rid = GetRuntimeIdentifier();
+ string libraryName = GetLibraryName();
+ string epLibPath = Path.GetFullPath(Path.Combine(rootDir,
+ "runtimes", rid,
+ "native",
+ libraryName));
+
+ if (!File.Exists(epLibPath))
+ {
+ throw new FileNotFoundException(
+ $"Did not find WebGPU EP library file: {epLibPath}");
+ }
+
+ return epLibPath;
+ }
+
+ ///
+ /// Returns the names of the EPs created by the WebGPU plugin EP library.
+ /// Can be used to select an OrtEpDevice from those returned by OrtEnv.GetEpDevices().
+ ///
+ /// Array of EP names.
+ public static string[] GetEpNames()
+ {
+ return new[] { "WebGpuExecutionProvider" };
+ }
+
+ ///
+ /// Returns the name of the one EP supported by this plugin EP library.
+ /// Convenience method for plugin EP packages that expose a single EP.
+ ///
+ /// The EP name string.
+ public static string GetEpName()
+ {
+ return GetEpNames()[0];
+ }
+
+ private static string GetNativeDirectory()
+ {
+ var assemblyDir = Path.GetDirectoryName(typeof(WebGpuEp).Assembly.Location);
+
+ if (!string.IsNullOrEmpty(assemblyDir) && Directory.Exists(assemblyDir))
+ return assemblyDir;
+
+ return AppContext.BaseDirectory;
+ }
+
+ private static string GetRuntimeIdentifier()
+ {
+ return GetOSTag() + "-" + GetArchTag();
+ }
+
+ private static string GetLibraryName()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return "onnxruntime_providers_webgpu.dll";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ return "libonnxruntime_providers_webgpu.so";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return "libonnxruntime_providers_webgpu.dylib";
+
+ return "onnxruntime_providers_webgpu";
+ }
+
+ private static string GetOSTag()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx";
+ return "unknown";
+ }
+
+ private static string GetArchTag()
+ {
+ return RuntimeInformation.OSArchitecture == Architecture.X64 ? "x64"
+ : RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64"
+ : "unknown";
+ }
+ }
+}
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
new file mode 100644
index 0000000000000..220280633dcc4
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -0,0 +1,162 @@
+# WebGPU Plugin EP — NuGet Packaging
+
+This directory contains the C# NuGet package project and test app for the WebGPU plugin Execution Provider.
+
+## Directory Structure
+
+```
+csharp/
+├── pack_nuget.ps1 # Helper script to build the NuGet package
+├── Microsoft.ML.OnnxRuntime.EP.WebGpu/
+│ ├── Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj # NuGet package project (netstandard2.0)
+│ ├── WebGpuEp.cs # Helper class for native library resolution
+│ └── readme.md # Package readme (shipped inside .nupkg)
+└── test/
+ └── WebGpuEpNuGetTest/
+ ├── WebGpuEpNuGetTest.csproj # Test console app (net8.0)
+ ├── Program.cs # Registers EP, runs inference, validates output
+ ├── nuget.config # NuGet source config (local feed for CI)
+ ├── mul.onnx # Test model (element-wise multiply)
+ └── generate_mul_model.py # Script to regenerate mul.onnx
+```
+
+## Prerequisites
+
+- .NET SDK 8.0 or later
+- A built WebGPU plugin EP binary (see below)
+
+## Building the Plugin EP Binary
+
+From the repo root:
+
+```powershell
+python tools/ci_build/build.py `
+ --build_dir ./build/webgpu.plugin `
+ --use_webgpu shared_lib `
+ --use_vcpkg `
+ --config Release `
+ --parallel `
+ --update --build
+```
+
+The plugin DLL will be at `build/webgpu.plugin/Release/Release/onnxruntime_providers_webgpu.dll`.
+
+## Building the NuGet Package
+
+Use `pack_nuget.ps1` to stage native binaries and run `dotnet pack`. The script copies
+everything into a temporary staging directory under the output folder — the source tree
+is never modified.
+
+### Pack with a local build (single platform)
+
+```powershell
+cd plugin-ep-webgpu/csharp
+
+.\pack_nuget.ps1 -Version 1.26.0-dev `
+ -BinaryDir_WinX64 ..\..\build\webgpu.plugin\Release\Release
+```
+
+### Pack multiple platforms
+
+```powershell
+.\pack_nuget.ps1 -Version 1.26.0-dev `
+ -BinaryDir_WinX64 C:\builds\win_x64 `
+ -BinaryDir_WinArm64 C:\builds\win_arm64 `
+ -BinaryDir_LinuxX64 /mnt/builds/linux_x64 `
+ -BinaryDir_OsxArm64 /mnt/builds/osx_arm64
+```
+
+### Script Parameters
+
+| Parameter | Required | Default | Description |
+|---|---|---|---|
+| `-Version` | Yes | — | Package version string (e.g. `1.26.0`, `1.26.0-dev`) |
+| `-OutputDir` | No | `./nuget_output` | Directory for the `.nupkg` and `.snupkg` output |
+| `-Configuration` | No | `Release` | Build configuration |
+| `-ArtifactsDir` | No | — | CI mode: root directory with `win_x64/bin/`, `linux_x64/bin/`, etc. |
+| `-BinaryDir_WinX64` | No | — | Path to directory containing win-x64 binaries |
+| `-BinaryDir_WinArm64` | No | — | Path to directory containing win-arm64 binaries |
+| `-BinaryDir_LinuxX64` | No | — | Path to directory containing linux-x64 binaries |
+| `-BinaryDir_OsxArm64` | No | — | Path to directory containing osx-arm64 binaries |
+
+At least one binary directory (or `-ArtifactsDir` with matching subdirectories) must be provided.
+Platforms without a binary directory are skipped.
+
+## Inspecting the Package
+
+The `.nupkg` is a ZIP file. To verify its contents:
+
+```powershell
+Expand-Archive nuget_output/Microsoft.ML.OnnxRuntime.EP.WebGpu.1.26.0-dev.nupkg `
+ -DestinationPath nuget_output/inspect -Force
+
+Get-ChildItem nuget_output/inspect -Recurse | Select-Object FullName
+```
+
+Expected layout inside the package:
+
+```
+lib/netstandard2.0/Microsoft.ML.OnnxRuntime.EP.WebGpu.dll
+runtimes/win-x64/native/onnxruntime_providers_webgpu.dll
+runtimes/win-x64/native/dxil.dll
+runtimes/win-x64/native/dxcompiler.dll
+runtimes/win-arm64/native/...
+runtimes/linux-x64/native/libonnxruntime_providers_webgpu.so
+runtimes/osx-arm64/native/libonnxruntime_providers_webgpu.dylib
+```
+
+## Testing the Package
+
+The test app registers the WebGPU EP, creates a session, runs a simple Mul model, and
+validates the output. It requires a GPU with D3D12 or Vulkan support.
+
+```powershell
+# Point the test project's nuget.config at the pack output
+$localFeed = (Resolve-Path nuget_output).Path
+@"
+
+
+
+
+
+
+
+
+"@ | Set-Content test/WebGpuEpNuGetTest/nuget.config
+
+# Build and run
+dotnet run --project test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj --configuration Release
+```
+
+A successful run prints `PASSED: All outputs match expected values.` and exits with code 0.
+
+## Regenerating the Test Model
+
+```bash
+python test/WebGpuEpNuGetTest/generate_mul_model.py
+```
+
+Requires the `onnx` Python package.
+
+## CI Pipeline
+
+The NuGet packaging is integrated into the WebGPU plugin pipeline:
+
+- **Pipeline:** `tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml`
+- **Packaging stage:** `tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml`
+
+The CI stage downloads build artifacts from all enabled platform stages, invokes
+`pack_nuget.ps1`, ESRP-signs the package, and runs the test app on a GPU agent.
+
+## Native Binaries Per Platform
+
+| RID | Required Files |
+|---|---|
+| `win-x64` | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` |
+| `win-arm64` | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` |
+| `linux-x64` | `libonnxruntime_providers_webgpu.so` |
+| `osx-arm64` | `libonnxruntime_providers_webgpu.dylib` |
+
+On Windows, `dxil.dll` and `dxcompiler.dll` are the DirectX Shader Compiler binaries
+downloaded from the [DXC GitHub releases](https://github.com/microsoft/DirectXShaderCompiler/releases).
+The CI pipeline handles this automatically.
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
new file mode 100644
index 0000000000000..4950256f66031
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/pack_nuget.ps1
@@ -0,0 +1,175 @@
+<#
+.SYNOPSIS
+ Builds the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.
+
+.DESCRIPTION
+ Stages native binaries from build artifacts into the runtimes/ layout expected by the
+ .csproj and runs 'dotnet pack' to produce the .nupkg and .snupkg files.
+
+ Can be invoked locally or from CI. In CI, pass -ArtifactsDir to point at the
+ downloaded pipeline artifacts. Locally, pass individual -BinaryDir_* parameters
+ or place binaries manually in the runtimes/ folders.
+
+.EXAMPLE
+ # Local: pack win-x64 only from a local build
+ .\pack_nuget.ps1 -Version 1.26.0-dev -BinaryDir_WinX64 ..\..\build\webgpu.plugin\Release\Release
+
+.EXAMPLE
+ # CI: pack all platforms from downloaded artifacts
+ .\pack_nuget.ps1 -Version $(PluginPackageVersion) -ArtifactsDir $(Build.BinariesDirectory)\artifacts -OutputDir $(Build.ArtifactStagingDirectory)\nuget
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [string]$Version,
+
+ [string]$OutputDir = (Join-Path $PSScriptRoot "nuget_output"),
+
+ [string]$Configuration = "Release",
+
+ # CI mode: root directory containing per-platform subdirectories (win_x64/bin/, win_arm64/bin/, etc.)
+ [string]$ArtifactsDir,
+
+ # Local mode: individual binary directories per platform (takes precedence over ArtifactsDir for that platform)
+ [string]$BinaryDir_WinX64,
+ [string]$BinaryDir_WinArm64,
+ [string]$BinaryDir_LinuxX64,
+ [string]$BinaryDir_OsxArm64
+)
+
+$ErrorActionPreference = 'Stop'
+
+$projectDir = Join-Path $PSScriptRoot "Microsoft.ML.OnnxRuntime.EP.WebGpu"
+$csproj = Join-Path $projectDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+
+if (-not (Test-Path $csproj)) {
+ Write-Error "Project file not found: $csproj"
+ exit 1
+}
+
+$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
+New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
+
+# Stage into a temporary directory under the output dir so we don't modify the source tree.
+$stagingDir = Join-Path $OutputDir "_staging"
+if (Test-Path $stagingDir) {
+ Remove-Item -Recurse -Force $stagingDir
+}
+New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
+
+# Copy project sources to staging
+Write-Host "Staging project files to $stagingDir"
+Copy-Item -Path (Join-Path $projectDir "*") -Destination $stagingDir -Recurse -Force
+$stagedCsproj = Join-Path $stagingDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+
+# --- Platform definitions ---
+$platforms = [ordered]@{
+ win_x64 = @{
+ rid = 'win-x64'
+ files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
+ param = $BinaryDir_WinX64
+ }
+ win_arm64 = @{
+ rid = 'win-arm64'
+ files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
+ param = $BinaryDir_WinArm64
+ }
+ linux_x64 = @{
+ rid = 'linux-x64'
+ files = @('libonnxruntime_providers_webgpu.so')
+ param = $BinaryDir_LinuxX64
+ }
+ macos_arm64 = @{
+ rid = 'osx-arm64'
+ files = @('libonnxruntime_providers_webgpu.dylib')
+ param = $BinaryDir_OsxArm64
+ }
+}
+
+# --- Stage binaries ---
+$anyStaged = $false
+
+foreach ($entry in $platforms.GetEnumerator()) {
+ $name = $entry.Key
+ $info = $entry.Value
+ $rid = $info.rid
+
+ # Resolve source directory: explicit param > ArtifactsDir > skip
+ $sourceDir = $info.param
+ if (-not $sourceDir -and $ArtifactsDir) {
+ $candidate = Join-Path $ArtifactsDir "$name\bin"
+ if (Test-Path $candidate) {
+ $sourceDir = $candidate
+ }
+ }
+
+ if (-not $sourceDir) {
+ Write-Host "Skipping $name (no binary directory provided)"
+ continue
+ }
+
+ if (-not (Test-Path $sourceDir)) {
+ Write-Error "Binary directory does not exist: $sourceDir"
+ exit 1
+ }
+
+ $targetDir = Join-Path $stagingDir "runtimes\$rid\native"
+ New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
+
+ Write-Host "Staging $name -> runtimes/$rid/native/"
+ foreach ($file in $info.files) {
+ $src = Join-Path $sourceDir $file
+ if (-not (Test-Path $src)) {
+ Write-Error "Expected binary not found: $src"
+ exit 1
+ }
+ Copy-Item -Path $src -Destination $targetDir -Force
+ Write-Host " $file"
+ }
+ $anyStaged = $true
+}
+
+if (-not $anyStaged) {
+ Write-Error "No platform binaries were staged. Provide at least one -BinaryDir_* parameter or -ArtifactsDir."
+ exit 1
+}
+
+Write-Host ""
+Write-Host "Runtimes layout:"
+Get-ChildItem -Recurse (Join-Path $stagingDir "runtimes") | ForEach-Object { Write-Host " $($_.FullName)" }
+
+# --- Pack ---
+Write-Host ""
+Write-Host "Running dotnet pack (Version=$Version, Configuration=$Configuration)..."
+
+dotnet pack $stagedCsproj --configuration $Configuration "-p:Version=$Version" --output $OutputDir
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "dotnet pack failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+}
+
+# --- Verify ---
+Write-Host ""
+$nupkgs = Get-ChildItem "$OutputDir\*.nupkg"
+if ($nupkgs.Count -eq 0) {
+ Write-Error "No .nupkg files found in $OutputDir"
+ exit 1
+}
+foreach ($pkg in $nupkgs) {
+ Write-Host "Produced: $($pkg.Name) ($([math]::Round($pkg.Length / 1MB, 2)) MB)"
+}
+$snupkgs = Get-ChildItem "$OutputDir\*.snupkg" -ErrorAction SilentlyContinue
+foreach ($pkg in $snupkgs) {
+ Write-Host "Produced: $($pkg.Name) ($([math]::Round($pkg.Length / 1MB, 2)) MB)"
+}
+
+# --- Clean up staging directory ---
+if (Test-Path $stagingDir) {
+ Remove-Item -Recurse -Force $stagingDir
+ Write-Host ""
+ Write-Host "Cleaned up staging directory."
+}
+
+Write-Host ""
+Write-Host "Done. Output: $OutputDir"
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
new file mode 100644
index 0000000000000..dbf679e14e602
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
@@ -0,0 +1,81 @@
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.EP.WebGpu;
+
+class Program
+{
+ static int Main()
+ {
+ string epLibPath = WebGpuEp.GetLibraryPath();
+ string epRegistrationName = "webgpu_ep_registration";
+ string epName = WebGpuEp.GetEpName();
+
+ Console.WriteLine($"WebGPU EP library path: {epLibPath}");
+
+ var env = OrtEnv.Instance();
+ env.RegisterExecutionProviderLibrary(epRegistrationName, epLibPath);
+ Console.WriteLine($"Registered EP library: {epLibPath}");
+
+ try
+ {
+ // Find the OrtEpDevice for the WebGPU EP
+ OrtEpDevice? epDevice = null;
+ foreach (var d in env.GetEpDevices())
+ {
+ if (string.Equals(epName, d.EpName, StringComparison.Ordinal))
+ {
+ epDevice = d;
+ }
+ }
+
+ if (epDevice == null)
+ {
+ Console.Error.WriteLine($"ERROR: Unable to find OrtEpDevice with name '{epName}'");
+ return 1;
+ }
+ Console.WriteLine($"Found OrtEpDevice for EP: {epName}");
+
+ // Create session with WebGPU EP
+ using var sessionOptions = new SessionOptions();
+ sessionOptions.AppendExecutionProvider(env, new[] { epDevice }, new Dictionary());
+ sessionOptions.AddSessionConfigEntry("session.disable_cpu_ep_fallback", "1");
+
+ string inputModelPath = Path.Combine(AppContext.BaseDirectory, "mul.onnx");
+ Console.WriteLine($"Loading model: {inputModelPath}");
+
+ using var session = new InferenceSession(inputModelPath, sessionOptions);
+
+ // Run model: mul(x, y) = x * y
+ float[] inputData = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f };
+ using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(inputData, new long[] { 2, 3 });
+ var inputValues = new List { inputOrtValue, inputOrtValue }.AsReadOnly();
+ var inputNames = new List { "x", "y" }.AsReadOnly();
+ using var runOptions = new RunOptions();
+
+ using var outputs = session.Run(runOptions, inputNames, inputValues, session.OutputNames);
+
+ float[] expected = { 1.0f, 4.0f, 9.0f, 16.0f, 25.0f, 36.0f };
+ var actual = outputs[0].GetTensorDataAsSpan().ToArray();
+
+ Console.WriteLine($"Input: {string.Join(", ", inputData)}");
+ Console.WriteLine($"Output: {string.Join(", ", actual)}");
+ Console.WriteLine($"Expected: {string.Join(", ", expected)}");
+
+ // Validate output
+ for (int i = 0; i < expected.Length; i++)
+ {
+ if (Math.Abs(actual[i] - expected[i]) > 1e-5f)
+ {
+ Console.Error.WriteLine($"ERROR: Output mismatch at index {i}: expected {expected[i]}, got {actual[i]}");
+ return 1;
+ }
+ }
+
+ Console.WriteLine("PASSED: All outputs match expected values.");
+ return 0;
+ }
+ finally
+ {
+ env.UnregisterExecutionProviderLibrary(epRegistrationName);
+ }
+ }
+}
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
new file mode 100644
index 0000000000000..59f655989d19f
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net8.0
+ latest
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
new file mode 100644
index 0000000000000..8c0266a13ffcd
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
@@ -0,0 +1,26 @@
+"""Generate a simple Mul ONNX model for testing.
+
+Produces mul.onnx in the same directory as this script.
+The model computes z = x * y (element-wise) for float32 tensors of shape [2, 3].
+"""
+
+import os
+
+import onnx
+from onnx import TensorProto, helper
+
+X = helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3])
+Y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 3])
+Z = helper.make_tensor_value_info("z", TensorProto.FLOAT, [2, 3])
+
+mul_node = helper.make_node("Mul", inputs=["x", "y"], outputs=["z"])
+
+graph = helper.make_graph([mul_node], "mul_graph", [X, Y], [Z])
+model = helper.make_model(graph, producer_name="onnxruntime-webgpu-ep-test")
+model.opset_import[0].version = 13
+
+onnx.checker.check_model(model)
+
+output_path = os.path.join(os.path.dirname(__file__), "mul.onnx")
+onnx.save(model, output_path)
+print(f"Saved {output_path}")
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
new file mode 100644
index 0000000000000..f17ee6aca730e
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
@@ -0,0 +1,17 @@
+
+test:Z
+
+x
+yz"Mul mul_graphZ
+x
+
+
+Z
+y
+
+
+b
+z
+
+
+B
\ No newline at end of file
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
new file mode 100644
index 0000000000000..4cee466ee22bc
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
new file mode 100644
index 0000000000000..19ba9bbd25936
--- /dev/null
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -0,0 +1,167 @@
+# NuGet packaging stage for WebGPU plugin EP.
+# Downloads platform-specific build artifacts, packs them into a single multi-platform NuGet package,
+# signs it, and runs a basic test.
+
+parameters:
+- name: package_version
+ type: string
+
+- name: cmake_build_type
+ type: string
+ default: 'Release'
+
+- name: DoEsrp
+ type: boolean
+ default: true
+
+- name: platforms
+ type: object
+ default:
+ win_x64: false
+ win_arm64: false
+ linux_x64: false
+ macos_arm64: false
+
+stages:
+- stage: NuGet_Packaging
+ displayName: 'NuGet Packaging'
+ dependsOn:
+ - ${{ if eq(parameters.platforms.win_x64, true) }}:
+ - Win_plugin_webgpu_x64_Build
+ - ${{ if eq(parameters.platforms.win_arm64, true) }}:
+ - Win_plugin_webgpu_arm64_Build
+ - ${{ if eq(parameters.platforms.linux_x64, true) }}:
+ - Linux_plugin_webgpu_x64_Build
+ - ${{ if eq(parameters.platforms.macos_arm64, true) }}:
+ - MacOS_plugin_webgpu_arm64_Build
+ jobs:
+ # ---------- Pack job ----------
+ - job: NuGet_Pack
+ displayName: 'Pack NuGet'
+ timeoutInMinutes: 30
+ workspace:
+ clean: all
+ pool:
+ name: onnxruntime-Win-CPU-VS2022-Latest
+ os: windows
+ templateContext:
+ outputs:
+ - output: pipelineArtifact
+ targetPath: '$(Build.ArtifactStagingDirectory)\nuget'
+ artifactName: webgpu_plugin_nuget
+ variables:
+ - template: ../templates/common-variables.yml
+ steps:
+ - checkout: self
+ clean: true
+ submodules: none
+
+ - template: ../templates/set-plugin-build-variables-step.yml
+ parameters:
+ package_version: ${{ parameters.package_version }}
+
+ # Download platform artifacts
+ - ${{ if eq(parameters.platforms.win_x64, true) }}:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download win-x64 artifacts'
+ inputs:
+ artifactName: webgpu_plugin_win_x64
+ targetPath: '$(Build.BinariesDirectory)\artifacts\win_x64'
+
+ - ${{ if eq(parameters.platforms.win_arm64, true) }}:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download win-arm64 artifacts'
+ inputs:
+ artifactName: webgpu_plugin_win_arm64
+ targetPath: '$(Build.BinariesDirectory)\artifacts\win_arm64'
+
+ - ${{ if eq(parameters.platforms.linux_x64, true) }}:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download linux-x64 artifacts'
+ inputs:
+ artifactName: webgpu_plugin_linux_x64
+ targetPath: '$(Build.BinariesDirectory)\artifacts\linux_x64'
+
+ - ${{ if eq(parameters.platforms.macos_arm64, true) }}:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download macos-arm64 artifacts'
+ inputs:
+ artifactName: webgpu_plugin_macos_arm64
+ targetPath: '$(Build.BinariesDirectory)\artifacts\macos_arm64'
+
+ # Stage binaries and pack NuGet
+ - task: PowerShell@2
+ displayName: 'Pack NuGet package'
+ inputs:
+ targetType: filePath
+ filePath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.ps1'
+ pwsh: true
+ arguments: >-
+ -Version "$(PluginPackageVersion)"
+ -ArtifactsDir "$(Build.BinariesDirectory)\artifacts"
+ -OutputDir "$(Build.ArtifactStagingDirectory)\nuget"
+ -Configuration Release
+
+ # ESRP sign
+ - template: ../templates/esrp_nuget.yml
+ parameters:
+ FolderPath: '$(Build.ArtifactStagingDirectory)\nuget'
+ DisplayName: 'ESRP - Sign NuGet package'
+ DoEsrp: ${{ parameters.DoEsrp }}
+
+ # ---------- Test job ----------
+ - ${{ if eq(parameters.platforms.win_x64, true) }}:
+ - job: NuGet_Test
+ displayName: 'Test NuGet Package'
+ dependsOn: NuGet_Pack
+ timeoutInMinutes: 30
+ workspace:
+ clean: all
+ pool:
+ name: onnxruntime-Win2022-VS2022-webgpu-A10
+ os: windows
+ steps:
+ - checkout: self
+ clean: true
+ submodules: none
+
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download NuGet package'
+ inputs:
+ artifactName: webgpu_plugin_nuget
+ targetPath: '$(Build.BinariesDirectory)\nuget_package'
+
+ # Set up local NuGet feed
+ - powershell: |
+ $ErrorActionPreference = 'Stop'
+ $localFeedDir = "$(Build.BinariesDirectory)\local_feed"
+ New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
+
+ # Copy .nupkg to local feed
+ Copy-Item "$(Build.BinariesDirectory)\nuget_package\*.nupkg" $localFeedDir -Force
+
+ # Update nuget.config in test project to point to local feed
+ $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
+ $configContent = @"
+
+
+
+
+
+
+
+
+ "@
+ Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
+ Write-Host "Updated nuget.config with local feed: $localFeedDir"
+ Write-Host "Local feed contents:"
+ Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
+ displayName: 'Set up local NuGet feed'
+
+ - script: |
+ dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
+ displayName: 'Build test project'
+
+ - script: |
+ dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
+ displayName: 'Run NuGet package test'
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 9db25f5727cc2..76f818a4271d7 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -77,3 +77,15 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
+
+ # NuGet packaging (runs after all platform builds)
+ - template: plugin-webgpu-nuget-packaging-stage.yml
+ parameters:
+ package_version: ${{ parameters.package_version }}
+ cmake_build_type: ${{ parameters.cmake_build_type }}
+ DoEsrp: true
+ platforms:
+ win_x64: ${{ parameters.build_windows_x64 }}
+ win_arm64: ${{ and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}
+ linux_x64: ${{ parameters.build_linux_x64 }}
+ macos_arm64: ${{ parameters.build_macos_arm64 }}
From 91878306a3c72082e6f2c135351641aedbbc6c6d Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Wed, 15 Apr 2026 18:27:20 -0700
Subject: [PATCH 02/41] Add setup-build-tools and set-nightly-build-option
templates to NuGet_Pack job
---
.../stages/plugin-webgpu-nuget-packaging-stage.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index 19ba9bbd25936..3928fb23db041 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -56,6 +56,12 @@ stages:
clean: true
submodules: none
+ - template: ../templates/setup-build-tools.yml
+ parameters:
+ host_cpu_arch: 'x64'
+
+ - template: ../templates/set-nightly-build-option-variable-step.yml
+
- template: ../templates/set-plugin-build-variables-step.yml
parameters:
package_version: ${{ parameters.package_version }}
From 02f4eb9ba0877eccca4084da97526da808b8394a Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 16:13:41 -0700
Subject: [PATCH 03/41] Move NuGet test job to plugin-webgpu-test-pipeline
---
.../plugin-webgpu-test-pipeline.yml | 9 +++
.../plugin-webgpu-nuget-packaging-stage.yml | 57 -------------------
.../stages/plugin-webgpu-nuget-test-stage.yml | 56 ++++++++++++++++++
3 files changed, 65 insertions(+), 57 deletions(-)
create mode 100644 tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
index c322437bf7c9f..ce4322cf24079 100644
--- a/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
+++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
@@ -47,6 +47,11 @@ parameters:
type: boolean
default: true
+- name: test_nuget
+ displayName: 'Test NuGet Package'
+ type: boolean
+ default: true
+
extends:
# The pipeline extends the 1ES PT which will inject SDL and compliance
# tasks. Uses "Official" to stay consistent with the companion
@@ -95,3 +100,7 @@ extends:
# macOS ARM64
- ${{ if eq(parameters.test_macos_arm64, true) }}:
- template: stages/plugin-mac-webgpu-test-stage.yml
+
+ # NuGet package
+ - ${{ if eq(parameters.test_nuget, true) }}:
+ - template: stages/plugin-webgpu-nuget-test-stage.yml
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index 3928fb23db041..70b007b1d73a3 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -114,60 +114,3 @@ stages:
FolderPath: '$(Build.ArtifactStagingDirectory)\nuget'
DisplayName: 'ESRP - Sign NuGet package'
DoEsrp: ${{ parameters.DoEsrp }}
-
- # ---------- Test job ----------
- - ${{ if eq(parameters.platforms.win_x64, true) }}:
- - job: NuGet_Test
- displayName: 'Test NuGet Package'
- dependsOn: NuGet_Pack
- timeoutInMinutes: 30
- workspace:
- clean: all
- pool:
- name: onnxruntime-Win2022-VS2022-webgpu-A10
- os: windows
- steps:
- - checkout: self
- clean: true
- submodules: none
-
- - task: DownloadPipelineArtifact@2
- displayName: 'Download NuGet package'
- inputs:
- artifactName: webgpu_plugin_nuget
- targetPath: '$(Build.BinariesDirectory)\nuget_package'
-
- # Set up local NuGet feed
- - powershell: |
- $ErrorActionPreference = 'Stop'
- $localFeedDir = "$(Build.BinariesDirectory)\local_feed"
- New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
-
- # Copy .nupkg to local feed
- Copy-Item "$(Build.BinariesDirectory)\nuget_package\*.nupkg" $localFeedDir -Force
-
- # Update nuget.config in test project to point to local feed
- $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
- $configContent = @"
-
-
-
-
-
-
-
-
- "@
- Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
- Write-Host "Updated nuget.config with local feed: $localFeedDir"
- Write-Host "Local feed contents:"
- Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
- displayName: 'Set up local NuGet feed'
-
- - script: |
- dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
- displayName: 'Build test project'
-
- - script: |
- dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
- displayName: 'Run NuGet package test'
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
new file mode 100644
index 0000000000000..809777b23add5
--- /dev/null
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
@@ -0,0 +1,56 @@
+stages:
+- stage: plugin_webgpu_nuget_Test
+ dependsOn: []
+ jobs:
+ - job: Win_plugin_webgpu_nuget_Test
+ timeoutInMinutes: 30
+ workspace:
+ clean: all
+ pool:
+ name: onnxruntime-Win2022-VS2022-webgpu-A10
+ os: windows
+ steps:
+ - checkout: self
+ clean: true
+ submodules: none
+
+ # Download the NuGet package produced by the packaging pipeline run that
+ # triggered this pipeline (or that was selected at queue time).
+ - download: build
+ artifact: webgpu_plugin_nuget
+ displayName: 'Download NuGet package'
+
+ # Set up local NuGet feed
+ - powershell: |
+ $ErrorActionPreference = 'Stop'
+ $localFeedDir = "$(Build.BinariesDirectory)\local_feed"
+ New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
+
+ # Copy .nupkg to local feed
+ Copy-Item "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\*.nupkg" $localFeedDir -Force
+
+ # Update nuget.config in test project to point to local feed
+ $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
+ $configContent = @"
+
+
+
+
+
+
+
+
+ "@
+ Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
+ Write-Host "Updated nuget.config with local feed: $localFeedDir"
+ Write-Host "Local feed contents:"
+ Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
+ displayName: 'Set up local NuGet feed'
+
+ - script: |
+ dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
+ displayName: 'Build test project'
+
+ - script: |
+ dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
+ displayName: 'Run NuGet package test'
From 3a484451818f58af00eda9cb364386b449a79b64 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 18:34:10 -0700
Subject: [PATCH 04/41] add version_file parameter
---
.../stages/plugin-webgpu-nuget-packaging-stage.yml | 4 ++++
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 1 +
2 files changed, 5 insertions(+)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index 70b007b1d73a3..e0ae6b32c9864 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -6,6 +6,9 @@ parameters:
- name: package_version
type: string
+- name: version_file
+ type: string
+
- name: cmake_build_type
type: string
default: 'Release'
@@ -65,6 +68,7 @@ stages:
- template: ../templates/set-plugin-build-variables-step.yml
parameters:
package_version: ${{ parameters.package_version }}
+ version_file: ${{ parameters.version_file }}
# Download platform artifacts
- ${{ if eq(parameters.platforms.win_x64, true) }}:
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 76f818a4271d7..2756acd5340ab 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -82,6 +82,7 @@ stages:
- template: plugin-webgpu-nuget-packaging-stage.yml
parameters:
package_version: ${{ parameters.package_version }}
+ version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
DoEsrp: true
platforms:
From 7a2e483291ae4bbec9c9b37c8e121afca4c931d8 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 27 Apr 2026 18:45:41 -0700
Subject: [PATCH 05/41] Move NuGet test job into Windows test stage
---
.../plugin-webgpu-test-pipeline.yml | 9 ---
.../stages/plugin-webgpu-nuget-test-stage.yml | 56 -------------------
.../stages/plugin-win-webgpu-test-stage.yml | 56 +++++++++++++++++++
3 files changed, 56 insertions(+), 65 deletions(-)
delete mode 100644 tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
index ce4322cf24079..c322437bf7c9f 100644
--- a/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
+++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-test-pipeline.yml
@@ -47,11 +47,6 @@ parameters:
type: boolean
default: true
-- name: test_nuget
- displayName: 'Test NuGet Package'
- type: boolean
- default: true
-
extends:
# The pipeline extends the 1ES PT which will inject SDL and compliance
# tasks. Uses "Official" to stay consistent with the companion
@@ -100,7 +95,3 @@ extends:
# macOS ARM64
- ${{ if eq(parameters.test_macos_arm64, true) }}:
- template: stages/plugin-mac-webgpu-test-stage.yml
-
- # NuGet package
- - ${{ if eq(parameters.test_nuget, true) }}:
- - template: stages/plugin-webgpu-nuget-test-stage.yml
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
deleted file mode 100644
index 809777b23add5..0000000000000
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-test-stage.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-stages:
-- stage: plugin_webgpu_nuget_Test
- dependsOn: []
- jobs:
- - job: Win_plugin_webgpu_nuget_Test
- timeoutInMinutes: 30
- workspace:
- clean: all
- pool:
- name: onnxruntime-Win2022-VS2022-webgpu-A10
- os: windows
- steps:
- - checkout: self
- clean: true
- submodules: none
-
- # Download the NuGet package produced by the packaging pipeline run that
- # triggered this pipeline (or that was selected at queue time).
- - download: build
- artifact: webgpu_plugin_nuget
- displayName: 'Download NuGet package'
-
- # Set up local NuGet feed
- - powershell: |
- $ErrorActionPreference = 'Stop'
- $localFeedDir = "$(Build.BinariesDirectory)\local_feed"
- New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
-
- # Copy .nupkg to local feed
- Copy-Item "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\*.nupkg" $localFeedDir -Force
-
- # Update nuget.config in test project to point to local feed
- $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
- $configContent = @"
-
-
-
-
-
-
-
-
- "@
- Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
- Write-Host "Updated nuget.config with local feed: $localFeedDir"
- Write-Host "Local feed contents:"
- Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
- displayName: 'Set up local NuGet feed'
-
- - script: |
- dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
- displayName: 'Build test project'
-
- - script: |
- dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
- displayName: 'Run NuGet package test'
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index af29a62d69329..3d47e303c4abc 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -57,3 +57,59 @@ stages:
echo "running test_webgpu_plugin_ep.py"
python -u "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\test\test_webgpu_plugin_ep.py"
+
+ # NuGet package test (x64 only — the NuGet package is multi-platform but
+ # the test runs on a single Windows agent that exercises the WebGPU EP).
+ - ${{ if eq(parameters.arch, 'x64') }}:
+ - job: Win_plugin_webgpu_nuget_Test
+ timeoutInMinutes: 30
+ workspace:
+ clean: all
+ pool:
+ name: onnxruntime-Win2022-VS2022-webgpu-A10
+ os: windows
+ steps:
+ - checkout: self
+ clean: true
+ submodules: none
+
+ # Download the NuGet package produced by the packaging pipeline run that
+ # triggered this pipeline (or that was selected at queue time).
+ - download: build
+ artifact: webgpu_plugin_nuget
+ displayName: 'Download NuGet package'
+
+ # Set up local NuGet feed
+ - powershell: |
+ $ErrorActionPreference = 'Stop'
+ $localFeedDir = "$(Build.BinariesDirectory)\local_feed"
+ New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
+
+ # Copy .nupkg to local feed
+ Copy-Item "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\*.nupkg" $localFeedDir -Force
+
+ # Update nuget.config in test project to point to local feed
+ $nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
+ $configContent = @"
+
+
+
+
+
+
+
+
+ "@
+ Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
+ Write-Host "Updated nuget.config with local feed: $localFeedDir"
+ Write-Host "Local feed contents:"
+ Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
+ displayName: 'Set up local NuGet feed'
+
+ - script: |
+ dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
+ displayName: 'Build test project'
+
+ - script: |
+ dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
+ displayName: 'Run NuGet package test'
From e36bacf6ea818470995f4402e22c079e1ae8d0d2 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Wed, 29 Apr 2026 16:22:10 -0700
Subject: [PATCH 06/41] Use internal NuGet feed for WebGPU plugin packaging and
test pipelines
Pack stage: add -NuGetConfig parameter to pack_nuget.ps1 and pass the repo-root NuGet.config (locked to the 1ES feed by setup-feeds-and-python-steps.yml). The staging dir lives outside the source tree, so NuGet's upward config walk would otherwise miss the lockdown and fall back to public nuget.org, which the network policy blocks (NU1301).
Test stage: pull in setup-feeds-and-python-steps.yml for the NuGet test job, and rewrite the inline nuget.config generation to add only the local feed. NuGet's hierarchical merge layers it on top of the repo-root config, giving effective sources of 1ES Feed + local. Drop the previously-checked-in project-level nuget.config (had a stale dev-only path); the pipeline generates it deterministically now.
---
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 22 +++++++++++++++++--
.../test/WebGpuEpNuGetTest/nuget.config | 8 -------
.../plugin-webgpu-nuget-packaging-stage.yml | 1 +
.../stages/plugin-win-webgpu-test-stage.yml | 12 +++++-----
4 files changed, 27 insertions(+), 16 deletions(-)
delete mode 100644 plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
index 4950256f66031..91dc5e11db063 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.ps1
+++ b/plugin-ep-webgpu/csharp/pack_nuget.ps1
@@ -35,7 +35,10 @@ param(
[string]$BinaryDir_WinX64,
[string]$BinaryDir_WinArm64,
[string]$BinaryDir_LinuxX64,
- [string]$BinaryDir_OsxArm64
+ [string]$BinaryDir_OsxArm64,
+
+ # Optional NuGet.config to pass to `dotnet pack` via --configfile.
+ [string]$NuGetConfig
)
$ErrorActionPreference = 'Stop'
@@ -143,7 +146,22 @@ Get-ChildItem -Recurse (Join-Path $stagingDir "runtimes") | ForEach-Object { Wri
Write-Host ""
Write-Host "Running dotnet pack (Version=$Version, Configuration=$Configuration)..."
-dotnet pack $stagedCsproj --configuration $Configuration "-p:Version=$Version" --output $OutputDir
+$packArgs = @(
+ $stagedCsproj,
+ '--configuration', $Configuration,
+ "-p:Version=$Version",
+ '--output', $OutputDir
+)
+if ($NuGetConfig) {
+ if (-not (Test-Path $NuGetConfig)) {
+ Write-Error "NuGet.config not found: $NuGetConfig"
+ exit 1
+ }
+ $packArgs += @('--configfile', $NuGetConfig)
+ Write-Host "Using NuGet.config: $NuGetConfig"
+}
+
+dotnet pack @packArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet pack failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
deleted file mode 100644
index 4cee466ee22bc..0000000000000
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/nuget.config
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index e0ae6b32c9864..2ae6853f349a8 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -111,6 +111,7 @@ stages:
-ArtifactsDir "$(Build.BinariesDirectory)\artifacts"
-OutputDir "$(Build.ArtifactStagingDirectory)\nuget"
-Configuration Release
+ -NuGetConfig "$(Build.SourcesDirectory)\NuGet.config"
# ESRP sign
- template: ../templates/esrp_nuget.yml
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index 3d47e303c4abc..28ff774d0039e 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -73,6 +73,8 @@ stages:
clean: true
submodules: none
+ - template: ../templates/setup-feeds-and-python-steps.yml
+
# Download the NuGet package produced by the packaging pipeline run that
# triggered this pipeline (or that was selected at queue time).
- download: build
@@ -88,20 +90,18 @@ stages:
# Copy .nupkg to local feed
Copy-Item "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\*.nupkg" $localFeedDir -Force
- # Update nuget.config in test project to point to local feed
+ # Write a project-level nuget.config that adds ONLY the local feed.
+ # NuGet merges this with the repo-root NuGet.config.
$nugetConfig = "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\nuget.config"
- $configContent = @"
+ Set-Content -Path $nugetConfig -Encoding UTF8 -Value @"
-
-
"@
- Set-Content -Path $nugetConfig -Value $configContent -Encoding UTF8
- Write-Host "Updated nuget.config with local feed: $localFeedDir"
+ Write-Host "Wrote project-level nuget.config with local feed: $localFeedDir"
Write-Host "Local feed contents:"
Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
displayName: 'Set up local NuGet feed'
From 0924c976e801934b48291d31cadf03ff5ef887d8 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 11:40:16 -0700
Subject: [PATCH 07/41] Sign managed WebGPU plugin DLL before packing into
NuGet
Split pack_nuget.ps1 into -BuildOnly and -PackOnly phases so the managed Microsoft.ML.OnnxRuntime.EP.WebGpu.dll can be ESRP-signed in between, satisfying CodeSign compliance for files inside the .nupkg.
Also add a -StagingDir parameter so the packaging YAML doesn't depend on script-internal layout, lift NuGetConfig validation to the top of the script, and share common pack_nuget.ps1 args between the build and pack steps via a WebGpuPackNuGetCommonArgs pipeline variable.
---
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 181 ++++++++++++------
.../plugin-webgpu-nuget-packaging-stage.yml | 39 +++-
2 files changed, 158 insertions(+), 62 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
index 91dc5e11db063..90074961bda55 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.ps1
+++ b/plugin-ep-webgpu/csharp/pack_nuget.ps1
@@ -38,9 +38,29 @@ param(
[string]$BinaryDir_OsxArm64,
# Optional NuGet.config to pass to `dotnet pack` via --configfile.
- [string]$NuGetConfig
+ [string]$NuGetConfig,
+
+ # Optional explicit staging directory. If unset, a directory under $OutputDir is used.
+ # Useful in CI when the caller wants to point ESRP signing at a known location.
+ [string]$StagingDir,
+
+ # Split-phase switches for CI signing: build the managed DLL in one task
+ # (so it can be ESRP-signed), then pack with --no-build in a later task.
+ # When neither is set, the script does the full build+pack end-to-end.
+ [switch]$BuildOnly,
+ [switch]$PackOnly
)
+if ($BuildOnly -and $PackOnly) {
+ Write-Error "-BuildOnly and -PackOnly are mutually exclusive."
+ exit 1
+}
+
+if ($NuGetConfig -and -not (Test-Path $NuGetConfig)) {
+ Write-Error "NuGet.config not found: $NuGetConfig"
+ exit 1
+}
+
$ErrorActionPreference = 'Stop'
$projectDir = Join-Path $PSScriptRoot "Microsoft.ML.OnnxRuntime.EP.WebGpu"
@@ -54,17 +74,34 @@ if (-not (Test-Path $csproj)) {
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
-# Stage into a temporary directory under the output dir so we don't modify the source tree.
-$stagingDir = Join-Path $OutputDir "_staging"
-if (Test-Path $stagingDir) {
- Remove-Item -Recurse -Force $stagingDir
+# Stage into a temporary directory so we don't modify the source tree.
+# When the caller provides $StagingDir explicitly, they own its lifecycle (no auto-cleanup).
+$ownsStaging = -not $StagingDir
+if ($StagingDir) {
+ $StagingDir = [System.IO.Path]::GetFullPath($StagingDir)
+}
+else {
+ $StagingDir = Join-Path $OutputDir "_staging"
+}
+$stagedCsproj = Join-Path $StagingDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+
+if ($PackOnly) {
+ if (-not (Test-Path $stagedCsproj)) {
+ Write-Error "Staged project not found at $stagedCsproj. Run with -BuildOnly first."
+ exit 1
+ }
+ Write-Host "Reusing existing staging directory: $StagingDir"
}
-New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null
+else {
+ if (Test-Path $StagingDir) {
+ Remove-Item -Recurse -Force $StagingDir
+ }
+ New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
-# Copy project sources to staging
-Write-Host "Staging project files to $stagingDir"
-Copy-Item -Path (Join-Path $projectDir "*") -Destination $stagingDir -Recurse -Force
-$stagedCsproj = Join-Path $stagingDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+ # Copy project sources to staging
+ Write-Host "Staging project files to $StagingDir"
+ Copy-Item -Path (Join-Path $projectDir "*") -Destination $StagingDir -Recurse -Force
+}
# --- Platform definitions ---
$platforms = [ordered]@{
@@ -91,58 +128,91 @@ $platforms = [ordered]@{
}
# --- Stage binaries ---
-$anyStaged = $false
-
-foreach ($entry in $platforms.GetEnumerator()) {
- $name = $entry.Key
- $info = $entry.Value
- $rid = $info.rid
-
- # Resolve source directory: explicit param > ArtifactsDir > skip
- $sourceDir = $info.param
- if (-not $sourceDir -and $ArtifactsDir) {
- $candidate = Join-Path $ArtifactsDir "$name\bin"
- if (Test-Path $candidate) {
- $sourceDir = $candidate
+if (-not $PackOnly) {
+ $anyStaged = $false
+
+ foreach ($entry in $platforms.GetEnumerator()) {
+ $name = $entry.Key
+ $info = $entry.Value
+ $rid = $info.rid
+
+ # Resolve source directory: explicit param > ArtifactsDir > skip
+ $sourceDir = $info.param
+ if (-not $sourceDir -and $ArtifactsDir) {
+ $candidate = Join-Path $ArtifactsDir "$name\bin"
+ if (Test-Path $candidate) {
+ $sourceDir = $candidate
+ }
+ }
+
+ if (-not $sourceDir) {
+ Write-Host "Skipping $name (no binary directory provided)"
+ continue
}
- }
- if (-not $sourceDir) {
- Write-Host "Skipping $name (no binary directory provided)"
- continue
+ if (-not (Test-Path $sourceDir)) {
+ Write-Error "Binary directory does not exist: $sourceDir"
+ exit 1
+ }
+
+ $targetDir = Join-Path $StagingDir "runtimes\$rid\native"
+ New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
+
+ Write-Host "Staging $name -> runtimes/$rid/native/"
+ foreach ($file in $info.files) {
+ $src = Join-Path $sourceDir $file
+ if (-not (Test-Path $src)) {
+ Write-Error "Expected binary not found: $src"
+ exit 1
+ }
+ Copy-Item -Path $src -Destination $targetDir -Force
+ Write-Host " $file"
+ }
+ $anyStaged = $true
}
- if (-not (Test-Path $sourceDir)) {
- Write-Error "Binary directory does not exist: $sourceDir"
+ if (-not $anyStaged) {
+ Write-Error "No platform binaries were staged. Provide at least one -BinaryDir_* parameter or -ArtifactsDir."
exit 1
}
- $targetDir = Join-Path $stagingDir "runtimes\$rid\native"
- New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
+ Write-Host ""
+ Write-Host "Runtimes layout:"
+ Get-ChildItem -Recurse (Join-Path $StagingDir "runtimes") | ForEach-Object { Write-Host " $($_.FullName)" }
+}
- Write-Host "Staging $name -> runtimes/$rid/native/"
- foreach ($file in $info.files) {
- $src = Join-Path $sourceDir $file
- if (-not (Test-Path $src)) {
- Write-Error "Expected binary not found: $src"
- exit 1
- }
- Copy-Item -Path $src -Destination $targetDir -Force
- Write-Host " $file"
+# --- Build / Pack ---
+if ($BuildOnly) {
+ Write-Host ""
+ Write-Host "Running dotnet build (Version=$Version, Configuration=$Configuration)..."
+
+ $buildArgs = @(
+ $stagedCsproj,
+ '--configuration', $Configuration,
+ "-p:Version=$Version"
+ )
+ if ($NuGetConfig) {
+ $buildArgs += @('--configfile', $NuGetConfig)
+ Write-Host "Using NuGet.config: $NuGetConfig"
}
- $anyStaged = $true
-}
-if (-not $anyStaged) {
- Write-Error "No platform binaries were staged. Provide at least one -BinaryDir_* parameter or -ArtifactsDir."
- exit 1
-}
+ dotnet build @buildArgs
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "dotnet build failed with exit code $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
-Write-Host ""
-Write-Host "Runtimes layout:"
-Get-ChildItem -Recurse (Join-Path $stagingDir "runtimes") | ForEach-Object { Write-Host " $($_.FullName)" }
+ $managedDll = Join-Path $StagingDir "bin\$Configuration\netstandard2.0\Microsoft.ML.OnnxRuntime.EP.WebGpu.dll"
+ if (-not (Test-Path $managedDll)) {
+ Write-Error "Managed DLL not found after build: $managedDll"
+ exit 1
+ }
+ Write-Host ""
+ Write-Host "Built managed DLL: $managedDll"
+ Write-Host "Staging directory preserved for subsequent -PackOnly invocation."
+ exit 0
+}
-# --- Pack ---
Write-Host ""
Write-Host "Running dotnet pack (Version=$Version, Configuration=$Configuration)..."
@@ -152,11 +222,10 @@ $packArgs = @(
"-p:Version=$Version",
'--output', $OutputDir
)
+if ($PackOnly) {
+ $packArgs += '--no-build'
+}
if ($NuGetConfig) {
- if (-not (Test-Path $NuGetConfig)) {
- Write-Error "NuGet.config not found: $NuGetConfig"
- exit 1
- }
$packArgs += @('--configfile', $NuGetConfig)
Write-Host "Using NuGet.config: $NuGetConfig"
}
@@ -183,8 +252,8 @@ foreach ($pkg in $snupkgs) {
}
# --- Clean up staging directory ---
-if (Test-Path $stagingDir) {
- Remove-Item -Recurse -Force $stagingDir
+if ($ownsStaging -and (Test-Path $StagingDir)) {
+ Remove-Item -Recurse -Force $StagingDir
Write-Host ""
Write-Host "Cleaned up staging directory."
}
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index 2ae6853f349a8..a1884f05bfbec 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -54,6 +54,16 @@ stages:
artifactName: webgpu_plugin_nuget
variables:
- template: ../templates/common-variables.yml
+ - name: WebGpuPackStagingDir
+ value: '$(Build.BinariesDirectory)\webgpu_pack_staging'
+ # Common arguments shared by the Build and Pack invocations of pack_nuget.ps1.
+ - name: WebGpuPackNuGetCommonArgs
+ value: >-
+ -Version "$(PluginPackageVersion)"
+ -OutputDir "$(Build.ArtifactStagingDirectory)\nuget"
+ -StagingDir "$(WebGpuPackStagingDir)"
+ -Configuration Release
+ -NuGetConfig "$(Build.SourcesDirectory)\NuGet.config"
steps:
- checkout: self
clean: true
@@ -99,19 +109,36 @@ stages:
artifactName: webgpu_plugin_macos_arm64
targetPath: '$(Build.BinariesDirectory)\artifacts\macos_arm64'
- # Stage binaries and pack NuGet
+ # Stage binaries and build the managed assembly (so it can be ESRP-signed before packing).
- task: PowerShell@2
- displayName: 'Pack NuGet package'
+ displayName: 'Build managed DLL'
inputs:
targetType: filePath
filePath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.ps1'
pwsh: true
arguments: >-
- -Version "$(PluginPackageVersion)"
+ $(WebGpuPackNuGetCommonArgs)
-ArtifactsDir "$(Build.BinariesDirectory)\artifacts"
- -OutputDir "$(Build.ArtifactStagingDirectory)\nuget"
- -Configuration Release
- -NuGetConfig "$(Build.SourcesDirectory)\NuGet.config"
+ -BuildOnly
+
+ # ESRP-sign the managed DLL before it gets embedded in the .nupkg.
+ - template: ../templates/win-esrp-dll.yml
+ parameters:
+ FolderPath: '$(WebGpuPackStagingDir)'
+ Pattern: '**\Microsoft.ML.OnnxRuntime.EP.WebGpu.dll'
+ DisplayName: 'ESRP - Sign managed DLL'
+ DoEsrp: ${{ parameters.DoEsrp }}
+
+ # Pack the (now-signed) managed DLL plus native binaries into the .nupkg.
+ - task: PowerShell@2
+ displayName: 'Pack NuGet package'
+ inputs:
+ targetType: filePath
+ filePath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.ps1'
+ pwsh: true
+ arguments: >-
+ $(WebGpuPackNuGetCommonArgs)
+ -PackOnly
# ESRP sign
- template: ../templates/esrp_nuget.yml
From 7fabfd151c59b6ae4b26cb46718799a977fcddc0 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 11:55:16 -0700
Subject: [PATCH 08/41] add build_wheel.py comment about assumption that
binaries are directly under binary_dir.
---
plugin-ep-webgpu/python/build_wheel.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin-ep-webgpu/python/build_wheel.py b/plugin-ep-webgpu/python/build_wheel.py
index 8f855a5d2179b..b4357bcdfbe0f 100644
--- a/plugin-ep-webgpu/python/build_wheel.py
+++ b/plugin-ep-webgpu/python/build_wheel.py
@@ -86,6 +86,7 @@ def prepare_staging_dir(staging_dir: Path, binary_dir: Path, version: str):
shutil.copytree(SCRIPT_DIR / "onnxruntime_ep_webgpu", staging_dir / "onnxruntime_ep_webgpu")
# Copy plugin binaries into the package directory
+ # Note: The binaries are assumed to be directly under `binary_dir`.
package_dir = staging_dir / "onnxruntime_ep_webgpu"
copied = []
for pattern in BINARY_PATTERNS:
From 11d41ee70156de6838f47ce3e4d8e51b583ac839 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 14:34:31 -0700
Subject: [PATCH 09/41] Derive WebGPU plugin EP NuGet onnxruntime dependency
from MIN_ONNXRUNTIME_VERSION
The .csproj reads the minimum required Microsoft.ML.OnnxRuntime version from a file pointed at by the OnnxRuntimeMinVersionFile MSBuild property, defaulting to plugin-ep-webgpu/MIN_ONNXRUNTIME_VERSION. pack_nuget.ps1 overrides the property with an absolute path so the staged build still resolves it correctly.
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 20 ++++++++++++++++++-
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 14 ++++++++++++-
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
index 6c44b9bf96176..3d055748e0179 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -25,8 +25,26 @@
snupkg
+
+
+ $(MSBuildThisFileDirectory)..\..\MIN_ONNXRUNTIME_VERSION
+ $([System.IO.File]::ReadAllText('$(OnnxRuntimeMinVersionFile)').Trim())
+
+
+
+
+
+
+
-
+
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
index 90074961bda55..ff67bd40b436b 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.ps1
+++ b/plugin-ep-webgpu/csharp/pack_nuget.ps1
@@ -71,6 +71,16 @@ if (-not (Test-Path $csproj)) {
exit 1
}
+# Resolve the minimum-required ORT version file to an absolute path. Passed to MSBuild
+# via -p:OnnxRuntimeMinVersionFile so the staged project (which builds out of a copy
+# under $StagingDir) can still find the file in the original source tree.
+$ortMinVersionFile = Join-Path $PSScriptRoot '..\MIN_ONNXRUNTIME_VERSION'
+if (-not (Test-Path $ortMinVersionFile)) {
+ Write-Error "MIN_ONNXRUNTIME_VERSION file not found: $ortMinVersionFile"
+ exit 1
+}
+$ortMinVersionFile = (Resolve-Path $ortMinVersionFile).Path
+
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
@@ -189,7 +199,8 @@ if ($BuildOnly) {
$buildArgs = @(
$stagedCsproj,
'--configuration', $Configuration,
- "-p:Version=$Version"
+ "-p:Version=$Version",
+ "-p:OnnxRuntimeMinVersionFile=$ortMinVersionFile"
)
if ($NuGetConfig) {
$buildArgs += @('--configfile', $NuGetConfig)
@@ -220,6 +231,7 @@ $packArgs = @(
$stagedCsproj,
'--configuration', $Configuration,
"-p:Version=$Version",
+ "-p:OnnxRuntimeMinVersionFile=$ortMinVersionFile",
'--output', $OutputDir
)
if ($PackOnly) {
From 685c3d196efeca41f51c5cc676549ac9c3cd1890 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 14:59:28 -0700
Subject: [PATCH 10/41] Fix ESRP signing pattern for WebGPU plugin managed DLL
EsrpCodeSigning@5 treats Pattern as a filename mask, not a path glob. The leading '**\' caused the task to build an invalid path '...\webgpu_pack_staging\**' and fail with an IOException. Drop the glob; the task already recurses into FolderPath.
---
.../stages/plugin-webgpu-nuget-packaging-stage.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index a1884f05bfbec..955687375754f 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -125,7 +125,7 @@ stages:
- template: ../templates/win-esrp-dll.yml
parameters:
FolderPath: '$(WebGpuPackStagingDir)'
- Pattern: '**\Microsoft.ML.OnnxRuntime.EP.WebGpu.dll'
+ Pattern: 'Microsoft.ML.OnnxRuntime.EP.WebGpu.dll'
DisplayName: 'ESRP - Sign managed DLL'
DoEsrp: ${{ parameters.DoEsrp }}
From a1b85cc77881000318344444719823e5ceca9056 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 16:29:59 -0700
Subject: [PATCH 11/41] Update readme files
---
plugin-ep-webgpu/README.md | 13 ++++++++++---
plugin-ep-webgpu/csharp/README.md | 18 +++++++++++++-----
plugin-ep-webgpu/python/README.md | 4 ++--
3 files changed, 25 insertions(+), 10 deletions(-)
diff --git a/plugin-ep-webgpu/README.md b/plugin-ep-webgpu/README.md
index dd874f8af1c3b..889fef10ae5e1 100644
--- a/plugin-ep-webgpu/README.md
+++ b/plugin-ep-webgpu/README.md
@@ -10,8 +10,12 @@ For more information about plugin EPs, see the documentation [here](https://onnx
- [`VERSION_NUMBER`](VERSION_NUMBER) — Base plugin EP version consumed by the CI pipeline. The pipeline derives the
final package version (release, dev) from this via
[`tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml`](../tools/ci_build/github/azure-pipelines/templates/set-plugin-build-variables-step.yml).
+- [`MIN_ONNXRUNTIME_VERSION`](MIN_ONNXRUNTIME_VERSION) — Minimum compatible core `onnxruntime` version. Single source
+ of truth shared by all packages built from this directory.
- [`python/`](python/) — Sources and build script for the `onnxruntime-ep-webgpu` Python wheel. See
[`python/README.md`](python/README.md) for build and test instructions.
+- [`csharp/`](csharp/) — Sources and packaging script for the `Microsoft.ML.OnnxRuntime.EP.WebGpu` NuGet package. See
+ [`csharp/README.md`](csharp/README.md) for build and test instructions.
## How it fits together
@@ -19,6 +23,7 @@ The plugin EP is built as a shared library (`onnxruntime_providers_webgpu.{dll,s
build (`--use_webgpu shared_lib`). The resulting binaries are then packaged into:
- A Python wheel (`onnxruntime-ep-webgpu`), built from [`python/`](python/).
+- A NuGet package (`Microsoft.ML.OnnxRuntime.EP.WebGpu`), built from [`csharp/`](csharp/).
- A universal package published to the internal ORT-Nightly feed for Windows (x64 / arm64), Linux x64, and macOS
arm64.
@@ -29,7 +34,7 @@ and post-build smoke tests run in the companion `WebGPU Plugin EP Test Pipeline`
## Usage
-Once installed, the plugin EP is registered at runtime:
+Once installed, the plugin EP is registered at runtime. Example in Python:
```python
import onnxruntime as ort
@@ -43,5 +48,7 @@ sess_options.add_provider_for_devices(devices, {})
session = ort.InferenceSession("model.onnx", sess_options=sess_options)
```
-See [`python/onnxruntime_ep_webgpu/README.md`](python/onnxruntime_ep_webgpu/README.md) for the user-facing package
-documentation (this README is bundled into the wheel).
+See the user-facing package READMEs (bundled into the published packages) for full per-language usage:
+
+- Python: [`python/onnxruntime_ep_webgpu/README.md`](python/onnxruntime_ep_webgpu/README.md)
+- C# / .NET: [`csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md`](csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index 220280633dcc4..0a31c0111380b 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -10,7 +10,7 @@ csharp/
├── Microsoft.ML.OnnxRuntime.EP.WebGpu/
│ ├── Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj # NuGet package project (netstandard2.0)
│ ├── WebGpuEp.cs # Helper class for native library resolution
-│ └── readme.md # Package readme (shipped inside .nupkg)
+│ └── README.md # Package readme (shipped inside .nupkg)
└── test/
└── WebGpuEpNuGetTest/
├── WebGpuEpNuGetTest.csproj # Test console app (net8.0)
@@ -52,14 +52,14 @@ is never modified.
```powershell
cd plugin-ep-webgpu/csharp
-.\pack_nuget.ps1 -Version 1.26.0-dev `
+.\pack_nuget.ps1 -Version 0.1.0-dev `
-BinaryDir_WinX64 ..\..\build\webgpu.plugin\Release\Release
```
### Pack multiple platforms
```powershell
-.\pack_nuget.ps1 -Version 1.26.0-dev `
+.\pack_nuget.ps1 -Version 0.1.0-dev `
-BinaryDir_WinX64 C:\builds\win_x64 `
-BinaryDir_WinArm64 C:\builds\win_arm64 `
-BinaryDir_LinuxX64 /mnt/builds/linux_x64 `
@@ -70,7 +70,7 @@ cd plugin-ep-webgpu/csharp
| Parameter | Required | Default | Description |
|---|---|---|---|
-| `-Version` | Yes | — | Package version string (e.g. `1.26.0`, `1.26.0-dev`) |
+| `-Version` | Yes | — | Package version string (e.g. `0.1.0`, `0.1.0-dev`) |
| `-OutputDir` | No | `./nuget_output` | Directory for the `.nupkg` and `.snupkg` output |
| `-Configuration` | No | `Release` | Build configuration |
| `-ArtifactsDir` | No | — | CI mode: root directory with `win_x64/bin/`, `linux_x64/bin/`, etc. |
@@ -78,16 +78,24 @@ cd plugin-ep-webgpu/csharp
| `-BinaryDir_WinArm64` | No | — | Path to directory containing win-arm64 binaries |
| `-BinaryDir_LinuxX64` | No | — | Path to directory containing linux-x64 binaries |
| `-BinaryDir_OsxArm64` | No | — | Path to directory containing osx-arm64 binaries |
+| `-StagingDir` | No | `/_staging` | Explicit staging directory. Caller owns its lifecycle (no auto-cleanup) |
+| `-BuildOnly` | No | `false` | Stage and build the managed DLL only; skip `dotnet pack`. Preserves the staging directory for a later `-PackOnly` run |
+| `-PackOnly` | No | `false` | Skip staging/build and run `dotnet pack` against an existing staging directory (mutually exclusive with `-BuildOnly`) |
At least one binary directory (or `-ArtifactsDir` with matching subdirectories) must be provided.
Platforms without a binary directory are skipped.
+## Versioning
+
+The package version is supplied to `pack_nuget.ps1` via `-Version`. In the packaging pipeline, the release or
+pre-release version is derived from [`plugin-ep-webgpu/VERSION_NUMBER`](../VERSION_NUMBER).
+
## Inspecting the Package
The `.nupkg` is a ZIP file. To verify its contents:
```powershell
-Expand-Archive nuget_output/Microsoft.ML.OnnxRuntime.EP.WebGpu.1.26.0-dev.nupkg `
+Expand-Archive nuget_output/Microsoft.ML.OnnxRuntime.EP.WebGpu.0.1.0-dev.nupkg `
-DestinationPath nuget_output/inspect -Force
Get-ChildItem nuget_output/inspect -Recurse | Select-Object FullName
diff --git a/plugin-ep-webgpu/python/README.md b/plugin-ep-webgpu/python/README.md
index ac14a84a70f48..00da1a4e89254 100644
--- a/plugin-ep-webgpu/python/README.md
+++ b/plugin-ep-webgpu/python/README.md
@@ -30,7 +30,7 @@ Example:
```bash
python build_wheel.py \
--binary_dir ./build/Release \
- --version 0.1.0.dev20260429 \
+ --version 0.1.0.devYYYYMMDD \
--output_dir ./dist
```
@@ -44,7 +44,7 @@ Install the wheel and dependencies in a clean environment, then run the smoke te
python -m venv test_venv
source test_venv/bin/activate # or test_venv\Scripts\Activate.ps1 on Windows
pip install onnx numpy
-pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in onnxruntime>=1.24.4
+pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in the minimum compatible onnxruntime (see ../MIN_ONNXRUNTIME_VERSION)
python test/test_webgpu_plugin_ep.py
```
From a0b2e20b0404a29609af6ae716156e0ac982c356 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 19:41:59 -0700
Subject: [PATCH 12/41] Address WebGPU plugin EP NuGet packaging review
feedback
- Add -RequiredPlatforms opt-in to pack_nuget.ps1 so CI fails fast when an expected platform's binaries are missing, while preserving dev flexibility.
- Add Validate_Parameters stage to fail loudly when build_windows_arm64=true but build_windows_x64=false, instead of silently disabling arm64.
- Compute and pass required platforms from the NuGet packaging stage (Python inline script).
- Drop redundant explicit Microsoft.ML.OnnxRuntime PackageReference from the smoke-test csproj; rely on the EP package's transitive dependency.
- Misc: README cleanup, comment consolidation, remove stale pointer comment in WebGpuEp.cs.
---
.../README.md | 4 -
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 97 +++++++++++++------
.../WebGpuEpNuGetTest.csproj | 2 +-
.../plugin-webgpu-nuget-packaging-stage.yml | 44 +++++++++
.../stages/plugin-webgpu-packaging-stage.yml | 25 ++++-
5 files changed, 133 insertions(+), 39 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
index 8f4836f86cf35..9bdefb51e976c 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -39,7 +39,3 @@ using var session = new InferenceSession("model.onnx", sessionOptions);
| win-arm64 | `onnxruntime_providers_webgpu.dll`, `dxil.dll`, `dxcompiler.dll` |
| linux-x64 | `libonnxruntime_providers_webgpu.so` |
| osx-arm64 | `libonnxruntime_providers_webgpu.dylib` |
-
-### Requirements
-
-- [Microsoft.ML.OnnxRuntime](https://www.nuget.org/packages/Microsoft.ML.OnnxRuntime) >= 1.24.4
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
index ff67bd40b436b..5bdc761a0f176 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.ps1
+++ b/plugin-ep-webgpu/csharp/pack_nuget.ps1
@@ -48,7 +48,16 @@ param(
# (so it can be ESRP-signed), then pack with --no-build in a later task.
# When neither is set, the script does the full build+pack end-to-end.
[switch]$BuildOnly,
- [switch]$PackOnly
+ [switch]$PackOnly,
+
+ # Optional list of platforms that MUST be staged successfully. CI passes the set
+ # of enabled platforms (derived from the pipeline parameters) so that a renamed
+ # or missing upstream artifact fails fast instead of silently producing a partial
+ # multi-RID package. When omitted (typical for local dev) the script just requires
+ # at least one platform to be staged.
+ # Accepted as a comma-separated string for compatibility with how Azure Pipelines
+ # expands variables into PowerShell arguments.
+ [string]$RequiredPlatforms
)
if ($BuildOnly -and $PackOnly) {
@@ -56,6 +65,40 @@ if ($BuildOnly -and $PackOnly) {
exit 1
}
+# --- Platform definitions ---
+$platforms = [ordered]@{
+ win_x64 = @{
+ rid = 'win-x64'
+ files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
+ param = $BinaryDir_WinX64
+ }
+ win_arm64 = @{
+ rid = 'win-arm64'
+ files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
+ param = $BinaryDir_WinArm64
+ }
+ linux_x64 = @{
+ rid = 'linux-x64'
+ files = @('libonnxruntime_providers_webgpu.so')
+ param = $BinaryDir_LinuxX64
+ }
+ macos_arm64 = @{
+ rid = 'osx-arm64'
+ files = @('libonnxruntime_providers_webgpu.dylib')
+ param = $BinaryDir_OsxArm64
+ }
+}
+
+$requiredPlatformList = @()
+if ($RequiredPlatforms) {
+ $requiredPlatformList = $RequiredPlatforms.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
+ $invalid = @($requiredPlatformList | Where-Object { $_ -notin $platforms.Keys })
+ if ($invalid.Count -gt 0) {
+ Write-Error "Unknown platform(s) in -RequiredPlatforms: $($invalid -join ', '). Valid: $($platforms.Keys -join ', ')."
+ exit 1
+ }
+}
+
if ($NuGetConfig -and -not (Test-Path $NuGetConfig)) {
Write-Error "NuGet.config not found: $NuGetConfig"
exit 1
@@ -108,43 +151,22 @@ else {
}
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
- # Copy project sources to staging
+ # Copy project sources to staging. Exclude bin/obj so a stale local build of the
+ # in-tree project doesn't get dragged into the staged copy (CI is unaffected since
+ # the workspace is clean per run).
Write-Host "Staging project files to $StagingDir"
- Copy-Item -Path (Join-Path $projectDir "*") -Destination $StagingDir -Recurse -Force
-}
-
-# --- Platform definitions ---
-$platforms = [ordered]@{
- win_x64 = @{
- rid = 'win-x64'
- files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
- param = $BinaryDir_WinX64
- }
- win_arm64 = @{
- rid = 'win-arm64'
- files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
- param = $BinaryDir_WinArm64
- }
- linux_x64 = @{
- rid = 'linux-x64'
- files = @('libonnxruntime_providers_webgpu.so')
- param = $BinaryDir_LinuxX64
- }
- macos_arm64 = @{
- rid = 'osx-arm64'
- files = @('libonnxruntime_providers_webgpu.dylib')
- param = $BinaryDir_OsxArm64
- }
+ Copy-Item -Path (Join-Path $projectDir "*") -Destination $StagingDir -Recurse -Force -Exclude bin,obj
}
# --- Stage binaries ---
if (-not $PackOnly) {
- $anyStaged = $false
+ $stagedPlatforms = [System.Collections.Generic.HashSet[string]]::new()
foreach ($entry in $platforms.GetEnumerator()) {
$name = $entry.Key
$info = $entry.Value
$rid = $info.rid
+ $isRequired = $requiredPlatformList -and ($requiredPlatformList -contains $name)
# Resolve source directory: explicit param > ArtifactsDir > skip
$sourceDir = $info.param
@@ -153,9 +175,17 @@ if (-not $PackOnly) {
if (Test-Path $candidate) {
$sourceDir = $candidate
}
+ elseif ($isRequired) {
+ Write-Error "Required platform '$name' artifact directory not found: $candidate"
+ exit 1
+ }
}
if (-not $sourceDir) {
+ if ($isRequired) {
+ Write-Error "Required platform '$name' has no binary directory (pass -BinaryDir_$($name -replace '_','') or -ArtifactsDir)."
+ exit 1
+ }
Write-Host "Skipping $name (no binary directory provided)"
continue
}
@@ -178,10 +208,17 @@ if (-not $PackOnly) {
Copy-Item -Path $src -Destination $targetDir -Force
Write-Host " $file"
}
- $anyStaged = $true
+ [void]$stagedPlatforms.Add($name)
}
- if (-not $anyStaged) {
+ if ($requiredPlatformList) {
+ $missing = @($requiredPlatformList | Where-Object { -not $stagedPlatforms.Contains($_) })
+ if ($missing.Count -gt 0) {
+ Write-Error "Required platforms not staged: $($missing -join ', ')"
+ exit 1
+ }
+ }
+ elseif ($stagedPlatforms.Count -eq 0) {
Write-Error "No platform binaries were staged. Provide at least one -BinaryDir_* parameter or -ArtifactsDir."
exit 1
}
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
index 59f655989d19f..adf0eef247a18 100644
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
@@ -9,8 +9,8 @@
+
-
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index 955687375754f..c221b8d87fbb6 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -109,6 +109,49 @@ stages:
artifactName: webgpu_plugin_macos_arm64
targetPath: '$(Build.BinariesDirectory)\artifacts\macos_arm64'
+ # Compute the set of required platforms from the pipeline parameters and verify the
+ # corresponding artifact directories actually downloaded. This catches renamed/moved
+ # upstream artifacts loudly before any pack work, and feeds pack_nuget.ps1 the same
+ # list so it fails fast if any required platform's binaries are missing.
+ - task: PythonScript@0
+ displayName: 'Compute required platforms'
+ inputs:
+ scriptSource: inline
+ script: |
+ import os
+ import sys
+
+ # The string literals below are filled in by ADO template expansion at queue
+ # time and resolve to 'True' or 'False'.
+ platforms_enabled = {
+ "win_x64": "${{ parameters.platforms.win_x64 }}" == "True",
+ "win_arm64": "${{ parameters.platforms.win_arm64 }}" == "True",
+ "linux_x64": "${{ parameters.platforms.linux_x64 }}" == "True",
+ "macos_arm64": "${{ parameters.platforms.macos_arm64 }}" == "True",
+ }
+ expected = [name for name, enabled in platforms_enabled.items() if enabled]
+
+ if not expected:
+ print("##vso[task.logissue type=error]No platforms enabled in 'platforms' parameter — nothing to pack.")
+ sys.exit(1)
+
+ artifacts_dir = r"$(Build.BinariesDirectory)\artifacts"
+ missing = [
+ f"{p} ({d})"
+ for p in expected
+ for d in [os.path.join(artifacts_dir, p, "bin")]
+ if not os.path.isdir(d)
+ ]
+ if missing:
+ print("##vso[task.logissue type=error]Expected artifact directories not found:")
+ for m in missing:
+ print(f"##vso[task.logissue type=error] {m}")
+ sys.exit(1)
+
+ required = ",".join(expected)
+ print(f"Required platforms: {required}")
+ print(f"##vso[task.setvariable variable=WebGpuRequiredPlatforms]{required}")
+
# Stage binaries and build the managed assembly (so it can be ESRP-signed before packing).
- task: PowerShell@2
displayName: 'Build managed DLL'
@@ -119,6 +162,7 @@ stages:
arguments: >-
$(WebGpuPackNuGetCommonArgs)
-ArtifactsDir "$(Build.BinariesDirectory)\artifacts"
+ -RequiredPlatforms $(WebGpuRequiredPlatforms)
-BuildOnly
# ESRP-sign the managed DLL before it gets embedded in the .nupkg.
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 2756acd5340ab..d942c961f642a 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -42,6 +42,25 @@ parameters:
- MinSizeRel
stages:
+ # Parameter validation. The ARM64 build requires the x64 tblgen.exe (used during the
+ # build), which is not correctly generated in a cross build. We require x64 to be built
+ # first and download tblgen.exe from it; surface a mismatched-flags request as an
+ # explicit failure rather than silently disabling arm64 packaging.
+ - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, false)) }}:
+ - stage: Validate_Parameters
+ displayName: 'Validate parameters'
+ jobs:
+ - job: Fail
+ displayName: 'Invalid parameter combination'
+ pool:
+ name: onnxruntime-Win-CPU-VS2022-Latest
+ os: windows
+ steps:
+ - powershell: |
+ Write-Error "build_windows_arm64=true requires build_windows_x64=true: the ARM64 build cross-uses the x64 tblgen.exe published by the Windows x64 stage."
+ exit 1
+ displayName: 'Fail: arm64 requires x64'
+
# Windows x64
- ${{ if eq(parameters.build_windows_x64, true) }}:
- template: plugin-win-webgpu-stage.yml
@@ -52,9 +71,7 @@ stages:
cmake_build_type: ${{ parameters.cmake_build_type }}
# Windows ARM64
- # ARM64 build requires the x64 tblgen.exe (used during the build), which is not correctly
- # generated in a cross build. So we require x64 to be built first and download tblgen.exe from it.
- - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}:
+ - ${{ if eq(parameters.build_windows_arm64, true) }}:
- template: plugin-win-webgpu-stage.yml
parameters:
arch: 'arm64'
@@ -87,6 +104,6 @@ stages:
DoEsrp: true
platforms:
win_x64: ${{ parameters.build_windows_x64 }}
- win_arm64: ${{ and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}
+ win_arm64: ${{ parameters.build_windows_arm64 }}
linux_x64: ${{ parameters.build_linux_x64 }}
macos_arm64: ${{ parameters.build_macos_arm64 }}
From 86433766b18cb21117cbc895652d51c737f81771 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 09:56:42 -0700
Subject: [PATCH 13/41] Port pack_nuget.ps1 to Python
Replaces the PowerShell packaging script with pack_nuget.py and updates the WebGPU plugin EP NuGet packaging pipeline stage to invoke it.
The Python version uses argparse with a Path-resolving converter for path arguments, raises a PackError exception for user-actionable failures, and uses tempfile.TemporaryDirectory for the unowned-staging-dir case (with --staging-dir required when --build-only or --pack-only is set).
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 4 +-
plugin-ep-webgpu/csharp/README.md | 49 +--
plugin-ep-webgpu/csharp/pack_nuget.ps1 | 311 ----------------
plugin-ep-webgpu/csharp/pack_nuget.py | 336 ++++++++++++++++++
.../plugin-webgpu-nuget-packaging-stage.yml | 36 +-
5 files changed, 380 insertions(+), 356 deletions(-)
delete mode 100644 plugin-ep-webgpu/csharp/pack_nuget.ps1
create mode 100644 plugin-ep-webgpu/csharp/pack_nuget.py
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
index 3d055748e0179..08d6bd3b7e261 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -28,7 +28,7 @@
@@ -53,7 +53,7 @@
/_staging` | Explicit staging directory. Caller owns its lifecycle (no auto-cleanup) |
-| `-BuildOnly` | No | `false` | Stage and build the managed DLL only; skip `dotnet pack`. Preserves the staging directory for a later `-PackOnly` run |
-| `-PackOnly` | No | `false` | Skip staging/build and run `dotnet pack` against an existing staging directory (mutually exclusive with `-BuildOnly`) |
-
-At least one binary directory (or `-ArtifactsDir` with matching subdirectories) must be provided.
+| `--version` | Yes | — | Package version string (e.g. `0.1.0`, `0.1.0-dev`) |
+| `--output-dir` | No | `./nuget_output` | Directory for the `.nupkg` and `.snupkg` output |
+| `--configuration` | No | `Release` | Build configuration |
+| `--artifacts-dir` | No | — | CI mode: root directory with `win_x64/bin/`, `linux_x64/bin/`, etc. |
+| `--binary-dir-win-x64` | No | — | Path to directory containing win-x64 binaries |
+| `--binary-dir-win-arm64` | No | — | Path to directory containing win-arm64 binaries |
+| `--binary-dir-linux-x64` | No | — | Path to directory containing linux-x64 binaries |
+| `--binary-dir-macos-arm64` | No | — | Path to directory containing osx-arm64 (macOS arm64) binaries |
+| `--staging-dir` | No | `/_staging` | Explicit staging directory. Caller owns its lifecycle (no auto-cleanup) |
+| `--build-only` | No | `false` | Stage and build the managed DLL only; skip `dotnet pack`. Preserves the staging directory for a later `--pack-only` run |
+| `--pack-only` | No | `false` | Skip staging/build and run `dotnet pack` against an existing staging directory (mutually exclusive with `--build-only`) |
+| `--required-platforms` | No | — | Comma-separated list of platforms that MUST be staged successfully (CI-mode safety net) |
+
+At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be provided.
Platforms without a binary directory are skipped.
## Versioning
-The package version is supplied to `pack_nuget.ps1` via `-Version`. In the packaging pipeline, the release or
+The package version is supplied to `pack_nuget.py` via `--version`. In the packaging pipeline, the release or
pre-release version is derived from [`plugin-ep-webgpu/VERSION_NUMBER`](../VERSION_NUMBER).
## Inspecting the Package
@@ -154,7 +155,7 @@ The NuGet packaging is integrated into the WebGPU plugin pipeline:
- **Packaging stage:** `tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml`
The CI stage downloads build artifacts from all enabled platform stages, invokes
-`pack_nuget.ps1`, ESRP-signs the package, and runs the test app on a GPU agent.
+`pack_nuget.py`, ESRP-signs the package, and runs the test app on a GPU agent.
## Native Binaries Per Platform
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.ps1 b/plugin-ep-webgpu/csharp/pack_nuget.ps1
deleted file mode 100644
index 5bdc761a0f176..0000000000000
--- a/plugin-ep-webgpu/csharp/pack_nuget.ps1
+++ /dev/null
@@ -1,311 +0,0 @@
-<#
-.SYNOPSIS
- Builds the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.
-
-.DESCRIPTION
- Stages native binaries from build artifacts into the runtimes/ layout expected by the
- .csproj and runs 'dotnet pack' to produce the .nupkg and .snupkg files.
-
- Can be invoked locally or from CI. In CI, pass -ArtifactsDir to point at the
- downloaded pipeline artifacts. Locally, pass individual -BinaryDir_* parameters
- or place binaries manually in the runtimes/ folders.
-
-.EXAMPLE
- # Local: pack win-x64 only from a local build
- .\pack_nuget.ps1 -Version 1.26.0-dev -BinaryDir_WinX64 ..\..\build\webgpu.plugin\Release\Release
-
-.EXAMPLE
- # CI: pack all platforms from downloaded artifacts
- .\pack_nuget.ps1 -Version $(PluginPackageVersion) -ArtifactsDir $(Build.BinariesDirectory)\artifacts -OutputDir $(Build.ArtifactStagingDirectory)\nuget
-#>
-
-[CmdletBinding()]
-param(
- [Parameter(Mandatory)]
- [string]$Version,
-
- [string]$OutputDir = (Join-Path $PSScriptRoot "nuget_output"),
-
- [string]$Configuration = "Release",
-
- # CI mode: root directory containing per-platform subdirectories (win_x64/bin/, win_arm64/bin/, etc.)
- [string]$ArtifactsDir,
-
- # Local mode: individual binary directories per platform (takes precedence over ArtifactsDir for that platform)
- [string]$BinaryDir_WinX64,
- [string]$BinaryDir_WinArm64,
- [string]$BinaryDir_LinuxX64,
- [string]$BinaryDir_OsxArm64,
-
- # Optional NuGet.config to pass to `dotnet pack` via --configfile.
- [string]$NuGetConfig,
-
- # Optional explicit staging directory. If unset, a directory under $OutputDir is used.
- # Useful in CI when the caller wants to point ESRP signing at a known location.
- [string]$StagingDir,
-
- # Split-phase switches for CI signing: build the managed DLL in one task
- # (so it can be ESRP-signed), then pack with --no-build in a later task.
- # When neither is set, the script does the full build+pack end-to-end.
- [switch]$BuildOnly,
- [switch]$PackOnly,
-
- # Optional list of platforms that MUST be staged successfully. CI passes the set
- # of enabled platforms (derived from the pipeline parameters) so that a renamed
- # or missing upstream artifact fails fast instead of silently producing a partial
- # multi-RID package. When omitted (typical for local dev) the script just requires
- # at least one platform to be staged.
- # Accepted as a comma-separated string for compatibility with how Azure Pipelines
- # expands variables into PowerShell arguments.
- [string]$RequiredPlatforms
-)
-
-if ($BuildOnly -and $PackOnly) {
- Write-Error "-BuildOnly and -PackOnly are mutually exclusive."
- exit 1
-}
-
-# --- Platform definitions ---
-$platforms = [ordered]@{
- win_x64 = @{
- rid = 'win-x64'
- files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
- param = $BinaryDir_WinX64
- }
- win_arm64 = @{
- rid = 'win-arm64'
- files = @('onnxruntime_providers_webgpu.dll', 'dxil.dll', 'dxcompiler.dll')
- param = $BinaryDir_WinArm64
- }
- linux_x64 = @{
- rid = 'linux-x64'
- files = @('libonnxruntime_providers_webgpu.so')
- param = $BinaryDir_LinuxX64
- }
- macos_arm64 = @{
- rid = 'osx-arm64'
- files = @('libonnxruntime_providers_webgpu.dylib')
- param = $BinaryDir_OsxArm64
- }
-}
-
-$requiredPlatformList = @()
-if ($RequiredPlatforms) {
- $requiredPlatformList = $RequiredPlatforms.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
- $invalid = @($requiredPlatformList | Where-Object { $_ -notin $platforms.Keys })
- if ($invalid.Count -gt 0) {
- Write-Error "Unknown platform(s) in -RequiredPlatforms: $($invalid -join ', '). Valid: $($platforms.Keys -join ', ')."
- exit 1
- }
-}
-
-if ($NuGetConfig -and -not (Test-Path $NuGetConfig)) {
- Write-Error "NuGet.config not found: $NuGetConfig"
- exit 1
-}
-
-$ErrorActionPreference = 'Stop'
-
-$projectDir = Join-Path $PSScriptRoot "Microsoft.ML.OnnxRuntime.EP.WebGpu"
-$csproj = Join-Path $projectDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
-
-if (-not (Test-Path $csproj)) {
- Write-Error "Project file not found: $csproj"
- exit 1
-}
-
-# Resolve the minimum-required ORT version file to an absolute path. Passed to MSBuild
-# via -p:OnnxRuntimeMinVersionFile so the staged project (which builds out of a copy
-# under $StagingDir) can still find the file in the original source tree.
-$ortMinVersionFile = Join-Path $PSScriptRoot '..\MIN_ONNXRUNTIME_VERSION'
-if (-not (Test-Path $ortMinVersionFile)) {
- Write-Error "MIN_ONNXRUNTIME_VERSION file not found: $ortMinVersionFile"
- exit 1
-}
-$ortMinVersionFile = (Resolve-Path $ortMinVersionFile).Path
-
-$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
-New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
-
-# Stage into a temporary directory so we don't modify the source tree.
-# When the caller provides $StagingDir explicitly, they own its lifecycle (no auto-cleanup).
-$ownsStaging = -not $StagingDir
-if ($StagingDir) {
- $StagingDir = [System.IO.Path]::GetFullPath($StagingDir)
-}
-else {
- $StagingDir = Join-Path $OutputDir "_staging"
-}
-$stagedCsproj = Join-Path $StagingDir "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
-
-if ($PackOnly) {
- if (-not (Test-Path $stagedCsproj)) {
- Write-Error "Staged project not found at $stagedCsproj. Run with -BuildOnly first."
- exit 1
- }
- Write-Host "Reusing existing staging directory: $StagingDir"
-}
-else {
- if (Test-Path $StagingDir) {
- Remove-Item -Recurse -Force $StagingDir
- }
- New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
-
- # Copy project sources to staging. Exclude bin/obj so a stale local build of the
- # in-tree project doesn't get dragged into the staged copy (CI is unaffected since
- # the workspace is clean per run).
- Write-Host "Staging project files to $StagingDir"
- Copy-Item -Path (Join-Path $projectDir "*") -Destination $StagingDir -Recurse -Force -Exclude bin,obj
-}
-
-# --- Stage binaries ---
-if (-not $PackOnly) {
- $stagedPlatforms = [System.Collections.Generic.HashSet[string]]::new()
-
- foreach ($entry in $platforms.GetEnumerator()) {
- $name = $entry.Key
- $info = $entry.Value
- $rid = $info.rid
- $isRequired = $requiredPlatformList -and ($requiredPlatformList -contains $name)
-
- # Resolve source directory: explicit param > ArtifactsDir > skip
- $sourceDir = $info.param
- if (-not $sourceDir -and $ArtifactsDir) {
- $candidate = Join-Path $ArtifactsDir "$name\bin"
- if (Test-Path $candidate) {
- $sourceDir = $candidate
- }
- elseif ($isRequired) {
- Write-Error "Required platform '$name' artifact directory not found: $candidate"
- exit 1
- }
- }
-
- if (-not $sourceDir) {
- if ($isRequired) {
- Write-Error "Required platform '$name' has no binary directory (pass -BinaryDir_$($name -replace '_','') or -ArtifactsDir)."
- exit 1
- }
- Write-Host "Skipping $name (no binary directory provided)"
- continue
- }
-
- if (-not (Test-Path $sourceDir)) {
- Write-Error "Binary directory does not exist: $sourceDir"
- exit 1
- }
-
- $targetDir = Join-Path $StagingDir "runtimes\$rid\native"
- New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
-
- Write-Host "Staging $name -> runtimes/$rid/native/"
- foreach ($file in $info.files) {
- $src = Join-Path $sourceDir $file
- if (-not (Test-Path $src)) {
- Write-Error "Expected binary not found: $src"
- exit 1
- }
- Copy-Item -Path $src -Destination $targetDir -Force
- Write-Host " $file"
- }
- [void]$stagedPlatforms.Add($name)
- }
-
- if ($requiredPlatformList) {
- $missing = @($requiredPlatformList | Where-Object { -not $stagedPlatforms.Contains($_) })
- if ($missing.Count -gt 0) {
- Write-Error "Required platforms not staged: $($missing -join ', ')"
- exit 1
- }
- }
- elseif ($stagedPlatforms.Count -eq 0) {
- Write-Error "No platform binaries were staged. Provide at least one -BinaryDir_* parameter or -ArtifactsDir."
- exit 1
- }
-
- Write-Host ""
- Write-Host "Runtimes layout:"
- Get-ChildItem -Recurse (Join-Path $StagingDir "runtimes") | ForEach-Object { Write-Host " $($_.FullName)" }
-}
-
-# --- Build / Pack ---
-if ($BuildOnly) {
- Write-Host ""
- Write-Host "Running dotnet build (Version=$Version, Configuration=$Configuration)..."
-
- $buildArgs = @(
- $stagedCsproj,
- '--configuration', $Configuration,
- "-p:Version=$Version",
- "-p:OnnxRuntimeMinVersionFile=$ortMinVersionFile"
- )
- if ($NuGetConfig) {
- $buildArgs += @('--configfile', $NuGetConfig)
- Write-Host "Using NuGet.config: $NuGetConfig"
- }
-
- dotnet build @buildArgs
- if ($LASTEXITCODE -ne 0) {
- Write-Error "dotnet build failed with exit code $LASTEXITCODE"
- exit $LASTEXITCODE
- }
-
- $managedDll = Join-Path $StagingDir "bin\$Configuration\netstandard2.0\Microsoft.ML.OnnxRuntime.EP.WebGpu.dll"
- if (-not (Test-Path $managedDll)) {
- Write-Error "Managed DLL not found after build: $managedDll"
- exit 1
- }
- Write-Host ""
- Write-Host "Built managed DLL: $managedDll"
- Write-Host "Staging directory preserved for subsequent -PackOnly invocation."
- exit 0
-}
-
-Write-Host ""
-Write-Host "Running dotnet pack (Version=$Version, Configuration=$Configuration)..."
-
-$packArgs = @(
- $stagedCsproj,
- '--configuration', $Configuration,
- "-p:Version=$Version",
- "-p:OnnxRuntimeMinVersionFile=$ortMinVersionFile",
- '--output', $OutputDir
-)
-if ($PackOnly) {
- $packArgs += '--no-build'
-}
-if ($NuGetConfig) {
- $packArgs += @('--configfile', $NuGetConfig)
- Write-Host "Using NuGet.config: $NuGetConfig"
-}
-
-dotnet pack @packArgs
-if ($LASTEXITCODE -ne 0) {
- Write-Error "dotnet pack failed with exit code $LASTEXITCODE"
- exit $LASTEXITCODE
-}
-
-# --- Verify ---
-Write-Host ""
-$nupkgs = Get-ChildItem "$OutputDir\*.nupkg"
-if ($nupkgs.Count -eq 0) {
- Write-Error "No .nupkg files found in $OutputDir"
- exit 1
-}
-foreach ($pkg in $nupkgs) {
- Write-Host "Produced: $($pkg.Name) ($([math]::Round($pkg.Length / 1MB, 2)) MB)"
-}
-$snupkgs = Get-ChildItem "$OutputDir\*.snupkg" -ErrorAction SilentlyContinue
-foreach ($pkg in $snupkgs) {
- Write-Host "Produced: $($pkg.Name) ($([math]::Round($pkg.Length / 1MB, 2)) MB)"
-}
-
-# --- Clean up staging directory ---
-if ($ownsStaging -and (Test-Path $StagingDir)) {
- Remove-Item -Recurse -Force $StagingDir
- Write-Host ""
- Write-Host "Cleaned up staging directory."
-}
-
-Write-Host ""
-Write-Host "Done. Output: $OutputDir"
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.py b/plugin-ep-webgpu/csharp/pack_nuget.py
new file mode 100644
index 0000000000000..c129fad8223b8
--- /dev/null
+++ b/plugin-ep-webgpu/csharp/pack_nuget.py
@@ -0,0 +1,336 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Build the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.
+
+Stages native binaries from build artifacts into the runtimes/ layout expected
+by the .csproj and runs `dotnet pack` to produce the .nupkg / .snupkg files.
+
+Can be invoked locally or from CI. In CI, pass --artifacts-dir to point at the
+downloaded pipeline artifacts. Locally, pass individual --binary-dir-* options
+or place binaries manually in the runtimes/ folders.
+
+Examples
+--------
+Local: pack win-x64 only from a local build:
+
+ python pack_nuget.py --version 1.26.0-dev \\
+ --binary-dir-win-x64 ../../build/webgpu.plugin/Release/Release
+
+CI: pack all platforms from downloaded artifacts:
+
+ python pack_nuget.py --version $(PluginPackageVersion) \\
+ --artifacts-dir $(Build.BinariesDirectory)/artifacts \\
+ --output-dir $(Build.ArtifactStagingDirectory)/nuget
+"""
+
+from __future__ import annotations
+
+import argparse
+import shutil
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+# Platform name -> (RID, list of native binary filenames expected in the source dir).
+PLATFORMS: dict[str, tuple[str, tuple[str, ...]]] = {
+ "win_x64": ("win-x64", ("onnxruntime_providers_webgpu.dll", "dxil.dll", "dxcompiler.dll")),
+ "win_arm64": ("win-arm64", ("onnxruntime_providers_webgpu.dll", "dxil.dll", "dxcompiler.dll")),
+ "linux_x64": ("linux-x64", ("libonnxruntime_providers_webgpu.so",)),
+ "macos_arm64": ("osx-arm64", ("libonnxruntime_providers_webgpu.dylib",)),
+}
+
+SCRIPT_DIR = Path(__file__).resolve().parent
+PROJECT_DIR = SCRIPT_DIR / "Microsoft.ML.OnnxRuntime.EP.WebGpu"
+CSPROJ = PROJECT_DIR / "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+MIN_ORT_VERSION_FILE = SCRIPT_DIR.parent / "MIN_ONNXRUNTIME_VERSION"
+
+
+class PackError(RuntimeError):
+ """Raised for any user-actionable failure during packaging."""
+
+
+def parse_args() -> argparse.Namespace:
+ def _absolute_path(value: str) -> Path:
+ """argparse `type` converter: parse a string as an absolute Path."""
+ return Path(value).resolve()
+
+ p = argparse.ArgumentParser(
+ description="Build the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ p.add_argument("--version", required=True, help="Package version (e.g. 1.26.0-dev).")
+ p.add_argument(
+ "--output-dir",
+ type=_absolute_path,
+ default=(SCRIPT_DIR / "nuget_output").resolve(),
+ help="Directory for the .nupkg / .snupkg output (default: ./nuget_output).",
+ )
+ p.add_argument("--configuration", default="Release", help="Build configuration (default: Release).")
+
+ # CI mode: a single root containing per-platform subdirectories.
+ p.add_argument(
+ "--artifacts-dir",
+ type=_absolute_path,
+ help="CI mode: root containing /bin/ subdirectories for each platform.",
+ )
+
+ # Local mode: explicit per-platform binary directories. Each takes precedence over
+ # --artifacts-dir for that platform.
+ for name in PLATFORMS:
+ flag = f"--binary-dir-{name.replace('_', '-')}"
+ p.add_argument(flag, type=_absolute_path, dest=f"binary_dir_{name}", help=f"Path to {name} native binaries.")
+
+ p.add_argument(
+ "--nuget-config", type=_absolute_path, help="Optional NuGet.config passed to dotnet via --configfile."
+ )
+ p.add_argument(
+ "--staging-dir",
+ type=_absolute_path,
+ help=(
+ "Explicit staging directory. Required with --build-only / --pack-only "
+ "(caller owns its lifecycle). When omitted, an auto-cleaned temporary "
+ "directory is used for the full build+pack flow."
+ ),
+ )
+
+ phase = p.add_mutually_exclusive_group()
+ phase.add_argument(
+ "--build-only",
+ action="store_true",
+ help="Stage and build the managed DLL only; skip dotnet pack. Preserves the staging dir.",
+ )
+ phase.add_argument(
+ "--pack-only",
+ action="store_true",
+ help="Skip staging/build and run dotnet pack against an existing staging directory.",
+ )
+
+ p.add_argument(
+ "--required-platforms",
+ default="",
+ help=(
+ "Comma-separated list of platforms that MUST be staged successfully. "
+ "When omitted, the script just requires at least one platform to be staged."
+ ),
+ )
+
+ return p.parse_args()
+
+
+def parse_required_platforms(value: str) -> list[str]:
+ names = [tok.strip() for tok in value.split(",") if tok.strip()]
+ invalid = [n for n in names if n not in PLATFORMS]
+ if invalid:
+ raise PackError(
+ f"unknown platform(s) in --required-platforms: {', '.join(invalid)}. valid: {', '.join(PLATFORMS)}."
+ )
+ return names
+
+
+def stage_sources(staging_dir: Path) -> None:
+ """Copy project sources into staging, excluding bin/obj."""
+ print(f"Staging project files to {staging_dir}")
+ if staging_dir.exists():
+ shutil.rmtree(staging_dir)
+ shutil.copytree(
+ PROJECT_DIR,
+ staging_dir,
+ ignore=shutil.ignore_patterns("bin", "obj"),
+ )
+
+
+def resolve_platform_source(
+ name: str,
+ binary_dir_override: Path | None,
+ artifacts_dir: Path | None,
+ is_required: bool,
+) -> Path | None:
+ """Return the source dir for a platform, or None to skip."""
+ if binary_dir_override is not None:
+ return binary_dir_override
+ if artifacts_dir is not None:
+ candidate = artifacts_dir / name / "bin"
+ if candidate.is_dir():
+ return candidate
+ if is_required:
+ raise PackError(f"required platform '{name}' artifact directory not found: {candidate}")
+ if is_required:
+ raise PackError(
+ f"required platform '{name}' has no binary directory "
+ f"(pass --binary-dir-{name.replace('_', '-')} or --artifacts-dir)."
+ )
+ return None
+
+
+def stage_binaries(
+ staging_dir: Path,
+ args: argparse.Namespace,
+ required_platforms: list[str],
+) -> None:
+ staged: set[str] = set()
+
+ for name, (rid, files) in PLATFORMS.items():
+ binary_dir_override: Path | None = getattr(args, f"binary_dir_{name}")
+ is_required = name in required_platforms
+ source_dir = resolve_platform_source(name, binary_dir_override, args.artifacts_dir, is_required)
+ if source_dir is None:
+ print(f"Skipping {name} (no binary directory provided)")
+ continue
+ if not source_dir.is_dir():
+ raise PackError(f"binary directory does not exist: {source_dir}")
+
+ target_dir = staging_dir / "runtimes" / rid / "native"
+ target_dir.mkdir(parents=True, exist_ok=True)
+
+ print(f"Staging {name} -> runtimes/{rid}/native/")
+ for filename in files:
+ src = source_dir / filename
+ if not src.is_file():
+ raise PackError(f"expected binary not found: {src}")
+ shutil.copy2(src, target_dir / filename)
+ print(f" {filename}")
+ staged.add(name)
+
+ if required_platforms:
+ missing = [n for n in required_platforms if n not in staged]
+ if missing:
+ raise PackError(f"required platforms not staged: {', '.join(missing)}")
+ elif not staged:
+ raise PackError("no platform binaries were staged. Provide at least one --binary-dir-* or --artifacts-dir.")
+
+ print()
+ print("Runtimes layout:")
+ for path in sorted((staging_dir / "runtimes").rglob("*")):
+ print(f" {path}")
+
+
+def dotnet_common_args(
+ staged_csproj: Path,
+ args: argparse.Namespace,
+ min_ort_version_file: Path,
+) -> list[str]:
+ common = [
+ str(staged_csproj),
+ "--configuration",
+ args.configuration,
+ f"-p:Version={args.version}",
+ f"-p:OnnxRuntimeMinVersionFile={min_ort_version_file}",
+ ]
+ if args.nuget_config:
+ common.extend(["--configfile", str(args.nuget_config)])
+ print(f"Using NuGet.config: {args.nuget_config}")
+ return common
+
+
+def do_build(staged_csproj: Path, staging_dir: Path, args: argparse.Namespace, min_ort_version_file: Path) -> None:
+ print()
+ print(f"Running dotnet build (Version={args.version}, Configuration={args.configuration})...")
+ cmd = ["dotnet", "build", *dotnet_common_args(staged_csproj, args, min_ort_version_file)]
+ print("+ " + " ".join(cmd))
+ subprocess.run(cmd, check=True)
+
+ managed_dll = staging_dir / "bin" / args.configuration / "netstandard2.0" / "Microsoft.ML.OnnxRuntime.EP.WebGpu.dll"
+ if not managed_dll.is_file():
+ raise PackError(f"managed DLL not found after build: {managed_dll}")
+ print()
+ print(f"Built managed DLL: {managed_dll}")
+ print("Staging directory preserved for subsequent --pack-only invocation.")
+
+
+def do_pack(
+ staged_csproj: Path,
+ output_dir: Path,
+ args: argparse.Namespace,
+ min_ort_version_file: Path,
+) -> None:
+ print()
+ print(f"Running dotnet pack (Version={args.version}, Configuration={args.configuration})...")
+ pack_args = [
+ "dotnet",
+ "pack",
+ *dotnet_common_args(staged_csproj, args, min_ort_version_file),
+ "--output",
+ str(output_dir),
+ ]
+ if args.pack_only:
+ pack_args.append("--no-build")
+ print("+ " + " ".join(pack_args))
+ subprocess.run(pack_args, check=True)
+
+ print()
+ nupkgs = sorted(output_dir.glob("*.nupkg"))
+ if not nupkgs:
+ raise PackError(f"no .nupkg files found in {output_dir}")
+ for pkg in nupkgs:
+ print(f"Produced: {pkg.name} ({pkg.stat().st_size / (1024 * 1024):.2f} MB)")
+ for pkg in sorted(output_dir.glob("*.snupkg")):
+ print(f"Produced: {pkg.name} ({pkg.stat().st_size / (1024 * 1024):.2f} MB)")
+
+
+def run_in_staging(args: argparse.Namespace, staging_dir: Path, min_ort_version_file: Path) -> None:
+ staged_csproj = staging_dir / "Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj"
+ output_dir: Path = args.output_dir
+ output_dir.mkdir(parents=True, exist_ok=True)
+ required_platforms = parse_required_platforms(args.required_platforms)
+
+ if args.pack_only:
+ if not staged_csproj.is_file():
+ raise PackError(f"staged project not found at {staged_csproj}. Run with --build-only first.")
+ print(f"Reusing existing staging directory: {staging_dir}")
+ else:
+ stage_sources(staging_dir)
+ stage_binaries(staging_dir, args, required_platforms)
+
+ if args.build_only:
+ do_build(staged_csproj, staging_dir, args, min_ort_version_file)
+ return
+
+ do_pack(staged_csproj, output_dir, args, min_ort_version_file)
+
+ print()
+ print(f"Done. Output: {output_dir}")
+
+
+def run(args: argparse.Namespace) -> None:
+ if not CSPROJ.is_file():
+ raise PackError(f"project file not found: {CSPROJ}")
+ if not MIN_ORT_VERSION_FILE.is_file():
+ raise PackError(f"MIN_ONNXRUNTIME_VERSION file not found: {MIN_ORT_VERSION_FILE}")
+ if args.nuget_config and not args.nuget_config.is_file():
+ raise PackError(f"NuGet.config not found: {args.nuget_config}")
+
+ if (args.build_only or args.pack_only) and not args.staging_dir:
+ raise PackError("--staging-dir is required when using --build-only or --pack-only.")
+
+ min_ort_version_file = MIN_ORT_VERSION_FILE.resolve()
+
+ if args.staging_dir:
+ staging_dir: Path = args.staging_dir
+ staging_dir.mkdir(parents=True, exist_ok=True)
+ run_in_staging(args, staging_dir, min_ort_version_file)
+ return
+
+ # Full build+pack flow with no caller-managed staging dir: use a temp dir that
+ # is cleaned up automatically (including on exception).
+ with tempfile.TemporaryDirectory(prefix="webgpu_pack_") as tmp:
+ run_in_staging(args, Path(tmp), min_ort_version_file)
+
+
+def main() -> int:
+ args = parse_args()
+ try:
+ run(args)
+ except PackError as e:
+ print(f"error: {e}", file=sys.stderr)
+ return 1
+ except subprocess.CalledProcessError as e:
+ cmd_name = e.cmd[0] if e.cmd else "subprocess"
+ print(f"error: {cmd_name} failed with exit code {e.returncode}", file=sys.stderr)
+ return e.returncode or 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index c221b8d87fbb6..ebb8a474eae10 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -56,14 +56,14 @@ stages:
- template: ../templates/common-variables.yml
- name: WebGpuPackStagingDir
value: '$(Build.BinariesDirectory)\webgpu_pack_staging'
- # Common arguments shared by the Build and Pack invocations of pack_nuget.ps1.
+ # Common arguments shared by the Build and Pack invocations of pack_nuget.py.
- name: WebGpuPackNuGetCommonArgs
value: >-
- -Version "$(PluginPackageVersion)"
- -OutputDir "$(Build.ArtifactStagingDirectory)\nuget"
- -StagingDir "$(WebGpuPackStagingDir)"
- -Configuration Release
- -NuGetConfig "$(Build.SourcesDirectory)\NuGet.config"
+ --version "$(PluginPackageVersion)"
+ --output-dir "$(Build.ArtifactStagingDirectory)\nuget"
+ --staging-dir "$(WebGpuPackStagingDir)"
+ --configuration Release
+ --nuget-config "$(Build.SourcesDirectory)\NuGet.config"
steps:
- checkout: self
clean: true
@@ -111,7 +111,7 @@ stages:
# Compute the set of required platforms from the pipeline parameters and verify the
# corresponding artifact directories actually downloaded. This catches renamed/moved
- # upstream artifacts loudly before any pack work, and feeds pack_nuget.ps1 the same
+ # upstream artifacts loudly before any pack work, and feeds pack_nuget.py the same
# list so it fails fast if any required platform's binaries are missing.
- task: PythonScript@0
displayName: 'Compute required platforms'
@@ -153,17 +153,16 @@ stages:
print(f"##vso[task.setvariable variable=WebGpuRequiredPlatforms]{required}")
# Stage binaries and build the managed assembly (so it can be ESRP-signed before packing).
- - task: PowerShell@2
+ - task: PythonScript@0
displayName: 'Build managed DLL'
inputs:
- targetType: filePath
- filePath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.ps1'
- pwsh: true
+ scriptSource: filePath
+ scriptPath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.py'
arguments: >-
$(WebGpuPackNuGetCommonArgs)
- -ArtifactsDir "$(Build.BinariesDirectory)\artifacts"
- -RequiredPlatforms $(WebGpuRequiredPlatforms)
- -BuildOnly
+ --artifacts-dir "$(Build.BinariesDirectory)\artifacts"
+ --required-platforms $(WebGpuRequiredPlatforms)
+ --build-only
# ESRP-sign the managed DLL before it gets embedded in the .nupkg.
- template: ../templates/win-esrp-dll.yml
@@ -174,15 +173,14 @@ stages:
DoEsrp: ${{ parameters.DoEsrp }}
# Pack the (now-signed) managed DLL plus native binaries into the .nupkg.
- - task: PowerShell@2
+ - task: PythonScript@0
displayName: 'Pack NuGet package'
inputs:
- targetType: filePath
- filePath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.ps1'
- pwsh: true
+ scriptSource: filePath
+ scriptPath: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\pack_nuget.py'
arguments: >-
$(WebGpuPackNuGetCommonArgs)
- -PackOnly
+ --pack-only
# ESRP sign
- template: ../templates/esrp_nuget.yml
From 52a6f9758349ba9c92e7a299a4776628f27cccb8 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 10:28:18 -0700
Subject: [PATCH 14/41] Update README to reflect pack_nuget.py staging-dir
behavior
---
plugin-ep-webgpu/csharp/README.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index f9d15c76b8fb0..3c94f5c697d41 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -44,8 +44,9 @@ The plugin DLL will be at `build/webgpu.plugin/Release/Release/onnxruntime_provi
## Building the NuGet Package
Use `pack_nuget.py` to stage native binaries and run `dotnet pack`. The script copies
-everything into a temporary staging directory under the output folder — the source tree
-is never modified.
+everything into a staging directory before building — the source tree is never modified.
+By default, an auto-cleaned temporary directory is used; pass `--staging-dir` to use an
+explicit one (required when running with `--build-only` or `--pack-only`).
### Pack with a local build (single platform)
@@ -78,7 +79,7 @@ python pack_nuget.py --version 0.1.0-dev `
| `--binary-dir-win-arm64` | No | — | Path to directory containing win-arm64 binaries |
| `--binary-dir-linux-x64` | No | — | Path to directory containing linux-x64 binaries |
| `--binary-dir-macos-arm64` | No | — | Path to directory containing osx-arm64 (macOS arm64) binaries |
-| `--staging-dir` | No | `/_staging` | Explicit staging directory. Caller owns its lifecycle (no auto-cleanup) |
+| `--staging-dir` | No | auto-cleaned temp dir | Explicit staging directory. Required with `--build-only` / `--pack-only`; caller owns its lifecycle (no auto-cleanup) |
| `--build-only` | No | `false` | Stage and build the managed DLL only; skip `dotnet pack`. Preserves the staging directory for a later `--pack-only` run |
| `--pack-only` | No | `false` | Skip staging/build and run `dotnet pack` against an existing staging directory (mutually exclusive with `--build-only`) |
| `--required-platforms` | No | — | Comma-separated list of platforms that MUST be staged successfully (CI-mode safety net) |
From 0e7f39595ed87d096573e6b61f3da79761a14524 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 10:31:32 -0700
Subject: [PATCH 15/41] Drop pack_nuget.py parameter table from README
Defer to --help to avoid drift between the script and the docs.
---
plugin-ep-webgpu/csharp/README.md | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index 3c94f5c697d41..99e47843e7902 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -69,20 +69,7 @@ python pack_nuget.py --version 0.1.0-dev `
### Script Parameters
-| Parameter | Required | Default | Description |
-|---|---|---|---|
-| `--version` | Yes | — | Package version string (e.g. `0.1.0`, `0.1.0-dev`) |
-| `--output-dir` | No | `./nuget_output` | Directory for the `.nupkg` and `.snupkg` output |
-| `--configuration` | No | `Release` | Build configuration |
-| `--artifacts-dir` | No | — | CI mode: root directory with `win_x64/bin/`, `linux_x64/bin/`, etc. |
-| `--binary-dir-win-x64` | No | — | Path to directory containing win-x64 binaries |
-| `--binary-dir-win-arm64` | No | — | Path to directory containing win-arm64 binaries |
-| `--binary-dir-linux-x64` | No | — | Path to directory containing linux-x64 binaries |
-| `--binary-dir-macos-arm64` | No | — | Path to directory containing osx-arm64 (macOS arm64) binaries |
-| `--staging-dir` | No | auto-cleaned temp dir | Explicit staging directory. Required with `--build-only` / `--pack-only`; caller owns its lifecycle (no auto-cleanup) |
-| `--build-only` | No | `false` | Stage and build the managed DLL only; skip `dotnet pack`. Preserves the staging directory for a later `--pack-only` run |
-| `--pack-only` | No | `false` | Skip staging/build and run `dotnet pack` against an existing staging directory (mutually exclusive with `--build-only`) |
-| `--required-platforms` | No | — | Comma-separated list of platforms that MUST be staged successfully (CI-mode safety net) |
+Run `python pack_nuget.py --help` for the full list of options and their defaults.
At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be provided.
Platforms without a binary directory are skipped.
From 3ca87d5c64d66931eefd21cf3b8e7f5b6f2a587e Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 10:32:43 -0700
Subject: [PATCH 16/41] Fold pack_nuget.py notes into parent section in README
---
plugin-ep-webgpu/csharp/README.md | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index 99e47843e7902..de67879dbb496 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -48,6 +48,10 @@ everything into a staging directory before building — the source tree is never
By default, an auto-cleaned temporary directory is used; pass `--staging-dir` to use an
explicit one (required when running with `--build-only` or `--pack-only`).
+At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be
+provided. Platforms without a binary directory are skipped. Run
+`python pack_nuget.py --help` for the full list of options and their defaults.
+
### Pack with a local build (single platform)
```powershell
@@ -67,13 +71,6 @@ python pack_nuget.py --version 0.1.0-dev `
--binary-dir-macos-arm64 /mnt/builds/macos_arm64
```
-### Script Parameters
-
-Run `python pack_nuget.py --help` for the full list of options and their defaults.
-
-At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be provided.
-Platforms without a binary directory are skipped.
-
## Versioning
The package version is supplied to `pack_nuget.py` via `--version`. In the packaging pipeline, the release or
From 095bd3ff944f60e7fa8bb1bfe26e98c042fcaac4 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 10:48:51 -0700
Subject: [PATCH 17/41] Address review nits in WebGPU EP NuGet packaging
- pack_nuget.py: note that the netstandard2.0 path segment must match in the csproj.
- README.md: replace mixed Windows/POSIX paths in the multi-platform example with placeholders.
---
plugin-ep-webgpu/csharp/README.md | 12 ++++++++----
plugin-ep-webgpu/csharp/pack_nuget.py | 1 +
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index de67879dbb496..31e425342a8a3 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -63,12 +63,16 @@ python pack_nuget.py --version 0.1.0-dev `
### Pack multiple platforms
+Each `--binary-dir-*` points at the directory containing that platform's already-built
+native binaries. In practice the four binaries are produced on different machines and
+combined in CI; locally you'd typically only set the one(s) you have available.
+
```powershell
python pack_nuget.py --version 0.1.0-dev `
- --binary-dir-win-x64 C:\builds\win_x64 `
- --binary-dir-win-arm64 C:\builds\win_arm64 `
- --binary-dir-linux-x64 /mnt/builds/linux_x64 `
- --binary-dir-macos-arm64 /mnt/builds/macos_arm64
+ --binary-dir-win-x64 `
+ --binary-dir-win-arm64 `
+ --binary-dir-linux-x64 `
+ --binary-dir-macos-arm64
```
## Versioning
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.py b/plugin-ep-webgpu/csharp/pack_nuget.py
index c129fad8223b8..21f25b6f4beb3 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.py
+++ b/plugin-ep-webgpu/csharp/pack_nuget.py
@@ -231,6 +231,7 @@ def do_build(staged_csproj: Path, staging_dir: Path, args: argparse.Namespace, m
print("+ " + " ".join(cmd))
subprocess.run(cmd, check=True)
+ # Note: "netstandard2.0" must match in Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj.
managed_dll = staging_dir / "bin" / args.configuration / "netstandard2.0" / "Microsoft.ML.OnnxRuntime.EP.WebGpu.dll"
if not managed_dll.is_file():
raise PackError(f"managed DLL not found after build: {managed_dll}")
From b9ccab7623e5c95666a72162265c1447ef973076 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 10:53:20 -0700
Subject: [PATCH 18/41] update example versions
---
plugin-ep-webgpu/csharp/pack_nuget.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.py b/plugin-ep-webgpu/csharp/pack_nuget.py
index 21f25b6f4beb3..a3bfe6addecee 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.py
+++ b/plugin-ep-webgpu/csharp/pack_nuget.py
@@ -14,7 +14,7 @@
--------
Local: pack win-x64 only from a local build:
- python pack_nuget.py --version 1.26.0-dev \\
+ python pack_nuget.py --version 0.1.0-dev \\
--binary-dir-win-x64 ../../build/webgpu.plugin/Release/Release
CI: pack all platforms from downloaded artifacts:
@@ -60,7 +60,7 @@ def _absolute_path(value: str) -> Path:
description="Build the Microsoft.ML.OnnxRuntime.EP.WebGpu NuGet package.",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
- p.add_argument("--version", required=True, help="Package version (e.g. 1.26.0-dev).")
+ p.add_argument("--version", required=True, help="Package version (e.g. 0.1.0-dev).")
p.add_argument(
"--output-dir",
type=_absolute_path,
From 74c06030feed06af5ec587e6c1be507fbf0846df Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Fri, 1 May 2026 14:24:06 -0700
Subject: [PATCH 19/41] use sentinel as csproj default version value
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
index 08d6bd3b7e261..f5eabeb9a6325 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -7,7 +7,8 @@
Microsoft.ML.OnnxRuntime.EP.WebGpu
- 1.0.0
+
+ 0.0.0-dev
Microsoft
Microsoft
WebGPU plugin Execution Provider for ONNX Runtime. Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
From 156173746fdcf4ce80c5af17199d8cc41311535e Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 08:49:42 -0700
Subject: [PATCH 20/41] Address PR review feedback
- generate_mul_model.py: use a single import style for onnx (CodeQL).
- WebGpuEp.cs: use ProcessArchitecture instead of OSArchitecture so emulated processes pick the matching native asset.
- README.md: simplify the usage example and note that error handling is omitted for readability.
- plugin-webgpu-nuget-packaging-stage.yml: drop the unused cmake_build_type parameter; compare templated booleans case-insensitively.
- WebGpuEpNuGetTest.csproj: add an OrtWebGpuPackageVersion property (default *-* for local dev) and reference it from PackageReference.
- plugin-win-webgpu-test-stage.yml: extract the exact package version from the .nupkg filename and pass it through to dotnet build via -p:OrtWebGpuPackageVersion; switch dotnet build/run steps to powershell with backtick continuations.
---
.../README.md | 9 +++---
.../WebGpuEp.cs | 4 +--
.../WebGpuEpNuGetTest.csproj | 9 +++++-
.../WebGpuEpNuGetTest/generate_mul_model.py | 7 ++---
.../plugin-webgpu-nuget-packaging-stage.yml | 14 ++++------
.../stages/plugin-win-webgpu-test-stage.yml | 28 +++++++++++++++----
6 files changed, 45 insertions(+), 26 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
index 9bdefb51e976c..4c4941ff2c8ac 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -6,6 +6,7 @@ Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
### Usage
```csharp
+// Error handling (e.g. WebGPU device not found) is omitted for readability.
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.EP.WebGpu;
@@ -14,12 +15,12 @@ var env = OrtEnv.Instance();
env.RegisterExecutionProviderLibrary("webgpu_ep", WebGpuEp.GetLibraryPath());
// Find the WebGPU EP device
-OrtEpDevice? webGpuDevice = null;
-foreach (var device in env.GetEpDevices())
+OrtEpDevice webGpuDevice = null!;
+foreach (var d in env.GetEpDevices())
{
- if (string.Equals(WebGpuEp.GetEpName(), device.EpName, StringComparison.Ordinal))
+ if (d.EpName == WebGpuEp.GetEpName())
{
- webGpuDevice = device;
+ webGpuDevice = d;
break;
}
}
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
index f2501ba6f5816..35a0e911ffe86 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
@@ -92,8 +92,8 @@ private static string GetOSTag()
private static string GetArchTag()
{
- return RuntimeInformation.OSArchitecture == Architecture.X64 ? "x64"
- : RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64"
+ return RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "x64"
+ : RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "arm64"
: "unknown";
}
}
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
index adf0eef247a18..9554161b1e978 100644
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/WebGpuEpNuGetTest.csproj
@@ -6,11 +6,18 @@
latest
enable
enable
+
+ *-*
-
+
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
index 8c0266a13ffcd..c64b4b7ec96bc 100644
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/generate_mul_model.py
@@ -6,8 +6,7 @@
import os
-import onnx
-from onnx import TensorProto, helper
+from onnx import TensorProto, checker, helper, save
X = helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3])
Y = helper.make_tensor_value_info("y", TensorProto.FLOAT, [2, 3])
@@ -19,8 +18,8 @@
model = helper.make_model(graph, producer_name="onnxruntime-webgpu-ep-test")
model.opset_import[0].version = 13
-onnx.checker.check_model(model)
+checker.check_model(model)
output_path = os.path.join(os.path.dirname(__file__), "mul.onnx")
-onnx.save(model, output_path)
+save(model, output_path)
print(f"Saved {output_path}")
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
index ebb8a474eae10..93210533d2dc0 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml
@@ -9,10 +9,6 @@ parameters:
- name: version_file
type: string
-- name: cmake_build_type
- type: string
- default: 'Release'
-
- name: DoEsrp
type: boolean
default: true
@@ -122,12 +118,12 @@ stages:
import sys
# The string literals below are filled in by ADO template expansion at queue
- # time and resolve to 'True' or 'False'.
+ # time and resolve to a boolean value 'True' or 'False'. Compare case-insensitively.
platforms_enabled = {
- "win_x64": "${{ parameters.platforms.win_x64 }}" == "True",
- "win_arm64": "${{ parameters.platforms.win_arm64 }}" == "True",
- "linux_x64": "${{ parameters.platforms.linux_x64 }}" == "True",
- "macos_arm64": "${{ parameters.platforms.macos_arm64 }}" == "True",
+ "win_x64": "${{ parameters.platforms.win_x64 }}".lower() == "true",
+ "win_arm64": "${{ parameters.platforms.win_arm64 }}".lower() == "true",
+ "linux_x64": "${{ parameters.platforms.linux_x64 }}".lower() == "true",
+ "macos_arm64": "${{ parameters.platforms.macos_arm64 }}".lower() == "true",
}
expected = [name for name, enabled in platforms_enabled.items() if enabled]
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index 28ff774d0039e..fcdf2a7374ba1 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -81,14 +81,24 @@ stages:
artifact: webgpu_plugin_nuget
displayName: 'Download NuGet package'
- # Set up local NuGet feed
+ # Set up local NuGet feed and extract the package version from the .nupkg filename
+ # so the test project can pin to it (instead of resolving via a floating version).
- powershell: |
$ErrorActionPreference = 'Stop'
$localFeedDir = "$(Build.BinariesDirectory)\local_feed"
New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
# Copy .nupkg to local feed
- Copy-Item "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\*.nupkg" $localFeedDir -Force
+ $nupkg = (Get-ChildItem "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\Microsoft.ML.OnnxRuntime.EP.WebGpu.*.nupkg")[0]
+ Copy-Item $nupkg.FullName $localFeedDir -Force
+
+ # Extract version from filename: Microsoft.ML.OnnxRuntime.EP.WebGpu..nupkg
+ if ($nupkg.BaseName -notmatch '^Microsoft\.ML\.OnnxRuntime\.EP\.WebGpu\.(.+)$') {
+ throw "Could not extract version from .nupkg filename: $($nupkg.Name)"
+ }
+ $packageVersion = $Matches[1]
+ Write-Host "Detected package version: $packageVersion"
+ Write-Host "##vso[task.setvariable variable=OrtWebGpuPackageVersion]$packageVersion"
# Write a project-level nuget.config that adds ONLY the local feed.
# NuGet merges this with the repo-root NuGet.config.
@@ -106,10 +116,16 @@ stages:
Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
displayName: 'Set up local NuGet feed'
- - script: |
- dotnet build "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release
+ - powershell: |
+ dotnet build `
+ "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
+ --configuration Release `
+ -p:OrtWebGpuPackageVersion=$(OrtWebGpuPackageVersion)
displayName: 'Build test project'
- - script: |
- dotnet run --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" --configuration Release --no-build
+ - powershell: |
+ dotnet run `
+ --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
+ --configuration Release `
+ --no-build
displayName: 'Run NuGet package test'
From ad740551dfd263ac8a94ac82351472ea603b0fc0 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 08:54:06 -0700
Subject: [PATCH 21/41] Use pwsh shorthand consistently in WebGPU test stage
Convert all PowerShell steps in plugin-win-webgpu-test-stage.yml to the - pwsh: shorthand for consistency. The Python test step previously used the PowerShell@2 task with pwsh: true; the NuGet test steps used the - powershell: shorthand which runs Windows PowerShell 5.1. All now run under PowerShell 7+.
---
.../stages/plugin-win-webgpu-test-stage.yml | 44 +++++++++----------
1 file changed, 20 insertions(+), 24 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index fcdf2a7374ba1..184c0ed099615 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -31,32 +31,28 @@ stages:
artifact: webgpu_plugin_python_win_${{ parameters.arch }}
displayName: 'Download Python wheel'
- - task: PowerShell@2
- displayName: 'Install and test Python package'
- env:
- ORT_TEST_VERBOSE: $(System.Debug)
- inputs:
- targetType: inline
- pwsh: true
- script: |
- $ErrorActionPreference = 'Stop'
+ - pwsh: |
+ $ErrorActionPreference = 'Stop'
- echo "creating test_venv"
- python -m venv "$(Build.BinariesDirectory)\test_venv"
+ echo "creating test_venv"
+ python -m venv "$(Build.BinariesDirectory)\test_venv"
- echo "activating test_venv"
- & "$(Build.BinariesDirectory)\test_venv\Scripts\Activate.ps1"
+ echo "activating test_venv"
+ & "$(Build.BinariesDirectory)\test_venv\Scripts\Activate.ps1"
- echo "installing onnxruntime onnx numpy"
- python -m pip install onnxruntime onnx numpy
+ echo "installing onnxruntime onnx numpy"
+ python -m pip install onnxruntime onnx numpy
- $wheelDir = "$(Pipeline.Workspace)\build\webgpu_plugin_python_win_${{ parameters.arch }}"
- $wheel = (Get-ChildItem "$wheelDir\onnxruntime_ep_webgpu-*.whl")[0]
- echo "installing ${wheel}"
- python -m pip install $wheel.FullName
+ $wheelDir = "$(Pipeline.Workspace)\build\webgpu_plugin_python_win_${{ parameters.arch }}"
+ $wheel = (Get-ChildItem "$wheelDir\onnxruntime_ep_webgpu-*.whl")[0]
+ echo "installing ${wheel}"
+ python -m pip install $wheel.FullName
- echo "running test_webgpu_plugin_ep.py"
- python -u "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\test\test_webgpu_plugin_ep.py"
+ echo "running test_webgpu_plugin_ep.py"
+ python -u "$(Build.SourcesDirectory)\plugin-ep-webgpu\python\test\test_webgpu_plugin_ep.py"
+ displayName: 'Install and test Python package'
+ env:
+ ORT_TEST_VERBOSE: $(System.Debug)
# NuGet package test (x64 only — the NuGet package is multi-platform but
# the test runs on a single Windows agent that exercises the WebGPU EP).
@@ -83,7 +79,7 @@ stages:
# Set up local NuGet feed and extract the package version from the .nupkg filename
# so the test project can pin to it (instead of resolving via a floating version).
- - powershell: |
+ - pwsh: |
$ErrorActionPreference = 'Stop'
$localFeedDir = "$(Build.BinariesDirectory)\local_feed"
New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
@@ -116,14 +112,14 @@ stages:
Get-ChildItem $localFeedDir | ForEach-Object { Write-Host " $($_.Name)" }
displayName: 'Set up local NuGet feed'
- - powershell: |
+ - pwsh: |
dotnet build `
"$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
--configuration Release `
-p:OrtWebGpuPackageVersion=$(OrtWebGpuPackageVersion)
displayName: 'Build test project'
- - powershell: |
+ - pwsh: |
dotnet run `
--project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
--configuration Release `
From 68ac995c7a658d0a00b1ac6f5f8fca0e8bf6fa61 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 09:23:50 -0700
Subject: [PATCH 22/41] Polish improvements after PR review fixes
- README.md: drop the null-forgiving operator from the usage example in favor of a nullable annotation.
- WebGpuEp.cs: throw PlatformNotSupportedException for unsupported OS or process architecture instead of silently returning placeholder strings; rewrite GetArchTag with a switch expression.
- plugin-win-webgpu-test-stage.yml: deduplicate the test csproj path via a job-level WebGpuTestProject variable; drop redundant 'clean: true' from 'checkout: self' (workspace.clean: all already covers it); tighten the .nupkg version regex to require a leading digit and add a clear error if no matching .nupkg is found.
---
.../README.md | 2 +-
.../WebGpuEp.cs | 20 ++++++++++++-------
.../stages/plugin-win-webgpu-test-stage.yml | 18 +++++++++++------
3 files changed, 26 insertions(+), 14 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
index 4c4941ff2c8ac..74b773f255318 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -15,7 +15,7 @@ var env = OrtEnv.Instance();
env.RegisterExecutionProviderLibrary("webgpu_ep", WebGpuEp.GetLibraryPath());
// Find the WebGPU EP device
-OrtEpDevice webGpuDevice = null!;
+OrtEpDevice? webGpuDevice = null;
foreach (var d in env.GetEpDevices())
{
if (d.EpName == WebGpuEp.GetEpName())
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
index 35a0e911ffe86..b34295a03e592 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
@@ -42,7 +42,7 @@ public static string GetLibraryPath()
/// Array of EP names.
public static string[] GetEpNames()
{
- return new[] { "WebGpuExecutionProvider" };
+ return new[] { GetEpName() };
}
///
@@ -52,7 +52,7 @@ public static string[] GetEpNames()
/// The EP name string.
public static string GetEpName()
{
- return GetEpNames()[0];
+ return "WebGpuExecutionProvider";
}
private static string GetNativeDirectory()
@@ -79,7 +79,8 @@ private static string GetLibraryName()
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "libonnxruntime_providers_webgpu.dylib";
- return "onnxruntime_providers_webgpu";
+ throw new PlatformNotSupportedException(
+ $"WebGPU plugin EP does not support OS platform: {RuntimeInformation.OSDescription}");
}
private static string GetOSTag()
@@ -87,14 +88,19 @@ private static string GetOSTag()
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "win";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux";
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "osx";
- return "unknown";
+ throw new PlatformNotSupportedException(
+ $"WebGPU plugin EP does not support OS platform: {RuntimeInformation.OSDescription}");
}
private static string GetArchTag()
{
- return RuntimeInformation.ProcessArchitecture == Architecture.X64 ? "x64"
- : RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "arm64"
- : "unknown";
+ return RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.X64 => "x64",
+ Architecture.Arm64 => "arm64",
+ _ => throw new PlatformNotSupportedException(
+ $"WebGPU plugin EP does not support process architecture: {RuntimeInformation.ProcessArchitecture}"),
+ };
}
}
}
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index 184c0ed099615..f30f45682e639 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -64,9 +64,10 @@ stages:
pool:
name: onnxruntime-Win2022-VS2022-webgpu-A10
os: windows
+ variables:
+ WebGpuTestProject: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj'
steps:
- checkout: self
- clean: true
submodules: none
- template: ../templates/setup-feeds-and-python-steps.yml
@@ -84,12 +85,17 @@ stages:
$localFeedDir = "$(Build.BinariesDirectory)\local_feed"
New-Item -ItemType Directory -Path $localFeedDir -Force | Out-Null
- # Copy .nupkg to local feed
- $nupkg = (Get-ChildItem "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\Microsoft.ML.OnnxRuntime.EP.WebGpu.*.nupkg")[0]
+ # Locate the .nupkg.
+ $nupkg = Get-ChildItem "$(Pipeline.Workspace)\build\webgpu_plugin_nuget\Microsoft.ML.OnnxRuntime.EP.WebGpu.*.nupkg" |
+ Select-Object -First 1
+ if (-not $nupkg) {
+ throw "No matching .nupkg found under $(Pipeline.Workspace)\build\webgpu_plugin_nuget"
+ }
Copy-Item $nupkg.FullName $localFeedDir -Force
# Extract version from filename: Microsoft.ML.OnnxRuntime.EP.WebGpu..nupkg
- if ($nupkg.BaseName -notmatch '^Microsoft\.ML\.OnnxRuntime\.EP\.WebGpu\.(.+)$') {
+ # The version starts with a digit, which disambiguates from any future filename suffixes.
+ if ($nupkg.BaseName -notmatch '^Microsoft\.ML\.OnnxRuntime\.EP\.WebGpu\.(\d.*)$') {
throw "Could not extract version from .nupkg filename: $($nupkg.Name)"
}
$packageVersion = $Matches[1]
@@ -114,14 +120,14 @@ stages:
- pwsh: |
dotnet build `
- "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
+ "$(WebGpuTestProject)" `
--configuration Release `
-p:OrtWebGpuPackageVersion=$(OrtWebGpuPackageVersion)
displayName: 'Build test project'
- pwsh: |
dotnet run `
- --project "$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj" `
+ --project "$(WebGpuTestProject)" `
--configuration Release `
--no-build
displayName: 'Run NuGet package test'
From 13f7e96d5ee86a2e25ac258db458d326e4d13fd5 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 09:47:27 -0700
Subject: [PATCH 23/41] remove cmake_build_type parameter from
plugin-webgpu-nuget-packaging-stage.yml invocation
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index d942c961f642a..52f7caafdb07c 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -100,7 +100,6 @@ stages:
parameters:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
- cmake_build_type: ${{ parameters.cmake_build_type }}
DoEsrp: true
platforms:
win_x64: ${{ parameters.build_windows_x64 }}
From 964f11c4a81a56b6b7ba68452636b5845fd5a6e5 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 10:53:00 -0700
Subject: [PATCH 24/41] address review comments
---
.../README.md | 3 ++-
.../WebGpuEp.cs | 22 ++++++++++++-------
plugin-ep-webgpu/csharp/README.md | 21 ++----------------
plugin-ep-webgpu/csharp/pack_nuget.py | 3 +--
4 files changed, 19 insertions(+), 30 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
index 74b773f255318..b5ec9b49a323a 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -6,7 +6,8 @@ Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
### Usage
```csharp
-// Error handling (e.g. WebGPU device not found) is omitted for readability.
+// Note: Error handling is omitted for brevity.
+
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.EP.WebGpu;
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
index b34295a03e592..2a5ec106aad0d 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/WebGpuEp.cs
@@ -21,18 +21,24 @@ public static string GetLibraryPath()
string rootDir = GetNativeDirectory();
string rid = GetRuntimeIdentifier();
string libraryName = GetLibraryName();
- string epLibPath = Path.GetFullPath(Path.Combine(rootDir,
- "runtimes", rid,
- "native",
- libraryName));
- if (!File.Exists(epLibPath))
+ // Probe the standard NuGet runtimes//native/ layout first, then fall back
+ // to the base directory for single-file/published layouts where native assets
+ // can land directly next to the managed assembly.
+ string[] candidates =
{
- throw new FileNotFoundException(
- $"Did not find WebGPU EP library file: {epLibPath}");
+ Path.Combine(rootDir, "runtimes", rid, "native", libraryName),
+ Path.Combine(rootDir, libraryName),
+ };
+
+ foreach (var candidate in candidates)
+ {
+ if (File.Exists(candidate))
+ return Path.GetFullPath(candidate);
}
- return epLibPath;
+ throw new FileNotFoundException(
+ $"Did not find WebGPU EP library file. Probed: {string.Join(", ", candidates)}");
}
///
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index 31e425342a8a3..d2eba37f6ee7c 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -15,7 +15,6 @@ csharp/
└── WebGpuEpNuGetTest/
├── WebGpuEpNuGetTest.csproj # Test console app (net8.0)
├── Program.cs # Registers EP, runs inference, validates output
- ├── nuget.config # NuGet source config (local feed for CI)
├── mul.onnx # Test model (element-wise multiply)
└── generate_mul_model.py # Script to regenerate mul.onnx
```
@@ -23,23 +22,7 @@ csharp/
## Prerequisites
- .NET SDK 8.0 or later
-- A built WebGPU plugin EP binary (see below)
-
-## Building the Plugin EP Binary
-
-From the repo root:
-
-```powershell
-python tools/ci_build/build.py `
- --build_dir ./build/webgpu.plugin `
- --use_webgpu shared_lib `
- --use_vcpkg `
- --config Release `
- --parallel `
- --update --build
-```
-
-The plugin DLL will be at `build/webgpu.plugin/Release/Release/onnxruntime_providers_webgpu.dll`.
+- A built WebGPU plugin EP shared library
## Building the NuGet Package
@@ -58,7 +41,7 @@ provided. Platforms without a binary directory are skipped. Run
cd plugin-ep-webgpu/csharp
python pack_nuget.py --version 0.1.0-dev `
- --binary-dir-win-x64 ..\..\build\webgpu.plugin\Release\Release
+ --binary-dir-win-x64
```
### Pack multiple platforms
diff --git a/plugin-ep-webgpu/csharp/pack_nuget.py b/plugin-ep-webgpu/csharp/pack_nuget.py
index a3bfe6addecee..9a29d067a4034 100644
--- a/plugin-ep-webgpu/csharp/pack_nuget.py
+++ b/plugin-ep-webgpu/csharp/pack_nuget.py
@@ -7,8 +7,7 @@
by the .csproj and runs `dotnet pack` to produce the .nupkg / .snupkg files.
Can be invoked locally or from CI. In CI, pass --artifacts-dir to point at the
-downloaded pipeline artifacts. Locally, pass individual --binary-dir-* options
-or place binaries manually in the runtimes/ folders.
+downloaded pipeline artifacts. Locally, pass individual --binary-dir-* options.
Examples
--------
From 2f871811416a8d269a57fc29e7f962d48cbcf6f1 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 11:36:05 -0700
Subject: [PATCH 25/41] update comment
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
index f5eabeb9a6325..e46c432f41b88 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -28,7 +28,7 @@
From f717236b45e4ba3785bc59db98a3dbde93c94fb9 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 11:47:26 -0700
Subject: [PATCH 26/41] add early exit in OrtEpDevice iteration in test program
---
plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
index dbf679e14e602..f5d1f0628c831 100644
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/Program.cs
@@ -24,6 +24,7 @@ static int Main()
if (string.Equals(epName, d.EpName, StringComparison.Ordinal))
{
epDevice = d;
+ break;
}
}
From 6a02745e8ec9f2ef6ecbc4cfadac015c291aa937 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 11:53:19 -0700
Subject: [PATCH 27/41] use model generated from script
---
plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
index f17ee6aca730e..6df01feb5cf58 100644
--- a/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
+++ b/plugin-ep-webgpu/csharp/test/WebGpuEpNuGetTest/mul.onnx
@@ -1,5 +1,4 @@
-
-test:Z
+
onnxruntime-webgpu-ep-test:Z
x
yz"Mul mul_graphZ
From 073b6296cd299e315242db24caaf0b8b6dcd68eb Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 18:58:06 -0700
Subject: [PATCH 28/41] Make Validate_Parameters always-on with explicit
dependsOn
Promote Validate_Parameters to a permanent first stage in the WebGPU plugin packaging pipeline. The stage now always runs, echoes the resolved parameter matrix, and only fails when build_windows_arm64=true and build_windows_x64=false.
Each platform-build stage template (plugin-{win,linux,mac}-webgpu-stage.yml) now accepts an explicit 'dependsOn' parameter (default []), and the orchestrator passes 'dependsOn: [Validate_Parameters]' to all four. The Windows ARM64 stage continues to add its internal dependency on the x64 build.
This addresses PR 28313 review feedback by making the dependency graph explicit and stable regardless of which platforms are enabled, and giving future cross-parameter checks an obvious home.
---
.../stages/plugin-linux-webgpu-stage.yml | 6 +-
.../stages/plugin-mac-webgpu-stage.yml | 6 +-
.../stages/plugin-webgpu-packaging-stage.yml | 64 ++++++++++++-------
.../stages/plugin-win-webgpu-stage.yml | 11 +++-
4 files changed, 61 insertions(+), 26 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
index c72d84036331c..53e71ebc4c299 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
@@ -23,9 +23,13 @@ parameters:
type: string
default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1'
+- name: dependsOn
+ type: object
+ default: []
+
stages:
- stage: Linux_plugin_webgpu_x64_Build
- dependsOn: []
+ dependsOn: ${{ parameters.dependsOn }}
jobs:
- job: Linux_plugin_webgpu_x64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
index eda45406f2480..b7e37a88b9e1e 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
@@ -15,9 +15,13 @@ parameters:
- RelWithDebInfo
- MinSizeRel
+- name: dependsOn
+ type: object
+ default: []
+
stages:
- stage: MacOS_plugin_webgpu_arm64_Build
- dependsOn: []
+ dependsOn: ${{ parameters.dependsOn }}
jobs:
- job: MacOS_plugin_webgpu_arm64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index c887b2db4f1ae..0a17acd4cc5ce 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -38,24 +38,39 @@ parameters:
- MinSizeRel
stages:
- # Parameter validation. The ARM64 build requires the x64 tblgen.exe (used during the
- # build), which is not correctly generated in a cross build. We require x64 to be built
- # first and download tblgen.exe from it; surface a mismatched-flags request as an
- # explicit failure rather than silently disabling arm64 packaging.
- - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, false)) }}:
- - stage: Validate_Parameters
- displayName: 'Validate parameters'
- jobs:
- - job: Fail
- displayName: 'Invalid parameter combination'
- pool:
- name: onnxruntime-Win-CPU-VS2022-Latest
- os: windows
- steps:
- - powershell: |
- Write-Error "build_windows_arm64=true requires build_windows_x64=true: the ARM64 build cross-uses the x64 tblgen.exe published by the Windows x64 stage."
+ # Parameter validation. Always runs as the first stage so it serves as a single,
+ # explicit `dependsOn` anchor for every downstream platform-build template, and
+ # provides an obvious home for any cross-parameter checks.
+ - stage: Validate_Parameters
+ displayName: 'Validate parameters'
+ dependsOn: []
+ jobs:
+ - job: Validate
+ displayName: 'Validate parameter combination'
+ pool:
+ name: onnxruntime-Win-CPU-VS2022-Latest
+ os: windows
+ steps:
+ - powershell: |
+ $x64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_x64 }}')
+ $arm64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_arm64 }}')
+ $linux = [System.Convert]::ToBoolean('${{ parameters.build_linux_x64 }}')
+ $mac = [System.Convert]::ToBoolean('${{ parameters.build_macos_arm64 }}')
+ Write-Host "Resolved parameter matrix:"
+ Write-Host " build_windows_x64 = $x64"
+ Write-Host " build_windows_arm64 = $arm64"
+ Write-Host " build_linux_x64 = $linux"
+ Write-Host " build_macos_arm64 = $mac"
+ Write-Host " package_version = ${{ parameters.package_version }}"
+ Write-Host " cmake_build_type = ${{ parameters.cmake_build_type }}"
+
+ if ($arm64 -and -not $x64) {
+ Write-Error ("build_windows_arm64=true requires build_windows_x64=true: " +
+ "the ARM64 build consumes the x64 tblgen.exe artifact published by the Windows x64 stage.")
exit 1
- displayName: 'Fail: arm64 requires x64'
+ }
+ Write-Host "Parameters OK."
+ displayName: 'Validate parameters'
# Windows x64
- ${{ if eq(parameters.build_windows_x64, true) }}:
@@ -65,6 +80,8 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
+ dependsOn:
+ - Validate_Parameters
# Windows ARM64
- ${{ if eq(parameters.build_windows_arm64, true) }}:
@@ -74,6 +91,8 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
+ dependsOn:
+ - Validate_Parameters
# Linux x64
- ${{ if eq(parameters.build_linux_x64, true) }}:
@@ -82,6 +101,8 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
+ dependsOn:
+ - Validate_Parameters
# macOS ARM64
- ${{ if eq(parameters.build_macos_arm64, true) }}:
@@ -90,6 +111,8 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
+ dependsOn:
+ - Validate_Parameters
# NuGet packaging (runs after all platform builds)
- template: plugin-webgpu-nuget-packaging-stage.yml
@@ -109,7 +132,7 @@ stages:
dependsOn:
- ${{ if eq(parameters.build_windows_x64, true) }}:
- Win_plugin_webgpu_x64_Build
- - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}:
+ - ${{ if eq(parameters.build_windows_arm64, true) }}:
- Win_plugin_webgpu_arm64_Build
- ${{ if eq(parameters.build_linux_x64, true) }}:
- Linux_plugin_webgpu_x64_Build
@@ -140,10 +163,7 @@ stages:
artifactName: webgpu_plugin_win_x64
targetPath: $(Build.SourcesDirectory)/webgpu-plugin-win-x64
- # Windows ARM64
- # ARM64 build requires the x64 tblgen.exe (used during the build), which is not correctly
- # generated in a cross build. So we require x64 to be built first and download tblgen.exe from it.
- - ${{ if and(eq(parameters.build_windows_arm64, true), eq(parameters.build_windows_x64, true)) }}:
+ - ${{ if eq(parameters.build_windows_arm64, true) }}:
- task: DownloadPipelineArtifact@2
displayName: 'Download webgpu_plugin_win_arm64'
inputs:
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
index acad674143961..52cb45e22766b 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
@@ -25,12 +25,19 @@ parameters:
type: string
default: 'https://pkgs.dev.azure.com/aiinfra/_packaging/ONNXRuntime_WebGPU_BuildDependencies/npm/registry/'
+- name: dependsOn
+ type: object
+ default: []
+
stages:
- stage: Win_plugin_webgpu_${{ parameters.arch }}_Build
${{ if eq(parameters.arch, 'arm64') }}:
- dependsOn: Win_plugin_webgpu_x64_Build
+ dependsOn:
+ - Win_plugin_webgpu_x64_Build
+ - ${{ each dep in parameters.dependsOn }}:
+ - ${{ dep }}
${{ else }}:
- dependsOn: []
+ dependsOn: ${{ parameters.dependsOn }}
jobs:
- job: Win_plugin_webgpu_${{ parameters.arch }}_Build
timeoutInMinutes: 360
From 600adb84072bc16de1777026836e0f666f5f2edd Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 19:00:01 -0700
Subject: [PATCH 29/41] update comment
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 0a17acd4cc5ce..71aba342dd70e 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -39,8 +39,7 @@ parameters:
stages:
# Parameter validation. Always runs as the first stage so it serves as a single,
- # explicit `dependsOn` anchor for every downstream platform-build template, and
- # provides an obvious home for any cross-parameter checks.
+ # explicit `dependsOn` anchor for every downstream platform-build template.
- stage: Validate_Parameters
displayName: 'Validate parameters'
dependsOn: []
From 479f9b41c3e77608ec6782274e11953cf8cc47da Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 19:01:22 -0700
Subject: [PATCH 30/41] Rename dependsOn parameter to depends_on for casing
consistency
---
.../azure-pipelines/stages/plugin-linux-webgpu-stage.yml | 4 ++--
.../azure-pipelines/stages/plugin-mac-webgpu-stage.yml | 4 ++--
.../stages/plugin-webgpu-packaging-stage.yml | 8 ++++----
.../azure-pipelines/stages/plugin-win-webgpu-stage.yml | 6 +++---
4 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
index 53e71ebc4c299..059b84e364947 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
@@ -23,13 +23,13 @@ parameters:
type: string
default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1'
-- name: dependsOn
+- name: depends_on
type: object
default: []
stages:
- stage: Linux_plugin_webgpu_x64_Build
- dependsOn: ${{ parameters.dependsOn }}
+ dependsOn: ${{ parameters.depends_on }}
jobs:
- job: Linux_plugin_webgpu_x64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
index b7e37a88b9e1e..f9adda0ee0735 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
@@ -15,13 +15,13 @@ parameters:
- RelWithDebInfo
- MinSizeRel
-- name: dependsOn
+- name: depends_on
type: object
default: []
stages:
- stage: MacOS_plugin_webgpu_arm64_Build
- dependsOn: ${{ parameters.dependsOn }}
+ dependsOn: ${{ parameters.depends_on }}
jobs:
- job: MacOS_plugin_webgpu_arm64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 71aba342dd70e..497c49dd3ec4a 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -79,7 +79,7 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- dependsOn:
+ depends_on:
- Validate_Parameters
# Windows ARM64
@@ -90,7 +90,7 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- dependsOn:
+ depends_on:
- Validate_Parameters
# Linux x64
@@ -100,7 +100,7 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- dependsOn:
+ depends_on:
- Validate_Parameters
# macOS ARM64
@@ -110,7 +110,7 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- dependsOn:
+ depends_on:
- Validate_Parameters
# NuGet packaging (runs after all platform builds)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
index 52cb45e22766b..f145f66e45d60 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
@@ -25,7 +25,7 @@ parameters:
type: string
default: 'https://pkgs.dev.azure.com/aiinfra/_packaging/ONNXRuntime_WebGPU_BuildDependencies/npm/registry/'
-- name: dependsOn
+- name: depends_on
type: object
default: []
@@ -34,10 +34,10 @@ stages:
${{ if eq(parameters.arch, 'arm64') }}:
dependsOn:
- Win_plugin_webgpu_x64_Build
- - ${{ each dep in parameters.dependsOn }}:
+ - ${{ each dep in parameters.depends_on }}:
- ${{ dep }}
${{ else }}:
- dependsOn: ${{ parameters.dependsOn }}
+ dependsOn: ${{ parameters.depends_on }}
jobs:
- job: Win_plugin_webgpu_${{ parameters.arch }}_Build
timeoutInMinutes: 360
From 67ad2f887d8da6714d467061ed0178f35b7847ba Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 19:05:47 -0700
Subject: [PATCH 31/41] Tighten Validate_Parameters comment wording
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 497c49dd3ec4a..b637c2f40071b 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -38,8 +38,8 @@ parameters:
- MinSizeRel
stages:
- # Parameter validation. Always runs as the first stage so it serves as a single,
- # explicit `dependsOn` anchor for every downstream platform-build template.
+ # Parameter validation. Runs first and unconditionally so it serves as a single,
+ # explicit dependency anchor for every downstream platform-build template.
- stage: Validate_Parameters
displayName: 'Validate parameters'
dependsOn: []
From ee7990aad894e8ebc6767c73b6869ee6c26367c8 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 19:12:33 -0700
Subject: [PATCH 32/41] disable RC value from top level pipeline
---
.../ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml
index 7d9f7c24b3360..673452d8b110a 100644
--- a/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml
+++ b/tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml
@@ -46,7 +46,7 @@ parameters:
type: string
values:
- release
- - RC
+ # - RC # not implemented yet
- dev
default: dev
From 9ad644e248ee8494628309cb753b7ab733482794 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Mon, 4 May 2026 19:21:23 -0700
Subject: [PATCH 33/41] Validate_Parameters: skip checkout and suppress SDL/TSA
scans
The Validate job only reads template parameters, so the implicit repo checkout and the 1ES PT SDL/TSA scans (which require .config/tsaoptions.json from the source tree) add cost without value. Set 'checkout: none' and disable per-job SDL scans via templateContext.sdl overrides.
---
.../stages/plugin-webgpu-packaging-stage.yml | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index b637c2f40071b..7c69d49291a19 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -49,7 +49,24 @@ stages:
pool:
name: onnxruntime-Win-CPU-VS2022-Latest
os: windows
+ # Skip the repo checkout entirely — this job only inspects template parameters.
+ # Suppress SDL/TSA scans that the 1ES PT would otherwise inject (they require
+ # `.config/tsaoptions.json` from the source tree and have nothing to scan here).
+ templateContext:
+ sdl:
+ tsa:
+ enabled: false
+ credscan:
+ enabled: false
+ policheck:
+ enabled: false
+ codeSignValidation:
+ enabled: false
+ binskim:
+ enabled: false
steps:
+ - checkout: none
+
- powershell: |
$x64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_x64 }}')
$arm64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_arm64 }}')
From b9f5df888744f27d50a9537bd2eabf5d9bf41098 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 08:12:23 -0700
Subject: [PATCH 34/41] update display name
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 7c69d49291a19..4a2627a9440a0 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -45,7 +45,7 @@ stages:
dependsOn: []
jobs:
- job: Validate
- displayName: 'Validate parameter combination'
+ displayName: 'Validate parameters'
pool:
name: onnxruntime-Win-CPU-VS2022-Latest
os: windows
From 744ae40eabc55dff25463f2eb361fa5b71a3ecfa Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 08:19:24 -0700
Subject: [PATCH 35/41] update comment
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 4a2627a9440a0..653c47e79745b 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -49,7 +49,7 @@ stages:
pool:
name: onnxruntime-Win-CPU-VS2022-Latest
os: windows
- # Skip the repo checkout entirely — this job only inspects template parameters.
+ # Skip the repo checkout entirely — this job only inspects pipeline parameters.
# Suppress SDL/TSA scans that the 1ES PT would otherwise inject (they require
# `.config/tsaoptions.json` from the source tree and have nothing to scan here).
templateContext:
From 137ea9f8a51c95f218a46ef9a363dfe102393b40 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 08:21:49 -0700
Subject: [PATCH 36/41] remove sdl parameters other than tsa
---
.../stages/plugin-webgpu-packaging-stage.yml | 8 --------
1 file changed, 8 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index 653c47e79745b..ff9c0e189b09d 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -56,14 +56,6 @@ stages:
sdl:
tsa:
enabled: false
- credscan:
- enabled: false
- policheck:
- enabled: false
- codeSignValidation:
- enabled: false
- binskim:
- enabled: false
steps:
- checkout: none
From e0f5596339a4be79d4edcaf8a3028b8c2706c766 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 08:27:08 -0700
Subject: [PATCH 37/41] update comment again
---
.../azure-pipelines/stages/plugin-webgpu-packaging-stage.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index ff9c0e189b09d..a8c8565cdc3ce 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -49,7 +49,7 @@ stages:
pool:
name: onnxruntime-Win-CPU-VS2022-Latest
os: windows
- # Skip the repo checkout entirely — this job only inspects pipeline parameters.
+ # Skip the repo checkout entirely — this job only inspects parameters.
# Suppress SDL/TSA scans that the 1ES PT would otherwise inject (they require
# `.config/tsaoptions.json` from the source tree and have nothing to scan here).
templateContext:
From 662b5ae99b2171401d2e92ac2bcfd7dc118939a2 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 08:40:04 -0700
Subject: [PATCH 38/41] remove extra validate parameter stage, remove
depends_on parameters
---
.../stages/plugin-linux-webgpu-stage.yml | 6 +--
.../stages/plugin-mac-webgpu-stage.yml | 6 +--
.../stages/plugin-webgpu-packaging-stage.yml | 50 -------------------
.../stages/plugin-win-webgpu-stage.yml | 12 ++---
4 files changed, 5 insertions(+), 69 deletions(-)
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
index 059b84e364947..c72d84036331c 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml
@@ -23,13 +23,9 @@ parameters:
type: string
default: 'onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cpu_x64_almalinux8_gcc14:20251017.1'
-- name: depends_on
- type: object
- default: []
-
stages:
- stage: Linux_plugin_webgpu_x64_Build
- dependsOn: ${{ parameters.depends_on }}
+ dependsOn: []
jobs:
- job: Linux_plugin_webgpu_x64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
index f9adda0ee0735..eda45406f2480 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml
@@ -15,13 +15,9 @@ parameters:
- RelWithDebInfo
- MinSizeRel
-- name: depends_on
- type: object
- default: []
-
stages:
- stage: MacOS_plugin_webgpu_arm64_Build
- dependsOn: ${{ parameters.depends_on }}
+ dependsOn: []
jobs:
- job: MacOS_plugin_webgpu_arm64_Build
timeoutInMinutes: 240
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
index a8c8565cdc3ce..996d6fa1af0a6 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-packaging-stage.yml
@@ -38,48 +38,6 @@ parameters:
- MinSizeRel
stages:
- # Parameter validation. Runs first and unconditionally so it serves as a single,
- # explicit dependency anchor for every downstream platform-build template.
- - stage: Validate_Parameters
- displayName: 'Validate parameters'
- dependsOn: []
- jobs:
- - job: Validate
- displayName: 'Validate parameters'
- pool:
- name: onnxruntime-Win-CPU-VS2022-Latest
- os: windows
- # Skip the repo checkout entirely — this job only inspects parameters.
- # Suppress SDL/TSA scans that the 1ES PT would otherwise inject (they require
- # `.config/tsaoptions.json` from the source tree and have nothing to scan here).
- templateContext:
- sdl:
- tsa:
- enabled: false
- steps:
- - checkout: none
-
- - powershell: |
- $x64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_x64 }}')
- $arm64 = [System.Convert]::ToBoolean('${{ parameters.build_windows_arm64 }}')
- $linux = [System.Convert]::ToBoolean('${{ parameters.build_linux_x64 }}')
- $mac = [System.Convert]::ToBoolean('${{ parameters.build_macos_arm64 }}')
- Write-Host "Resolved parameter matrix:"
- Write-Host " build_windows_x64 = $x64"
- Write-Host " build_windows_arm64 = $arm64"
- Write-Host " build_linux_x64 = $linux"
- Write-Host " build_macos_arm64 = $mac"
- Write-Host " package_version = ${{ parameters.package_version }}"
- Write-Host " cmake_build_type = ${{ parameters.cmake_build_type }}"
-
- if ($arm64 -and -not $x64) {
- Write-Error ("build_windows_arm64=true requires build_windows_x64=true: " +
- "the ARM64 build consumes the x64 tblgen.exe artifact published by the Windows x64 stage.")
- exit 1
- }
- Write-Host "Parameters OK."
- displayName: 'Validate parameters'
-
# Windows x64
- ${{ if eq(parameters.build_windows_x64, true) }}:
- template: plugin-win-webgpu-stage.yml
@@ -88,8 +46,6 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- depends_on:
- - Validate_Parameters
# Windows ARM64
- ${{ if eq(parameters.build_windows_arm64, true) }}:
@@ -99,8 +55,6 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- depends_on:
- - Validate_Parameters
# Linux x64
- ${{ if eq(parameters.build_linux_x64, true) }}:
@@ -109,8 +63,6 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- depends_on:
- - Validate_Parameters
# macOS ARM64
- ${{ if eq(parameters.build_macos_arm64, true) }}:
@@ -119,8 +71,6 @@ stages:
package_version: ${{ parameters.package_version }}
version_file: ${{ parameters.version_file }}
cmake_build_type: ${{ parameters.cmake_build_type }}
- depends_on:
- - Validate_Parameters
# NuGet packaging (runs after all platform builds)
- template: plugin-webgpu-nuget-packaging-stage.yml
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
index f145f66e45d60..3d2ce8072845a 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml
@@ -25,19 +25,13 @@ parameters:
type: string
default: 'https://pkgs.dev.azure.com/aiinfra/_packaging/ONNXRuntime_WebGPU_BuildDependencies/npm/registry/'
-- name: depends_on
- type: object
- default: []
-
stages:
- stage: Win_plugin_webgpu_${{ parameters.arch }}_Build
${{ if eq(parameters.arch, 'arm64') }}:
- dependsOn:
- - Win_plugin_webgpu_x64_Build
- - ${{ each dep in parameters.depends_on }}:
- - ${{ dep }}
+ # The ARM64 build consumes the x64 tblgen.exe artifact published by the Windows x64 stage.
+ dependsOn: Win_plugin_webgpu_x64_Build
${{ else }}:
- dependsOn: ${{ parameters.depends_on }}
+ dependsOn: []
jobs:
- job: Win_plugin_webgpu_${{ parameters.arch }}_Build
timeoutInMinutes: 360
From 1fc1a3109628672fc93118d48d1096141a182fbf Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 10:34:32 -0700
Subject: [PATCH 39/41] remove explicit mention of backends from
readme/description
---
.../Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj | 2 +-
.../csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md | 1 -
plugin-ep-webgpu/csharp/README.md | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
index e46c432f41b88..94be6bec6ea46 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/Microsoft.ML.OnnxRuntime.EP.WebGpu.csproj
@@ -11,7 +11,7 @@
0.0.0-dev
Microsoft
Microsoft
- WebGPU plugin Execution Provider for ONNX Runtime. Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
+ ONNX Runtime WebGPU Plugin Execution Provider.
README.md
ONNX;ONNX Runtime;Machine Learning;AI;Deep Learning;WebGPU
diff --git a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
index b5ec9b49a323a..f4a717b8836d5 100644
--- a/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
+++ b/plugin-ep-webgpu/csharp/Microsoft.ML.OnnxRuntime.EP.WebGpu/README.md
@@ -1,7 +1,6 @@
## Microsoft.ML.OnnxRuntime.EP.WebGpu
WebGPU plugin Execution Provider for [ONNX Runtime](https://github.com/microsoft/onnxruntime).
-Provides GPU acceleration via WebGPU (Dawn) with D3D12 and Vulkan backends.
### Usage
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index d2eba37f6ee7c..6e7884ab1f8bc 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -89,7 +89,7 @@ runtimes/osx-arm64/native/libonnxruntime_providers_webgpu.dylib
## Testing the Package
The test app registers the WebGPU EP, creates a session, runs a simple Mul model, and
-validates the output. It requires a GPU with D3D12 or Vulkan support.
+validates the output.
```powershell
# Point the test project's nuget.config at the pack output
From c48abd0c2a8c166a9f398a3f80ae99b3e466c60e Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 10:41:24 -0700
Subject: [PATCH 40/41] wrap lines to 120 chars
---
plugin-ep-webgpu/csharp/README.md | 31 ++++++++++++++-----------------
1 file changed, 14 insertions(+), 17 deletions(-)
diff --git a/plugin-ep-webgpu/csharp/README.md b/plugin-ep-webgpu/csharp/README.md
index 6e7884ab1f8bc..7a2b2041e364f 100644
--- a/plugin-ep-webgpu/csharp/README.md
+++ b/plugin-ep-webgpu/csharp/README.md
@@ -26,14 +26,12 @@ csharp/
## Building the NuGet Package
-Use `pack_nuget.py` to stage native binaries and run `dotnet pack`. The script copies
-everything into a staging directory before building — the source tree is never modified.
-By default, an auto-cleaned temporary directory is used; pass `--staging-dir` to use an
-explicit one (required when running with `--build-only` or `--pack-only`).
+Use `pack_nuget.py` to stage native binaries and run `dotnet pack`. The script copies everything into a staging
+directory before building — the source tree is never modified. By default, an auto-cleaned temporary directory is used;
+pass `--staging-dir` to use an explicit one (required when running with `--build-only` or `--pack-only`).
-At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be
-provided. Platforms without a binary directory are skipped. Run
-`python pack_nuget.py --help` for the full list of options and their defaults.
+At least one binary directory (or `--artifacts-dir` with matching subdirectories) must be provided. Platforms without
+a binary directory are skipped. Run `python pack_nuget.py --help` for the full list of options and their defaults.
### Pack with a local build (single platform)
@@ -46,9 +44,9 @@ python pack_nuget.py --version 0.1.0-dev `
### Pack multiple platforms
-Each `--binary-dir-*` points at the directory containing that platform's already-built
-native binaries. In practice the four binaries are produced on different machines and
-combined in CI; locally you'd typically only set the one(s) you have available.
+Each `--binary-dir-*` points at the directory containing that platform's already-built native binaries. In practice
+the four binaries are produced on different machines and combined in CI; locally you'd typically only set the one(s)
+you have available.
```powershell
python pack_nuget.py --version 0.1.0-dev `
@@ -88,8 +86,7 @@ runtimes/osx-arm64/native/libonnxruntime_providers_webgpu.dylib
## Testing the Package
-The test app registers the WebGPU EP, creates a session, runs a simple Mul model, and
-validates the output.
+The test app registers the WebGPU EP, creates a session, runs a simple Mul model, and validates the output.
```powershell
# Point the test project's nuget.config at the pack output
@@ -126,8 +123,8 @@ The NuGet packaging is integrated into the WebGPU plugin pipeline:
- **Pipeline:** `tools/ci_build/github/azure-pipelines/plugin-webgpu-pipeline.yml`
- **Packaging stage:** `tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml`
-The CI stage downloads build artifacts from all enabled platform stages, invokes
-`pack_nuget.py`, ESRP-signs the package, and runs the test app on a GPU agent.
+The CI stage downloads build artifacts from all enabled platform stages, invokes `pack_nuget.py`, ESRP-signs the
+package, and runs the test app on a GPU agent.
## Native Binaries Per Platform
@@ -138,6 +135,6 @@ The CI stage downloads build artifacts from all enabled platform stages, invokes
| `linux-x64` | `libonnxruntime_providers_webgpu.so` |
| `osx-arm64` | `libonnxruntime_providers_webgpu.dylib` |
-On Windows, `dxil.dll` and `dxcompiler.dll` are the DirectX Shader Compiler binaries
-downloaded from the [DXC GitHub releases](https://github.com/microsoft/DirectXShaderCompiler/releases).
-The CI pipeline handles this automatically.
+On Windows, `dxil.dll` and `dxcompiler.dll` are the DirectX Shader Compiler binaries downloaded from the
+[DXC GitHub releases](https://github.com/microsoft/DirectXShaderCompiler/releases). The CI pipeline handles this
+automatically.
From 8324fd719c1493f4cbc696fc4e1718a1eafdef35 Mon Sep 17 00:00:00 2001
From: edgchen1 <18449977+edgchen1@users.noreply.github.com>
Date: Tue, 5 May 2026 10:46:22 -0700
Subject: [PATCH 41/41] update python readme, remove explicit onnxruntime
install from Python test
---
plugin-ep-webgpu/python/README.md | 12 +++---------
.../stages/plugin-linux-webgpu-test-stage.yml | 2 +-
.../stages/plugin-mac-webgpu-test-stage.yml | 2 +-
.../stages/plugin-win-webgpu-test-stage.yml | 4 ++--
4 files changed, 7 insertions(+), 13 deletions(-)
diff --git a/plugin-ep-webgpu/python/README.md b/plugin-ep-webgpu/python/README.md
index 00da1a4e89254..849105a439396 100644
--- a/plugin-ep-webgpu/python/README.md
+++ b/plugin-ep-webgpu/python/README.md
@@ -19,19 +19,13 @@ Wheels are built via `build_wheel.py`. Running `pip install` or `pip wheel` dire
supported — the source tree contains `pyproject.toml.in` (a template), not a real `pyproject.toml`.
```bash
-python build_wheel.py \
- --binary_dir \
- --version \
- --output_dir
+python build_wheel.py --binary_dir --version --output_dir
```
Example:
```bash
-python build_wheel.py \
- --binary_dir ./build/Release \
- --version 0.1.0.devYYYYMMDD \
- --output_dir ./dist
+python build_wheel.py --binary_dir ./build/Release --version 0.1.0.devYYYYMMDD --output_dir ./dist
```
The script combines the pre-built plugin EP binaries with the package source to produce a platform-specific wheel.
@@ -44,7 +38,7 @@ Install the wheel and dependencies in a clean environment, then run the smoke te
python -m venv test_venv
source test_venv/bin/activate # or test_venv\Scripts\Activate.ps1 on Windows
pip install onnx numpy
-pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in the minimum compatible onnxruntime (see ../MIN_ONNXRUNTIME_VERSION)
+pip install dist/onnxruntime_ep_webgpu-*.whl # pulls in the minimum compatible onnxruntime
python test/test_webgpu_plugin_ep.py
```
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml
index 9ce494d4b3a36..12ee9ca68bb4e 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml
@@ -71,7 +71,7 @@ stages:
set -e -x
python3 -m venv /build/test_venv
source /build/test_venv/bin/activate
- python3 -m pip install onnxruntime onnx numpy
+ python3 -m pip install onnx numpy
wheel=\$(find /build/python_wheel -name 'onnxruntime_ep_webgpu-*.whl' | head -1)
python3 -m pip install \"\$wheel\"
python3 -u /onnxruntime_src/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml
index 5ad4e170b2855..6dca5dd450fd0 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml
@@ -30,7 +30,7 @@ stages:
set -e -x
python3 -m venv "$(Build.BinariesDirectory)/test_venv"
source "$(Build.BinariesDirectory)/test_venv/bin/activate"
- python3 -m pip install onnxruntime onnx numpy
+ python3 -m pip install onnx numpy
wheel=$(find "$(Pipeline.Workspace)/build/webgpu_plugin_python_macos_arm64" -name "onnxruntime_ep_webgpu-*.whl" | head -1)
python3 -m pip install "$wheel"
python3 -u "$(Build.SourcesDirectory)/plugin-ep-webgpu/python/test/test_webgpu_plugin_ep.py"
diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
index f30f45682e639..1494584ff98fd 100644
--- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
+++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml
@@ -40,8 +40,8 @@ stages:
echo "activating test_venv"
& "$(Build.BinariesDirectory)\test_venv\Scripts\Activate.ps1"
- echo "installing onnxruntime onnx numpy"
- python -m pip install onnxruntime onnx numpy
+ echo "installing test dependencies"
+ python -m pip install onnx numpy
$wheelDir = "$(Pipeline.Workspace)\build\webgpu_plugin_python_win_${{ parameters.arch }}"
$wheel = (Get-ChildItem "$wheelDir\onnxruntime_ep_webgpu-*.whl")[0]