Created
June 3, 2026 15:00
-
-
Save rmarinho/d291e84931720ffffda3b67bd29d5487 to your computer and use it in GitHub Desktop.
maui-coreclr.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: "maui-coreclr" | |
| description: "End-to-end setup for a .NET MAUI app on CoreCLR (not Mono), iOS Simulator + Android Emulator, with VS Code F5 debugging. Use when the user mentions CoreCLR on iOS, MAUI iOS simulator, vsdbg iOS, breakpoints not hitting in iOS Debug, or setting up MAUI with CoreCLR runtime." | |
| --- | |
| # MAUI CoreCLR Debugging Workflow | |
| Use this skill for a .NET MAUI app on CoreCLR with VS Code F5 debugging, especially iOS Simulator CoreCLR and Android emulator/USB debugging. | |
| This guide targets macOS on Apple Silicon (darwin-arm64). Windows and Linux setups require different VSIX platform suffixes, path conventions, and simulator tooling and are not covered here. | |
| Known-good versions from the June 2026 setup: | |
| | Component | Version / source | | |
| | --- | --- | | |
| | .NET SDK | `11.0.100-preview.6.26277.116` | | |
| | Workload set | `11.0.100-preview.4.26261.2` | | |
| | C# extension | `ms-dotnettools.csharp@2.142.16` | | |
| | MAUI extension latest tested main | DevDiv pipeline 18189, `1.16.45-g68ae8b76d9` | | |
| | MAUI extension PR 744644 tested | DevDiv build 14262550, `1.16.47-g61aa27dd03` | | |
| ## 1. VS Code extensions | |
| Verify the VS Code CLI is available before running any `code` commands: | |
| ```bash | |
| command -v code || echo "Install the shell command via VS Code > Shell Command: Install 'code' command in PATH" | |
| ``` | |
| Install the C# extension: | |
| ```bash | |
| code --install-extension ms-dotnettools.csharp@2.142.16 --pre-release --force | |
| ``` | |
| ### Install latest DevDiv vscode-maui extension from main | |
| The public marketplace extension can be too old for CoreCLR-on-iOS debugger support. Download the internal VSIX from DevDiv pipeline 18189 (`vscode-maui release`): | |
| ```bash | |
| VSIX_DIR="${TMPDIR:-/tmp}/vscode-maui-vsix-main" | |
| rm -rf "$VSIX_DIR" | |
| mkdir -p "$VSIX_DIR" | |
| RUN=$(az pipelines runs list --org https://devdiv.visualstudio.com --project DevDiv \ | |
| --pipeline-ids 18189 --top 30 --branch main \ | |
| --query "[?result=='succeeded'] | sort_by(@, &finishTime) | reverse(@) | [0].id" -o tsv) | |
| az pipelines runs artifact download --org https://devdiv.visualstudio.com --project DevDiv \ | |
| --run-id "$RUN" --artifact-name VSIX --path "$VSIX_DIR" | |
| VSIX=$(find "$VSIX_DIR" -name 'dotnet-maui-darwin-arm64-*.vsix' -print -quit) | |
| code --install-extension "$VSIX" --force | |
| ``` | |
| The VSIX artifact contains all platforms and can be about 1.8 GB. Check disk space first and clean old session VSIX caches if needed. After installing, keep only the platform VSIX you need: | |
| ```bash | |
| find "$VSIX_DIR" -type f -name '*.vsix' ! -name 'dotnet-maui-darwin-arm64-*.vsix' -delete | |
| df -h "$HOME" | |
| ``` | |
| ### Install vscode-maui extension from an Azure DevOps PR | |
| Use this when testing a specific `vscode-maui` PR, for example: | |
| `https://dev.azure.com/devdiv/DevDiv/_git/vscode-maui/pullrequest/744644` | |
| 1. Get PR metadata and source/merge commit: | |
| ```bash | |
| PR_ID=744644 | |
| REPO_ID=$(az repos show --org https://devdiv.visualstudio.com --project DevDiv \ | |
| --repository vscode-maui --query id -o tsv) | |
| PR_JSON=$(az repos pr show --org https://devdiv.visualstudio.com --id "$PR_ID" -o json) | |
| SOURCE_BRANCH=$(printf '%s' "$PR_JSON" | jq -r '.sourceRefName') | |
| SOURCE_SHA=$(printf '%s' "$PR_JSON" | jq -r '.lastMergeSourceCommit.commitId') | |
| MERGE_SHA=$(printf '%s' "$PR_JSON" | jq -r '.lastMergeCommit.commitId') | |
| printf 'PR %s source=%s sourceSha=%s mergeSha=%s\n' \ | |
| "$PR_ID" "$SOURCE_BRANCH" "$SOURCE_SHA" "$MERGE_SHA" | |
| ``` | |
| If `az repos pr show` cannot resolve the project, open the PR in a browser at `https://dev.azure.com/devdiv/DevDiv/_git/vscode-maui/pullrequest/<PR_ID>` and use the Azure DevOps PR MCP tool (`mcp_azure_devops__pipelines_*`, if available) to retrieve `sourceRefName`, `lastMergeSourceCommit.commitId`, and `lastMergeCommit.commitId`. | |
| 2. Find the PR build that produced the VSIX artifact. Prefer a succeeded unofficial build for `refs/pull/<PR_ID>/merge`. | |
| ```bash | |
| BUILDS_JSON=$(az rest --method get --uri \ | |
| "https://devdiv.visualstudio.com/DevDiv/_apis/build/builds?repositoryId=$REPO_ID&repositoryType=TfsGit&branchName=refs/pull/$PR_ID/merge&queryOrder=queueTimeDescending&api-version=7.1") | |
| BUILD_ID=$(printf '%s' "$BUILDS_JSON" | jq -r ' | |
| .value[] | |
| | select((.result == "succeeded" or .result == "partiallySucceeded") | |
| and (.definition.name | test("vscode-maui.*Unofficial|Unofficial Build|vscode-maui"; "i"))) | |
| | .id' | head -1) | |
| printf 'Using PR build %s\n' "$BUILD_ID" | |
| ``` | |
| If `BUILD_ID` is empty, no succeeded unofficial build exists for the PR merge ref. Check the PR pipeline status in Azure DevOps under the Build tab for `refs/pull/<PR_ID>/merge`. You may need to trigger a new build or manually set `BUILD_ID` to a `partiallySucceeded` build ID. | |
| For PR 744644, the working build was: | |
| ```text | |
| Build ID: 14262550 | |
| Build number: 1.16.47+61aa27dd03 | |
| Commit: 61aa27dd03e5702f3291e0664063a02ea3ccb9a2 | |
| VSIX: dotnet-maui-darwin-arm64-1.16.47-g61aa27dd03.vsix | |
| ``` | |
| 3. Download and install the PR VSIX: | |
| ```bash | |
| VSIX_DIR="${TMPDIR:-/tmp}/vscode-maui-pr-$PR_ID-vsix" | |
| rm -rf "$VSIX_DIR" | |
| mkdir -p "$VSIX_DIR" | |
| az pipelines runs artifact download --org https://devdiv.visualstudio.com --project DevDiv \ | |
| --run-id "$BUILD_ID" --artifact-name VSIX --path "$VSIX_DIR" | |
| VSIX=$(find "$VSIX_DIR" -name 'dotnet-maui-darwin-arm64-*.vsix' -print -quit) | |
| code --install-extension "$VSIX" --force | |
| code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csharp|dotnet-maui|csdevkit)' | |
| ``` | |
| ### Prevent extension auto-update while testing a VSIX | |
| When launching VS Code for a local/internal VSIX test, disable extension auto-update for that VS Code process: | |
| ```bash | |
| code --disable-extension-auto-update --reuse-window "$APP_DIR" | |
| ``` | |
| If the installed `code` CLI does not support that flag, launch normally and immediately verify the extension version: | |
| ```bash | |
| code --reuse-window "$APP_DIR" | |
| code --list-extensions --show-versions | grep -E '^ms-dotnettools\.dotnet-maui@' | |
| ``` | |
| Do not assume the VSIX stayed installed; VS Code may auto-update back to marketplace/latest unless auto-update is disabled for the test session. | |
| ## 2. .NET SDK and workload | |
| Install the pinned .NET 11 SDK: | |
| ```bash | |
| curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- \ | |
| --version 11.0.100-preview.6.26277.116 \ | |
| --install-dir "$HOME/.dotnet" | |
| ``` | |
| Use `~/.dotnet/dotnet` for SDK/workload installation: | |
| ```bash | |
| export DOTNET_ROOT="$HOME/.dotnet" | |
| export PATH="$HOME/.dotnet:$PATH" | |
| dotnet workload install ios maui android \ | |
| --version 11.0.100-preview.4.26261.2 \ | |
| --skip-sign-check | |
| ``` | |
| If workload install fails with `NU1301` feed errors, complete Section 3 first (create the project and add `NuGet.config`), then re-run the workload install command from the project directory. | |
| Important: the global `maui` DevTools CLI may target .NET 10. If `DOTNET_ROOT="$HOME/.dotnet"` hides the .NET 10 runtime installed under `/usr/local/share/dotnet`, `maui` can fail with "Microsoft.NETCore.App, version 10.0.0 was not found". For `maui` CLI commands, prefer: | |
| ```bash | |
| unset DOTNET_ROOT | |
| export PATH="$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH" | |
| maui version --json | |
| ``` | |
| ## 3. Project setup | |
| Create the app: | |
| ```bash | |
| dotnet new maui -n MyApp | |
| cd MyApp | |
| ``` | |
| Create `global.json` in the project root. Include `sdk.paths` so VS Code's `/usr/local/share/dotnet/dotnet` host can find the pinned SDK installed under `~/.dotnet`. | |
| Correct shape: | |
| ```json | |
| { | |
| "sdk": { | |
| "version": "11.0.100-preview.6.26277.116", | |
| "allowPrerelease": true, | |
| "rollForward": "disable", | |
| "paths": [ | |
| "$HOME/.dotnet" | |
| ] | |
| } | |
| } | |
| ``` | |
| The `paths` property belongs under `sdk`, not at the JSON root. Replace `$HOME/.dotnet` with the literal expanded path (for example, `/Users/yourname/.dotnet`). JSON does not support shell variables; the actual absolute path is required. | |
| Validate with the same dotnet host VS Code uses: | |
| ```bash | |
| cd "$APP_DIR" | |
| /usr/local/share/dotnet/dotnet --version | |
| # expected: 11.0.100-preview.6.26277.116 | |
| ``` | |
| Pin the workload set for the repo: | |
| ```bash | |
| dotnet workload update --version 11.0.100-preview.4.26261.2 | |
| ``` | |
| If stale global NuGet feeds break restore with disabled-feed `NU1301` errors, add a project-local `NuGet.config` that clears global sources: | |
| ```xml | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <configuration> | |
| <packageSources> | |
| <clear /> | |
| <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> | |
| <add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" /> | |
| <add key="dotnet9-workloads" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-workloads/nuget/v3/index.json" /> | |
| <add key="xamarin-macios" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-xamarin-xamarin-macios-8354d6d9/nuget/v3/index.json" /> | |
| <add key="dotnet-maui" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-maui-a33a875e/nuget/v3/index.json" /> | |
| </packageSources> | |
| </configuration> | |
| ``` | |
| ## 4. VS Code workspace settings | |
| Use a workspace `.vscode/settings.json`. | |
| Set `dotnet.preferCSharpExtension` to `false` (use C# Dev Kit). This was the tested working configuration for both Android and iOS CoreCLR debugging: | |
| ```json | |
| { | |
| "dotnet.preferCSharpExtension": false | |
| } | |
| ``` | |
| Only set this to `true` if C# Dev Kit is not installed or if the MAUI extension explicitly logs an error recommending the standalone C# extension. | |
| Always relaunch VS Code after changing this setting. Verify the installed extensions: | |
| ```bash | |
| code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csdevkit|csharp|dotnet-maui)' | |
| ``` | |
| ## 5. Required csproj overrides | |
| Edit `MyApp.csproj`. The required overrides target Debug iOS Simulator builds. | |
| ```xml | |
| <PublishReadyToRunStripDebugInfo Condition="'$(Configuration)' == 'Debug'">false</PublishReadyToRunStripDebugInfo> | |
| <PublishReadyToRunStripInliningInfo Condition="'$(Configuration)' == 'Debug'">false</PublishReadyToRunStripInliningInfo> | |
| ``` | |
| Android required no csproj overrides in the tested workflow. | |
| ## 6. Android setup and emulator with the `maui` CLI | |
| Use the `maui` DevTools CLI for Android environment and emulator management: | |
| ```bash | |
| unset DOTNET_ROOT | |
| export PATH="$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH" | |
| export ANDROID_HOME="$HOME/Library/Android/sdk" | |
| export ANDROID_SDK_ROOT="$ANDROID_HOME" | |
| export JAVA_HOME="$HOME/Library/Developer/Android/jdk/jdk-21" | |
| export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$JAVA_HOME/bin:$PATH" | |
| maui doctor --json | |
| maui android emulator list --sdk "$ANDROID_HOME" --json | |
| maui device list --platform android --json | |
| ``` | |
| Start an existing API 35 emulator. If no emulator named `MAUI_android_35_test` exists, ask the user whether to create one before proceeding. Do not create an emulator silently: | |
| ```bash | |
| maui android emulator start MAUI_android_35_test --sdk "$ANDROID_HOME" --jdk "$JAVA_HOME" | |
| adb -s emulator-5554 shell getprop sys.boot_completed | |
| maui device list --platform android --json | |
| ``` | |
| If `maui android emulator start` exits with `E2106`, the message may say stale locks, but the real cause can be low disk space. Check: | |
| ```bash | |
| df -h "$HOME" | |
| du -sh "$HOME/.android/avd"/* | sort -h | tail -20 | |
| "$ANDROID_HOME/emulator/emulator" -avd <AVD_NAME> -verbose -no-window -no-snapshot-load | |
| ``` | |
| Do not delete AVDs without confirmation. If deletion is approved, prefer the `maui` CLI: | |
| ```bash | |
| maui android emulator delete <AVD_NAME> --sdk "$ANDROID_HOME" | |
| ``` | |
| ## 7. Build and run | |
| ### iOS Simulator | |
| Boot a simulator first, then build: | |
| ```bash | |
| SIM_UDID=$(xcrun simctl list devices booted -j | python3 -c \ | |
| 'import json,sys;print(next(d["udid"] for v in json.load(sys.stdin)["devices"].values() for d in v if d["state"]=="Booted"))') | |
| dotnet build MyApp.csproj -c Debug -f net11.0-ios \ | |
| -p:RuntimeIdentifier=iossimulator-arm64 \ | |
| -p:_DeviceName=":v2:udid=$SIM_UDID" | |
| ``` | |
| In VS Code press F5 -> Debug iOS / Simulator. The MAUI extension copies `libvsdbgremotecoreclrtarget.dylib` into the `.app` and launches with the required `CORECLR_*` environment variables. Set a breakpoint in `MainPage.xaml.cs` inside `OnCounterClicked` and click the button in the simulator. | |
| ### Android Emulator | |
| When multiple Android devices are connected, always target the desired emulator explicitly. VS Code passes `AdbTarget` URL-encoded: | |
| ```bash | |
| /usr/local/share/dotnet/dotnet build -t:Run \ | |
| -p:Configuration=Debug \ | |
| -f net11.0-android \ | |
| -p:AdbTarget=-s%20emulator-5554 \ | |
| -p:AndroidAttachDebugger=true \ | |
| -p:AndroidSdkDirectory="$ANDROID_HOME" \ | |
| -p:JavaSdkDirectory="$JAVA_HOME" \ | |
| MyApp.csproj | |
| ``` | |
| Validate install/launch: | |
| ```bash | |
| adb -s emulator-5554 shell pm list packages com.companyname.myapp | |
| adb -s emulator-5554 shell pidof com.companyname.myapp | |
| ``` | |
| In VS Code press F5 -> Debug Android / Emulator. Set a breakpoint in `MainPage.xaml.cs` inside `OnCounterClicked` and click the button in the emulator. | |
| ## 8. Launch VS Code with the correct setup | |
| Launch from the project directory so `global.json` is discovered and `sdk.paths` is applied. | |
| Do not export `DOTNET_ROOT` when launching VS Code; the `global.json` `sdk.paths` mechanism handles SDK resolution. Only set `DOTNET_ROOT` when running `dotnet workload` or `dotnet restore` commands directly. | |
| ```bash | |
| APP_DIR="/path/to/MyApp" | |
| unset DOTNET_ROOT | |
| export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH" | |
| export ANDROID_HOME="$HOME/Library/Android/sdk" | |
| export ANDROID_SDK_ROOT="$ANDROID_HOME" | |
| export JAVA_HOME="$HOME/Library/Developer/Android/jdk/jdk-21" | |
| export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$JAVA_HOME/bin:$PATH" | |
| cd "$APP_DIR" | |
| /usr/local/share/dotnet/dotnet --version | |
| code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csdevkit|csharp|dotnet-maui)' | |
| code --disable-extension-auto-update --reuse-window . | |
| ``` | |
| If `code --disable-extension-auto-update` is unavailable, use `code --reuse-window .` and re-check the MAUI extension version after launch. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment