Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
22d34da
`dotnetup dotnet` experiment design doc
nagilson Jan 9, 2026
78ab6a5
grammar fix
nagilson Jan 12, 2026
ba923d1
grammar
nagilson Jan 12, 2026
63dbc24
capitalize windows
nagilson Jan 12, 2026
c3e5aae
Merge branch 'release/dnup' into nagilson-dotnetup-dotnet-design
nagilson Mar 12, 2026
cace20d
fix dotnetup dotnet doc
nagilson Mar 13, 2026
67bc643
Basic implementation
nagilson Mar 13, 2026
43ee7c0
Merge remote-tracking branch 'upstream/release/dnup' into nagilson-do…
nagilson Mar 13, 2026
b22e7d0
interactivity test
nagilson Mar 13, 2026
68cc5af
Walkthrough Improvements, WIP
nagilson Mar 14, 2026
97265bd
copilot instructions for multiple agents running tests
nagilson Mar 14, 2026
330e07a
Revert "copilot instructions for multiple agents running tests"
nagilson Mar 14, 2026
6f2fdf0
Copilot instructions for dotnet up running multiple agents
nagilson Mar 14, 2026
86532d7
codework space launches properly
nagilson Mar 14, 2026
5090726
Dim first run notice text.
nagilson Mar 14, 2026
d0fceb7
Add Meziantou Analyzer
nagilson Mar 14, 2026
6f0aba9
More improvements to walkthrough
nagilson Mar 14, 2026
1c2a6a2
split up too long functions into sub components
nagilson Mar 14, 2026
a7cb3c1
reduce duplicated code for admin prompt
nagilson Mar 14, 2026
91b7591
Improve option selector and dimming / progress has 'shimmer'
nagilson Mar 14, 2026
21a13e9
improve formatting.
nagilson Mar 14, 2026
050aa43
walkthrough untracked and improve shimmer speed
nagilson Mar 14, 2026
506425e
Add aliases for runtime component specs for easier typing
nagilson Mar 14, 2026
794bcb6
Concurrent SDK install, actually install admin installs into local
nagilson Mar 14, 2026
27ab458
enforce utf8 encoding for pretty text bar
nagilson Mar 14, 2026
01e65f7
Various fixes for color display and whitespaces
nagilson Mar 14, 2026
849b684
try to display summary line at the end
nagilson Mar 14, 2026
0208485
leverage concurrent install in the toolset
nagilson Mar 14, 2026
6991aff
concurrent cache load
nagilson Mar 14, 2026
80e6042
add throttle limit so we don't download an insane amount of stuff if …
nagilson Mar 14, 2026
40e0615
separate global json so paths path isnt the default install location …
nagilson Mar 14, 2026
fa37b29
extrct + download concurrently
nagilson Mar 14, 2026
277226c
improved throttling - dont spawn 20 threads lol
nagilson Mar 14, 2026
9908c3c
validate manifest contents
nagilson Mar 15, 2026
e8ecb61
padding and lower concurrency as the cdn throttles
nagilson Mar 15, 2026
7491bd3
lower concurrency as cdn throttles
nagilson Mar 15, 2026
2380d63
padding (again)
nagilson Mar 15, 2026
007f33d
Allow selection of installs to migrate
nagilson Mar 16, 2026
3008045
Disable ansi support for console rendering in banner tests
nagilson Mar 16, 2026
2da36bd
Don't print warnings for intermediate directories on unix - add verbo…
nagilson Mar 16, 2026
4a00ef8
Comment to improve clarity on the code
nagilson Mar 16, 2026
727edd5
Alignment test improvements
nagilson Mar 16, 2026
b47281a
Removed the dotnet bot art - for now it looks tacky
nagilson Mar 16, 2026
623b1ed
Dont select because select all / deselect all is not supported
nagilson Mar 16, 2026
6b96466
improve instruction prompt on conversions
nagilson Mar 16, 2026
e6a7617
Properly defer install copies, use past tense verbs on completion
nagilson Mar 17, 2026
225123b
don't copy installed output code
nagilson Mar 17, 2026
a3805c0
improved error message outputs
nagilson Mar 17, 2026
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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -535,3 +535,15 @@ indent_style = unset
insert_final_newline = false
tab_width = unset
trim_trailing_whitespace = false

# Meziantou.Analyzer suppressions for shared src/Cli code linked into dotnetup.
# These rules do not apply to code owned by other teams.
[{src/Cli/**/*.cs,src/Resolvers/**/*.cs}]
dotnet_diagnostic.MA0006.severity = none
dotnet_diagnostic.MA0008.severity = none
dotnet_diagnostic.MA0011.severity = none
dotnet_diagnostic.MA0016.severity = none
dotnet_diagnostic.MA0048.severity = none
dotnet_diagnostic.MA0084.severity = none
dotnet_diagnostic.MA0099.severity = none
dotnet_diagnostic.MA0144.severity = none
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
<PackageVersion Include="runtime.linux-musl-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
<PackageVersion Include="runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
<PackageVersion Include="runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" />
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.302" />
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="$(StyleCopAnalyzersPackageVersion)" />
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
Expand Down
49 changes: 49 additions & 0 deletions documentation/general/dotnetup/designs/dotnetup-dotnet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `dotnetup dotnet`

# Motivation

Manipulating the `PATH` environment variable can be tricky when Visual Studio and other installers are involved. These applications are automatically run with updates. They override the system level path on a regular basis which blocks `dotnetup` installs from being used.

To provide an experience during the prototype of `dotnetup` before any official product is changed to work well with `dotnetup`, we propose 'aliasing' or 'shadowing' dotnet commands via `dotnetup` as one option.

One downside to this is that IDE components whose processes call `dotnet` would still be broken by a change to the `PATH`. However, this prevents user interactions with terminals and scripts from being broken.

Another downside is that this requires scripts to be updated to call `dotnetup dotnet` instead of `dotnet`. For many individuals, this is a no-go.
This also adds the overhead of an additional process call.

Yet, until the `dotnet` muxer itself or .NET Installer can be modified, this provides a consistent way for the user to enforce their intended install of `dotnet` when running commands.

This also enables the `PATH` to have the admin install and for the two install types to co-exist - such that
`dotnetup` based installs can still be used by the local user. That makes `dotnetup` useful even when IT Admins prevent the user from overriding the system path, such that it can be used in tandem with admin installs. This also gives `dotnetup` full control over the process call, such that environment variables like `DOTNET_ROOT` can be set.

# Commands

`dotnetup dotnet <>`
`dotnetup do <>` (alias for the same thing)

Arguments in `<>` are forwarded transparently to `dotnet.exe` in the determined location which limits our ability to configure the command itself.
The `dotnet.exe` hive used will follow the logic `dotnetup` uses for installation location. (e.g. `global.json` vs `dotnetup hive` priority.)

# Technical Details

### Permissions

The subprocess inherits the current elevation level. We considered de-elevating when `dotnetup` runs under elevation, but decided against it: it's confusing for an admin prompt to de-elevate, and workload commands often require elevation. There is also more risk trying obscure methods to de-elevate. The subprocess will run with whatever privileges `dotnetup` currently has.

### Return values

We should return with the return value of `dotnet` and mimic that behavior.

### Interactive Mode

The command uses `shell=true` (UseShellExecute=false with shell dispatch) so that interactive commands (e.g. `dotnetup dotnet interactive`) work correctly with stdin/stdout/stderr streaming. This also supports shell redirection techniques such as `<<` and piping.

### Environment Settings

The spawned process should modify the `PATH` and set `DOTNET_ROOT` to the value of the determined `dotnet.exe` location, so that `runtime` based interactions (debug, test, run) can work as expected. This would override any custom `DOTNET_ROOT` to ensure the hive is used, for scenarios like when an admin install is side by side with `dotnetup` installs. It should also preserve the `cwd` of the shell and the other environment variables contained within. Since we use `shell=true`, environment variables are expanded by the shell and we don't need to call `ExpandEnvironmentVariables` manually.

When cross-architecture support is added, we should consider setting variables such as `DOTNET_ROOT_x64`.

### Future: Native AOT In-Process Invocation

If `dotnetup` is published as native AOT, we could potentially invoke the dotnet hive's `hostfxr` directly in-process instead of spawning a subprocess. This would eliminate the process overhead entirely. This is a future work item — for now, we use the subprocess approach.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Cross Architecture Considerations

One scenario our admin installers support is installing the x64 .NET SDK on an ARM64 device.
We should consider how to handle these hives in `dotnetup`.

When making this implementation, please also consider whether we should set `DOTNET_ROOT_x64` with `dotnetup dotnet <>` as defined by [`the dotnetup dotnet command design`](./dotnetup-dotnet.md).
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
& .\restore.cmd $(_officialBuildProperties)
displayName: 🍱 Bootstrap toolset (Windows)
- powershell: |
& .\.dotnet\dotnet build test\dotnetup.Tests\dotnetup.Tests.csproj -c Release -bl:$(Build.SourcesDirectory)/artifacts/binlogs/dotnetup-library-build.binlog $(_officialBuildProperties)
& .\.dotnet\dotnet build test\dotnetup.Tests\dotnetup.Tests.csproj -c Release -p:EnforceCodeStyleInBuild=false -bl:$(Build.SourcesDirectory)/artifacts/binlogs/dotnetup-library-build.binlog $(_officialBuildProperties)
displayName: 💻 Build Windows
- powershell: |
& .\.dotnet\dotnet pack .\src\Installer\Microsoft.Dotnet.Installation\Microsoft.Dotnet.Installation.csproj -bl:$(Build.SourcesDirectory)/artifacts/binlogs/dotnetup-library-package.binlog $(_officialBuildProperties)
Expand Down
30 changes: 17 additions & 13 deletions eng/restore-toolset.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ function InitializeCustomSDKToolset {
# Build dotnetup if not already present (needs SDK to be installed first)
EnsureDotnetupBuilt

InstallDotNetSharedFramework "6.0.0"
InstallDotNetSharedFramework "7.0.0"
InstallDotNetSharedFramework "8.0.0"
InstallDotNetSharedFramework "9.0.0"
InstallDotNetSharedFramework "10.0.0"
InstallDotNetSharedFrameworks "6.0.0", "7.0.0", "8.0.0", "9.0.0", "10.0.0"

CreateBuildEnvScripts
CreateVSShortcut
Expand Down Expand Up @@ -147,18 +143,26 @@ function CreateVSShortcut() {
$shortcut.Save()
}

function InstallDotNetSharedFramework([string]$version) {
function InstallDotNetSharedFrameworks([string[]]$versions) {
$dotnetRoot = $env:DOTNET_INSTALL_DIR
$fxDir = Join-Path $dotnetRoot "shared\Microsoft.NETCore.App\$version"
$dotnetupExe = Join-Path $PSScriptRoot "dotnetup\dotnetup.exe"

$versionsToInstall = @()
foreach ($version in $versions) {
$fxDir = Join-Path $dotnetRoot "shared\Microsoft.NETCore.App\$version"
if (!(Test-Path $fxDir)) {
$versionsToInstall += $version
}
}

if (!(Test-Path $fxDir)) {
$dotnetupExe = Join-Path $PSScriptRoot "dotnetup\dotnetup.exe"
if ($versionsToInstall.Count -eq 0) {
return
}

& $dotnetupExe runtime install "$version" --install-path $dotnetRoot --no-progress --set-default-install false --untracked
& $dotnetupExe runtime install @versionsToInstall --install-path $dotnetRoot --no-progress --set-default-install false --untracked

if ($lastExitCode -ne 0) {
throw "Failed to install shared Framework $version to '$dotnetRoot' using dotnetup (exit code '$lastExitCode')."
}
if ($lastExitCode -ne 0) {
throw "Failed to install shared frameworks ($($versionsToInstall -join ', ')) to '$dotnetRoot' using dotnetup (exit code '$lastExitCode')."
}
}

Expand Down
42 changes: 41 additions & 1 deletion src/Installer/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -673,4 +673,44 @@ dotnet_diagnostic.IDE0005.severity = silent
dotnet_diagnostic.CA2007.severity = silent

[*.{cs,vb}]
dotnet_diagnostic.ASPIREEVENTING001.severity = none
dotnet_diagnostic.ASPIREEVENTING001.severity = none

# Meziantou.Analyzer rules scoped to dotnetup projects only
[{dotnetup/**.cs,Microsoft.Dotnet.Installation/**.cs,dotnetup.Tests/**.cs}]

# MA0051: Method is too long (max 60 lines)
dotnet_diagnostic.MA0051.severity = warning
dotnet_code_quality.MA0051.maximum_lines_per_method = 60
dotnet_code_quality.MA0051.skip_local_functions = true

# Meziantou.Analyzer rules scoped to dotnetup projects only
[{dotnetup/**.cs,Microsoft.Dotnet.Installation/**.cs,dotnetup.Tests/**.cs}]

# MA0051: Method is too long (max 60 lines; enforced as build error via TreatWarningsAsErrors)
dotnet_diagnostic.MA0051.severity = warning
dotnet_code_quality.MA0051.maximum_lines_per_method = 60
dotnet_code_quality.MA0051.skip_local_functions = true

# Suppress rules we don't want to enforce in dotnetup
# MA0026/MA0025: TODO/FIXME comments and throw NotImplementedException are acceptable
dotnet_diagnostic.MA0026.severity = none
dotnet_diagnostic.MA0025.severity = none
# MA0048: Multiple types per file is an accepted C# convention here
dotnet_diagnostic.MA0048.severity = none
# MA0008: StructLayout not required for our value types
dotnet_diagnostic.MA0008.severity = none
# MA0009: Regex DoS - our patterns are anchored/bounded
dotnet_diagnostic.MA0009.severity = none
# MA0006/MA0099/MA0144/MA0011: Fired in linked NativeWrapper code we don't own
dotnet_diagnostic.MA0006.severity = none
dotnet_diagnostic.MA0099.severity = none
dotnet_diagnostic.MA0144.severity = none
dotnet_diagnostic.MA0011.severity = none
# MA0002: Distinct() without IEqualityComparer - string default equality is correct here
dotnet_diagnostic.MA0002.severity = none
# MA0016: Prefer collection abstraction - suppressed, concrete types used intentionally here
dotnet_diagnostic.MA0016.severity = none
# MA0084: Local variable hiding parameter suppressed - handled case-by-case
dotnet_diagnostic.MA0084.severity = none
# MA0158: Use System.Threading.Lock - suppressed, lock(object) is acceptable for our use case
dotnet_diagnostic.MA0158.severity = none
17 changes: 17 additions & 0 deletions src/Installer/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,20 @@ Code Style:
- To auto-format a project: `d:\sdk\.dotnet\dotnet format <project.csproj> --no-restore`
- When debugging or iterating on a bug fix, it is fine to temporarily ignore style issues and fix them afterward. Prefer working code over perfect formatting during active troubleshooting.
- Do not reformat unrelated code in the same commit as a bug fix — keep formatting changes in separate commits to preserve clean git blame.

Testing:
- When running tests after a change, first run only the tests relevant to the code you modified. Use `--filter` to target specific test classes or methods (e.g., `--filter "FullyQualifiedName~ParserTests"`) rather than running the entire test suite.
- Only run the full test suite if the targeted tests pass and you have reason to believe the change could affect other areas.

Concurrency:
- Multiple agents or terminals may be building or running tests concurrently in this workspace. To avoid file-lock conflicts on the dotnetup executable and build outputs, **always build and test into an isolated output directory** using `/p:ArtifactsDir=`.
- Choose a short, descriptive name based on what you are working on (e.g., the bug, feature, or test class name). Use that name to create a unique artifacts path under `d:\sdk\artifacts\tmp\`.
- Build command: `d:\sdk\.dotnet\dotnet build d:\sdk\src\Installer\dotnetup\dotnetup.csproj "/p:ArtifactsDir=d:\sdk\artifacts\tmp\<descriptive-name>\"`
- Test command: `d:\sdk\.dotnet\dotnet test d:\sdk\test\dotnetup.Tests\dotnetup.Tests.csproj "/p:ArtifactsDir=d:\sdk\artifacts\tmp\<descriptive-name>\"`
- Use the **same** `/p:ArtifactsDir=` value for both the build and the test so the test project picks up the build output.
- Example for a parser fix:
```
d:\sdk\.dotnet\dotnet build d:\sdk\src\Installer\dotnetup\dotnetup.csproj "/p:ArtifactsDir=d:\sdk\artifacts\tmp\parser-fix\"
d:\sdk\.dotnet\dotnet test d:\sdk\test\dotnetup.Tests\dotnetup.Tests.csproj "/p:ArtifactsDir=d:\sdk\artifacts\tmp\parser-fix\" --filter "FullyQualifiedName~ParserTests"
```
- Clean up temporary artifacts directories when you are done: `Remove-Item -Recurse -Force d:\sdk\artifacts\tmp\<descriptive-name>`
8 changes: 8 additions & 0 deletions src/Installer/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<!-- Pin RepoRoot to the real repo root so Arcade does not infer it from this directory's global.json. -->
<PropertyGroup>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', '..', '..'))</RepoRoot>
</PropertyGroup>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..'))" />
</Project>
32 changes: 32 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/IProgressTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,35 @@ public NullProgressTask(string description)
public double MaxValue { get; set; }
}
}

/// <summary>
/// Defers creation of the underlying <see cref="IProgressReporter"/> until the first task is added.
/// This avoids Spectre.Console rendering an empty progress bar (and leaving a blank line)
/// when an error occurs before any progress is reported.
/// </summary>
public sealed class LazyProgressReporter : IProgressReporter
{
private readonly IProgressTarget _target;
private readonly object _lock = new();
private IProgressReporter? _inner;

public LazyProgressReporter(IProgressTarget target)
{
_target = target;
}

public IProgressTask AddTask(string description, double maxValue)
{
lock (_lock)
{
_inner ??= _target.CreateProgressReporter();
}

return _inner.AddTask(description, maxValue);
}

public void Dispose()
{
_inner?.Dispose();
}
}
70 changes: 70 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/InstallComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public enum InstallComponent

public static class InstallComponentExtensions
{
/// <summary>
/// The longest display name across all components. Used to pad shorter names
/// so progress rows align when multiple component types are shown together.
/// </summary>
private static readonly int s_maxDisplayNameLength =
Enum.GetValues<InstallComponent>().Max(c => c.GetDisplayName().Length);

/// <summary>
/// Gets the human-readable display name for the component.
/// Uses shorter, punchier names rather than the full Microsoft.* framework names.
Expand All @@ -26,6 +33,57 @@ public static class InstallComponentExtensions
_ => component.ToString()
};

/// <summary>
/// Gets the display name right-padded to the length of the longest component name,
/// so that version numbers align when multiple component types appear together.
/// </summary>
public static string GetPaddedDisplayName(this InstallComponent component) =>
component.GetDisplayName().PadRight(s_maxDisplayNameLength);

/// <summary>
/// Formats a version string to a fixed display width so progress rows align.
/// Short versions like "9.0.312" are left-padded; long versions like
/// "11.0.100-preview.2.26159.112" are truncated to "..59.112".
/// Target width matches the common format "10.0.201" (8 chars).
/// </summary>
public static string FormatVersionForDisplay(string version)
{
const int targetWidth = 8;
if (version.Length <= targetWidth)
{
return version.PadLeft(targetWidth);
}

return ".." + version[^(targetWidth - 2)..];
}

/// <summary>
/// Width of the " (nnn.n MB / nnn.n MB)" suffix appended during download progress.
/// Used to pad non-download descriptions (e.g. "Installing") to the same total width
/// so all progress rows stay aligned.
/// </summary>
public const int DownloadSuffixWidth = 22;

/// <summary>
/// Fixed width for action verbs in progress descriptions ("Downloading", "Downloaded",
/// "Installing", "Installed"). Matches the longest verb so shorter ones are right-padded.
/// </summary>
private const int ActionWidth = 11; // "Downloading".Length

/// <summary>
/// Builds a progress-bar description such as "Downloading aspnet (runtime) 9.0.312".
/// Component names and versions are padded so all rows align vertically.
/// </summary>
public static string FormatProgressDescription(string action, InstallComponent component, string version) =>
$"{action.PadRight(ActionWidth)} {component.GetPaddedDisplayName()} {FormatVersionForDisplay(version)}";

/// <summary>
/// Formats bytes as MB, right-aligned to 8 characters (e.g. " 0.7 MB", "290.4 MB").
/// Always uses MB so the unit width is consistent across all progress rows.
/// </summary>
public static string FormatMB(long bytes) =>
FormattableString.Invariant($"{bytes / (1024.0 * 1024.0),5:F1} MB");

/// <summary>
/// Gets the official framework name for the component (e.g., "Microsoft.NETCore.App").
/// Used for JSON/machine-readable output.
Expand All @@ -38,4 +96,16 @@ public static class InstallComponentExtensions
InstallComponent.WindowsDesktop => "Microsoft.WindowsDesktop.App",
_ => component.ToString()
};

/// <summary>
/// Resolves a framework name (e.g. "Microsoft.NETCore.App") to the corresponding <see cref="InstallComponent"/>.
/// Returns null when the name is not recognized.
/// </summary>
public static InstallComponent? FromFrameworkName(string frameworkName) => frameworkName switch
{
"Microsoft.NETCore.App" => InstallComponent.Runtime,
"Microsoft.AspNetCore.App" => InstallComponent.ASPNETCore,
"Microsoft.WindowsDesktop.App" => InstallComponent.WindowsDesktop,
_ => null
};
}
Loading
Loading