diff --git a/documentation/specs/dotnet-run-for-maui.md b/documentation/specs/dotnet-run-for-maui.md
index 6ee879a91010..4dc6178e4409 100644
--- a/documentation/specs/dotnet-run-for-maui.md
+++ b/documentation/specs/dotnet-run-for-maui.md
@@ -56,21 +56,24 @@ to make extensible for .NET MAUI (and future) scenarios.
```xml
-
-
-
+
+
+
-
-
-
-
+
+
+
+
```
-_NOTE: each workload can decide which metadata values for `%(Type)`
-and `%(Status)` are useful, filtering offline devices, etc. The output
-above would be analogous to running `adb devices`, `xcrun simctl list
-devices`, or `xcrun devicectl list devices`._
+_NOTE: each workload can decide which metadata values for `%(Type)`,
+`%(Status)`, and `%(RuntimeIdentifier)` are useful, filtering offline
+devices, etc. The output above would be analogous to running `adb
+devices`, `xcrun simctl list devices`, or `xcrun devicectl list
+devices`. The `%(RuntimeIdentifier)` metadata is optional but
+recommended, as it allows the build system to pass the appropriate RID
+to subsequent build, deploy, and run steps._
* Continuing on...
@@ -81,7 +84,8 @@ devices`, or `xcrun devicectl list devices`._
`--device` switch. Listing the options returned by the
`ComputeAvailableDevices` MSBuild target.
-* `build`: unchanged, but is passed `-p:Device`.
+* `build`: unchanged, but is passed `-p:Device` and optionally `-p:RuntimeIdentifier`
+ if the selected device provided a `%(RuntimeIdentifier)` metadata value.
* `deploy`
@@ -89,16 +93,19 @@ devices`, or `xcrun devicectl list devices`._
iOS or Android workload, etc.
* Call the MSBuild target, passing in the identifier for the selected
- `-p:Device` global MSBuild property.
+ `-p:Device` global MSBuild property, and optionally `-p:RuntimeIdentifier`
+ if the selected device provided a `%(RuntimeIdentifier)` metadata value.
* This step needs to run, even with `--no-build`, as you may have
selected a different device.
-* `ComputeRunArguments`: unchanged, but is passed `-p:Device`.
+* `ComputeRunArguments`: unchanged, but is passed `-p:Device` and optionally
+ `-p:RuntimeIdentifier` if the selected device provided a `%(RuntimeIdentifier)`
+ metadata value.
* `run`: unchanged. `ComputeRunArguments` should have set a valid
`$(RunCommand)` and `$(RunArguments)` using the value supplied by
- `-p:Device`.
+ `-p:Device` and optionally `-p:RuntimeIdentifier`.
## New `dotnet run` Command-line Switches
@@ -139,6 +146,19 @@ A new `--device` switch will:
* The iOS and Android workloads will know how to interpret `$(Device)`
to select an appropriate device, emulator, or simulator.
+## Binary Logs for Device Selection
+
+When using the `-bl:` argument with `dotnet run`, binary logs (`.binlog` files)
+are created to help diagnose issues with device selection and the build process.
+
+For device selection operations (when calling the `ComputeAvailableDevices` target),
+the binlog files follow this naming pattern:
+
+* If you specify `-bl:filename.binlog`, the actual file created will be
+ `filename-dotnet-run-devices.binlog`
+* If you specify `-bl` without a filename, the file created will be
+ `msbuild-dotnet-run-devices.binlog`
+
## What about Launch Profiles?
The iOS and Android workloads ignore all
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
index 0c5aef161367..f2038baae3ef 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
@@ -29,6 +29,7 @@ public static class Constants
// MSBuild targets
public const string Build = nameof(Build);
public const string ComputeRunArguments = nameof(ComputeRunArguments);
+ public const string ComputeAvailableDevices = nameof(ComputeAvailableDevices);
public const string CoreCompile = nameof(CoreCompile);
// MSBuild item metadata
diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx
index 7df5c89ae6c4..fbb06189dc5e 100644
--- a/src/Cli/dotnet/Commands/CliCommandStrings.resx
+++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx
@@ -1786,6 +1786,30 @@ Your project targets multiple frameworks. Specify which framework to run using '
The target runtime to run for.
+
+ The device identifier to use for running the application.
+
+
+ DEVICE
+
+
+ List available devices for running the application.
+
+
+ Available devices:
+
+
+ No devices are available for this project.
+
+
+ Select a device to run on:
+
+
+ Move up and down to reveal more devices
+
+
+ Unable to run this project because multiple devices are available. Please specify which device to use by passing the {0} argument with one of the following values:
+
Path to <application>.runtimeconfig.json file.
diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
index 4a48c4054d79..d6037a58c746 100644
--- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
@@ -102,6 +102,8 @@ public override RunApiOutput Execute()
launchProfile: null,
noLaunchProfile: false,
noLaunchProfileArguments: false,
+ device: null,
+ listDevices: false,
noRestore: false,
noCache: false,
interactive: false,
diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs
index 09cfbe91720f..05e868fce78c 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs
@@ -13,9 +13,9 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.DotNet.Cli.CommandFactory;
+using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;
-using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
@@ -85,6 +85,16 @@ public class RunCommand
///
public bool NoLaunchProfileArguments { get; }
+ ///
+ /// Device identifier to use for running the application.
+ ///
+ public string? Device { get; }
+
+ ///
+ /// Whether to list available devices and exit.
+ ///
+ public bool ListDevices { get; }
+
/// unparsed/arbitrary CLI tokens to be passed to the running application
public RunCommand(
bool noBuild,
@@ -93,6 +103,8 @@ public RunCommand(
string? launchProfile,
bool noLaunchProfile,
bool noLaunchProfileArguments,
+ string? device,
+ bool listDevices,
bool noRestore,
bool noCache,
bool interactive,
@@ -112,6 +124,8 @@ public RunCommand(
LaunchProfile = launchProfile;
NoLaunchProfile = noLaunchProfile;
NoLaunchProfileArguments = noLaunchProfileArguments;
+ Device = device;
+ ListDevices = listDevices;
ApplicationArgs = applicationArgs;
Interactive = interactive;
NoRestore = noRestore;
@@ -128,10 +142,11 @@ public int Execute()
return 1;
}
- // Pre-run evaluation: Handle target framework selection for multi-targeted projects
- if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkIfNeeded())
+ // Pre-run evaluation: Handle target framework and device selection for project-based scenarios
+ if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded())
{
- return 1;
+ // If --list-devices was specified, this is a successful exit
+ return ListDevices ? 0 : 1;
}
// For file-based projects, check for multi-targeting before building
@@ -199,26 +214,88 @@ 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.
+ /// Checks if target framework selection and device selection are needed.
+ /// Uses a single RunCommandSelector instance for both operations, re-evaluating
+ /// the project after framework selection to get the correct device list.
///
/// True if we can continue, false if we should exit
- private bool TrySelectTargetFrameworkIfNeeded()
+ private bool TrySelectTargetFrameworkAndDeviceIfNeeded()
{
Debug.Assert(ProjectFileFullPath is not null);
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
- if (TargetFrameworkSelector.TrySelectTargetFramework(
- ProjectFileFullPath,
- globalProperties,
- Interactive,
- out string? selectedFramework))
+
+ // If user specified --device on command line, add it to global properties and MSBuildArgs
+ if (!string.IsNullOrWhiteSpace(Device))
+ {
+ globalProperties["Device"] = Device;
+ var properties = new Dictionary { { "Device", Device } };
+ var additionalProperties = new ReadOnlyDictionary(properties);
+ MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
+ }
+
+ // Optimization: If BOTH framework AND device are already specified (and we're not listing devices),
+ // we can skip both framework selection and device selection entirely
+ bool hasFramework = globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework);
+ bool hasDevice = globalProperties.TryGetValue("Device", out var preSpecifiedDevice) && !string.IsNullOrWhiteSpace(preSpecifiedDevice);
+
+ if (!ListDevices && hasFramework && hasDevice)
+ {
+ // Both framework and device are pre-specified, no need to create selector or logger
+ return true;
+ }
+
+ // Create a single selector for both framework and device selection
+ FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run-devices");
+ using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, logger);
+
+ // Step 1: Select target framework if needed
+ if (!selector.TrySelectTargetFramework(out string? selectedFramework))
+ {
+ return false;
+ }
+
+ if (selectedFramework is not null)
{
ApplySelectedFramework(selectedFramework);
+
+ // Re-evaluate project with the selected framework so device selection sees the right devices
+ var properties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
+ selector.InvalidateGlobalProperties(properties);
+ }
+
+ // Step 2: Check if device is now pre-specified after framework selection
+ if (!ListDevices && hasDevice)
+ {
+ // Device was pre-specified, we can skip device selection
return true;
}
+ // Step 3: Select device if needed
+ if (selector.TrySelectDevice(
+ ListDevices,
+ out string? selectedDevice,
+ out string? runtimeIdentifier))
+ {
+ // If a device was selected (either by user or by prompt), apply it to MSBuildArgs
+ if (selectedDevice is not null)
+ {
+ var properties = new Dictionary { { "Device", selectedDevice } };
+
+ // If the device provided a RuntimeIdentifier, add it too
+ if (!string.IsNullOrEmpty(runtimeIdentifier))
+ {
+ properties["RuntimeIdentifier"] = runtimeIdentifier;
+ }
+
+ var additionalProperties = new ReadOnlyDictionary(properties);
+ MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
+ }
+
+ // If ListDevices was set, we return true but the caller will exit after listing
+ return !ListDevices;
+ }
+
return false;
}
@@ -246,8 +323,8 @@ private bool TrySelectTargetFrameworkForFileBasedProject()
return true; // Not multi-targeted
}
- // Use TargetFrameworkSelector to handle multi-target selection (or single framework selection)
- if (TargetFrameworkSelector.TrySelectTargetFramework(frameworks, Interactive, out string? selectedFramework))
+ // Use RunCommandSelector to handle multi-target selection (or single framework selection)
+ if (RunCommandSelector.TrySelectTargetFramework(frameworks, Interactive, out string? selectedFramework))
{
ApplySelectedFramework(selectedFramework);
return true;
@@ -805,6 +882,8 @@ public static RunCommand FromParseResult(ParseResult parseResult)
launchProfile: launchProfile,
noLaunchProfile: parseResult.HasOption(RunCommandParser.NoLaunchProfileOption),
noLaunchProfileArguments: parseResult.HasOption(RunCommandParser.NoLaunchProfileArgumentsOption),
+ device: parseResult.GetValue(RunCommandParser.DeviceOption),
+ listDevices: parseResult.HasOption(RunCommandParser.ListDevicesOption),
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),
noCache: parseResult.HasOption(RunCommandParser.NoCacheOption),
interactive: parseResult.GetValue(RunCommandParser.InteractiveOption),
diff --git a/src/Cli/dotnet/Commands/Run/RunCommandParser.cs b/src/Cli/dotnet/Commands/Run/RunCommandParser.cs
index 0f851f575c7e..787ab2a3b863 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommandParser.cs
@@ -48,6 +48,18 @@ internal static class RunCommandParser
Description = CliCommandStrings.CommandOptionNoLaunchProfileArgumentsDescription
};
+ public static readonly Option DeviceOption = new("--device")
+ {
+ Description = CliCommandStrings.CommandOptionDeviceDescription,
+ HelpName = CliCommandStrings.CommandOptionDeviceHelpName
+ };
+
+ public static readonly Option ListDevicesOption = new("--list-devices")
+ {
+ Description = CliCommandStrings.CommandOptionListDevicesDescription,
+ Arity = ArgumentArity.Zero
+ };
+
public static readonly Option NoBuildOption = new("--no-build")
{
Description = CliCommandStrings.CommandOptionNoBuildDescription,
@@ -98,6 +110,8 @@ private static Command ConstructCommand()
command.Options.Add(PropertyOption);
command.Options.Add(LaunchProfileOption);
command.Options.Add(NoLaunchProfileOption);
+ command.Options.Add(DeviceOption);
+ command.Options.Add(ListDevicesOption);
command.Options.Add(NoBuildOption);
command.Options.Add(InteractiveOption);
command.Options.Add(NoRestoreOption);
diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
new file mode 100644
index 000000000000..205f607e07cc
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
@@ -0,0 +1,439 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Build.Evaluation;
+using Microsoft.Build.Exceptions;
+using Microsoft.Build.Execution;
+using Microsoft.DotNet.Cli.Utils;
+using Spectre.Console;
+
+namespace Microsoft.DotNet.Cli.Commands.Run;
+
+///
+/// Handles target framework and device selection for dotnet run.
+/// Caches the project instance to avoid reloading it multiple times.
+///
+internal sealed class RunCommandSelector : IDisposable
+{
+ // Spectre.Console markup color constants
+ private const string CyanMarkup = "[cyan]";
+ private const string GrayMarkup = "[gray]";
+ private const string EndMarkup = "[/]";
+
+ private readonly string _projectFilePath;
+ private readonly Dictionary _globalProperties;
+ private readonly FacadeLogger? _binaryLogger;
+ private readonly bool _isInteractive;
+
+ private ProjectCollection? _collection;
+ private Microsoft.Build.Evaluation.Project? _project;
+ private ProjectInstance? _projectInstance;
+ private bool _disposed;
+
+ public RunCommandSelector(
+ string projectFilePath,
+ Dictionary globalProperties,
+ bool isInteractive,
+ FacadeLogger? binaryLogger = null)
+ {
+ _projectFilePath = projectFilePath;
+ _globalProperties = globalProperties;
+ _isInteractive = isInteractive;
+ _binaryLogger = binaryLogger;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The selected target framework, or null if not needed
+ /// True if we should continue, false if we should exit with error
+ public bool TrySelectTargetFramework(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
+ if (!OpenProjectIfNeeded(out var projectInstance))
+ {
+ // Invalid project file, return true to continue for normal error handling
+ return true;
+ }
+ string targetFrameworks = projectInstance.GetPropertyValue("TargetFrameworks");
+
+ // 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);
+ }
+
+ ///
+ /// Invalidates the loaded project with updated global properties.
+ /// This is needed after framework selection to get the correct device list for that framework.
+ ///
+ public void InvalidateGlobalProperties(Dictionary updatedProperties)
+ {
+ // Update our stored global properties
+ foreach (var (key, value) in updatedProperties)
+ {
+ _globalProperties[key] = value;
+ }
+
+ // Dispose existing project to force re-evaluation
+ _project = null;
+ _projectInstance = null;
+ _collection?.Dispose();
+ _collection = null;
+ }
+
+ ///
+ /// Opens the project if it hasn't been opened yet.
+ ///
+ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projectInstance)
+ {
+ if (_project is not null)
+ {
+ Debug.Assert(_projectInstance is not null);
+ projectInstance = _projectInstance;
+ return true;
+ }
+
+ try
+ {
+ _collection = new ProjectCollection(
+ globalProperties: _globalProperties,
+ loggers: null,
+ toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
+ _project = _collection.LoadProject(_projectFilePath);
+ _projectInstance = _project.CreateProjectInstance();
+ projectInstance = _projectInstance;
+ return true;
+ }
+ catch (InvalidProjectFileException)
+ {
+ // Invalid project file, return false
+ projectInstance = null;
+ return false;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _binaryLogger?.ReallyShutdown();
+ _collection?.Dispose();
+ _disposed = true;
+ }
+
+ ///
+ /// 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)
+ {
+ 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;
+ }
+ }
+
+ ///
+ /// 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($"{CyanMarkup}{Markup.Escape(CliCommandStrings.RunCommandSelectTargetFrameworkPrompt)}{EndMarkup}")
+ .PageSize(10)
+ .MoreChoicesText($"{GrayMarkup}({Markup.Escape(CliCommandStrings.RunCommandMoreFrameworksText)}){EndMarkup}")
+ .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;
+ }
+ }
+
+ ///
+ /// Represents a device item returned from the ComputeAvailableDevices MSBuild target.
+ ///
+ public record DeviceItem(string Id, string? Description, string? Type, string? Status, string? RuntimeIdentifier);
+
+ ///
+ /// Computes available devices by calling the ComputeAvailableDevices MSBuild target if it exists.
+ ///
+ /// List of available devices if the target exists, null otherwise
+ /// True if the target was found and executed, false otherwise
+ public bool TryComputeAvailableDevices(out List? devices)
+ {
+ devices = null;
+
+ if (!OpenProjectIfNeeded(out var projectInstance))
+ {
+ // Invalid project file, return false
+ return false;
+ }
+
+ // Check if the ComputeAvailableDevices target exists
+ if (!projectInstance.Targets.ContainsKey(Constants.ComputeAvailableDevices))
+ {
+ return false;
+ }
+
+ // Build the target
+ var buildResult = projectInstance.Build(
+ targets: [Constants.ComputeAvailableDevices],
+ loggers: _binaryLogger is null ? null : [_binaryLogger],
+ remoteLoggers: null,
+ out var targetOutputs);
+
+ if (!buildResult)
+ {
+ return false;
+ }
+
+ // Get the Devices items from the target output
+ if (!targetOutputs.TryGetValue(Constants.ComputeAvailableDevices, out var targetResult))
+ {
+ return false;
+ }
+
+ devices = new(targetResult.Items.Length);
+
+ foreach (var item in targetResult.Items)
+ {
+ devices.Add(new DeviceItem(
+ item.ItemSpec,
+ item.GetMetadata("Description"),
+ item.GetMetadata("Type"),
+ item.GetMetadata("Status"),
+ item.GetMetadata("RuntimeIdentifier")
+ ));
+ }
+
+ return true;
+ }
+
+ ///
+ /// Attempts to select a device for running the application.
+ /// If devices are available and none was specified, prompts the user to select one (interactive mode)
+ /// or shows an error (non-interactive mode).
+ ///
+ /// Whether to list devices and exit
+ /// The selected device, or null if not needed
+ /// The RuntimeIdentifier for the selected device, or null if not provided
+ /// True if we should continue, false if we should exit
+ public bool TrySelectDevice(
+ bool listDevices,
+ out string? selectedDevice,
+ out string? runtimeIdentifier)
+ {
+ selectedDevice = null;
+ runtimeIdentifier = null;
+
+ // Try to get available devices from the project
+ bool targetExists = TryComputeAvailableDevices(out var devices);
+
+ // If the target doesn't exist, continue without device selection
+ if (!targetExists)
+ {
+ // No device support in this project
+ return true;
+ }
+
+ // Target exists - check if we have devices
+ if (devices is null || devices.Count == 0)
+ {
+ if (listDevices)
+ {
+ Reporter.Output.WriteLine(CliCommandStrings.RunCommandNoDevicesAvailable);
+ return true;
+ }
+
+ // Target exists but no devices available - this is an error
+ Reporter.Error.WriteLine(CliCommandStrings.RunCommandNoDevicesAvailable);
+ return false;
+ }
+
+ // If listing devices, display them and exit
+ if (listDevices)
+ {
+ Reporter.Output.WriteLine(CliCommandStrings.RunCommandAvailableDevices);
+ Reporter.Output.WriteLine();
+
+ for (int i = 0; i < devices.Count; i++)
+ {
+ var device = devices[i];
+ var displayBuilder = new StringBuilder($" {i + 1}. {device.Id}");
+
+ if (!string.IsNullOrWhiteSpace(device.Description))
+ {
+ displayBuilder.Append($" - {device.Description}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(device.Type))
+ {
+ displayBuilder.Append($" ({device.Type}");
+ if (!string.IsNullOrWhiteSpace(device.Status))
+ {
+ displayBuilder.Append($", {device.Status}");
+ }
+ displayBuilder.Append(')');
+ }
+ else if (!string.IsNullOrWhiteSpace(device.Status))
+ {
+ displayBuilder.Append($" ({device.Status})");
+ }
+
+ Reporter.Output.WriteLine(displayBuilder.ToString());
+ }
+
+ Reporter.Output.WriteLine();
+ Reporter.Output.WriteLine($"{CliCommandStrings.RunCommandExampleText}: dotnet run --device {devices[0].Id}");
+ Reporter.Output.WriteLine();
+ return true;
+ }
+
+ // If there's only one device, automatically select it (similar to single framework selection)
+ if (devices.Count == 1)
+ {
+ selectedDevice = devices[0].Id;
+ runtimeIdentifier = devices[0].RuntimeIdentifier;
+ return true;
+ }
+
+
+
+ if (_isInteractive)
+ {
+ var deviceItem = PromptForDevice(devices);
+ if (deviceItem is null)
+ {
+ return false;
+ }
+
+ selectedDevice = deviceItem.Id;
+ runtimeIdentifier = deviceItem.RuntimeIdentifier;
+ return true;
+ }
+ else
+ {
+ Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyDevice, "--device"));
+ Reporter.Error.WriteLine();
+ Reporter.Error.WriteLine(CliCommandStrings.RunCommandAvailableDevices);
+ Reporter.Error.WriteLine();
+
+ for (int i = 0; i < devices.Count; i++)
+ {
+ var device = devices[i];
+ var displayText = $" {i + 1}. {device.Id}";
+
+ if (!string.IsNullOrWhiteSpace(device.Description))
+ {
+ displayText += $" - {device.Description}";
+ }
+
+ Reporter.Error.WriteLine(displayText);
+ }
+
+ Reporter.Error.WriteLine();
+ Reporter.Error.WriteLine($"{CliCommandStrings.RunCommandExampleText}: dotnet run --device {devices[0].Id}");
+ Reporter.Error.WriteLine();
+ return false;
+ }
+ }
+
+ ///
+ /// Prompts the user to select a device from the available options using Spectre.Console.
+ ///
+ private static DeviceItem? PromptForDevice(List devices)
+ {
+ List<(string Display, DeviceItem Device)> choices = new(devices.Count);
+ foreach (var d in devices)
+ {
+ var display = d.Id;
+ if (!string.IsNullOrWhiteSpace(d.Description))
+ {
+ display += $" - {d.Description}";
+ }
+ choices.Add((display, d));
+ }
+
+ try
+ {
+ var prompt = new SelectionPrompt<(string Display, DeviceItem Device)>()
+ .Title($"{CyanMarkup}{Markup.Escape(CliCommandStrings.RunCommandSelectDevicePrompt)}{EndMarkup}")
+ .PageSize(10)
+ .MoreChoicesText($"{GrayMarkup}({Markup.Escape(CliCommandStrings.RunCommandMoreDevicesText)}){EndMarkup}")
+ .AddChoices(choices)
+ .UseConverter(choice => choice.Display)
+ .EnableSearch()
+ .SearchPlaceholderText(CliCommandStrings.RunCommandSearchPlaceholderText);
+
+ var (Display, Device) = Spectre.Console.AnsiConsole.Prompt(prompt);
+ return Device;
+ }
+ catch (Exception)
+ {
+ // If Spectre.Console fails (e.g., terminal doesn't support it), return null
+ return null;
+ }
+ }
+}
diff --git a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs b/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs
deleted file mode 100644
index 82f8d7c152ba..000000000000
--- a/src/Cli/dotnet/Commands/Run/TargetFrameworkSelector.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-// 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
-{
- ///
- /// 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
- 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);
- }
-
- ///
- /// 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)
- {
- 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;
- }
- }
-
- ///
- /// 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)
- .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;
- }
- }
-}
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
index 530a26354f44..3805b9b8be5c 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
@@ -92,6 +92,21 @@
.NET Builder
+
+ The device identifier to use for running the application.
+ The device identifier to use for running the application.
+
+
+
+ DEVICE
+ DEVICE
+
+
+
+ List available devices for running the application.
+ List available devices for running the application.
+
+ Do not display the startup banner or the copyright message.Nezobrazovat úvodní nápis ani zprávu o autorských právech
@@ -2657,6 +2672,11 @@ Ve výchozím nastavení je publikována aplikace závislá na architektuře.Příkaz rozhraní .NET pro spuštění
+
+ Available devices:
+ Available devices:
+
+ Available target frameworks:Available target frameworks:
@@ -2717,6 +2737,11 @@ Cílem spustitelného projektu by mělo být TFM (například net5.0) a jeho Out
Aktuální {1} je {2}.
{0} is project file path. {1} is dotnet framework version. {2} is the project output type.{Locked="OutputType"}{Locked="Exe"}
+
+ Unable to run this project because multiple devices are available. Please specify which device to use by passing the {0} argument with one of the following values:
+ Unable to run this project because multiple devices are available. Please specify which device to use by passing the {0} argument with one of the following values:
+
+ Unable to run your project
Your project targets multiple frameworks. Specify which framework to run using '{0}'.
@@ -2724,11 +2749,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 devices
+ Move up and down to reveal more devices
+
+ Move up and down to reveal more frameworksMove up and down to reveal more frameworks
+
+ No devices are available for this project.
+ No devices are available for this project.
+
+ 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.
@@ -2739,6 +2774,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch
Type to search
+
+ Select a device to run on:
+ Select a device to run on:
+
+ Select the target framework to run:Select the target framework to run:
@@ -4153,4 +4193,4 @@ Pokud chcete zobrazit hodnotu, zadejte odpovídající volbu příkazového řá