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
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 @@ -30,6 +30,7 @@ public static class Constants
public const string Build = nameof(Build);
public const string ComputeRunArguments = nameof(ComputeRunArguments);
public const string ComputeAvailableDevices = nameof(ComputeAvailableDevices);
public const string DeployToDevice = nameof(DeployToDevice);
public const string CoreCompile = nameof(CoreCompile);

// MSBuild item metadata
Expand Down
3 changes: 3 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,9 @@ The default is to publish a framework-dependent application.</value>
<data name="RunCommandException" xml:space="preserve">
<value>The build failed. Fix the build errors and run again.</value>
</data>
<data name="RunCommandDeployFailed" xml:space="preserve">
<value>Deployment to device failed. Fix any deployment errors and run again.</value>
</data>
<data name="RunCommandExceptionCouldNotApplyLaunchSettings" xml:space="preserve">
<value>The launch profile "{0}" could not be applied.
{1}</value>
Expand Down
27 changes: 18 additions & 9 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ public int Execute()
try
{
// Pre-run evaluation: Handle target framework and device selection for project-based scenarios
if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(logger))
using var selector = ProjectFileFullPath is not null
? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, logger)
: null;
if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector))
{
// If --list-devices was specified, this is a successful exit
return ListDevices ? 0 : 1;
Expand Down Expand Up @@ -200,6 +203,17 @@ public int Execute()
}
}

// Deploy step: Call DeployToDevice target if available
// This must run even with --no-build, as the user may have selected a different device
if (selector is not null && !selector.TryDeployToDevice())
{
// Only error if we have a valid project (not a .sln file, etc.)
if (selector.HasValidProject)
{
throw new GracefulException(CliCommandStrings.RunCommandDeployFailed);
}
}

ICommand targetCommand = GetTargetCommand(projectFactory, cachedRunProperties, logger);
ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);

Expand Down Expand Up @@ -234,12 +248,10 @@ public int Execute()
/// Uses a single RunCommandSelector instance for both operations, re-evaluating
/// the project after framework selection to get the correct device list.
/// </summary>
/// <param name="logger">Optional logger for MSBuild operations (device selection)</param>
/// <param name="selector">The RunCommandSelector instance to use for selection</param>
/// <returns>True if we can continue, false if we should exit</returns>
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(RunCommandSelector selector)
{
Debug.Assert(ProjectFileFullPath is not null);

var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);

// If user specified --device on command line, add it to global properties and MSBuildArgs
Expand All @@ -258,13 +270,10 @@ private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)

if (!ListDevices && hasFramework && hasDevice)
{
// Both framework and device are pre-specified, no need to create selector or logger
// Both framework and device are pre-specified
return true;
}

// Create a single selector for both framework and device selection
using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, MSBuildArgs, logger);

// Step 1: Select target framework if needed
if (!selector.TrySelectTargetFramework(out string? selectedFramework))
{
Expand Down
54 changes: 45 additions & 9 deletions src/Cli/dotnet/Commands/Run/RunCommandSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,25 @@ internal sealed class RunCommandSelector : IDisposable

private ProjectCollection? _collection;
private Microsoft.Build.Evaluation.Project? _project;
private ProjectInstance? _projectInstance;

/// <summary>
/// Gets whether the selector has a valid project that can be evaluated.
/// This is false for .sln files or other invalid project files.
/// </summary>
public bool HasValidProject { get; private set; }

/// <param name="projectFilePath">Path to the project file to evaluate</param>
/// <param name="globalProperties">Global MSBuild properties to use during evaluation</param>
/// <param name="isInteractive">Whether to prompt the user for selections</param>
/// <param name="msbuildArgs">MSBuild arguments containing properties and verbosity settings</param>
/// <param name="binaryLogger">Optional binary logger for MSBuild operations. The logger will not be disposed by this class.</param>
public RunCommandSelector(
string projectFilePath,
Dictionary<string, string> globalProperties,
bool isInteractive,
MSBuildArgs msbuildArgs,
FacadeLogger? binaryLogger = null)
{
_projectFilePath = projectFilePath;
_globalProperties = globalProperties;
_globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
_isInteractive = isInteractive;
_msbuildArgs = msbuildArgs;
_binaryLogger = binaryLogger;
Expand Down Expand Up @@ -102,9 +106,9 @@ public void InvalidateGlobalProperties(Dictionary<string, string> updatedPropert

// Dispose existing project to force re-evaluation
_project = null;
_projectInstance = null;
_collection?.Dispose();
_collection = null;
HasValidProject = false;
}

/// <summary>
Expand All @@ -114,8 +118,9 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec
{
if (_project is not null)
{
Debug.Assert(_projectInstance is not null);
projectInstance = _projectInstance;
// Create a fresh ProjectInstance for each build operation
// to avoid accumulating state (existing item groups) from previous builds
projectInstance = _project.CreateProjectInstance();
return true;
}

Expand All @@ -126,14 +131,15 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec
loggers: GetLoggers(),
toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
_project = _collection.LoadProject(_projectFilePath);
_projectInstance = _project.CreateProjectInstance();
projectInstance = _projectInstance;
projectInstance = _project.CreateProjectInstance();
HasValidProject = true;
return true;
}
catch (InvalidProjectFileException)
{
// Invalid project file, return false
projectInstance = null;
HasValidProject = false;
return false;
}
}
Expand Down Expand Up @@ -461,6 +467,36 @@ public bool TrySelectDevice(
}
}

/// <summary>
/// Attempts to deploy to a device by calling the DeployToDevice MSBuild target if it exists.
/// This reuses the already-loaded project instance for performance.
/// </summary>
/// <returns>True if deployment succeeded or was skipped (no target), false if deployment failed</returns>
public bool TryDeployToDevice()
{
if (!OpenProjectIfNeeded(out var projectInstance))
{
// Invalid project file
return false;
}

// Check if the DeployToDevice target exists in the project
if (!projectInstance.Targets.ContainsKey(Constants.DeployToDevice))
{
// Target doesn't exist, skip deploy step
return true;
}

// Build the DeployToDevice target
var buildResult = projectInstance.Build(
targets: [Constants.DeployToDevice],
loggers: GetLoggers(),
remoteLoggers: null,
out _);

return buildResult;
}

/// <summary>
/// Gets the list of loggers to use for MSBuild operations.
/// Creates a fresh console logger each time to avoid disposal issues when calling Build() multiple times.
Expand Down
5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

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

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

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

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

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

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

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf

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

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf

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

Loading