Skip to content
Draft
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
135 changes: 135 additions & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/InstalledRuntimeEnumerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.NativeWrapper;
using NuGet.Frameworks;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Utils;

/// <summary>
/// Enumerates installed .NET runtimes on the system using hostfxr
/// </summary>
internal static class InstalledRuntimeEnumerator
{
private const string NetCoreAppFrameworkName = "Microsoft.NETCore.App";

/// <summary>
/// Checks if a tool can run with the installed runtimes by using hostfxr to resolve frameworks
/// </summary>
/// <param name="runtimeConfigPath">Path to the tool's runtimeconfig.json file</param>
/// <returns>True if the tool can run with installed runtimes</returns>
public static bool CanResolveFrameworks(string runtimeConfigPath)
{
if (!File.Exists(runtimeConfigPath))
{
return false;
}

try
{
var bundleProvider = new NETBundlesNativeWrapper();
return bundleProvider.CanResolveFrameworks(runtimeConfigPath);
}
catch
{
// If hostfxr call fails, return false
return false;
}
}

/// <summary>
/// Gets all installed .NET Core runtimes using hostfxr_get_dotnet_environment_info
/// </summary>
/// <returns>List of installed .NET Core runtime versions</returns>
public static IEnumerable<NuGetVersion> GetInstalledRuntimes()
{
var runtimes = new List<NuGetVersion>();

try
{
// Get the dotnet executable directory to pass to hostfxr
var muxer = new Muxer();
var dotnetPath = Path.GetDirectoryName(muxer.MuxerPath);

// Use hostfxr to enumerate runtimes
var bundleProvider = new NETBundlesNativeWrapper();
var envInfo = bundleProvider.GetDotnetEnvironmentInfo(dotnetPath ?? string.Empty);

// Filter to only Microsoft.NETCore.App runtimes and convert to NuGetVersion
foreach (var runtime in envInfo.RuntimeInfo)
{
if (runtime.Name == NetCoreAppFrameworkName)
{
if (NuGetVersion.TryParse(runtime.Version.ToString(), out var version))
{
runtimes.Add(version);
}
}
}
}
catch
{
// If we fail to enumerate runtimes, return empty list
// This ensures the tool installation continues with default behavior
}

return runtimes;
}

/// <summary>
/// Checks if a compatible runtime is available for the given framework requirement
/// </summary>
/// <param name="requiredFramework">The framework required by the tool</param>
/// <param name="allowRollForward">Whether roll-forward is allowed</param>
/// <returns>True if a compatible runtime is available</returns>
public static bool IsCompatibleRuntimeAvailable(NuGetFramework requiredFramework, bool allowRollForward = false)
{
if (requiredFramework.Framework != FrameworkConstants.FrameworkIdentifiers.NetCoreApp)
{
// Only check .NET Core runtimes
return true;
}

var installedRuntimes = GetInstalledRuntimes();
var requiredVersion = requiredFramework.Version;

foreach (var installedVersion in installedRuntimes)
{
// Exact match or higher minor version in same major
if (installedVersion.Major == requiredVersion.Major)
{
if (installedVersion.Minor >= requiredVersion.Minor)
{
return true;
}
}
// If roll-forward is allowed, check for higher major versions
else if (allowRollForward && installedVersion.Major > requiredVersion.Major)
{
return true;
}
}

return false;
}

/// <summary>
/// Checks if allowing roll-forward would help find a compatible runtime
/// </summary>
/// <param name="requiredFramework">The framework required by the tool</param>
/// <returns>True if roll-forward would find a compatible runtime</returns>
public static bool WouldRollForwardHelp(NuGetFramework requiredFramework)
{
if (requiredFramework.Framework != FrameworkConstants.FrameworkIdentifiers.NetCoreApp)
{
return false;
}

var installedRuntimes = GetInstalledRuntimes();
var requiredVersion = requiredFramework.Version;

// Check if there's any runtime with a higher major version
return installedRuntimes.Any(v => v.Major > requiredVersion.Major);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Resolvers\Microsoft.DotNet.NativeWrapper\Microsoft.DotNet.NativeWrapper.csproj" />
</ItemGroup>

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>
17 changes: 17 additions & 0 deletions src/Cli/dotnet/CliStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -841,4 +841,21 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is
<data name="Error_NU1302_HttpSourceUsed" xml:space="preserve">
<value>You are running the 'tool install' operation with an 'HTTP' source: {0}. NuGet requires HTTPS sources. To use an HTTP source, you must explicitly set 'allowInsecureConnections' to true in your NuGet.Config file. Refer to https://aka.ms/nuget-https-everywhere for more information.</value>
</data>
<data name="ToolRequiresRuntimeNotInstalled" xml:space="preserve">
<value>Tool '{0}' requires .NET runtime {1} which is not installed.</value>
</data>
<data name="ToolRequiresRuntimeNotInstalledWithRollForward" xml:space="preserve">
<value>Tool '{0}' requires .NET runtime {1} which is not installed. However, a compatible runtime could be used with --allow-roll-forward.</value>
</data>
<data name="ToolRequiresRuntimeSuggestions" xml:space="preserve">
<value>To install this tool, you can:
- Install {0} or a compatible version
- Choose a different version of the tool that is compatible with your installed runtimes
- Use --allow-roll-forward to allow the tool to run on a newer runtime version</value>
</data>
<data name="ToolRequiresRuntimeSuggestionsNoRollForward" xml:space="preserve">
<value>To install this tool, you can:
- Install {0} or a compatible version
- Choose a different version of the tool that is compatible with your installed runtimes</value>
</data>
</root>
112 changes: 93 additions & 19 deletions src/Cli/dotnet/ToolPackage/ToolPackageInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,29 +190,82 @@ private static ToolConfiguration DeserializeToolConfiguration(LockFileTargetLibr

if (availableFrameworks.Count > 0)
{
var currentFramework = new NuGetFramework(FrameworkConstants.FrameworkIdentifiers.NetCoreApp, new Version(Environment.Version.Major, Environment.Version.Minor));

// Find the minimum framework version required by the tool
var minRequiredFramework = availableFrameworks.MinBy(f => f.Version);

// If all available frameworks require a higher version than current runtime
if (minRequiredFramework != null && minRequiredFramework.Version > currentFramework.Version)
if (minRequiredFramework != null)
{
var requiredVersionString = $".NET {minRequiredFramework.Version.Major}.{minRequiredFramework.Version.Minor}";
var currentVersionString = $".NET {currentFramework.Version.Major}.{currentFramework.Version.Minor}";

var errorMessage = string.Format(
CliStrings.ToolRequiresHigherDotNetVersion,
packageId,
requiredVersionString,
currentVersionString);

var suggestion = string.Format(
CliStrings.ToolRequiresHigherDotNetVersionSuggestion,
minRequiredFramework.Version.Major,
currentFramework.Version.Major);

throw new GracefulException($"{errorMessage} {suggestion}", isUserError: false);
bool hasCompatibleRuntime = false;

// First, try to use hostfxr to resolve frameworks using the runtimeconfig.json
// This is the most accurate way to check compatibility
try
{
// Try to find the runtimeconfig.json file
// It should be in tools/{framework}/{rid}/*.runtimeconfig.json
var frameworkPath = Path.Combine(toolsPackagePath, $"net{minRequiredFramework.Version.Major}.{minRequiredFramework.Version.Minor}");
if (fileSystem.Directory.Exists(frameworkPath))
{
// Search for runtimeconfig.json files with max depth of 3 levels
var runtimeConfigFiles = new List<string>();
SearchForRuntimeConfigFiles(frameworkPath, runtimeConfigFiles, fileSystem, currentDepth: 0, maxDepth: 3);

if (runtimeConfigFiles.Any())
{
// Use the first runtimeconfig.json we find (sorted for determinism)
// All runtimeconfig.json files in a tool package should have the same framework requirements
var runtimeConfigPath = runtimeConfigFiles.OrderBy(f => f).First();
hasCompatibleRuntime = InstalledRuntimeEnumerator.CanResolveFrameworks(runtimeConfigPath);
}
}
}
catch
{
// If hostfxr resolution fails, fall back to version-based check
// This ensures tool installation continues even if hostfxr is unavailable
}

// If hostfxr resolution didn't work, fall back to version-based check
if (!hasCompatibleRuntime)
{
hasCompatibleRuntime = InstalledRuntimeEnumerator.IsCompatibleRuntimeAvailable(minRequiredFramework, allowRollForward: false);
}

if (!hasCompatibleRuntime)
{
var requiredVersionString = $".NET {minRequiredFramework.Version.Major}.{minRequiredFramework.Version.Minor}";

// Check if roll-forward would help
bool rollForwardWouldHelp = InstalledRuntimeEnumerator.WouldRollForwardHelp(minRequiredFramework);

string errorMessage;
string suggestions;

if (rollForwardWouldHelp)
{
errorMessage = string.Format(
CliStrings.ToolRequiresRuntimeNotInstalledWithRollForward,
packageId,
requiredVersionString);

suggestions = string.Format(
CliStrings.ToolRequiresRuntimeSuggestions,
requiredVersionString);
}
else
{
errorMessage = string.Format(
CliStrings.ToolRequiresRuntimeNotInstalled,
packageId,
requiredVersionString);

suggestions = string.Format(
CliStrings.ToolRequiresRuntimeSuggestionsNoRollForward,
requiredVersionString);
}

throw new GracefulException($"{errorMessage}\n\n{suggestions}", isUserError: false);
}
}
}
}
Expand Down Expand Up @@ -241,6 +294,27 @@ private static ToolConfiguration DeserializeToolConfiguration(LockFileTargetLibr
}
}

private static void SearchForRuntimeConfigFiles(string directory, List<string> results, IFileSystem fileSystem, int currentDepth, int maxDepth)
{
if (currentDepth >= maxDepth)
{
return;
}

foreach (var file in fileSystem.Directory.EnumerateFiles(directory))
{
if (file.EndsWith(".runtimeconfig.json", StringComparison.OrdinalIgnoreCase))
{
results.Add(file);
}
}

foreach (var subdir in fileSystem.Directory.EnumerateDirectories(directory))
{
SearchForRuntimeConfigFiles(subdir, results, fileSystem, currentDepth + 1, maxDepth);
}
}

private static LockFileTargetLibrary FindLibraryInLockFile(LockFile lockFile)
{
return lockFile
Expand Down
30 changes: 30 additions & 0 deletions src/Cli/dotnet/xlf/CliStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions src/Cli/dotnet/xlf/CliStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading