Skip to content

Commit 09f9f0d

Browse files
[dotnet-run] Implement DeployToDevice target invocation
Context: dotnet/android#10631 Add support for calling the `DeployToDevice` MSBuild target during `dotnet run`. The target is invoked after the build step (or with --no-build) to enable deployment to physical devices or emulators. - Create `RunCommandSelector` once per run for framework/device selection and deployment - Call `DeployToDevice` target if it exists in the project - Reuse cached `ProjectInstance` for performance - Add localized message for deployment failures - Added tests
1 parent e300dea commit 09f9f0d

19 files changed

+257
-16
lines changed

src/Cli/Microsoft.DotNet.Cli.Utils/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public static class Constants
3030
public const string Build = nameof(Build);
3131
public const string ComputeRunArguments = nameof(ComputeRunArguments);
3232
public const string ComputeAvailableDevices = nameof(ComputeAvailableDevices);
33+
public const string DeployToDevice = nameof(DeployToDevice);
3334
public const string CoreCompile = nameof(CoreCompile);
3435

3536
// MSBuild item metadata

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,6 +1717,9 @@ The default is to publish a framework-dependent application.</value>
17171717
<data name="RunCommandException" xml:space="preserve">
17181718
<value>The build failed. Fix the build errors and run again.</value>
17191719
</data>
1720+
<data name="RunCommandDeployFailed" xml:space="preserve">
1721+
<value>Deployment to device failed. Fix any deployment errors and run again.</value>
1722+
</data>
17201723
<data name="RunCommandExceptionCouldNotApplyLaunchSettings" xml:space="preserve">
17211724
<value>The launch profile "{0}" could not be applied.
17221725
{1}</value>

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,20 @@ public int Execute()
148148
FacadeLogger? logger = ProjectFileFullPath is not null
149149
? LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run")
150150
: null;
151+
152+
// Create selector for project-based runs to handle framework/device selection and deploy
153+
using var selector = ProjectFileFullPath is not null
154+
? new RunCommandSelector(
155+
ProjectFileFullPath,
156+
MSBuildArgs,
157+
Interactive,
158+
logger)
159+
: null;
160+
151161
try
152162
{
153163
// Pre-run evaluation: Handle target framework and device selection for project-based scenarios
154-
if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(logger))
164+
if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector))
155165
{
156166
// If --list-devices was specified, this is a successful exit
157167
return ListDevices ? 0 : 1;
@@ -194,6 +204,17 @@ public int Execute()
194204
}
195205
}
196206

207+
// Deploy step: Call DeployToDevice target if available
208+
// This must run even with --no-build, as the user may have selected a different device
209+
if (selector is not null && !selector.TryDeployToDevice())
210+
{
211+
// Only error if we have a valid project (not a .sln file, etc.)
212+
if (selector.HasValidProject)
213+
{
214+
throw new GracefulException(CliCommandStrings.RunCommandDeployFailed);
215+
}
216+
}
217+
197218
ICommand targetCommand = GetTargetCommand(projectFactory, cachedRunProperties, logger);
198219
ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
199220

@@ -228,11 +249,11 @@ public int Execute()
228249
/// Uses a single RunCommandSelector instance for both operations, re-evaluating
229250
/// the project after framework selection to get the correct device list.
230251
/// </summary>
231-
/// <param name="logger">Optional logger for MSBuild operations (device selection)</param>
252+
/// <param name="selector">The selector to use for framework and device selection</param>
232253
/// <returns>True if we can continue, false if we should exit</returns>
233-
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)
254+
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(RunCommandSelector selector)
234255
{
235-
Debug.Assert(ProjectFileFullPath is not null);
256+
Debug.Assert(selector is not null);
236257

237258
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
238259

@@ -246,19 +267,16 @@ private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)
246267
}
247268

248269
// Optimization: If BOTH framework AND device are already specified (and we're not listing devices),
249-
// we can skip both framework selection and device selection entirely
270+
// we can skip device selection UI
250271
bool hasFramework = globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework);
251272
bool hasDevice = globalProperties.TryGetValue("Device", out var preSpecifiedDevice) && !string.IsNullOrWhiteSpace(preSpecifiedDevice);
252273

253274
if (!ListDevices && hasFramework && hasDevice)
254275
{
255-
// Both framework and device are pre-specified, no need to create selector or logger
276+
// Both framework and device are pre-specified, skip device selection UI
256277
return true;
257278
}
258279

259-
// Create a single selector for both framework and device selection
260-
using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, logger);
261-
262280
// Step 1: Select target framework if needed
263281
if (!selector.TrySelectTargetFramework(out string? selectedFramework))
264282
{

src/Cli/dotnet/Commands/Run/RunCommandSelector.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.Build.Evaluation;
77
using Microsoft.Build.Exceptions;
88
using Microsoft.Build.Execution;
9+
using Microsoft.Build.Framework;
910
using Microsoft.DotNet.Cli.Utils;
1011
using Spectre.Console;
1112

@@ -25,26 +26,34 @@ internal sealed class RunCommandSelector : IDisposable
2526
private readonly string _projectFilePath;
2627
private readonly Dictionary<string, string> _globalProperties;
2728
private readonly FacadeLogger? _binaryLogger;
29+
private readonly ILogger? _consoleLogger;
2830
private readonly bool _isInteractive;
2931

3032
private ProjectCollection? _collection;
3133
private Microsoft.Build.Evaluation.Project? _project;
3234
private ProjectInstance? _projectInstance;
3335

36+
/// <summary>
37+
/// Gets whether the selector has a valid project that can be evaluated.
38+
/// This is false for .sln files or other invalid project files.
39+
/// </summary>
40+
public bool HasValidProject { get; private set; }
41+
3442
/// <param name="projectFilePath">Path to the project file to evaluate</param>
35-
/// <param name="globalProperties">Global MSBuild properties to use during evaluation</param>
43+
/// <param name="msbuildArgs">MSBuild arguments containing properties and verbosity settings</param>
3644
/// <param name="isInteractive">Whether to prompt the user for selections</param>
3745
/// <param name="binaryLogger">Optional binary logger for MSBuild operations. The logger will not be disposed by this class.</param>
3846
public RunCommandSelector(
3947
string projectFilePath,
40-
Dictionary<string, string> globalProperties,
48+
MSBuildArgs msbuildArgs,
4149
bool isInteractive,
4250
FacadeLogger? binaryLogger = null)
4351
{
4452
_projectFilePath = projectFilePath;
45-
_globalProperties = globalProperties;
53+
_globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
4654
_isInteractive = isInteractive;
4755
_binaryLogger = binaryLogger;
56+
_consoleLogger = CommonRunHelpers.GetConsoleLogger(msbuildArgs);
4857
}
4958

5059
/// <summary>
@@ -101,6 +110,7 @@ public void InvalidateGlobalProperties(Dictionary<string, string> updatedPropert
101110
_projectInstance = null;
102111
_collection?.Dispose();
103112
_collection = null;
113+
HasValidProject = false;
104114
}
105115

106116
/// <summary>
@@ -124,12 +134,14 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec
124134
_project = _collection.LoadProject(_projectFilePath);
125135
_projectInstance = _project.CreateProjectInstance();
126136
projectInstance = _projectInstance;
137+
HasValidProject = true;
127138
return true;
128139
}
129140
catch (InvalidProjectFileException)
130141
{
131142
// Invalid project file, return false
132143
projectInstance = null;
144+
HasValidProject = false;
133145
return false;
134146
}
135147
}
@@ -237,7 +249,7 @@ public bool TryComputeAvailableDevices(out List<DeviceItem>? devices)
237249
// Build the target
238250
var buildResult = projectInstance.Build(
239251
targets: [Constants.ComputeAvailableDevices],
240-
loggers: _binaryLogger is null ? null : [_binaryLogger],
252+
loggers: GetLoggers(),
241253
remoteLoggers: null,
242254
out var targetOutputs);
243255

@@ -433,4 +445,45 @@ public bool TrySelectDevice(
433445
return null;
434446
}
435447
}
448+
449+
/// <summary>
450+
/// Attempts to deploy to a device by calling the DeployToDevice MSBuild target if it exists.
451+
/// This reuses the already-loaded project instance for performance.
452+
/// </summary>
453+
/// <returns>True if deployment succeeded or was skipped (no target), false if deployment failed</returns>
454+
public bool TryDeployToDevice()
455+
{
456+
if (!OpenProjectIfNeeded(out var projectInstance))
457+
{
458+
// Invalid project file
459+
return false;
460+
}
461+
462+
// Check if the DeployToDevice target exists in the project
463+
if (!projectInstance.Targets.ContainsKey(Constants.DeployToDevice))
464+
{
465+
// Target doesn't exist, skip deploy step
466+
return true;
467+
}
468+
469+
// Build the DeployToDevice target
470+
var buildResult = projectInstance.Build(
471+
targets: [Constants.DeployToDevice],
472+
loggers: GetLoggers(),
473+
remoteLoggers: null,
474+
out _);
475+
476+
return buildResult;
477+
}
478+
479+
/// <summary>
480+
/// Gets the list of loggers to use for MSBuild operations.
481+
/// </summary>
482+
private IEnumerable<ILogger> GetLoggers()
483+
{
484+
if (_binaryLogger is not null)
485+
yield return _binaryLogger;
486+
if (_consoleLogger is not null)
487+
yield return _consoleLogger;
488+
}
436489
}

src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)