Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions documentation/specs/dotnet-run-for-maui.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,24 @@ to make extensible for .NET MAUI (and future) scenarios.
```xml
<ItemGroup>
<!-- Android examples -->
<Devices Include="emulator-5554" Description="Pixel 7 - API 35" Type="Emulator" Status="Offline" />
<Devices Include="emulator-5555" Description="Pixel 7 - API 36" Type="Emulator" Status="Online" />
<Devices Include="0A041FDD400327" Description="Pixel 7 Pro" Type="Device" Status="Online" />
<Devices Include="emulator-5554" Description="Pixel 7 - API 35" Type="Emulator" Status="Offline" RuntimeIdentifier="android-x64" />
<Devices Include="emulator-5555" Description="Pixel 7 - API 36" Type="Emulator" Status="Online" RuntimeIdentifier="android-x64" />
<Devices Include="0A041FDD400327" Description="Pixel 7 Pro" Type="Device" Status="Online" RuntimeIdentifier="android-arm64" />
<!-- iOS examples -->
<Devices Include="94E71AE5-8040-4DB2-8A9C-6CD24EF4E7DE" Description="iPhone 11 - iOS 18.6" Type="Simulator" Status="Shutdown" />
<Devices Include="FBF5DCE8-EE2B-4215-8118-3A2190DE1AD7" Description="iPhone 14 - iOS 26.0" Type="Simulator" Status="Booted" />
<Devices Include="23261B78-1E31-469C-A46E-1776D386EFD8" Description="My iPhone 13" Type="Device" Status="Unavailable" />
<Devices Include="AF40CC64-2CDB-5F16-9651-86BCDF380881" Description="My iPhone 15" Type="Device" Status="Paired" />
<Devices Include="94E71AE5-8040-4DB2-8A9C-6CD24EF4E7DE" Description="iPhone 11 - iOS 18.6" Type="Simulator" Status="Shutdown" RuntimeIdentifier="iossimulator-arm64" />
<Devices Include="FBF5DCE8-EE2B-4215-8118-3A2190DE1AD7" Description="iPhone 14 - iOS 26.0" Type="Simulator" Status="Booted" RuntimeIdentifier="iossimulator-arm64" />
<Devices Include="23261B78-1E31-469C-A46E-1776D386EFD8" Description="My iPhone 13" Type="Device" Status="Unavailable" RuntimeIdentifier="ios-arm64" />
<Devices Include="AF40CC64-2CDB-5F16-9651-86BCDF380881" Description="My iPhone 15" Type="Device" Status="Paired" RuntimeIdentifier="ios-arm64" />
</ItemGroup>
```

_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...

Expand All @@ -81,24 +84,28 @@ 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`

* If a `DeployToDevice` MSBuild target is available, provided by the
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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1786,6 +1786,30 @@ Your project targets multiple frameworks. Specify which framework to run using '
<data name="RunRuntimeOptionDescription" xml:space="preserve">
<value>The target runtime to run for.</value>
</data>
<data name="CommandOptionDeviceDescription" xml:space="preserve">
<value>The device identifier to use for running the application.</value>
</data>
<data name="CommandOptionDeviceHelpName" xml:space="preserve">
<value>DEVICE</value>
</data>
<data name="CommandOptionListDevicesDescription" xml:space="preserve">
<value>List available devices for running the application.</value>
</data>
<data name="RunCommandAvailableDevices" xml:space="preserve">
<value>Available devices:</value>
</data>
<data name="RunCommandNoDevicesAvailable" xml:space="preserve">
<value>No devices are available for this project.</value>
</data>
<data name="RunCommandSelectDevicePrompt" xml:space="preserve">
<value>Select a device to run on:</value>
</data>
<data name="RunCommandMoreDevicesText" xml:space="preserve">
<value>Move up and down to reveal more devices</value>
</data>
<data name="RunCommandExceptionUnableToRunSpecifyDevice" xml:space="preserve">
<value>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:</value>
</data>
<data name="RuntimeConfigDefinition" xml:space="preserve">
<value>Path to &lt;application&gt;.runtimeconfig.json file.</value>
</data>
Expand Down
2 changes: 2 additions & 0 deletions src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public override RunApiOutput Execute()
launchProfile: null,
noLaunchProfile: false,
noLaunchProfileArguments: false,
device: null,
listDevices: false,
noRestore: false,
noCache: false,
interactive: false,
Expand Down
109 changes: 94 additions & 15 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,6 +85,16 @@ public class RunCommand
/// </summary>
public bool NoLaunchProfileArguments { get; }

/// <summary>
/// Device identifier to use for running the application.
/// </summary>
public string? Device { get; }

/// <summary>
/// Whether to list available devices and exit.
/// </summary>
public bool ListDevices { get; }

/// <param name="applicationArgs">unparsed/arbitrary CLI tokens to be passed to the running application</param>
public RunCommand(
bool noBuild,
Expand All @@ -93,6 +103,8 @@ public RunCommand(
string? launchProfile,
bool noLaunchProfile,
bool noLaunchProfileArguments,
string? device,
bool listDevices,
bool noRestore,
bool noCache,
bool interactive,
Expand All @@ -112,6 +124,8 @@ public RunCommand(
LaunchProfile = launchProfile;
NoLaunchProfile = noLaunchProfile;
NoLaunchProfileArguments = noLaunchProfileArguments;
Device = device;
ListDevices = listDevices;
ApplicationArgs = applicationArgs;
Interactive = interactive;
NoRestore = noRestore;
Expand All @@ -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
Expand Down Expand Up @@ -199,26 +214,88 @@ public int Execute()
}

/// <summary>
/// Checks if target framework selection is needed for multi-targeted projects.
/// If needed and we're in interactive mode, prompts the user to select a framework.
/// If needed and we're in non-interactive mode, shows an error.
/// 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.
/// </summary>
/// <returns>True if we can continue, false if we should exit</returns>
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<string, string> { { "Device", Device } };
var additionalProperties = new ReadOnlyDictionary<string, string>(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<string, string> { { "Device", selectedDevice } };

// If the device provided a RuntimeIdentifier, add it too
if (!string.IsNullOrEmpty(runtimeIdentifier))
{
properties["RuntimeIdentifier"] = runtimeIdentifier;
}

var additionalProperties = new ReadOnlyDictionary<string, string>(properties);
MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
}

// If ListDevices was set, we return true but the caller will exit after listing
return !ListDevices;
}

return false;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions src/Cli/dotnet/Commands/Run/RunCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ internal static class RunCommandParser
Description = CliCommandStrings.CommandOptionNoLaunchProfileArgumentsDescription
};

public static readonly Option<string> DeviceOption = new("--device")
{
Description = CliCommandStrings.CommandOptionDeviceDescription,
HelpName = CliCommandStrings.CommandOptionDeviceHelpName
};

public static readonly Option<bool> ListDevicesOption = new("--list-devices")
{
Description = CliCommandStrings.CommandOptionListDevicesDescription,
Arity = ArgumentArity.Zero
};

public static readonly Option<bool> NoBuildOption = new("--no-build")
{
Description = CliCommandStrings.CommandOptionNoBuildDescription,
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading