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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.VisualStudio.ProjectSystem.Debug;
using Microsoft.VisualStudio.ProjectSystem.HotReload;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Microsoft.VisualStudio.ProjectSystem.Utilities;
using Microsoft.VisualStudio.ProjectSystem.VS.HotReload;
using Microsoft.VisualStudio.Shell.Interop;
using Newtonsoft.Json;
Expand Down Expand Up @@ -36,7 +35,7 @@ internal class ProjectLaunchTargetsProvider :
private readonly IUnconfiguredProjectVsServices _unconfiguredProjectVsServices;
private readonly IDebugTokenReplacer _tokenReplacer;
private readonly IFileSystem _fileSystem;
private readonly IEnvironmentHelper _environment;
private readonly IEnvironment _environment;
private readonly IActiveDebugFrameworkServices _activeDebugFramework;
private readonly IProjectThreadingService _threadingService;
private readonly IVsUIService<IVsDebugger10> _debugger;
Expand All @@ -50,7 +49,7 @@ public ProjectLaunchTargetsProvider(
ConfiguredProject project,
IDebugTokenReplacer tokenReplacer,
IFileSystem fileSystem,
IEnvironmentHelper environment,
IEnvironment environment,
IActiveDebugFrameworkServices activeDebugFramework,
IOutputTypeChecker outputTypeChecker,
IProjectThreadingService threadingService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

using System.Text.Json;
using Microsoft.VisualStudio.ProjectSystem.VS.Setup;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
Expand All @@ -18,6 +19,7 @@ internal sealed partial class ProjectRetargetHandler : IProjectRetargetHandler,
private readonly IProjectThreadingService _projectThreadingService;
private readonly IVsService<SVsTrackProjectRetargeting, IVsTrackProjectRetargeting2> _projectRetargetingService;
private readonly IVsService<SVsSolution, IVsSolution> _solutionService;
private readonly IDotNetEnvironment _dotnetEnvironment;

private Guid _currentSdkDescriptionId = Guid.Empty;
private Guid _sdkRetargetId = Guid.Empty;
Expand All @@ -28,13 +30,15 @@ public ProjectRetargetHandler(
IFileSystem fileSystem,
IProjectThreadingService projectThreadingService,
IVsService<SVsTrackProjectRetargeting, IVsTrackProjectRetargeting2> projectRetargetingService,
IVsService<SVsSolution, IVsSolution> solutionService)
IVsService<SVsSolution, IVsSolution> solutionService,
IDotNetEnvironment dotnetEnvironment)
{
_releasesProvider = releasesProvider;
_fileSystem = fileSystem;
_projectThreadingService = projectThreadingService;
_projectRetargetingService = projectRetargetingService;
_solutionService = solutionService;
_dotnetEnvironment = dotnetEnvironment;
}

public Task<IProjectTargetChange?> CheckForRetargetAsync(RetargetCheckOptions options)
Expand Down Expand Up @@ -89,6 +93,12 @@ public Task RetargetAsync(TextWriter outputLogger, RetargetOptions options, IPro
return null;
}

// Check if the retarget is already installed globally
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Check if the retarget is already installed globally
// Check if the version we retarget to is already installed

If IsSdkInstalledAsync is only checking global locations, maybe include that detail in the method name: IsSdkInstalledGloballyAsync

if (_dotnetEnvironment.IsSdkInstalled(retargetVersion))
{
return null;
}

if (_currentSdkDescriptionId == Guid.Empty)
{
// register the current and retarget versions, note there is a bug in the current implementation
Expand Down Expand Up @@ -142,7 +152,7 @@ public Task RetargetAsync(TextWriter outputLogger, RetargetOptions options, IPro
{
try
{
using Stream stream = File.OpenRead(globalJsonPath);
using Stream stream = _fileSystem.OpenTextStream(globalJsonPath);
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated to this PR, but if you're in an IDE could you rename OpenTextStream to just OpenReadStream please? There's nothing "text" about what's returned. Streams deal in bytes. There's no encoding at this layer.

using JsonDocument doc = await JsonDocument.ParseAsync(stream);
if (doc.RootElement.TryGetProperty("sdk", out JsonElement sdkProp) &&
sdkProp.TryGetProperty("version", out JsonElement versionProp))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

using System.Runtime.InteropServices;
using IFileSystem = Microsoft.VisualStudio.IO.IFileSystem;
using IRegistry = Microsoft.VisualStudio.ProjectSystem.VS.Utilities.IRegistry;

namespace Microsoft.VisualStudio.ProjectSystem.VS.Setup;

/// <summary>
/// Provides information about the .NET environment and installed SDKs by querying the Windows registry.
/// </summary>
[Export(typeof(IDotNetEnvironment))]
internal class DotNetEnvironment : IDotNetEnvironment
{
private readonly IFileSystem _fileSystem;
private readonly IRegistry _registry;
private readonly IEnvironment _environment;

[ImportingConstructor]
public DotNetEnvironment(IFileSystem fileSystem, IRegistry registry, IEnvironment environment)
{
_fileSystem = fileSystem;
_registry = registry;
_environment = environment;
}

/// <inheritdoc/>
public bool IsSdkInstalled(string sdkVersion)
{
try
{
string archSubKey = GetArchitectureSubKey(_environment.ProcessArchitecture);
string registryKey = $@"SOFTWARE\dotnet\Setup\InstalledVersions\{archSubKey}\sdk";

// Get all value names from the sdk subkey
string[] installedVersions = _registry.GetValueNames(
Win32.RegistryHive.LocalMachine,
Win32.RegistryView.Registry32,
registryKey);

// Check if the requested SDK version is in the list
foreach (string installedVersion in installedVersions)
{
if (string.Equals(installedVersion, sdkVersion, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}
catch
{
// If we fail to check, assume the SDK is not installed
return false;
}
}

/// <inheritdoc/>
public string? GetDotNetHostPath()
{
// First check the registry
string archSubKey = GetArchitectureSubKey(_environment.ProcessArchitecture);
string registryKey = $@"SOFTWARE\dotnet\Setup\InstalledVersions\{archSubKey}";

string? installLocation = _registry.GetValue(
Win32.RegistryHive.LocalMachine,
Win32.RegistryView.Registry32,
registryKey,
"InstallLocation");

if (!string.IsNullOrEmpty(installLocation))
{
string dotnetExePath = Path.Combine(installLocation, "dotnet.exe");
if (_fileSystem.FileExists(dotnetExePath))
{
return dotnetExePath;
}
}

// Fallback to Program Files
string? programFiles = _environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
if (programFiles is not null)
{
string dotnetPath = Path.Combine(programFiles, "dotnet", "dotnet.exe");

if (_fileSystem.FileExists(dotnetPath))
{
return dotnetPath;
}
}

return null;
}

/// <inheritdoc/>
public string[]? GetInstalledRuntimeVersions(Architecture architecture)
{
// https://github.com/dotnet/designs/blob/96d2ddad13dcb795ff2c5c6a051753363bdfcf7d/accepted/2020/install-locations.md#globally-registered-install-location-new

string archSubKey = GetArchitectureSubKey(architecture);
string registryKey = $@"SOFTWARE\dotnet\Setup\InstalledVersions\{archSubKey}\sharedfx\Microsoft.NETCore.App";

string[] valueNames = _registry.GetValueNames(
Win32.RegistryHive.LocalMachine,
Win32.RegistryView.Registry32,
registryKey);

return valueNames.Length == 0 ? null : valueNames;
}

private static string GetArchitectureSubKey(Architecture architecture)
{
return architecture switch
{
Architecture.X86 => "x86",
Architecture.X64 => "x64",
Architecture.Arm => "arm",
Architecture.Arm64 => "arm64",
_ => architecture.ToString().ToLower()
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

namespace Microsoft.VisualStudio.ProjectSystem.VS.Setup;

/// <summary>
/// Provides information about the .NET environment and installed SDKs.
/// </summary>
[ProjectSystemContract(ProjectSystemContractScope.Global, ProjectSystemContractProvider.Private, Cardinality = ImportCardinality.ExactlyOne)]
internal interface IDotNetEnvironment
{
/// <summary>
/// Checks if a specific .NET SDK version is installed on the system.
/// </summary>
/// <param name="sdkVersion">The SDK version to check for (e.g., "8.0.415").</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains
/// <see langword="true"/> if the SDK version is installed; otherwise, <see langword="false"/>.
/// </returns>
bool IsSdkInstalled(string sdkVersion);

/// <summary>
/// Gets the path to the dotnet.exe executable.
/// </summary>
/// <returns>
/// The full path to dotnet.exe if found; otherwise, <see langword="null"/>.
/// </returns>
string? GetDotNetHostPath();

/// <summary>
/// Reads the list of installed .NET Core runtimes for the specified architecture from the registry.
/// </summary>
/// <remarks>
/// Returns runtimes installed both as standalone packages, and through VS Setup.
/// Values have the form <c>3.1.32</c>, <c>7.0.11</c>, <c>8.0.0-preview.7.23375.6</c>, <c>8.0.0-rc.1.23419.4</c>.
/// If results could not be determined, <see langword="null"/> is returned.
/// </remarks>
/// <param name="architecture">The runtime architecture to report results for.</param>
/// <returns>An array of runtime versions, or <see langword="null"/> if results could not be determined or no runtimes were found.</returns>
string[]? GetInstalledRuntimeVersions(System.Runtime.InteropServices.Architecture architecture);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public SetupComponentRegistrationService(
IVsService<SVsSetupCompositionService, IVsSetupCompositionService> vsSetupCompositionService,
ISolutionService solutionService,
IProjectFaultHandlerService projectFaultHandlerService,
IDotNetEnvironment dotnetEnvironment,
JoinableTaskContext joinableTaskContext)
: base(new(joinableTaskContext))
{
Expand All @@ -51,9 +52,9 @@ public SetupComponentRegistrationService(
_solutionService = solutionService;
_projectFaultHandlerService = projectFaultHandlerService;

_installedRuntimeComponentIds = new Lazy<HashSet<string>?>(FindInstalledRuntimeComponentIds);
_installedRuntimeComponentIds = new Lazy<HashSet<string>?>(() => FindInstalledRuntimeComponentIds(dotnetEnvironment));

static HashSet<string>? FindInstalledRuntimeComponentIds()
static HashSet<string>? FindInstalledRuntimeComponentIds(IDotNetEnvironment dotnetEnvironment)
{
// Workaround for https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1460328
// VS Setup doesn't know about runtimes installed outside of VS. Deep detection is not suggested for performance reasons.
Expand All @@ -66,7 +67,7 @@ public SetupComponentRegistrationService(
// TODO consider the architecture of the project itself
Architecture architecture = RuntimeInformation.ProcessArchitecture;

string[]? runtimeVersions = NetCoreRuntimeVersionsRegistryReader.ReadRuntimeVersionsInstalledInLocalMachine(architecture);
string[]? runtimeVersions = dotnetEnvironment.GetInstalledRuntimeVersions(architecture);

if (runtimeVersions is null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

using Microsoft.Win32;

namespace Microsoft.VisualStudio.ProjectSystem.VS.Utilities;

/// <summary>
/// Provides access to the Windows registry in a testable manner.
/// </summary>
[ProjectSystem.ProjectSystemContract(ProjectSystem.ProjectSystemContractScope.Global, ProjectSystem.ProjectSystemContractProvider.Private, Cardinality = ImportCardinality.ExactlyOne)]
internal interface IRegistry
{
/// <summary>
/// Opens a registry key with the specified path under the given base key.
/// </summary>
/// <param name="hive">The registry hive to open (e.g., LocalMachine, CurrentUser).</param>
/// <param name="view">The registry view to use (e.g., Registry32, Registry64).</param>
/// <param name="subKeyPath">The path to the subkey to open.</param>
/// <param name="valueName">The name of the value to retrieve.</param>
/// <returns>
/// The registry key value as a string if found; otherwise, <see langword="null"/>.
/// </returns>
string? GetValue(RegistryHive hive, RegistryView view, string subKeyPath, string valueName);

/// <summary>
/// Gets the names of all values under the specified registry key.
/// </summary>
/// <param name="hive">The registry hive to open (e.g., LocalMachine, CurrentUser).</param>
/// <param name="view">The registry view to use (e.g., Registry32, Registry64).</param>
/// <param name="subKeyPath">The path to the subkey to open.</param>
/// <returns>
/// An array of value names if the key exists; otherwise, an empty array.
/// </returns>
string[] GetValueNames(RegistryHive hive, RegistryView view, string subKeyPath);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

using Microsoft.Win32;

namespace Microsoft.VisualStudio.ProjectSystem.VS.Utilities;

/// <summary>
/// Provides access to the Windows registry.
/// </summary>
[Export(typeof(IRegistry))]
internal class RegistryService : IRegistry
{
/// <inheritdoc/>
public string? GetValue(RegistryHive hive, RegistryView view, string subKeyPath, string valueName)
{
using RegistryKey? subKey = OpenSubKey(hive, view, subKeyPath);
return subKey?.GetValue(valueName) as string;
}

/// <inheritdoc/>
public string[] GetValueNames(RegistryHive hive, RegistryView view, string subKeyPath)
{
using RegistryKey? subKey = OpenSubKey(hive, view, subKeyPath);
return subKey?.GetValueNames() ?? [];
}

private static RegistryKey? OpenSubKey(RegistryHive hive, RegistryView view, string subKeyPath)
{
try
{
using RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view);
return baseKey.OpenSubKey(subKeyPath);
}
catch (Exception ex) when (ex.IsCatchable())
{
// Return null on catchable registry access errors
return null;
}
}
}
Loading