diff --git a/eng/restore-toolset.ps1 b/eng/restore-toolset.ps1 new file mode 100644 index 0000000000..b12b38332e --- /dev/null +++ b/eng/restore-toolset.ps1 @@ -0,0 +1,46 @@ +# Installs .NET SDK workloads needed by projects in this repository. +# +# This script is dot-sourced by eng/common/build.ps1's InitializeCustomToolset +# AFTER InitializeDotNetCli has bootstrapped the repo-local .dotnet/ SDK, so +# $RepoRoot and the helpers from eng/common/tools.ps1 are in scope. +# +# Currently required by samples/WasiPlayground/WasiPlayground.csproj which +# targets net10.0 with true +# and would otherwise fail with NETSDK1147 in CI. + +$RequiredWorkloads = @('wasi-experimental-net10') + +# Note: `wasm-tools-net10` is documented in samples/WasiPlayground/README.md +# as a prerequisite for `dotnet publish`, but it is not needed by the repo's +# `dotnet build` so we keep the CI install minimal. + +$dotnetRoot = if (-not [string]::IsNullOrEmpty($env:DOTNET_INSTALL_DIR)) { + $env:DOTNET_INSTALL_DIR +} else { + Join-Path $RepoRoot '.dotnet' +} + +$dotnet = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') + +if (-not (Test-Path $dotnet)) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "restore-toolset.ps1: dotnet executable not found at '$dotnet'." + ExitWithExitCode 1 +} + +# Cheap, network-free probe: parse `dotnet workload list` text to skip already-installed workloads. +$listOutput = & $dotnet workload list 2>&1 | Out-String +$missing = @($RequiredWorkloads | Where-Object { + -not ($listOutput -match "(?m)^\s*$([regex]::Escape($_))\s") +}) + +if ($missing.Count -eq 0) { + Write-Host "All required workloads already installed: $($RequiredWorkloads -join ', ')" + return +} + +Write-Host "Installing .NET SDK workloads: $($missing -join ', ')" +& $dotnet workload install @missing +if ($LASTEXITCODE -ne 0) { + Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Failed to install workloads '$($missing -join ', ')' (dotnet workload install exit code $LASTEXITCODE)." + ExitWithExitCode $LASTEXITCODE +} diff --git a/eng/restore-toolset.sh b/eng/restore-toolset.sh new file mode 100644 index 0000000000..4542399044 --- /dev/null +++ b/eng/restore-toolset.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Installs .NET SDK workloads needed by projects in this repository. +# +# This script is dot-sourced by eng/common/build.sh's InitializeCustomToolset +# AFTER InitializeDotNetCli has bootstrapped the repo-local .dotnet/ SDK, so +# $repo_root and the helpers from eng/common/tools.sh are in scope. +# +# Currently required by samples/WasiPlayground/WasiPlayground.csproj which +# targets net10.0 with true +# and would otherwise fail with NETSDK1147 in CI. + +required_workloads=('wasi-experimental-net10') + +# Note: `wasm-tools-net10` is documented in samples/WasiPlayground/README.md +# as a prerequisite for `dotnet publish`, but it is not needed by the repo's +# `dotnet build` so we keep the CI install minimal. + +if [[ -n "${DOTNET_INSTALL_DIR:-}" ]]; then + dotnet_root="$DOTNET_INSTALL_DIR" +else + dotnet_root="${repo_root}.dotnet" +fi + +dotnet_exe="$dotnet_root/dotnet" + +if [[ ! -x "$dotnet_exe" ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "restore-toolset.sh: dotnet executable not found at '$dotnet_exe'." + ExitWithExitCode 1 +fi + +# Cheap, network-free probe: parse `dotnet workload list` text to skip already-installed workloads. +list_output=$("$dotnet_exe" workload list 2>&1) +missing=() +for workload in "${required_workloads[@]}"; do + if ! grep -qE "^[[:space:]]*${workload}[[:space:]]" <<<"$list_output"; then + missing+=("$workload") + fi +done + +if [[ ${#missing[@]} -eq 0 ]]; then + echo "All required workloads already installed: ${required_workloads[*]}" + return 0 +fi + +echo "Installing .NET SDK workloads: ${missing[*]}" +"$dotnet_exe" workload install "${missing[@]}" +workload_install_exit_code=$? +if [[ $workload_install_exit_code -ne 0 ]]; then + Write-PipelineTelemetryError -category 'InitializeToolset' "Failed to install workloads '${missing[*]}' (dotnet workload install exit code $workload_install_exit_code)." + ExitWithExitCode $workload_install_exit_code +fi diff --git a/samples/WasiPlayground/Program.cs b/samples/WasiPlayground/Program.cs index 049b9e41d4..2e02978391 100644 --- a/samples/WasiPlayground/Program.cs +++ b/samples/WasiPlayground/Program.cs @@ -14,7 +14,8 @@ testApplicationBuilder.AddTrxReportProvider(); testApplicationBuilder.AddAppInsightsTelemetryProvider(); testApplicationBuilder.AddCrashDumpProvider(); -testApplicationBuilder.AddHangDumpProvider(); +// AddHangDumpProvider is intentionally not registered: hang dumps rely on +// System.Diagnostics.Process which is unsupported on wasi (see #8557). testApplicationBuilder.AddAzureDevOpsProvider(); using ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); return await testApplication.RunAsync(); diff --git a/samples/WasiPlayground/README.md b/samples/WasiPlayground/README.md index fb6789b966..22038453db 100644 --- a/samples/WasiPlayground/README.md +++ b/samples/WasiPlayground/README.md @@ -1,21 +1,85 @@ # WasiPlayground -To run this project: +Demonstrates hosting [Microsoft.Testing.Platform](https://aka.ms/testingplatform/) on the WebAssembly System Interface (WASI). -1. Run `dotnet workload install wasi-experimental` -2. Run `dotnet build` -3. Install wasmtime. See docs at . -4. Open command-line in AppBundle directory (`artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle`) -5. Run `wasmtime run --wasi http --dir . -- dotnet.wasm WasiPlayground` +## Build & run + +Prerequisites: + +- The repo-local .NET SDK at `.dotnet\dotnet.exe`. Bootstrap it once by running + `.\build.cmd` (Windows) or `./build.sh` (Linux/macOS) from the repo root; + this also installs the `wasi-experimental-net10` workload required to build + the sample (see [`eng/restore-toolset.ps1`](../../eng/restore-toolset.ps1) + / [`eng/restore-toolset.sh`](../../eng/restore-toolset.sh)). +- The `wasm-tools-net10` workload, which is only needed for `dotnet publish` + (not by the repo's `dotnet build`), so install it manually: + + ```cmd + .\.dotnet\dotnet.exe workload install wasm-tools-net10 + ``` + +- [wasmtime](https://docs.wasmtime.dev/cli-install.html) on `PATH`. + +> All commands below use `.\.dotnet\dotnet.exe` so that the `.dotnet\packs` +> lookup in step 2 resolves. If you prefer a machine-installed SDK, swap +> `dotnet` in and replace `.dotnet\packs` with the corresponding `packs` +> folder reported by `dotnet --info` (look for *.NET SDKs installed*). + +Then: + +1. From the repo root, publish the sample: + + ```cmd + .\.dotnet\dotnet.exe publish samples\WasiPlayground\WasiPlayground.csproj -c Debug -f net10.0 + ``` + +2. The pre-built `dotnet.wasm` does not embed the ICU data file, so copy it + next to the bundle. First list the installed runtime-pack version (a + single folder name such as `10.0.8`), then substitute it into the copy + command: + + ```cmd + dir /b .dotnet\packs\Microsoft.NETCore.App.Runtime.Mono.wasi-wasm + + copy .dotnet\packs\Microsoft.NETCore.App.Runtime.Mono.wasi-wasm\\runtimes\wasi-wasm\native\icudt.dat ^ + artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle\ + ``` + +3. Switch to the bundle directory and invoke `wasmtime` (the `-S http` flag is required because the runtime imports `wasi:http`): + + ```cmd + cd artifacts\bin\WasiPlayground\Debug\net10.0\wasi-wasm\AppBundle + wasmtime run -S http --dir . -- dotnet.wasm WasiPlayground + ``` ## Status -As of today, this will produce this exception (it's not yet supported by MTP). +After publishing, Microsoft.Testing.Platform now boots on `wasi-wasm` (the +banner reads `[wasi-wasm - net10.0]`) thanks to [#7137](https://github.com/microsoft/testfx/pull/7137). +Test execution itself currently fails with: ```console +Microsoft.Testing.Platform v2.3.0-dev (UTC ...) [wasi-wasm - net10.0] +... Unhandled Exception: System.PlatformNotSupportedException: Arg_PlatformNotSupported - at System.Threading.Tasks.Task.InternalWaitCore(Int32 millisecondsTimeout, CancellationToken cancellationToken) - at System.Threading.Tasks.Task.InternalWait(Int32 millisecondsTimeout, CancellationToken cancellationToken) + at System.Threading.Tasks.Task.InternalWaitCore(...) + at System.Threading.Tasks.Task.InternalWait(...) at Program.
(String[] args) ``` + +The exception comes from the C# compiler's synthetic `Main` wrapper for +`async Task Main`, which calls `Task.GetAwaiter().GetResult()` → +`Task.Wait()`. On single-threaded `wasi-wasm` (no thread pool) this throws +`PlatformNotSupportedException`. Tracked in [#5366](https://github.com/microsoft/testfx/issues/5366) +so this sample can act as the canonical repro. + +## Build configuration notes + +`WasiPlayground.csproj` ships with a few non-obvious switches: + +| Property | Reason | +| --- | --- | +| `UsingWasiRuntimeWorkload=true` | Workaround for an SDK manifest bug in `11.0.100-preview.5` where `$(UsingWasiRuntimeWorkload)` never resolves to `true` for net10.0 projects, so the WASI Sdk targets are never imported and no `dotnet.wasm` is produced. | +| `WasmSingleFileBundle=false` | Single-file bundling requires the [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) (clang) toolchain to relink the native runtime. Keeping the managed assemblies on disk avoids that requirement. | +| `InvariantGlobalization=true` | Prevents the trimmer from crashing during publish, but the pre-built `dotnet.wasm` still loads ICU at runtime, so `icudt.dat` must still be staged next to the bundle (see step 2). | diff --git a/samples/WasiPlayground/WasiPlayground.csproj b/samples/WasiPlayground/WasiPlayground.csproj index ed0972d7ce..035401e937 100644 --- a/samples/WasiPlayground/WasiPlayground.csproj +++ b/samples/WasiPlayground/WasiPlayground.csproj @@ -5,10 +5,29 @@ net10.0 wasi-wasm true + + false + true + + + true enable - Exe - true + + true