Skip to content
Merged
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
1 change: 1 addition & 0 deletions dnup.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"path": "sdk.slnx",
"projects": [
"src\\Installer\\dnup\\dnup.csproj",
"src\\Installer\\Microsoft.Dotnet.Installation\\Microsoft.Dotnet.Installation.csproj",
"test\\dnup.Tests\\dnup.Tests.csproj",
]
}
Expand Down
3 changes: 2 additions & 1 deletion sdk.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
</Folder>
<Folder Name="/src/Installer/">
<Project Path="src/Installer/dnup/dnup.csproj" />
<Project Path="src/Installer/Microsoft.Dotnet.Installation/Microsoft.Dotnet.Installation.csproj" />
</Folder>
<Folder Name="/src/Layout/">
<Project Path="src/Layout/finalizer/finalizer.csproj" />
Expand Down Expand Up @@ -289,12 +290,12 @@
<Project Path="test/ArgumentForwarding.Tests/ArgumentForwarding.Tests.csproj" />
<Project Path="test/ArgumentsReflector/ArgumentsReflector.csproj" />
<Project Path="test/containerize.UnitTests/containerize.UnitTests.csproj" />
<Project Path="test/dnup.Tests/dnup.Tests.csproj" />
<Project Path="test/dotnet-format.UnitTests/dotnet-format.UnitTests.csproj" />
<Project Path="test/dotnet-MsiInstallation.Tests/dotnet-MsiInstallation.Tests.csproj" />
<Project Path="test/dotnet-new.IntegrationTests/dotnet-new.IntegrationTests.csproj" />
<Project Path="test/dotnet-watch.Tests/dotnet-watch.Tests.csproj" />
<Project Path="test/dotnet.Tests/dotnet.Tests.csproj" />
<Project Path="test/dnup.Tests/dnup.Tests.csproj" />
<Project Path="test/EndToEnd.Tests/EndToEnd.Tests.csproj" />
<Project Path="test/HelixTasks/HelixTasks.csproj" />
<Project Path="test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj" />
Expand Down
11 changes: 11 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/DotnetInstallRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.Dotnet.Installation;
public record DotnetInstallRoot(
string Path,
InstallArchitecture Architecture);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Deployment.DotNet.Releases;

namespace Microsoft.Dotnet.Installation;

public interface IDotnetInstallDiscoverer
{
DotnetInstallRoot GetDotnetInstallRootFromPath();

IEnumerable<ReleaseVersion> GetInstalledVersions(DotnetInstallRoot dotnetRoot, InstallComponent component);
}
12 changes: 12 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/IDotnetInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Deployment.DotNet.Releases;

namespace Microsoft.Dotnet.Installation;

public interface IDotnetInstaller
{
void Install(DotnetInstallRoot dotnetRoot, InstallComponent component, ReleaseVersion version);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we provide a doc string mentioning this Installer interface does not support updating / tracking of these installers? I imagine we'll end up implementing an 'archive' installer for local zip installs just like how the other one works today. That allows us to expand with an admin installer in the future (msi, pkg, distro feed pk mgr)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support a custom interface for our output? (e.g. spectre .console abstraction)
We could do similarly for logging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious on @MichaelSimons opinion on this!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is something to ask Aspire. Would we also make use of it in the DNUP usage scenario?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, how we want to hook up logging (and in general console output) is a good question.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aspire asked for update support. Is the vision that they would manage the update via the install/uninstall apis vs providing a direct api?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daniel and I discussed and the vision was that there would be a separate installer interface that creates the 'dnup manifest' but just not in a dnup directory and instead in the dotnet root. The first release of the lib would just support basic install and a more complex interface would later be available that does more of the heavy lifting. I think we called it IDotnetInstallerManager.

void Uninstall(DotnetInstallRoot dotnetRoot, InstallComponent component, ReleaseVersion version);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Deployment.DotNet.Releases;

namespace Microsoft.Dotnet.Installation;

public interface IDotnetReleaseInfoProvider
{
IEnumerable<string> GetAvailableChannels();

ReleaseVersion GetLatestVersion(InstallComponent component, string channel);

// Get all versions in a channel - do we have a scenario for this?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using the term channel currently to mean, 'latest' or 'lts' or '9.0' or '9.0.102' in DNUP we may want to consider the terminology here. Channel has a specific meaning the release manifest context which is separate from how we are treating it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does channel mean in the context of the release manifest? Any ideas on how to disambiguate?

Copy link
Member

@nagilson nagilson Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A channel version typically describes the major.minor that defines the manifest: https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/2.1/releases.json ( see at the top of the file.) @joeloff mentioned channel is considered an overloaded term. (Note that I take ownership of calling it a channel too in the code 😁 )

The LTS/STS disambiguation is a term called 'release-type'.

The fully specified version is a 'version'.

So far in our code base, the 'channel' can be any of those - a fully specified version, unfully specified version (or even a channel-version per se), a release type, or something else ('latest', 'preview').

//IEnumerable<ReleaseVersion> GetAllVersions(InstallComponent component, string channel);

SupportType GetSupportType(InstallComponent component, ReleaseVersion version);
}

public enum SupportType
{
OutOfSupport,
LongTermSupport,
StandardTermSupport
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
using System.Collections.Generic;
using System.Text;

namespace Microsoft.DotNet.Tools.Bootstrapper
namespace Microsoft.Dotnet.Installation;

public enum InstallArchitecture
{
public enum InstallArchitecture
{
x86,
x64,
arm64
}
x86,
x64,
arm64
}
12 changes: 12 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/InstallComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Dotnet.Installation;

public enum InstallComponent
{
SDK,
Runtime,
ASPNETCore,
WindowsDesktop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Deployment.DotNet.Releases" />
</ItemGroup>

</Project>
35 changes: 8 additions & 27 deletions src/Installer/dnup/BootstrapperController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ public BootstrapperController(IEnvironmentProvider? environmentProvider = null)
_environmentProvider = environmentProvider ?? new EnvironmentProvider();
}

public DotnetInstallRoot GetConfiguredInstallType()
public DotnetInstallRootConfiguration? GetConfiguredInstallType()
{

string? foundDotnet = _environmentProvider.GetCommandPath("dotnet");
if (string.IsNullOrEmpty(foundDotnet))
{
return new(null, InstallType.None, DnupUtilities.GetDefaultInstallArchitecture());
return null;
}

string installDir = Path.GetDirectoryName(foundDotnet)!;
Expand All @@ -36,27 +36,12 @@ public DotnetInstallRoot GetConfiguredInstallType()
string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
bool isAdminInstall = installDir.StartsWith(Path.Combine(programFiles, "dotnet"), StringComparison.OrdinalIgnoreCase) ||
installDir.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase);

if (isAdminInstall)
{
// Admin install: DOTNET_ROOT should not be set, or if set, should match installDir
if (!string.IsNullOrEmpty(dotnetRoot) && !DnupUtilities.PathsEqual(dotnetRoot, installDir) &&
!dotnetRoot.StartsWith(Path.Combine(programFiles, "dotnet"), StringComparison.OrdinalIgnoreCase) &&
!dotnetRoot.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase))
{
return new(installDir, InstallType.Inconsistent, DnupUtilities.GetDefaultInstallArchitecture());
}
return new(installDir, InstallType.Admin, DnupUtilities.GetDefaultInstallArchitecture());
}
else
{
// User install: DOTNET_ROOT must be set and match installDir
if (string.IsNullOrEmpty(dotnetRoot) || !DnupUtilities.PathsEqual(dotnetRoot, installDir))
{
return new(installDir, InstallType.Inconsistent, DnupUtilities.GetDefaultInstallArchitecture());
}
return new(installDir, InstallType.User, DnupUtilities.GetDefaultInstallArchitecture());
}

var installRoot = new DotnetInstallRoot(installDir, DnupUtilities.GetDefaultInstallArchitecture());

bool isSetAsDotnetRoot = DnupUtilities.PathsEqual(dotnetRoot, installDir);

return new(installRoot, isAdminInstall ? InstallType.Admin : InstallType.User, IsOnPath: true, isSetAsDotnetRoot);
}

public string GetDefaultDotnetInstallPath()
Expand Down Expand Up @@ -153,10 +138,6 @@ public void ConfigureInstallType(InstallType installType, string? dotnetRoot = n
// Unset DOTNET_ROOT
Environment.SetEnvironmentVariable("DOTNET_ROOT", null, EnvironmentVariableTarget.User);
break;
case InstallType.None:
// Unset DOTNET_ROOT
Environment.SetEnvironmentVariable("DOTNET_ROOT", null, EnvironmentVariableTarget.User);
break;
default:
throw new ArgumentException($"Unknown install type: {installType}", nameof(installType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ public string GetDefaultDotnetInstallPath()
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dotnet");
}

public DotnetInstallRoot GetConfiguredInstallType()
public DotnetInstallRootConfiguration? GetConfiguredInstallType()
{
var testHookDefaultInstall = Environment.GetEnvironmentVariable("DOTNET_TESTHOOK_DEFAULT_INSTALL");
InstallType installtype = InstallType.None;
InstallType installtype;
if (!Enum.TryParse<InstallType>(testHookDefaultInstall, out installtype))
{
installtype = InstallType.None;
return null;
}
var installPath = Environment.GetEnvironmentVariable("DOTNET_TESTHOOK_CURRENT_INSTALL_PATH");
return new(installPath, installtype, DnupUtilities.GetDefaultInstallArchitecture());
var installPath = Environment.GetEnvironmentVariable("DOTNET_TESTHOOK_CURRENT_INSTALL_PATH") ?? GetDefaultDotnetInstallPath();
return new(new(installPath, DnupUtilities.GetDefaultInstallArchitecture()), installtype, true, true);
}

public string? GetLatestInstalledAdminVersion()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Tools.Bootstrapper.Commands.Sdk.Install;

namespace Microsoft.DotNet.Tools.Bootstrapper.Commands.Sdk.Install
{
internal class EnvironmentVariableMockReleaseInfoProvider : IReleaseInfoProvider
internal class EnvironmentVariableMockReleaseInfoProvider : IDotnetReleaseInfoProvider
{
public List<string> GetAvailableChannels()
IEnumerable<string> IDotnetReleaseInfoProvider.GetAvailableChannels()
{
var channels = Environment.GetEnvironmentVariable("DOTNET_TESTHOOK_AVAILABLE_CHANNELS");
if (string.IsNullOrEmpty(channels))
Expand All @@ -16,34 +17,45 @@ public List<string> GetAvailableChannels()
}
return channels.Split(',').ToList();
}
public string GetLatestVersion(string channel)
public ReleaseVersion GetLatestVersion(InstallComponent component, string channel)
{
if (component != InstallComponent.SDK)
{
throw new NotImplementedException("Only SDK component is supported in this mock provider");
}

string version;
if (channel == "preview")
{
return "11.0.100-preview.1.42424";
version = "11.0.100-preview.1.42424";
}
else if (channel == "latest" || channel == "10" || channel == "10.0.2xx")
{
return "10.0.0-preview.7";
version = "10.0.0-preview.7";
}
else if (channel == "10.0.1xx")
{
return "10.0.106";
version = "10.0.106";
}
else if (channel == "9" || channel == "9.0.3xx")
{
return "9.0.309";
version = "9.0.309";
}
else if (channel == "9.0.2xx")
{
return "9.0.212";
version = "9.0.212";
}
else if (channel == "9.0.1xx")
{
return "9.0.115";
version = "9.0.115";
}

return channel;
version = channel;

return new ReleaseVersion(version);
}

public SupportType GetSupportType(InstallComponent component, ReleaseVersion version) => throw new NotImplementedException();

}
}
23 changes: 10 additions & 13 deletions src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class SdkInstallCommand(ParseResult result) : CommandBase(result)
private readonly bool _interactive = result.GetValue(SdkInstallCommandParser.InteractiveOption);

private readonly IBootstrapperController _dotnetInstaller = new BootstrapperController();
private readonly IReleaseInfoProvider _releaseInfoProvider = new EnvironmentVariableMockReleaseInfoProvider();
private readonly IDotnetReleaseInfoProvider _releaseInfoProvider = new EnvironmentVariableMockReleaseInfoProvider();
private readonly ManifestChannelVersionResolver _channelVersionResolver = new ManifestChannelVersionResolver();

public override int Execute()
Expand Down Expand Up @@ -54,7 +54,7 @@ public override int Execute()
resolvedInstallPath = _installPath;
}

if (resolvedInstallPath == null && currentDotnetInstallRoot.Type == InstallType.User)
if (resolvedInstallPath == null && currentDotnetInstallRoot != null && currentDotnetInstallRoot.InstallType == InstallType.User)
{
// If a user installation is already set up, we don't need to prompt for the install path
resolvedInstallPath = currentDotnetInstallRoot.Path;
Expand Down Expand Up @@ -132,13 +132,13 @@ public override int Execute()
// If global.json specified an install path, we don't prompt for setting the default install path (since you probably don't want to do that for a repo-local path)
if (_interactive && installPathFromGlobalJson == null)
{
if (currentDotnetInstallRoot.Type == InstallType.None)
if (currentDotnetInstallRoot == null)
{
resolvedSetDefaultInstall = SpectreAnsiConsole.Confirm(
$"Do you want to set the install path ({resolvedInstallPath}) as the default dotnet install? This will update the PATH and DOTNET_ROOT environment variables.",
defaultValue: true);
}
else if (currentDotnetInstallRoot.Type == InstallType.User)
else if (currentDotnetInstallRoot.InstallType == InstallType.User)
{
if (DnupUtilities.PathsEqual(resolvedInstallPath, currentDotnetInstallRoot.Path))
{
Expand All @@ -151,7 +151,7 @@ public override int Execute()
defaultValue: false);
}
}
else if (currentDotnetInstallRoot.Type == InstallType.Admin)
else if (currentDotnetInstallRoot.InstallType == InstallType.Admin)
{
SpectreAnsiConsole.WriteLine($"You have an existing admin install of .NET in {currentDotnetInstallRoot.Path}. We can configure your system to use the new install of .NET " +
$"in {resolvedInstallPath} instead. This would mean that the admin install of .NET would no longer be accessible from the PATH or from Visual Studio.");
Expand All @@ -160,11 +160,8 @@ public override int Execute()
$"Do you want to set the user install path ({resolvedInstallPath}) as the default dotnet install? This will update the PATH and DOTNET_ROOT environment variables.",
defaultValue: true);
}
else if (currentDotnetInstallRoot.Type == InstallType.Inconsistent)
{
// TODO: Figure out what to do here
resolvedSetDefaultInstall = false;
}

// TODO: Add checks for whether PATH and DOTNET_ROOT need to be updated, or if the install is in an inconsistent state
}
else
{
Expand All @@ -176,14 +173,14 @@ public override int Execute()

// Create a request and resolve it using the channel version resolver
var installRequest = new DotnetInstallRequest(
new DotnetInstallRoot(resolvedInstallPath, InstallType.User, DnupUtilities.GetInstallArchitecture(RuntimeInformation.ProcessArchitecture)),
new DotnetInstallRoot(resolvedInstallPath, DnupUtilities.GetInstallArchitecture(RuntimeInformation.ProcessArchitecture)),
new UpdateChannel(resolvedChannel),
InstallComponent.SDK,
new InstallRequestOptions());

var resolvedVersion = _channelVersionResolver.Resolve(installRequest);

if (resolvedSetDefaultInstall == true && currentDotnetInstallRoot.Type == InstallType.Admin)
if (resolvedSetDefaultInstall == true && currentDotnetInstallRoot?.InstallType == InstallType.Admin)
{
if (_interactive)
{
Expand Down Expand Up @@ -227,7 +224,7 @@ public override int Execute()
{
// Create the request for the additional version
var additionalRequest = new DotnetInstallRequest(
new DotnetInstallRoot(resolvedInstallPath, InstallType.User, DnupUtilities.GetInstallArchitecture(RuntimeInformation.ProcessArchitecture)),
new DotnetInstallRoot(resolvedInstallPath, DnupUtilities.GetInstallArchitecture(RuntimeInformation.ProcessArchitecture)),
new UpdateChannel(additionalVersion),
InstallComponent.SDK,
new InstallRequestOptions());
Expand Down
2 changes: 1 addition & 1 deletion src/Installer/dnup/DnupManifestJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.DotNet.Tools.Bootstrapper
[JsonSerializable(typeof(List<DotnetInstall>))]
[JsonSerializable(typeof(DotnetVersion))]
[JsonSerializable(typeof(DotnetVersionType))]
[JsonSerializable(typeof(InstallMode))]
[JsonSerializable(typeof(InstallComponent))]
[JsonSerializable(typeof(InstallArchitecture))]
[JsonSerializable(typeof(InstallType))]
[JsonSerializable(typeof(ManagementCadence))]
Expand Down
Loading