diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 994b94b39..0c2f14637 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,6 +22,9 @@ /plugins/dotnet/skills/nuget-trusted-publishing/ @lewing @kartheekp-ms /tests/dotnet/nuget-trusted-publishing/ @lewing @kartheekp-ms +/plugins/dotnet/skills/setup-local-sdk/ @jfversluis @Redth +/tests/dotnet/setup-local-sdk/ @jfversluis @Redth + /plugins/dotnet/agents/optimizing-dotnet-performance.agent.md @dotnet/appmodel /plugins/dotnet-ai/skills/mcp-csharp-create/ @leslierichardson95 @mikekistler diff --git a/plugins/dotnet/skills/setup-local-sdk/SKILL.md b/plugins/dotnet/skills/setup-local-sdk/SKILL.md new file mode 100644 index 000000000..843e5ea22 --- /dev/null +++ b/plugins/dotnet/skills/setup-local-sdk/SKILL.md @@ -0,0 +1,250 @@ +--- +name: setup-local-sdk +description: > + Install a .NET SDK locally for safe preview testing, specific-version pinning, or + reproducible team setups — without modifying the system-wide installation. + USE FOR: trying .NET previews safely, testing specific SDK versions, installing MAUI + or other workloads on a preview, updating or replacing an existing local SDK, + creating reproducible team/CI install scripts, configuring global.json paths. + DO NOT USE FOR: system-wide SDK installs, .NET hosts older than 10, runtime-only + installs, or projects not using SDK-style commands. +--- + +# setup-local-sdk + +## Purpose + +Guide the user through installing a .NET SDK into a project-local `.dotnet/` +directory and wiring it up via the `global.json` `paths` feature (.NET 10+). +The examples use .NET 11, but this works with any version — prerelease or stable. + +The result is a fully isolated SDK that: +- Does **not** modify the system-wide .NET installation. +- Is picked up automatically by `dotnet` commands from the project root. +- Can be deleted to revert (`rm -rf .dotnet/` or `Remove-Item -Recurse -Force .\.dotnet`). + +## When NOT to use + +- User wants a **system-wide** install — direct to the official installer. +- Host `dotnet` is **older than v10** — `paths` doesn't exist; explain and stop. +- User needs a **runtime-only** install — `paths` applies to SDK resolution only. + +## Inputs / Prerequisites + +| Input | Required | Default | Notes | +|---|---|---|---| +| Channel or version | No | `11.0` | e.g. `11.0`, `STS`, `LTS`, or an exact version like `11.0.100-preview.2.26159.112` | +| Quality | No | `preview` | One of: `daily`, `preview`, `GA` | + +### Prerequisites + +1. **A .NET 10+ SDK is installed globally** — run `dotnet --version`; major ≥ 10. +2. **curl** (macOS/Linux) or **PowerShell** (Windows) is available. + +## Workflow + +### Step 1 — Clarify what to install + +If the user didn't specify, ask what .NET SDK version they want (e.g., "latest +.NET 11 preview" or an exact version like `11.0.100-preview.2.26159.112`). +Map the answer to `--channel`/`--quality` or `--version` flags. + +### Step 2 — Verify .NET 10+ host + +```bash +dotnet --version +``` + +If major version < 10, stop: the `paths` feature requires .NET 10+. + +### Step 3 — Detect operating system + +Run `uname -s 2>/dev/null`. If it succeeds (including `MINGW*`, `MSYS*`, `CYGWIN*` — +these are bash-capable environments like Git Bash) → use bash/`dotnet-install.sh`. +If it fails (native Windows without Git Bash) → use PowerShell/`dotnet-install.ps1`. + +### Step 4 — Check for existing local SDK + +```bash +test -d .dotnet && echo "exists" || echo "not found" +``` + +If `.dotnet/` exists, ask: update with the new version, or skip and keep it? + +### Step 5 — Download and run the install script + +**macOS / Linux:** + +```bash +curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh +bash /tmp/dotnet-install.sh --channel --quality --install-dir .dotnet +``` + +**Windows (PowerShell):** + +```powershell +Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile "$env:TEMP\dotnet-install.ps1" +& "$env:TEMP\dotnet-install.ps1" -Channel -Quality -InstallDir .dotnet +``` + +For exact versions: use `--version ` (bash) or `-Version ` (PowerShell) +instead of channel/quality flags. The install script is from Microsoft's official +URL (https://dot.net/v1/dotnet-install.sh). + +### Step 6 — Identify the installed version + +```bash +./.dotnet/dotnet --version # macOS/Linux +.\.dotnet\dotnet.exe --version # Windows +``` + +Record the exact version string (e.g., `11.0.100-preview.2.26159.112`) for `global.json`. + +### Step 7 — Install workloads (if requested) + +If the user mentioned MAUI, mobile, workload, Blazor WASM, or cross-platform, +install using the **local** binary (no sudo needed): + +```bash +./.dotnet/dotnet workload install # macOS/Linux +.\.dotnet\dotnet.exe workload install # Windows +``` + +Verify: `./.dotnet/dotnet workload list` (or `.\.dotnet\dotnet.exe workload list`). + +> **Always use the local dotnet binary for workload commands.** Workload metadata +> is stored relative to the host process's dotnet root. The system `dotnet` puts +> metadata in the wrong location. (See [dotnet/sdk#49825](https://github.com/dotnet/sdk/issues/49825).) + +### Step 8 — Create or update global.json + +```json +{ + "sdk": { + "version": "", + "allowPrerelease": true, + "rollForward": "latestFeature", + "paths": [".dotnet", "$host$"], + "errorMessage": "Required .NET SDK not found. Run ./install-dotnet.sh (or .ps1) to install it locally." + } +} +``` + +- `paths`: `.dotnet` first (local priority), `$host$` = system-wide fallback. +- `rollForward: "latestFeature"`: rolls forward across feature bands, not just patches. +- `allowPrerelease`: set to `true` only when installing a prerelease SDK. Omit for stable versions. +- `errorMessage`: include only when team install scripts are created (Step 10). Otherwise omit. + +If `global.json` already exists, preserve existing properties (`msbuild-sdks`, +`tools`, etc.) and only add/update the `sdk` section. Warn about overwrites. + +**Minimal config** (when version pinning isn't needed): +`{"sdk":{"paths":[".dotnet","$host$"]}}` + +### Step 9 — Update .gitignore + +**macOS / Linux (or Git Bash):** + +```bash +grep -qxF '.dotnet/' .gitignore 2>/dev/null || echo '.dotnet/' >> .gitignore +``` + +**Windows (PowerShell):** + +```powershell +if (-not (Test-Path .gitignore) -or -not (Select-String -Path .gitignore -Pattern '^\\.dotnet/$' -Quiet)) { + Add-Content -Path .gitignore -Value '.dotnet/' +} +``` + +### Step 10 — Create team install scripts + +Create if user mentioned "team", "share", "CI", "scripts", etc. Otherwise offer. + +**install-dotnet.sh:** + +```bash +#!/usr/bin/env bash +set -euo pipefail +INSTALL_DIR=".dotnet" +CHANNEL="11.0" +QUALITY="preview" +WORKLOADS=("${@}") +curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh +bash /tmp/dotnet-install.sh --channel "$CHANNEL" --quality "$QUALITY" --install-dir "$INSTALL_DIR" +SDK_VERSION=$("$INSTALL_DIR/dotnet" --version) +[ -f global.json ] && cp global.json global.json.bak +cat > global.json </dev/null || echo '.dotnet/' >> .gitignore +[ ${#WORKLOADS[@]} -gt 0 ] && "$INSTALL_DIR/dotnet" workload install "${WORKLOADS[@]}" +echo "Done. SDK: $SDK_VERSION" +``` + +```bash +chmod +x install-dotnet.sh +``` + +**install-dotnet.ps1:** + +```powershell +param([string[]]$Workloads = @()) +$ErrorActionPreference = 'Stop' +$installDir = '.dotnet'; $channel = '11.0'; $quality = 'preview' +Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile "$env:TEMP\dotnet-install.ps1" +& "$env:TEMP\dotnet-install.ps1" -Channel $channel -Quality $quality -InstallDir $installDir +$sdkVersion = & "$installDir\dotnet.exe" --version +if (Test-Path 'global.json') { Copy-Item 'global.json' 'global.json.bak' } +@" +{ + "sdk": { + "version": "$sdkVersion", + "allowPrerelease": true, + "rollForward": "latestFeature", + "paths": [".dotnet", "`$host`$"], + "errorMessage": "Required .NET SDK not found. Run ./install-dotnet.sh (or .ps1) to install it locally." + } +} +"@ | Set-Content -Path 'global.json' -Encoding UTF8 +if (-not (Test-Path .gitignore) -or -not (Select-String -Path .gitignore -Pattern '^\\.dotnet/$' -Quiet)) { + Add-Content -Path .gitignore -Value '.dotnet/' +} +if ($Workloads.Count -gt 0) { & "$installDir\dotnet.exe" workload install @Workloads } +Write-Host "Done. SDK: $sdkVersion" +``` + +Commit these scripts to the repo so teammates can run them. + +### Step 11 — Verify SDK resolution + +```bash +dotnet --version +``` + +Output should match the locally installed version. If not, check: global.json +location, `paths` array contents, host dotnet version ≥ 10. + +### Step 12 — Summarize and explain cleanup + +Tell the user: SDK installed, global.json configured, .dotnet/ gitignored, system +install untouched. Cleanup: delete `.dotnet/`, remove `paths`/`errorMessage` from +global.json, optionally delete install scripts. + +## Common pitfalls + +| Pitfall | Cause | Fix | +|---|---|---| +| `paths` ignored | Host `dotnet` < v10 | Install .NET 10+ system-wide | +| Wrong SDK resolves | `global.json` in parent directory | Check for global.json up the tree | +| Teammates get "SDK not found" | `.dotnet/` gitignored, no install script run | Use `errorMessage` in global.json | +| Workloads missing | Used system `dotnet` instead of local | Use `./.dotnet/dotnet workload install` | +| `dotnet app.dll` wrong runtime | `paths` is SDK-only, not apphost | Use `dotnet run` or set `DOTNET_ROOT` | diff --git a/tests/dotnet/setup-local-sdk/eval.yaml b/tests/dotnet/setup-local-sdk/eval.yaml new file mode 100644 index 000000000..8b45266d7 --- /dev/null +++ b/tests/dotnet/setup-local-sdk/eval.yaml @@ -0,0 +1,70 @@ +scenarios: + - name: "Basic local SDK setup with .NET 11 preview" + prompt: "I want to try the latest .NET 11 preview in my project without affecting my global install. Set it up locally for me." + assertions: + - type: "output_contains" + value: "dotnet-install" + - type: "output_contains" + value: "global.json" + - type: "output_contains" + value: ".dotnet" + rubric: + - "The agent installed the SDK into a .dotnet/ directory using the dotnet-install script" + - "The agent created or updated global.json with a paths array" + - "The agent added .dotnet/ to .gitignore" + - "The agent explained how to clean up" + expect_tools: ["bash"] + timeout: 180 + + - name: "Install a specific SDK version locally" + prompt: "Install .NET SDK version 11.0.100-preview.2.26159.112 locally using global.json paths." + assertions: + - type: "output_contains" + value: "11.0.100-preview.2.26159.112" + - type: "output_contains" + value: "global.json" + rubric: + - "The agent used a version flag instead of channel/quality to install the exact version" + - "The agent configured global.json with the paths array and exact version" + - "The agent verified the installed version matches the request" + expect_tools: ["bash"] + timeout: 180 + + - name: "Set up local SDK with MAUI workload" + prompt: "I need to test my .NET MAUI app with the latest .NET 11 preview. Set up a local SDK with the MAUI workload installed." + assertions: + - type: "output_contains" + value: "workload" + - type: "output_contains" + value: ".dotnet" + rubric: + - "The agent installed the SDK locally and the MAUI workload" + - "The agent used ./.dotnet/dotnet (not system dotnet) for workload commands" + - "The agent configured global.json with paths" + expect_tools: ["bash"] + timeout: 180 + + - name: "Create team install scripts" + prompt: "Set up a local .NET 11 preview SDK and create install scripts so teammates can reproduce the setup on any OS." + assertions: + - type: "output_contains" + value: "install-dotnet" + - type: "output_contains" + value: "global.json" + rubric: + - "The agent created install-dotnet.sh and install-dotnet.ps1 scripts" + - "The agent configured global.json with an errorMessage directing to the scripts" + - "The agent explained that teammates run the script after cloning" + expect_tools: ["bash"] + timeout: 180 + + - name: "Detect incompatible .NET host version" + prompt: "Set up a local .NET 11 preview SDK for my project. My dotnet --version shows 9.0.306." + assertions: + - type: "output_contains" + value: "10" + rubric: + - "The agent detected that version 9.0.306 is below the .NET 10 requirement" + - "The agent explained that .NET 10+ is required for the paths feature" + - "The agent told the user to install .NET 10+ system-wide first" + timeout: 60