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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,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="Spectre.Console" Version="0.52.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="$(StyleCopAnalyzersPackageVersion)" />
<PackageVersion Include="System.CodeDom" Version="$(SystemCodeDomPackageVersion)" />
<PackageVersion Include="System.CommandLine" Version="$(SystemCommandLineVersion)" />
Expand Down
12 changes: 12 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,18 @@ The current OutputType is '{2}'.</value>
<value>Unable to run your project
Your project targets multiple frameworks. Specify which framework to run using '{0}'.</value>
</data>
<data name="RunCommandSelectTargetFrameworkPrompt" xml:space="preserve">
<value>Select the target framework to run:</value>
</data>
<data name="RunCommandMoreFrameworksText" xml:space="preserve">
<value>Move up and down to reveal more frameworks</value>
</data>
<data name="RunCommandAvailableTargetFrameworks" xml:space="preserve">
<value>Available target frameworks:</value>
</data>
<data name="RunCommandExampleText" xml:space="preserve">
<value>Example</value>
</data>
<data name="RunCommandProjectAbbreviationDeprecated" xml:space="preserve">
<value>Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project.</value>
<comment>{Locked="--project"}</comment>
Expand Down
48 changes: 37 additions & 11 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class RunCommand
/// <summary>
/// Parsed structure representing the MSBuild arguments that will be used to build the project.
/// </summary>
public MSBuildArgs MSBuildArgs { get; }
public MSBuildArgs MSBuildArgs { get; private set; }
public bool Interactive { get; }

/// <summary>
Expand Down Expand Up @@ -122,6 +122,12 @@ public int Execute()
return 1;
}

// Pre-run evaluation: Handle target framework selection for multi-targeted projects
if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkIfNeeded())
{
return 1;
}

Func<ProjectCollection, ProjectInstance>? projectFactory = null;
RunProperties? cachedRunProperties = null;
VirtualProjectBuildingCommand? virtualCommand = null;
Expand Down Expand Up @@ -180,6 +186,36 @@ public int Execute()
}
}

/// <summary>
/// Checks if target framework selection is needed for multi-targeted projects.
/// If needed and we're in interactive mode, prompts the user to select a framework.
/// If needed and we're in non-interactive mode, shows an error.
/// </summary>
/// <returns>True if we can continue, false if we should exit</returns>
private bool TrySelectTargetFrameworkIfNeeded()
{
Debug.Assert(ProjectFileFullPath is not null);

var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
if (TargetFrameworkSelector.TrySelectTargetFramework(
ProjectFileFullPath,
globalProperties,
Interactive,
out string? selectedFramework))
{
// If a framework was selected, add it to MSBuildArgs
if (selectedFramework is not null)
{
var additionalProperties = new ReadOnlyDictionary<string, string>(
new Dictionary<string, string> { { "TargetFramework", selectedFramework } });
MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
}
return true;
}

return false;
}

internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
{
if (launchSettings == null)
Expand Down Expand Up @@ -499,16 +535,6 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca

internal static void ThrowUnableToRunError(ProjectInstance project)
{
string targetFrameworks = project.GetPropertyValue("TargetFrameworks");
if (!string.IsNullOrEmpty(targetFrameworks))
{
string targetFramework = project.GetPropertyValue("TargetFramework");
if (string.IsNullOrEmpty(targetFramework))
{
throw new GracefulException(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework");
}
}

throw new GracefulException(
string.Format(
CliCommandStrings.RunCommandExceptionUnableToRun,
Expand Down
110 changes: 110 additions & 0 deletions src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.DotNet.Cli.Utils;
using Spectre.Console;

namespace Microsoft.DotNet.Cli.Commands.Run;

internal static class TargetFrameworkSelector
{
/// <summary>
/// Evaluates the project to determine if target framework selection is needed.
/// If the project has multiple target frameworks and none was specified, prompts the user to select one.
/// </summary>
/// <param name="projectFilePath">Path to the project file</param>
/// <param name="globalProperties">Global properties for MSBuild evaluation</param>
/// <param name="isInteractive">Whether we're running in interactive mode (can prompt user)</param>
/// <param name="selectedFramework">The selected target framework, or null if not needed</param>
/// <returns>True if we should continue, false if we should exit with error</returns>
public static bool TrySelectTargetFramework(
string projectFilePath,
Dictionary<string, string> globalProperties,
bool isInteractive,
out string? selectedFramework)
{
selectedFramework = null;

// If a framework is already specified, no need to prompt
if (globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework))
{
return true;
}

// Evaluate the project to get TargetFrameworks
Microsoft.Build.Evaluation.Project project;
try
{
using var collection = new ProjectCollection(globalProperties: globalProperties);
project = collection.LoadProject(projectFilePath);
}
catch (InvalidProjectFileException)
{
// Invalid project file, return true to continue for normal error handling
return true;
}

string targetFrameworks = project.GetPropertyValue("TargetFrameworks");

// If there's no TargetFrameworks property or only one framework, no selection needed
if (string.IsNullOrWhiteSpace(targetFrameworks))
{
return true;
}

var frameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries);

// If there's only one framework, no selection needed
if (frameworks.Length <= 1)
{
return true;
}

if (isInteractive)
{
selectedFramework = PromptForTargetFramework(frameworks);
return selectedFramework != null;
}
else
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework"));
Reporter.Error.WriteLine();
Reporter.Error.WriteLine(CliCommandStrings.RunCommandAvailableTargetFrameworks);
Reporter.Error.WriteLine();

for (int i = 0; i < frameworks.Length; i++)
{
Reporter.Error.WriteLine($" {i + 1}. {frameworks[i]}");
}

Reporter.Error.WriteLine();
Reporter.Error.WriteLine($"{CliCommandStrings.RunCommandExampleText}: dotnet run --framework {frameworks[0]}");
Reporter.Error.WriteLine();
return false;
}
}

/// <summary>
/// Prompts the user to select a target framework from the available options using Spectre.Console.
/// </summary>
private static string? PromptForTargetFramework(string[] frameworks)
{
try
{
var prompt = new SelectionPrompt<string>()
.Title($"[cyan]{Markup.Escape(CliCommandStrings.RunCommandSelectTargetFrameworkPrompt)}[/]")
.PageSize(10)
.MoreChoicesText($"[grey]({Markup.Escape(CliCommandStrings.RunCommandMoreFrameworksText)})[/]")
.AddChoices(frameworks);

return Spectre.Console.AnsiConsole.Prompt(prompt);
}
catch (Exception)
{
// If Spectre.Console fails (e.g., terminal doesn't support it), return null
return null;
}
}
}
20 changes: 20 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

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

20 changes: 20 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

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

20 changes: 20 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

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

20 changes: 20 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

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

Loading
Loading