From 48a663adc508c8c9d68cd66017de15242535f658 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 22 Oct 2025 14:51:31 -0500 Subject: [PATCH 01/20] [dotnet-cli] prompt for target framework using `Spectre.Console` Implements the first part of the spec mentioned in: https://github.com/dotnet/sdk/blob/522c88a6abfc4a011556f839d15844d07ba62cd9/documentation/specs/dotnet-run-for-maui.md?plain=1#L35-L46 Add interactive target framework selection to `dotnet run` When running a multi-targeted project without specifying `--framework`, `dotnet run` now: * Prompts interactively (using `Spectre.Console`) to select a framework with arrow keys * Shows a formatted error list in non-interactive mode with available frameworks * Handles selection early before project build/evaluation * Removes redundant multi-TFM error checking from `ThrowUnableToRunError()` * Adds a few unit tests to validate these changes. This is currently WIP, as it introduces a "pre-built" `Spectre.Console` that I will need to setup in source-build in a PRs elsewhere. --- Directory.Packages.props | 1 + .../dotnet/Commands/CliCommandStrings.resx | 12 + src/Cli/dotnet/Commands/Run/RunCommand.cs | 52 +++- .../Commands/Run/TargetFrameworkSelector.cs | 100 +++++++ .../Commands/xlf/CliCommandStrings.cs.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.de.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.es.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.fr.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.it.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.ja.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.ko.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.pl.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.pt-BR.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.ru.xlf | 20 ++ .../Commands/xlf/CliCommandStrings.tr.xlf | 20 ++ .../xlf/CliCommandStrings.zh-Hans.xlf | 20 ++ .../xlf/CliCommandStrings.zh-Hant.xlf | 20 ++ src/Cli/dotnet/dotnet.csproj | 1 + .../DotnetRunMultiTarget.csproj | 8 + .../DotnetRunMultiTarget/Program.cs | 17 ++ .../GivenDotnetRunSelectsTargetFramework.cs | 256 ++++++++++++++++++ 21 files changed, 696 insertions(+), 11 deletions(-) create mode 100644 src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs create mode 100644 test/TestAssets/TestProjects/DotnetRunMultiTarget/DotnetRunMultiTarget.csproj create mode 100644 test/TestAssets/TestProjects/DotnetRunMultiTarget/Program.cs create mode 100644 test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 788923bf48d6..f3872d8af5b9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -113,6 +113,7 @@ + diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 6ca03e2f5fe6..5a01a54c293b 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1790,6 +1790,18 @@ The current OutputType is '{2}'. Unable to run your project Your project targets multiple frameworks. Specify which framework to run using '{0}'. + + Select the target framework to run: + + + Move up and down to reveal more frameworks + + + Available target frameworks: + + + Example + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. {Locked="--project"} diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 33ec7b5e3219..5d26d32ba5e6 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -55,7 +55,7 @@ public class RunCommand /// /// Parsed structure representing the MSBuild arguments that will be used to build the project. /// - public MSBuildArgs MSBuildArgs { get; } + public MSBuildArgs MSBuildArgs { get; private set; } public bool Interactive { get; } /// @@ -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? projectFactory = null; RunProperties? cachedRunProperties = null; VirtualProjectBuildingCommand? virtualCommand = null; @@ -180,6 +186,40 @@ public int Execute() } } + /// + /// 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. + /// + /// True if we can continue, false if we should exit + private bool TrySelectTargetFrameworkIfNeeded() + { + Debug.Assert(ProjectFileFullPath is not null); + + var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs); + + // Check if we're in an interactive terminal + bool isInteractive = !Console.IsInputRedirected && !Console.IsOutputRedirected; + + if (TargetFrameworkSelector.TrySelectTargetFramework( + ProjectFileFullPath, + globalProperties, + isInteractive, + out string? selectedFramework)) + { + // If a framework was selected, add it to MSBuildArgs + if (selectedFramework is not null) + { + var additionalProperties = new ReadOnlyDictionary( + new Dictionary { { "TargetFramework", selectedFramework } }); + MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties); + } + return true; + } + + return false; + } + internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings) { if (launchSettings == null) @@ -499,16 +539,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, diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs new file mode 100644 index 000000000000..a9a13cc16199 --- /dev/null +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -0,0 +1,100 @@ +// 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.DotNet.Cli.Utils; +using Spectre.Console; + +namespace Microsoft.DotNet.Cli.Commands.Run; + +internal static class TargetFrameworkSelector +{ + /// + /// 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. + /// + /// Path to the project file + /// Global properties for MSBuild evaluation + /// Whether we're running in interactive mode (can prompt user) + /// The selected target framework, or null if not needed + /// True if we should continue, false if we should exit with error + public static bool TrySelectTargetFramework( + string projectFilePath, + Dictionary 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 + using var collection = new ProjectCollection(globalProperties: globalProperties); + var project = collection.LoadProject(projectFilePath); + + 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; + } + } + + /// + /// Prompts the user to select a target framework from the available options using Spectre.Console. + /// + private static string? PromptForTargetFramework(string[] frameworks) + { + try + { + var prompt = new SelectionPrompt() + .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; + } + } +} diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index d1d8709624e6..f987124ae574 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -2702,6 +2702,11 @@ Ve výchozím nastavení je publikována aplikace závislá na architektuře.Příkaz rozhraní .NET pro spuštění + + Available target frameworks: + Available target frameworks: + + Building... Sestavování... @@ -2712,6 +2717,11 @@ Ve výchozím nastavení je publikována aplikace závislá na architektuře.Spuštění cíle {0} ke zjištění příkazů spuštění pro tento projekt se nezdařilo. Opravte chyby a upozornění a spusťte je znovu. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Sestavení se nepovedlo. Opravte v sestavení chyby a spusťte ho znovu. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Cílem projektu je více architektur. Pomocí parametru {0} určete, která architektura se má spustit. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Upozornění NETSDK1174: Zkratka -p pro --project je zastaralá. Použijte prosím --project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. {0} není platný soubor projektu. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index 2a39befb9d3c..a394a608bdd8 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -2702,6 +2702,11 @@ Standardmäßig wird eine Framework-abhängige Anwendung veröffentlicht..NET-Befehl "Run" + + Available target frameworks: + Available target frameworks: + + Building... Buildvorgang wird ausgeführt... @@ -2712,6 +2717,11 @@ Standardmäßig wird eine Framework-abhängige Anwendung veröffentlicht.Das {0} Ziel ausführen, um zu ermitteln, dass ein Fehler bei den Ausführungsbefehlen für dieses Projekt aufgetreten ist. Beheben Sie die Fehler und Warnungen, und führen Sie dies erneut aus. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Fehler beim Buildvorgang. Beheben Sie die Buildfehler, und versuchen Sie es anschließend noch mal. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches Framework ausgeführt werden soll. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Warnung NETSDK1174: Die Abkürzung von „-p“ für „--project“ ist veraltet. Verwenden Sie „--project“. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. "{0}" ist keine gültige Projektdatei. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index 84f4aabb9069..3c89d8542134 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -2702,6 +2702,11 @@ El valor predeterminado es publicar una aplicación dependiente del marco.Ejecutar comando de .NET + + Available target frameworks: + Available target frameworks: + + Building... Compilando... @@ -2712,6 +2717,11 @@ El valor predeterminado es publicar una aplicación dependiente del marco.Error al ejecutar el destino {0} para detectar comandos de ejecución para este proyecto. Corrija los errores y advertencias y vuelva a ejecutarlo. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. No se pudo llevar a cabo la compilación. Corrija los errores de compilación y vuelva a ejecutar el proyecto. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Su proyecto tiene como destino varias plataformas. Especifique la que quiere usar mediante "{0}". + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Advertencia NETSDK1174: La abreviatura de -p para --project está en desuso. Use --project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. "{0}" no es un archivo de proyecto válido. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 6a637323a60e..a3c02245fd64 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -2702,6 +2702,11 @@ La valeur par défaut est de publier une application dépendante du framework.Commande d'exécution .NET + + Available target frameworks: + Available target frameworks: + + Building... Génération... @@ -2712,6 +2717,11 @@ La valeur par défaut est de publier une application dépendante du framework.L’exécution de la {0} cible pour découvrir les commandes d’exécution a échoué pour ce projet. Corrigez les erreurs et les avertissements, puis réexécutez. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. La build a échoué. Corrigez les erreurs de la build et réexécutez-la. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à l'aide de '{0}'. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. AVERTISSEMENT NETSDK1174 : l’abréviation de-p pour--project est déconseillée. Veuillez utiliser--project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' n'est pas un fichier projet valide. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index 1f255f43c794..5c1a45d07d69 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -2702,6 +2702,11 @@ Per impostazione predefinita, viene generato un pacchetto dipendente dal framewo Comando esecuzione .NET + + Available target frameworks: + Available target frameworks: + + Building... Compilazione... @@ -2712,6 +2717,11 @@ Per impostazione predefinita, viene generato un pacchetto dipendente dal framewo L'esecuzione della destinazione {0} per individuare i comandi di esecuzione non è riuscita per questo progetto. Correggere gli errori e gli avvisi ed eseguire di nuovo. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. La compilazione non è riuscita. Correggere gli errori di compilazione e ripetere l'esecuzione. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Il progetto è destinato a più framework. Specificare il framework da eseguire con '{0}'. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Avviso NETSDK1174: l'abbreviazione di -p per --project è deprecata. Usare --project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' non è un file di progetto valido. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index 7aa8108b6954..24de7a1dca1d 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -2702,6 +2702,11 @@ The default is to publish a framework-dependent application. .NET Run コマンド + + Available target frameworks: + Available target frameworks: + + Building... ビルドしています... @@ -2712,6 +2717,11 @@ The default is to publish a framework-dependent application. このプロジェクトで実行コマンドを検出するための {0} ターゲットの実行に失敗しました。エラーと警告を修正して、もう一度実行してください。 {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. ビルドに失敗しました。ビルド エラーを修正して、もう一度実行してください。 @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' プロジェクトは複数のフレームワークを対象としています。'{0}' を使用して、実行するフレームワークを指定してください。 + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. 警告 NETSDK1174: --project の省略形である -p は推奨されていません。--project を使用してください。 {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' は有効なプロジェクト ファイルではありません。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index 6544ed0d7051..0db06a3686de 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -2702,6 +2702,11 @@ The default is to publish a framework-dependent application. .NET 실행 명령 + + Available target frameworks: + Available target frameworks: + + Building... 빌드하는 중... @@ -2712,6 +2717,11 @@ The default is to publish a framework-dependent application. 이 프로젝트에 대해 실행 명령을 검색하기 위해 {0} 대상을 실행하지 못했습니다. 오류 및 경고를 수정하고 다시 실행합니다. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. 빌드하지 못했습니다. 빌드 오류를 수정하고 다시 실행하세요. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' 프로젝트에서 여러 프레임워크를 대상으로 합니다. '{0}'을(를) 사용하여 실행할 프레임워크를 지정하세요. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. 경고 NETSDK1174: --project에 대한 약어 -p는 더 이상 사용되지 않습니다. --project를 사용하세요. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}'은(는) 유효한 프로젝트 파일이 아닙니다. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 6e7de9021244..637bf0bfd014 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -2702,6 +2702,11 @@ Domyślnie publikowana jest aplikacja zależna od struktury. Uruchamianie polecenia platformy .NET + + Available target frameworks: + Available target frameworks: + + Building... Trwa kompilowanie... @@ -2712,6 +2717,11 @@ Domyślnie publikowana jest aplikacja zależna od struktury. Uruchomienie obiektu docelowego {0} w celu odnalezienia poleceń przebiegu dla tego projektu nie powiodło się. Usuń błędy i ostrzeżenia, a następnie uruchom ponownie. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Kompilacja nie powiodła się. Napraw błędy kompilacji i uruchom ją ponownie. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy użyciu elementu „{0}”. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Ostrzeżenie NETSDK1174: Skrót -p dla polecenia --project jest przestarzały. Użyj polecenia --project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. „{0}” nie jest prawidłowym plikiem projektu. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 032d2caf149f..389393572dce 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -2702,6 +2702,11 @@ O padrão é publicar uma aplicação dependente de framework. Comando Run do .NET + + Available target frameworks: + Available target frameworks: + + Building... Compilando... @@ -2712,6 +2717,11 @@ O padrão é publicar uma aplicação dependente de framework. Falha na execução do destino {0} para descobrir comandos de execução para este projeto. Corrija os erros e avisos e execute novamente. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Ocorreu uma falha no build. Corrija os erros de build e execute novamente. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Ele tem diversas estruturas como destino. Especifique que estrutura executar usando '{0}'. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Aviso NETSDK1174: a abreviação de-p para--project é preterida. Use --project. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' não é um arquivo de projeto válido. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 05edfdd693ef..9b2052d1b9d6 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -2702,6 +2702,11 @@ The default is to publish a framework-dependent application. Команда .NET Run + + Available target frameworks: + Available target frameworks: + + Building... Сборка… @@ -2712,6 +2717,11 @@ The default is to publish a framework-dependent application. Не удалось запустить цель {0} для обнаружения команд выполнения для этого проекта. Исправьте ошибки и предупреждения и повторите попытку. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Ошибка сборки. Устраните ошибки сборки и повторите попытку. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Проект предназначен для нескольких платформ. Укажите платформу, для которой следует запустить проект, с помощью "{0}". + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Предупреждение NETSDK1174: сокращение "-p" для "--project" не рекомендуется. Используйте "--project". {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. "{0}" не является допустимым файлом проекта. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index 228d3a11bd02..3dae95e5e3e0 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -2702,6 +2702,11 @@ Varsayılan durum, çerçeveye bağımlı bir uygulama yayımlamaktır. .NET Run Komutu + + Available target frameworks: + Available target frameworks: + + Building... Derleniyor... @@ -2712,6 +2717,11 @@ Varsayılan durum, çerçeveye bağımlı bir uygulama yayımlamaktır. Çalıştırma komutlarını bulmak için {0} hedefini çalıştırma bu proje için başarısız oldu. Hataları ve uyarıları düzeltip yeniden çalıştırın. {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. Derleme başarısız oldu. Derleme hatalarını düzeltip yeniden çalıştırın. @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework'ün çalıştırılacağını belirtin. + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. Uyarı NETSDK1174: --project için -p kısaltması kullanımdan kaldırıldı. Lütfen --project kullanın. {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' geçerli bir proje dosyası değil. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 8dce87da752d..076fec45f2ba 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -2702,6 +2702,11 @@ The default is to publish a framework-dependent application. .NET 运行命令 + + Available target frameworks: + Available target frameworks: + + Building... 正在生成... @@ -2712,6 +2717,11 @@ The default is to publish a framework-dependent application. 为此项目运行 {0} 目标以发现运行命令失败。请修复错误和警告,然后再次运行。 {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. 生成失败。请修复生成错误并重新运行。 @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' 你的项目面向多个框架。请指定要使用“{0}”运行的框架。 + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. 警告 NETSDK1174: 已弃用使用缩写“-p”来代表“--project”。请使用“--project”。 {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. “{0}”不是有效的项目文件。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index 4402fa7bc229..a053b1da5e26 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -2702,6 +2702,11 @@ The default is to publish a framework-dependent application. .NET 執行命令 + + Available target frameworks: + Available target frameworks: + + Building... 正在建置... @@ -2712,6 +2717,11 @@ The default is to publish a framework-dependent application. 執行 {0} 目標以探索對此專案的執行命令失敗。修正錯誤和警告,然後重新執行。 {0} is the name of an MSBuild target + + Example + Example + + The build failed. Fix the build errors and run again. 建置失敗。請修正建置錯誤後,再執行一次。 @@ -2759,11 +2769,21 @@ Your project targets multiple frameworks. Specify which framework to run using ' 您的專案以多重架構為目標。請使用 '{0}' 指定要執行的架構。 + + Move up and down to reveal more frameworks + Move up and down to reveal more frameworks + + Warning NETSDK1174: The abbreviation of -p for --project is deprecated. Please use --project. 警告 NETSDK1174: --project 已取代縮寫 -p。請使用 --project。 {Locked="--project"} + + Select the target framework to run: + Select the target framework to run: + + '{0}' is not a valid project file. '{0}' 並非有效的專案名稱。 diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 2e63f9a3c5a8..36265a85a975 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -59,6 +59,7 @@ + diff --git a/test/TestAssets/TestProjects/DotnetRunMultiTarget/DotnetRunMultiTarget.csproj b/test/TestAssets/TestProjects/DotnetRunMultiTarget/DotnetRunMultiTarget.csproj new file mode 100644 index 000000000000..fa9bbe9e7439 --- /dev/null +++ b/test/TestAssets/TestProjects/DotnetRunMultiTarget/DotnetRunMultiTarget.csproj @@ -0,0 +1,8 @@ + + + + Exe + net8.0;net9.0;$(CurrentTargetFramework) + + + diff --git a/test/TestAssets/TestProjects/DotnetRunMultiTarget/Program.cs b/test/TestAssets/TestProjects/DotnetRunMultiTarget/Program.cs new file mode 100644 index 000000000000..9c2839e43ffd --- /dev/null +++ b/test/TestAssets/TestProjects/DotnetRunMultiTarget/Program.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace DotNetRunMultiTarget +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from multi-targeted app!"); + Console.WriteLine($"Target Framework: {AppContext.TargetFrameworkName}"); + Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); + } + } +} diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs new file mode 100644 index 000000000000..67c244d42cd8 --- /dev/null +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -0,0 +1,256 @@ +// 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.Cli.Utils; + +namespace Microsoft.DotNet.Cli.Run.Tests; + +/// +/// Integration tests for target framework selection in dotnet run +/// +public class GivenDotnetRunSelectsTargetFramework : SdkTest +{ + public GivenDotnetRunSelectsTargetFramework(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public void ItRunsMultiTFMProjectWhenFrameworkIsSpecified() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .Execute("--framework", ToolsetInfo.CurrentTargetFramework) + .Should().Pass() + .And.HaveStdOutContaining("This string came from the test library!"); + } + + [Fact] + public void ItFailsInNonInteractiveMode_WhenMultiTFMProjectHasNoFrameworkSpecified() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .Execute("--no-interactive"); + + result.Should().Fail(); + + if (!TestContext.IsLocalized()) + { + result.Should().HaveStdErrContaining("Unable to run your project"); + result.Should().HaveStdErrContaining("multiple frameworks"); + result.Should().HaveStdErrContaining("--framework"); + } + } + + [Fact] + public void ItRunsWithShortFormFrameworkOption() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .Execute("-f", ToolsetInfo.CurrentTargetFramework) + .Should().Pass() + .And.HaveStdOutContaining("This string came from the test library!"); + } + + [Fact] + public void ItRunsWithFrameworkPropertySyntax() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .Execute("-p:TargetFramework=" + ToolsetInfo.CurrentTargetFramework) + .Should().Pass() + .And.HaveStdOutContaining("This string came from the test library!"); + } + + [Fact] + public void ItPrefersExplicitFrameworkOptionOverProperty() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + // Pass both --framework and -p:TargetFramework + // The --framework option should take precedence + new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .Execute( + "--framework", ToolsetInfo.CurrentTargetFramework, + "-p:TargetFramework=net462") + .Should().Pass() + .And.HaveStdOutContaining("This string came from the test library!"); + } + + [Fact] + public void ItShowsErrorMessageWithAvailableFrameworks_InNonInteractiveMode() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .Execute("--no-interactive"); + + result.Should().Fail(); + + if (!TestContext.IsLocalized()) + { + // Should show the "Unable to run your project" message + result.Should().HaveStdErrContaining("Unable to run your project"); + result.Should().HaveStdErrContaining("Your project targets multiple frameworks"); + + // Should show available frameworks + result.Should().HaveStdErrContaining("Available target frameworks:"); + + // Should show example command + result.Should().HaveStdErrContaining("Example:"); + result.Should().HaveStdErrContaining("dotnet run --framework"); + } + } + + [Fact] + public void ItFailsForMultiTargetedAppWithoutFramework_InNonInteractiveMode() + { + var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") + .WithSource(); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .Execute("--no-interactive"); + + result.Should().Fail(); + + if (!TestContext.IsLocalized()) + { + result.Should().HaveStdErrContaining("Unable to run your project"); + result.Should().HaveStdErrContaining("multiple frameworks"); + } + } + + [Theory] + [InlineData("net8.0", ".NETCoreApp,Version=v8.0")] + [InlineData("net9.0", ".NETCoreApp,Version=v9.0")] + [InlineData(ToolsetInfo.CurrentTargetFramework, ToolsetInfo.CurrentTargetFrameworkMoniker)] + public void ItRunsDifferentFrameworksInMultiTargetedApp(string targetFramework, string expectedMoniker) + { + var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") + .WithSource(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute("--framework", targetFramework) + .Should().Pass() + .And.HaveStdOutContaining($"Target Framework: {expectedMoniker}"); + } + + [Fact] + public void ItRunsSingleTFMProjectWithoutFrameworkSpecification() + { + var testInstance = _testAssetsManager.CopyTestAsset("HelloWorld") + .WithSource(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World!"); + } + + [Fact] + public void ItRunsProjectWithOnlyTargetFrameworkProperty() + { + // Projects with only TargetFramework (not TargetFrameworks) should run without needing --framework + var testInstance = _testAssetsManager.CopyTestAsset("MSBuildTestApp") + .WithSource(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World!"); + } + + [Fact] + public void ItTreatsEmptyFrameworkSpecificationAsNotSpecified() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .Execute("-p:TargetFramework=", "--no-interactive"); // Empty string + + result.Should().Fail(); + + if (!TestContext.IsLocalized()) + { + result.Should().HaveStdErrContaining("Unable to run your project"); + result.Should().HaveStdErrContaining("multiple frameworks"); + } + } + + [Fact] + public void ItTreatsWhitespaceFrameworkSpecificationAsNotSpecified() + { + var testInstance = _testAssetsManager.CopyTestAsset( + "NETFrameworkReferenceNETStandard20", + testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + .WithSource(); + + string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); + + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(projectDirectory) + .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .Execute("-p:TargetFramework= ", "--no-interactive"); // Whitespace + + result.Should().Fail(); + + if (!TestContext.IsLocalized()) + { + result.Should().HaveStdErrContaining("Unable to run your project"); + result.Should().HaveStdErrContaining("multiple frameworks"); + } + } +} From f5b4ff4f0b2d5a14c74008c100f45ac99b45aa97 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Oct 2025 14:17:14 -0500 Subject: [PATCH 02/20] Fix for `.sln` file passed in --- .../dotnet/Commands/Run/TargetFrameworkSelector.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index a9a13cc16199..92fd65d6b5cc 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -2,6 +2,7 @@ // 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; @@ -33,8 +34,17 @@ public static bool TrySelectTargetFramework( } // Evaluate the project to get TargetFrameworks - using var collection = new ProjectCollection(globalProperties: globalProperties); - var project = collection.LoadProject(projectFilePath); + 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"); From 0c1445eb0e7300c4f32ece8a2ff2a5ac4e8c1d3b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Oct 2025 14:21:38 -0500 Subject: [PATCH 03/20] Use `Interactive` property --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 5d26d32ba5e6..21e11ba32257 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -197,14 +197,10 @@ private bool TrySelectTargetFrameworkIfNeeded() Debug.Assert(ProjectFileFullPath is not null); var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs); - - // Check if we're in an interactive terminal - bool isInteractive = !Console.IsInputRedirected && !Console.IsOutputRedirected; - if (TargetFrameworkSelector.TrySelectTargetFramework( ProjectFileFullPath, globalProperties, - isInteractive, + Interactive, out string? selectedFramework)) { // If a framework was selected, add it to MSBuildArgs From 78afad32a0bdaac1f7de7c74d7fce734599d6398 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Oct 2025 15:56:40 -0500 Subject: [PATCH 04/20] Update GivenDotnetRunSelectsTargetFramework.cs So it doesn't require `mono` --- .../Run/GivenDotnetRunSelectsTargetFramework.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index 67c244d42cd8..73ed4d09d46e 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -93,22 +93,18 @@ public void ItRunsWithFrameworkPropertySyntax() [Fact] public void ItPrefersExplicitFrameworkOptionOverProperty() { - var testInstance = _testAssetsManager.CopyTestAsset( - "NETFrameworkReferenceNETStandard20", - testAssetSubdirectory: TestAssetSubdirectories.DesktopTestProjects) + var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") .WithSource(); - string projectDirectory = Path.Combine(testInstance.Path, "MultiTFMTestApp"); - // Pass both --framework and -p:TargetFramework // The --framework option should take precedence new DotnetCommand(Log, "run") - .WithWorkingDirectory(projectDirectory) + .WithWorkingDirectory(testInstance.Path) .Execute( "--framework", ToolsetInfo.CurrentTargetFramework, - "-p:TargetFramework=net462") + "-p:TargetFramework=net8.0") .Should().Pass() - .And.HaveStdOutContaining("This string came from the test library!"); + .And.HaveStdOutContaining("Hello from multi-targeted app!"); } [Fact] From ea80a04f7f58fd105477f1fb00fe2b7d605d35b9 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Oct 2025 10:34:59 -0500 Subject: [PATCH 05/20] Remove duplicative tests There are other tests that do this --- .../GivenDotnetRunSelectsTargetFramework.cs | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index 73ed4d09d46e..b666485933d2 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -175,33 +175,6 @@ public void ItRunsDifferentFrameworksInMultiTargetedApp(string targetFramework, .And.HaveStdOutContaining($"Target Framework: {expectedMoniker}"); } - [Fact] - public void ItRunsSingleTFMProjectWithoutFrameworkSpecification() - { - var testInstance = _testAssetsManager.CopyTestAsset("HelloWorld") - .WithSource(); - - new DotnetCommand(Log, "run") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello World!"); - } - - [Fact] - public void ItRunsProjectWithOnlyTargetFrameworkProperty() - { - // Projects with only TargetFramework (not TargetFrameworks) should run without needing --framework - var testInstance = _testAssetsManager.CopyTestAsset("MSBuildTestApp") - .WithSource(); - - new DotnetCommand(Log, "run") - .WithWorkingDirectory(testInstance.Path) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining("Hello World!"); - } - [Fact] public void ItTreatsEmptyFrameworkSpecificationAsNotSpecified() { From 36e5271ccd6a820319d551caf0ad5ed7eb3be47d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Oct 2025 10:35:07 -0500 Subject: [PATCH 06/20] Better assertion --- .../CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index b666485933d2..b54ad99fdb83 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -104,7 +104,7 @@ public void ItPrefersExplicitFrameworkOptionOverProperty() "--framework", ToolsetInfo.CurrentTargetFramework, "-p:TargetFramework=net8.0") .Should().Pass() - .And.HaveStdOutContaining("Hello from multi-targeted app!"); + .And.HaveStdOutContaining($"Target Framework: {ToolsetInfo.CurrentTargetFrameworkMoniker}"); } [Fact] From 6f66408d9f488c058f4a6a6bf2c85a41c0ffeabe Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Oct 2025 10:40:56 -0500 Subject: [PATCH 07/20] Skip previous TFMs on arm64, x64 passes on these --- .../Run/GivenDotnetRunSelectsTargetFramework.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index b54ad99fdb83..04cc9e1ed837 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -1,7 +1,7 @@ // 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.Cli.Utils; +using System.Runtime.InteropServices; namespace Microsoft.DotNet.Cli.Run.Tests; @@ -165,6 +165,13 @@ public void ItFailsForMultiTargetedAppWithoutFramework_InNonInteractiveMode() [InlineData(ToolsetInfo.CurrentTargetFramework, ToolsetInfo.CurrentTargetFrameworkMoniker)] public void ItRunsDifferentFrameworksInMultiTargetedApp(string targetFramework, string expectedMoniker) { + // Skip net8.0 and net9.0 on arm64 as they may not be available on CI + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64 && + (targetFramework == "net8.0" || targetFramework == "net9.0")) + { + return; + } + var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") .WithSource(); From ae1b1d71d5c1841a38bc3d5173bc2cc4f4248da4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Oct 2025 11:04:27 -0500 Subject: [PATCH 08/20] Remove ItPrefersExplicitFrameworkOptionOverProperty This test fails after I made it check TF at runtime. This was not even the existing behavior, I think we can remove this test. --- .../Run/GivenDotnetRunSelectsTargetFramework.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index 04cc9e1ed837..3775daee0173 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -90,23 +90,6 @@ public void ItRunsWithFrameworkPropertySyntax() .And.HaveStdOutContaining("This string came from the test library!"); } - [Fact] - public void ItPrefersExplicitFrameworkOptionOverProperty() - { - var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") - .WithSource(); - - // Pass both --framework and -p:TargetFramework - // The --framework option should take precedence - new DotnetCommand(Log, "run") - .WithWorkingDirectory(testInstance.Path) - .Execute( - "--framework", ToolsetInfo.CurrentTargetFramework, - "-p:TargetFramework=net8.0") - .Should().Pass() - .And.HaveStdOutContaining($"Target Framework: {ToolsetInfo.CurrentTargetFrameworkMoniker}"); - } - [Fact] public void ItShowsErrorMessageWithAvailableFrameworks_InNonInteractiveMode() { From e18870ae18c3365e5051c439614ddb0166a8ed10 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 31 Oct 2025 10:10:16 -0500 Subject: [PATCH 09/20] Update src/Cli/dotnet/Commands/Run/RunCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 21e11ba32257..8af74c8dfe77 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -54,6 +54,9 @@ public class RunCommand /// /// 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. /// public MSBuildArgs MSBuildArgs { get; private set; } public bool Interactive { get; } From 9141a231e5b90d0a8d36d4e929fbdadb9d9b4bf4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 3 Nov 2025 08:23:00 -0600 Subject: [PATCH 10/20] Sign `Spectre.Console.dll` --- eng/Signing.props | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/Signing.props b/eng/Signing.props index 484697efecbc..52a7baf325a6 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -64,6 +64,7 @@ + From 3847cfb9809fce3b6bf5a58b0c68f37e10fa11e9 Mon Sep 17 00:00:00 2001 From: --get Date: Mon, 3 Nov 2025 14:45:54 -0600 Subject: [PATCH 11/20] small tweaks to enable running against singular-valued targetframeworks lists --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 19 ++++++++++--------- .../Commands/Run/TargetFrameworkSelector.cs | 16 ++++++++++------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 8af74c8dfe77..5536411b8181 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -54,7 +54,7 @@ public class RunCommand /// /// 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. /// @@ -468,7 +468,8 @@ static ProjectInstance EvaluateProject(string? projectFilePath, Func(StringComparer.OrdinalIgnoreCase); globalProperties[Constants.EnableDefaultItems] = "false"; globalProperties[Constants.MSBuildExtensionsPath] = AppContext.BaseDirectory; - + using var collection = new ProjectCollection(globalProperties: globalProperties); var project = collection.LoadProject(ProjectFileFullPath).CreateProjectInstance(); - + packageReferenceCount = RunTelemetry.CountPackageReferences(project); projectReferenceCount = RunTelemetry.CountProjectReferences(project); } @@ -898,10 +899,10 @@ private void SendProjectBasedTelemetry(ProjectLaunchSettingsModel? launchSetting { try { - var currentDir = ProjectFileFullPath != null + var currentDir = ProjectFileFullPath != null ? Path.GetDirectoryName(ProjectFileFullPath) : Directory.GetCurrentDirectory(); - + while (currentDir != null) { if (Directory.Exists(Path.Combine(currentDir, ".git"))) @@ -915,7 +916,7 @@ private void SendProjectBasedTelemetry(ProjectLaunchSettingsModel? launchSetting { // Ignore errors when trying to find repo root } - + return null; } } diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index 92fd65d6b5cc..7ea1a5d664b7 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -54,11 +54,15 @@ public static bool TrySelectTargetFramework( return true; } - var frameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries); + // 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); - // If there's only one framework, no selection needed - if (frameworks.Length <= 1) + // 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; } @@ -73,12 +77,12 @@ public static bool TrySelectTargetFramework( 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(); @@ -98,7 +102,7 @@ public static bool TrySelectTargetFramework( .PageSize(10) .MoreChoicesText($"[grey]({Markup.Escape(CliCommandStrings.RunCommandMoreFrameworksText)})[/]") .AddChoices(frameworks); - + return Spectre.Console.AnsiConsole.Prompt(prompt); } catch (Exception) From 18241ce28be213f410b5280716cd610f5484d4cf Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 3 Nov 2025 15:01:42 -0600 Subject: [PATCH 12/20] Add `EnableSearch()` w/ localization support --- src/Cli/dotnet/Commands/CliCommandStrings.resx | 3 +++ src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs | 4 +++- src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf | 5 +++++ src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf | 5 +++++ 15 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 5a01a54c293b..84dc129ff9c6 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1796,6 +1796,9 @@ Your project targets multiple frameworks. Specify which framework to run using ' Move up and down to reveal more frameworks + + Type to search + Available target frameworks: diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index 7ea1a5d664b7..306304989121 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -101,7 +101,9 @@ public static bool TrySelectTargetFramework( .Title($"[cyan]{Markup.Escape(CliCommandStrings.RunCommandSelectTargetFrameworkPrompt)}[/]") .PageSize(10) .MoreChoicesText($"[grey]({Markup.Escape(CliCommandStrings.RunCommandMoreFrameworksText)})[/]") - .AddChoices(frameworks); + .AddChoices(frameworks) + .EnableSearch() + .SearchPlaceholderText(CliCommandStrings.RunCommandSearchPlaceholderText); return Spectre.Console.AnsiConsole.Prompt(prompt); } diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index f987124ae574..d5ef98445dca 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -2779,6 +2779,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch Upozornění NETSDK1174: Zkratka -p pro --project je zastaralá. Použijte prosím --project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index a394a608bdd8..79cc47c63324 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -2779,6 +2779,11 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches Warnung NETSDK1174: Die Abkürzung von „-p“ für „--project“ ist veraltet. Verwenden Sie „--project“. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index 3c89d8542134..6f061733d32d 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -2779,6 +2779,11 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa Advertencia NETSDK1174: La abreviatura de -p para --project está en desuso. Use --project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index a3c02245fd64..2f0a77f65030 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -2779,6 +2779,11 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à AVERTISSEMENT NETSDK1174 : l’abréviation de-p pour--project est déconseillée. Veuillez utiliser--project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index 5c1a45d07d69..7a7a473b3e24 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -2779,6 +2779,11 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire Avviso NETSDK1174: l'abbreviazione di -p per --project è deprecata. Usare --project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index 24de7a1dca1d..395e19e6f486 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -2779,6 +2779,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 警告 NETSDK1174: --project の省略形である -p は推奨されていません。--project を使用してください。 {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index 0db06a3686de..b232ca4bc37e 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -2779,6 +2779,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 경고 NETSDK1174: --project에 대한 약어 -p는 더 이상 사용되지 않습니다. --project를 사용하세요. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 637bf0bfd014..c9a89ff324fd 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -2779,6 +2779,11 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u Ostrzeżenie NETSDK1174: Skrót -p dla polecenia --project jest przestarzały. Użyj polecenia --project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 389393572dce..99b91524e540 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -2779,6 +2779,11 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa Aviso NETSDK1174: a abreviação de-p para--project é preterida. Use --project. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 9b2052d1b9d6..3b29b97274c3 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -2779,6 +2779,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' Предупреждение NETSDK1174: сокращение "-p" для "--project" не рекомендуется. Используйте "--project". {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index 3dae95e5e3e0..864a72c052e1 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -2779,6 +2779,11 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' Uyarı NETSDK1174: --project için -p kısaltması kullanımdan kaldırıldı. Lütfen --project kullanın. {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 076fec45f2ba..017dbe487ad3 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -2779,6 +2779,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 警告 NETSDK1174: 已弃用使用缩写“-p”来代表“--project”。请使用“--project”。 {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index a053b1da5e26..c5c29b6ada70 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -2779,6 +2779,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' 警告 NETSDK1174: --project 已取代縮寫 -p。請使用 --project。 {Locked="--project"} + + Type to search + Type to search + + Select the target framework to run: Select the target framework to run: From ad8a20ee83e942e901130143b8aaea204e5f6155 Mon Sep 17 00:00:00 2001 From: Premek Vysoky Date: Tue, 4 Nov 2025 11:50:54 +0100 Subject: [PATCH 13/20] Pin darc --- eng/common/vmr-sync.ps1 | 2 +- eng/common/vmr-sync.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/common/vmr-sync.ps1 b/eng/common/vmr-sync.ps1 index 97302f3205be..76e2f9d08fe8 100755 --- a/eng/common/vmr-sync.ps1 +++ b/eng/common/vmr-sync.ps1 @@ -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.." diff --git a/eng/common/vmr-sync.sh b/eng/common/vmr-sync.sh index 44239e331c0c..c038012a55aa 100755 --- a/eng/common/vmr-sync.sh +++ b/eng/common/vmr-sync.sh @@ -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 From 69864e8d41807c26517631b1849d2beb38b91132 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 4 Nov 2025 13:11:14 -0600 Subject: [PATCH 14/20] Fix test failure --- src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index 306304989121..b9a8d9e170a8 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -34,11 +34,12 @@ public static bool TrySelectTargetFramework( } // Evaluate the project to get TargetFrameworks - Microsoft.Build.Evaluation.Project project; + string targetFrameworks; try { using var collection = new ProjectCollection(globalProperties: globalProperties); - project = collection.LoadProject(projectFilePath); + var project = collection.LoadProject(projectFilePath); + targetFrameworks = project.GetPropertyValue("TargetFrameworks"); } catch (InvalidProjectFileException) { @@ -46,8 +47,6 @@ public static bool TrySelectTargetFramework( return true; } - string targetFrameworks = project.GetPropertyValue("TargetFrameworks"); - // If there's no TargetFrameworks property or only one framework, no selection needed if (string.IsNullOrWhiteSpace(targetFrameworks)) { From 3e34f5ad03e941c99451148bd35a536745ffbe07 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 4 Nov 2025 13:21:09 -0600 Subject: [PATCH 15/20] Test for 3847cfb9809fce3b6bf5a58b0c68f37e10fa11e9 --- .../GivenDotnetRunSelectsTargetFramework.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index 3775daee0173..22d21a0e87f6 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -1,14 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.InteropServices; +using System.Text.RegularExpressions; namespace Microsoft.DotNet.Cli.Run.Tests; /// /// Integration tests for target framework selection in dotnet run /// -public class GivenDotnetRunSelectsTargetFramework : SdkTest +public partial class GivenDotnetRunSelectsTargetFramework : SdkTest { public GivenDotnetRunSelectsTargetFramework(ITestOutputHelper log) : base(log) { @@ -212,4 +212,32 @@ public void ItTreatsWhitespaceFrameworkSpecificationAsNotSpecified() result.Should().HaveStdErrContaining("multiple frameworks"); } } + + [Fact] + public void ItAutoSelectsSingleFrameworkInTargetFrameworksProperty() + { + // Reuse the DotnetRunMultiTarget project and modify it to have only one framework + var testInstance = _testAssetsManager.CopyTestAsset("DotnetRunMultiTarget") + .WithSource(); + + // Read the existing .csproj file + var projectPath = Path.Combine(testInstance.Path, "DotnetRunMultiTarget.csproj"); + var projectContent = File.ReadAllText(projectPath); + + // Replace TargetFrameworks with a single framework + projectContent = TargetFrameworksRegex() + .Replace(projectContent, $"{ToolsetInfo.CurrentTargetFramework}"); + File.WriteAllText(projectPath, projectContent); + + // Run without specifying --framework - it should auto-select the single framework + var result = new DotnetCommand(Log, "run") + .WithWorkingDirectory(testInstance.Path) + .Execute(); + + result.Should().Pass() + .And.HaveStdOutContaining($"Target Framework: {ToolsetInfo.CurrentTargetFrameworkMoniker}"); + } + + [GeneratedRegex(@".*?")] + private static partial Regex TargetFrameworksRegex(); } From e47ba5a19cf81fece0ea22069b733979e976b33d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 4 Nov 2025 16:27:04 -0600 Subject: [PATCH 16/20] Update RunFileTests.cs --- test/dotnet.Tests/CommandTests/Run/RunFileTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 3f0b93c776ea..296ed8f6a0eb 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1673,9 +1673,9 @@ class C; new DotnetCommand(Log, "run", "lib.cs") .WithWorkingDirectory(testInstance.Path) - .Execute() + .Execute("--no-interactive") .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); new DotnetCommand(Log, "run", "lib.cs", "--framework", ToolsetInfo.CurrentTargetFramework) .WithWorkingDirectory(testInstance.Path) From 7823fe51521bf2962f7a5e21672277c409932f12 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 4 Nov 2025 16:27:18 -0600 Subject: [PATCH 17/20] Update GivenDotnetRunSelectsTargetFramework.cs --- .../GivenDotnetRunSelectsTargetFramework.cs | 55 ++++--------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs index 22d21a0e87f6..33c83c564103 100644 --- a/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs +++ b/test/dotnet.Tests/CommandTests/Run/GivenDotnetRunSelectsTargetFramework.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.RegularExpressions; +using Microsoft.DotNet.Cli.Commands; namespace Microsoft.DotNet.Cli.Run.Tests; @@ -46,14 +47,8 @@ public void ItFailsInNonInteractiveMode_WhenMultiTFMProjectHasNoFrameworkSpecifi .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") .Execute("--no-interactive"); - result.Should().Fail(); - - if (!TestContext.IsLocalized()) - { - result.Should().HaveStdErrContaining("Unable to run your project"); - result.Should().HaveStdErrContaining("multiple frameworks"); - result.Should().HaveStdErrContaining("--framework"); - } + result.Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); } [Fact] @@ -105,21 +100,8 @@ public void ItShowsErrorMessageWithAvailableFrameworks_InNonInteractiveMode() .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") .Execute("--no-interactive"); - result.Should().Fail(); - - if (!TestContext.IsLocalized()) - { - // Should show the "Unable to run your project" message - result.Should().HaveStdErrContaining("Unable to run your project"); - result.Should().HaveStdErrContaining("Your project targets multiple frameworks"); - - // Should show available frameworks - result.Should().HaveStdErrContaining("Available target frameworks:"); - - // Should show example command - result.Should().HaveStdErrContaining("Example:"); - result.Should().HaveStdErrContaining("dotnet run --framework"); - } + result.Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); } [Fact] @@ -133,13 +115,8 @@ public void ItFailsForMultiTargetedAppWithoutFramework_InNonInteractiveMode() .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") .Execute("--no-interactive"); - result.Should().Fail(); - - if (!TestContext.IsLocalized()) - { - result.Should().HaveStdErrContaining("Unable to run your project"); - result.Should().HaveStdErrContaining("multiple frameworks"); - } + result.Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); } [Theory] @@ -180,13 +157,8 @@ public void ItTreatsEmptyFrameworkSpecificationAsNotSpecified() .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") .Execute("-p:TargetFramework=", "--no-interactive"); // Empty string - result.Should().Fail(); - - if (!TestContext.IsLocalized()) - { - result.Should().HaveStdErrContaining("Unable to run your project"); - result.Should().HaveStdErrContaining("multiple frameworks"); - } + result.Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); } [Fact] @@ -204,13 +176,8 @@ public void ItTreatsWhitespaceFrameworkSpecificationAsNotSpecified() .WithEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") .Execute("-p:TargetFramework= ", "--no-interactive"); // Whitespace - result.Should().Fail(); - - if (!TestContext.IsLocalized()) - { - result.Should().HaveStdErrContaining("Unable to run your project"); - result.Should().HaveStdErrContaining("multiple frameworks"); - } + result.Should().Fail() + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); } [Fact] From 5f5a8f8ed9019ea902a572c370a33149792b8dc6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 4 Nov 2025 16:28:14 -0600 Subject: [PATCH 18/20] Fix multi-targeted 'dotnet run lib.cs' --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 84 +++++++++++++++++-- .../Commands/Run/TargetFrameworkSelector.cs | 15 ++++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 393dc73c38a2..c90c98537c67 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -132,6 +132,12 @@ public int Execute() return 1; } + // For file-based projects, check for multi-targeting before building + if (EntryPointFileFullPath is not null && !TrySelectTargetFrameworkForFileBasedProject()) + { + return 1; + } + Func? projectFactory = null; RunProperties? cachedRunProperties = null; VirtualProjectBuildingCommand? virtualCommand = null; @@ -207,19 +213,83 @@ private bool TrySelectTargetFrameworkIfNeeded() Interactive, out string? selectedFramework)) { - // If a framework was selected, add it to MSBuildArgs - if (selectedFramework is not null) - { - var additionalProperties = new ReadOnlyDictionary( - new Dictionary { { "TargetFramework", selectedFramework } }); - MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties); - } + ApplySelectedFramework(selectedFramework); + return true; + } + + return false; + } + + /// + /// Checks if target framework selection is needed for file-based projects. + /// Parses directives from the source file to detect multi-targeting. + /// + /// True if we can continue, false if we should exit + 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; } + /// + /// Parses a source file to extract target frameworks from directives. + /// + /// Array of frameworks if TargetFrameworks is specified, null otherwise + private static string[]? GetTargetFrameworksFromSourceFile(string sourceFilePath) + { + var sourceFile = SourceFile.Load(sourceFilePath); + var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.Ignore()); + + var targetFrameworksDirective = directives.OfType() + .FirstOrDefault(p => string.Equals(p.Name, "TargetFrameworks", StringComparison.OrdinalIgnoreCase)); + + if (targetFrameworksDirective is null) + { + return null; + } + + return targetFrameworksDirective.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + /// + /// Applies the selected target framework to MSBuildArgs if a framework was provided. + /// + /// The framework to apply, or null if no framework selection was needed + 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( + new Dictionary { { "TargetFramework", selectedFramework } }); + MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties); + } + } + internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings) { if (launchSettings == null) diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs index b9a8d9e170a8..82f8d7c152ba 100644 --- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs +++ b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs @@ -57,6 +57,20 @@ public static bool TrySelectTargetFramework( // users may have added for formatting reasons. var frameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return TrySelectTargetFramework(frameworks, isInteractive, out selectedFramework); + } + + /// + /// 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). + /// + /// Array of target frameworks to choose from + /// Whether we're running in interactive mode (can prompt user) + /// The selected target framework, or null if selection was cancelled + /// True if we should continue, false if we should exit with error + 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) @@ -85,6 +99,7 @@ public static bool TrySelectTargetFramework( Reporter.Error.WriteLine(); Reporter.Error.WriteLine($"{CliCommandStrings.RunCommandExampleText}: dotnet run --framework {frameworks[0]}"); Reporter.Error.WriteLine(); + selectedFramework = null; return false; } } From 5ad22873de829dd52853b703a1c36db098e673b2 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 5 Nov 2025 08:22:45 -0600 Subject: [PATCH 19/20] Update RunFileTests.cs --- test/dotnet.Tests/CommandTests/Run/RunFileTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 9d0c13c25ad1..b0669769555b 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1800,7 +1800,7 @@ public void Build_Exe_MultiTarget() .WithWorkingDirectory(testInstance.Path) .Execute() .Should().Fail() - .And.HaveStdErr(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); + .And.HaveStdErrContaining(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework")); new DotnetCommand(Log, "run", "exe.cs", "--framework", ToolsetInfo.CurrentTargetFramework) .WithWorkingDirectory(testInstance.Path) From 6472ff02821fb94d69a90dcce8640e1915d14110 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 19 Nov 2025 14:01:14 -0600 Subject: [PATCH 20/20] Source code changes from merge w/ main --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 4c0ac02c57ae..09cfbe91720f 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -19,6 +19,7 @@ using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -262,7 +263,7 @@ private bool TrySelectTargetFrameworkForFileBasedProject() private static string[]? GetTargetFrameworksFromSourceFile(string sourceFilePath) { var sourceFile = SourceFile.Load(sourceFilePath); - var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.Ignore()); + var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.Ignore()); var targetFrameworksDirective = directives.OfType() .FirstOrDefault(p => string.Equals(p.Name, "TargetFrameworks", StringComparison.OrdinalIgnoreCase));