Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
48a663a
[dotnet-cli] prompt for target framework using `Spectre.Console`
jonathanpeppers Oct 22, 2025
f5b4ff4
Fix for `.sln` file passed in
jonathanpeppers Oct 23, 2025
0c1445e
Use `Interactive` property
jonathanpeppers Oct 23, 2025
78afad3
Update GivenDotnetRunSelectsTargetFramework.cs
jonathanpeppers Oct 23, 2025
ea80a04
Remove duplicative tests
jonathanpeppers Oct 24, 2025
36e5271
Better assertion
jonathanpeppers Oct 24, 2025
6f66408
Skip previous TFMs on arm64, x64 passes on these
jonathanpeppers Oct 24, 2025
ae1b1d7
Remove ItPrefersExplicitFrameworkOptionOverProperty
jonathanpeppers Oct 24, 2025
e18870a
Update src/Cli/dotnet/Commands/Run/RunCommand.cs
jonathanpeppers Oct 31, 2025
9141a23
Sign `Spectre.Console.dll`
jonathanpeppers Nov 3, 2025
3847cfb
small tweaks to enable running against singular-valued targetframewor…
baronfel Nov 3, 2025
18241ce
Add `EnableSearch()` w/ localization support
jonathanpeppers Nov 3, 2025
ad8a20e
Pin darc
premun Nov 4, 2025
69864e8
Fix test failure
jonathanpeppers Nov 4, 2025
3e34f5a
Test for 3847cfb9809fce3b6bf5a58b0c68f37e10fa11e9
jonathanpeppers Nov 4, 2025
1be25bf
Merge branch 'main' into dev/peppers/tf-selection
jonathanpeppers Nov 4, 2025
e47ba5a
Update RunFileTests.cs
jonathanpeppers Nov 4, 2025
7823fe5
Update GivenDotnetRunSelectsTargetFramework.cs
jonathanpeppers Nov 4, 2025
5f5a8f8
Fix multi-targeted 'dotnet run lib.cs'
jonathanpeppers Nov 4, 2025
6785ac5
Merge branch 'main' into dev/peppers/tf-selection
jonathanpeppers Nov 5, 2025
5ad2287
Update RunFileTests.cs
jonathanpeppers Nov 5, 2025
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 @@ -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="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
1 change: 1 addition & 0 deletions eng/Signing.props
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<FileSignInfo Include="MessagePack.dll" CertificateName="$(ExternalCertificateId)" />
<FileSignInfo Include="Nerdbank.Streams.dll" CertificateName="$(ExternalCertificateId)" />
<FileSignInfo Include="Newtonsoft.Json.dll" CertificateName="$(ExternalCertificateId)" />
<FileSignInfo Include="Spectre.Console.dll" CertificateName="$(ExternalCertificateId)" />
<FileSignInfo Include="Valleysoft.DockerCredsProvider.dll" CertificateName="$(ExternalCertificateId)" />

<!-- Additionally, we need to notarize any .pkg files -->
Expand Down
2 changes: 1 addition & 1 deletion eng/common/vmr-sync.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Set-StrictMode -Version Latest
Highlight 'Installing .NET, preparing the tooling..'
. .\eng\common\tools.ps1
$dotnetRoot = InitializeDotNetCli -install:$true
$darc = Get-Darc
$darc = Get-Darc "1.1.0-beta.25514.2"
$dotnet = "$dotnetRoot\dotnet.exe"

Highlight "Starting the synchronization of VMR.."
Expand Down
2 changes: 1 addition & 1 deletion eng/common/vmr-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ set -e
highlight 'Installing .NET, preparing the tooling..'
source "./eng/common/tools.sh"
InitializeDotNetCli true
GetDarc
GetDarc "1.1.0-beta.25514.2"
dotnetDir=$( cd ./.dotnet/; pwd -P )
dotnet=$dotnetDir/dotnet

Expand Down
15 changes: 15 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,21 @@ 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="RunCommandSearchPlaceholderText" xml:space="preserve">
<value>Type to search</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
124 changes: 112 additions & 12 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ public class RunCommand

/// <summary>
/// Parsed structure representing the MSBuild arguments that will be used to build the project.
///
/// Note: This property has a private setter and is mutated within the class when framework selection modifies it.
/// This mutability is necessary to allow the command to update MSBuild arguments after construction based on framework selection.
/// </summary>
public MSBuildArgs MSBuildArgs { get; }
public MSBuildArgs MSBuildArgs { get; private set; }
public bool Interactive { get; }

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

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

// For file-based projects, check for multi-targeting before building
if (EntryPointFileFullPath is not null && !TrySelectTargetFrameworkForFileBasedProject())
{
return 1;
}

Func<ProjectCollection, ProjectInstance>? projectFactory = null;
RunProperties? cachedRunProperties = null;
VirtualProjectBuildingCommand? virtualCommand = null;
Expand Down Expand Up @@ -182,6 +197,100 @@ 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))
{
ApplySelectedFramework(selectedFramework);
return true;
}

return false;
}

/// <summary>
/// Checks if target framework selection is needed for file-based projects.
/// Parses directives from the source file to detect multi-targeting.
/// </summary>
/// <returns>True if we can continue, false if we should exit</returns>
private bool TrySelectTargetFrameworkForFileBasedProject()
{
Debug.Assert(EntryPointFileFullPath is not null);

var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);

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

// Get frameworks from source file directives
var frameworks = GetTargetFrameworksFromSourceFile(EntryPointFileFullPath);
if (frameworks is null || frameworks.Length == 0)
{
return true; // Not multi-targeted
}

// Use TargetFrameworkSelector to handle multi-target selection (or single framework selection)
if (TargetFrameworkSelector.TrySelectTargetFramework(frameworks, Interactive, out string? selectedFramework))
{
ApplySelectedFramework(selectedFramework);
return true;
}

return false;
}

/// <summary>
/// Parses a source file to extract target frameworks from directives.
/// </summary>
/// <returns>Array of frameworks if TargetFrameworks is specified, null otherwise</returns>
private static string[]? GetTargetFrameworksFromSourceFile(string sourceFilePath)
{
var sourceFile = SourceFile.Load(sourceFilePath);
var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.Ignore());

var targetFrameworksDirective = directives.OfType<CSharpDirective.Property>()
.FirstOrDefault(p => string.Equals(p.Name, "TargetFrameworks", StringComparison.OrdinalIgnoreCase));

if (targetFrameworksDirective is null)
{
return null;
}

return targetFrameworksDirective.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}

/// <summary>
/// Applies the selected target framework to MSBuildArgs if a framework was provided.
/// </summary>
/// <param name="selectedFramework">The framework to apply, or null if no framework selection was needed</param>
private void ApplySelectedFramework(string? selectedFramework)
{
// If selectedFramework is null, it means no framework selection was needed
// (e.g., user already specified --framework, or single-target project)
if (selectedFramework is not null)
{
var additionalProperties = new ReadOnlyDictionary<string, string>(
new Dictionary<string, string> { { "TargetFramework", selectedFramework } });
MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
}
}

internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
{
if (launchSettings == null)
Expand Down Expand Up @@ -431,7 +540,8 @@ static ProjectInstance EvaluateProject(string? projectFilePath, Func<ProjectColl

static void ValidatePreconditions(ProjectInstance project)
{
if (string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework")))
// there must be some kind of TFM available to run a project
if (string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework")) && string.IsNullOrEmpty(project.GetPropertyValue("TargetFrameworks")))
{
ThrowUnableToRunError(project);
}
Expand Down Expand Up @@ -504,16 +614,6 @@ static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, Faca
[DoesNotReturn]
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
130 changes: 130 additions & 0 deletions src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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
string targetFrameworks;
try
{
using var collection = new ProjectCollection(globalProperties: globalProperties);
var project = collection.LoadProject(projectFilePath);
targetFrameworks = project.GetPropertyValue("TargetFrameworks");
}
catch (InvalidProjectFileException)
{
// Invalid project file, return true to continue for normal error handling
return true;
}

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

// parse the TargetFrameworks property and make sure to account for any additional whitespace
// users may have added for formatting reasons.
var frameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

return TrySelectTargetFramework(frameworks, isInteractive, out selectedFramework);
}

/// <summary>
/// Handles target framework selection when given an array of frameworks.
/// If there's only one framework, selects it automatically.
/// If there are multiple frameworks, prompts the user (interactive) or shows an error (non-interactive).
/// </summary>
/// <param name="frameworks">Array of target frameworks to choose from</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 selection was cancelled</param>
/// <returns>True if we should continue, false if we should exit with error</returns>
public static bool TrySelectTargetFramework(string[] frameworks, bool isInteractive, out string? selectedFramework)
{
// If there's only one framework in the TargetFrameworks, we do need to pick it to force the subsequent builds/evaluations
// to act against the correct 'view' of the project
if (frameworks.Length == 1)
{
selectedFramework = frameworks[0];
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();
selectedFramework = null;
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)
.EnableSearch()
.SearchPlaceholderText(CliCommandStrings.RunCommandSearchPlaceholderText);

return Spectre.Console.AnsiConsole.Prompt(prompt);
}
catch (Exception)
{
// If Spectre.Console fails (e.g., terminal doesn't support it), return null
return null;
}
}
}
25 changes: 25 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.

Loading
Loading