Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
252 changes: 252 additions & 0 deletions plugins/dotnet/skills/setup-local-sdk/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
---
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 -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh
bash /tmp/dotnet-install.sh --channel <CHANNEL> --quality <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 <CHANNEL> -Quality <QUALITY> -InstallDir .dotnet
```

For exact versions: use `--version <VERSION>` (bash) or `-Version <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 <workload> # macOS/Linux
.\.dotnet\dotnet.exe workload install <workload> # 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": "<INSTALLED_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, **merge** carefully: preserve existing properties (`msbuild-sdks`,
`tools`, etc.) and only add/update the `sdk` section. Read the existing file first, update/add
the `sdk` object, then write it back. This ensures cross-project config (e.g., MSBuild settings)
isn't lost. Always back up the original file (e.g., `global.json.bak`) before modifying.

**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 -fsSL 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 <<EOF
{
"sdk": {
"version": "$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."
}
}
EOF
grep -qxF '.dotnet/' .gitignore 2>/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` |
66 changes: 66 additions & 0 deletions tests/dotnet/setup-local-sdk/eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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"
timeout: 300

- 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"
timeout: 300

- 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"
timeout: 300

- 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"
timeout: 300

- 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: ".NET 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: 120