Skip to content

Commit cb6877a

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 a498307 commit cb6877a

19 files changed

+244
-19
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: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ public int Execute()
157157
try
158158
{
159159
// Pre-run evaluation: Handle target framework and device selection for project-based scenarios
160-
if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(logger))
160+
// This must happen before build so the selected framework/device are used during build
161+
// The method will run restore first (if needed) then create and return a selector
162+
RunCommandSelector? selector = null;
163+
if (ProjectFileFullPath is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(logger, out selector))
161164
{
165+
selector?.Dispose();
162166
// If --list-devices was specified, this is a successful exit
163167
return ListDevices ? 0 : 1;
164168
}
@@ -200,6 +204,21 @@ public int Execute()
200204
}
201205
}
202206

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)
210+
{
211+
if (!selector.TryDeployToDevice())
212+
{
213+
// Only error if we have a valid project (not a .sln file, etc.)
214+
if (selector.HasValidProject)
215+
{
216+
throw new GracefulException(CliCommandStrings.RunCommandDeployFailed);
217+
}
218+
}
219+
selector.Dispose();
220+
}
221+
203222
ICommand targetCommand = GetTargetCommand(projectFactory, cachedRunProperties, logger);
204223
ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
205224

@@ -234,12 +253,15 @@ public int Execute()
234253
/// Uses a single RunCommandSelector instance for both operations, re-evaluating
235254
/// the project after framework selection to get the correct device list.
236255
/// </summary>
237-
/// <param name="logger">Optional logger for MSBuild operations (device selection)</param>
256+
/// <param name="logger">Optional logger for MSBuild operations</param>
257+
/// <param name="selector">Output parameter for the created selector, needed for later deployment</param>
238258
/// <returns>True if we can continue, false if we should exit</returns>
239-
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)
259+
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger, out RunCommandSelector? selector)
240260
{
241261
Debug.Assert(ProjectFileFullPath is not null);
242262

263+
// Create the selector - note that project evaluation happens lazily, not here
264+
selector = new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, logger);
243265
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
244266

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

259281
if (!ListDevices && hasFramework && hasDevice)
260282
{
261-
// Both framework and device are pre-specified, no need to create selector or logger
283+
// Both framework and device are pre-specified
262284
return true;
263285
}
264286

265-
// Create a single selector for both framework and device selection
266-
using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, MSBuildArgs, logger);
267-
268287
// Step 1: Select target framework if needed
269288
if (!selector.TrySelectTargetFramework(out string? selectedFramework))
270289
{

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

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,25 @@ internal sealed class RunCommandSelector : IDisposable
3131

3232
private ProjectCollection? _collection;
3333
private Microsoft.Build.Evaluation.Project? _project;
34-
private ProjectInstance? _projectInstance;
34+
35+
/// <summary>
36+
/// Gets whether the selector has a valid project that can be evaluated.
37+
/// This is false for .sln files or other invalid project files.
38+
/// </summary>
39+
public bool HasValidProject { get; private set; }
3540

3641
/// <param name="projectFilePath">Path to the project file to evaluate</param>
37-
/// <param name="globalProperties">Global MSBuild properties to use during evaluation</param>
3842
/// <param name="isInteractive">Whether to prompt the user for selections</param>
43+
/// <param name="msbuildArgs">MSBuild arguments containing properties and verbosity settings</param>
3944
/// <param name="binaryLogger">Optional binary logger for MSBuild operations. The logger will not be disposed by this class.</param>
4045
public RunCommandSelector(
4146
string projectFilePath,
42-
Dictionary<string, string> globalProperties,
4347
bool isInteractive,
4448
MSBuildArgs msbuildArgs,
4549
FacadeLogger? binaryLogger = null)
4650
{
4751
_projectFilePath = projectFilePath;
48-
_globalProperties = globalProperties;
52+
_globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
4953
_isInteractive = isInteractive;
5054
_msbuildArgs = msbuildArgs;
5155
_binaryLogger = binaryLogger;
@@ -102,9 +106,9 @@ public void InvalidateGlobalProperties(Dictionary<string, string> updatedPropert
102106

103107
// Dispose existing project to force re-evaluation
104108
_project = null;
105-
_projectInstance = null;
106109
_collection?.Dispose();
107110
_collection = null;
111+
HasValidProject = false;
108112
}
109113

110114
/// <summary>
@@ -114,8 +118,9 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec
114118
{
115119
if (_project is not null)
116120
{
117-
Debug.Assert(_projectInstance is not null);
118-
projectInstance = _projectInstance;
121+
// Create a fresh ProjectInstance for each build operation
122+
// to avoid accumulating state from previous builds
123+
projectInstance = _project.CreateProjectInstance();
119124
return true;
120125
}
121126

@@ -126,14 +131,15 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec
126131
loggers: GetLoggers(),
127132
toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
128133
_project = _collection.LoadProject(_projectFilePath);
129-
_projectInstance = _project.CreateProjectInstance();
130-
projectInstance = _projectInstance;
134+
projectInstance = _project.CreateProjectInstance();
135+
HasValidProject = true;
131136
return true;
132137
}
133138
catch (InvalidProjectFileException)
134139
{
135140
// Invalid project file, return false
136141
projectInstance = null;
142+
HasValidProject = false;
137143
return false;
138144
}
139145
}
@@ -461,6 +467,36 @@ public bool TrySelectDevice(
461467
}
462468
}
463469

470+
/// <summary>
471+
/// Attempts to deploy to a device by calling the DeployToDevice MSBuild target if it exists.
472+
/// This reuses the already-loaded project instance for performance.
473+
/// </summary>
474+
/// <returns>True if deployment succeeded or was skipped (no target), false if deployment failed</returns>
475+
public bool TryDeployToDevice()
476+
{
477+
if (!OpenProjectIfNeeded(out var projectInstance))
478+
{
479+
// Invalid project file
480+
return false;
481+
}
482+
483+
// Check if the DeployToDevice target exists in the project
484+
if (!projectInstance.Targets.ContainsKey(Constants.DeployToDevice))
485+
{
486+
// Target doesn't exist, skip deploy step
487+
return true;
488+
}
489+
490+
// Build the DeployToDevice target
491+
var buildResult = projectInstance.Build(
492+
targets: [Constants.DeployToDevice],
493+
loggers: GetLoggers(),
494+
remoteLoggers: null,
495+
out _);
496+
497+
return buildResult;
498+
}
499+
464500
/// <summary>
465501
/// Gets the list of loggers to use for MSBuild operations.
466502
/// Creates a fresh console logger each time to avoid disposal issues when calling Build() multiple times.

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)