Skip to content

Commit 3933774

Browse files
[dotnet-run] Implement DeployToDevice target invocation
Context: dotnet/android#10631 Context: dotnet/android#10640 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. The main change here is to create the `RunCommandSelector` earlier in the `RunCommand` execution, so that it can be used both for selecting the target framework and device before build, and for invoking the `DeployToDevice` target after build. I tested this by making `DotnetRunDevices.csproj` include a target: <Target Name="DeployToDevice" DependsOnTargets="ResolveFrameworkReferences"> <Message Text="DeployToDevice: Deployed to device $(Device) with RuntimeIdentifier $(RuntimeIdentifier)" Importance="high" /> </Target> Where `DependsOnTargets="ResolveFrameworkReferences"` mimics what can happen in the Android workload. I had to stop caching each `ProjectInstance` in `RunCommandSelector`, as doing so can cause: 》"artifacts\tmp\Debug\testing\ItDoesNotCall---A078BA19\DotnetRunDevices.csproj" (DeployToDevice target) (1) -> 》(ResolveFrameworkReferences target) -> 》 artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: The "ResolveFrameworkReferences" task failed unexpectedly. 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: System.ArgumentException: An item with the same key has already been added. Key: Microsoft.NETCore.App 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at System.Linq.Enumerable.SpanToDictionary[TSource,TKey](ReadOnlySpan`1 source, Func`2 keySelector, IEqualityComparer`1 comparer) 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at System.Linq.Enumerable.ToDictionary[TSource,TKey](IEnumerable`1 source, Func`2 keySelector, IEqualityComparer`1 comparer) 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at Microsoft.NET.Build.Tasks.ResolveFrameworkReferences.ExecuteCore() in src\Tasks\Microsoft.NET.Build.Tasks\ResolveFrameworkReferences.cs:line 29 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at Microsoft.NET.Build.Tasks.TaskBase.Execute() in src\Tasks\Common\TaskBase.cs:line 36 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at Microsoft.Build.BackEnd.TaskExecutionHost.Execute() 》artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(410,5): error MSB4018: at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) The `ResolveFrameworkReferences` target will add duplicate `Microsoft.NETCore.App` items within a `ProjectInstance` causing the exception above.
1 parent 49c0d73 commit 3933774

19 files changed

+236
-21
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: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ 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+
using var selector = ProjectFileFullPath is not null
161+
? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, logger)
162+
: null;
163+
if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector))
161164
{
162165
// If --list-devices was specified, this is a successful exit
163166
return ListDevices ? 0 : 1;
@@ -200,6 +203,17 @@ public int Execute()
200203
}
201204
}
202205

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

@@ -234,12 +248,10 @@ public int Execute()
234248
/// Uses a single RunCommandSelector instance for both operations, re-evaluating
235249
/// the project after framework selection to get the correct device list.
236250
/// </summary>
237-
/// <param name="logger">Optional logger for MSBuild operations (device selection)</param>
251+
/// <param name="selector">The RunCommandSelector instance to use for selection</param>
238252
/// <returns>True if we can continue, false if we should exit</returns>
239-
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger)
253+
private bool TrySelectTargetFrameworkAndDeviceIfNeeded(RunCommandSelector selector)
240254
{
241-
Debug.Assert(ProjectFileFullPath is not null);
242-
243255
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
244256

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

259271
if (!ListDevices && hasFramework && hasDevice)
260272
{
261-
// Both framework and device are pre-specified, no need to create selector or logger
273+
// Both framework and device are pre-specified
262274
return true;
263275
}
264276

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

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 (existing item groups) 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)