diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index ba5801ff9b8c..fb5dffd622f5 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -96,6 +96,12 @@ public class RunCommand /// public bool ListDevices { get; } + /// + /// Tracks whether restore was performed during device selection phase. + /// If true, we should skip restore in the build phase to avoid redundant work. + /// + private bool _restoreDoneForDeviceSelection; + /// unparsed/arbitrary CLI tokens to be passed to the running application public RunCommand( bool noBuild, @@ -257,7 +263,7 @@ private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger) } // Create a single selector for both framework and device selection - using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, logger); + using var selector = new RunCommandSelector(ProjectFileFullPath, globalProperties, Interactive, MSBuildArgs, logger); // Step 1: Select target framework if needed if (!selector.TrySelectTargetFramework(out string? selectedFramework)) @@ -284,8 +290,10 @@ private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger) // Step 3: Select device if needed if (selector.TrySelectDevice( ListDevices, + NoRestore, out string? selectedDevice, - out string? runtimeIdentifier)) + out string? runtimeIdentifier, + out _restoreDoneForDeviceSelection)) { // If a device was selected (either by user or by prompt), apply it to MSBuildArgs if (selectedDevice is not null) @@ -296,6 +304,10 @@ private bool TrySelectTargetFrameworkAndDeviceIfNeeded(FacadeLogger? logger) if (!string.IsNullOrEmpty(runtimeIdentifier)) { properties["RuntimeIdentifier"] = runtimeIdentifier; + + // If the device added a RuntimeIdentifier, we need to re-restore with that RID + // because the previous restore (if any) didn't include it + _restoreDoneForDeviceSelection = false; } var additionalProperties = new ReadOnlyDictionary(properties); @@ -474,7 +486,7 @@ private void EnsureProjectIsBuilt(out Func? virtualCommand = null; buildResult = new RestoringCommand( MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]), - NoRestore, + NoRestore || _restoreDoneForDeviceSelection, advertiseWorkloadUpdates: false ).Execute(); } diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs index 2faf4764a65d..f10239673125 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs @@ -6,6 +6,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.DotNet.Cli.Utils; using Spectre.Console; @@ -26,6 +27,7 @@ internal sealed class RunCommandSelector : IDisposable private readonly Dictionary _globalProperties; private readonly FacadeLogger? _binaryLogger; private readonly bool _isInteractive; + private readonly MSBuildArgs _msbuildArgs; private ProjectCollection? _collection; private Microsoft.Build.Evaluation.Project? _project; @@ -39,11 +41,13 @@ public RunCommandSelector( string projectFilePath, Dictionary globalProperties, bool isInteractive, + MSBuildArgs msbuildArgs, FacadeLogger? binaryLogger = null) { _projectFilePath = projectFilePath; _globalProperties = globalProperties; _isInteractive = isInteractive; + _msbuildArgs = msbuildArgs; _binaryLogger = binaryLogger; } @@ -119,7 +123,7 @@ private bool OpenProjectIfNeeded([NotNullWhen(true)] out ProjectInstance? projec { _collection = new ProjectCollection( globalProperties: _globalProperties, - loggers: null, + loggers: GetLoggers(), toolsetDefinitionLocations: ToolsetDefinitionLocations.Default); _project = _collection.LoadProject(_projectFilePath); _projectInstance = _project.CreateProjectInstance(); @@ -216,11 +220,14 @@ public record DeviceItem(string Id, string? Description, string? Type, string? S /// /// Computes available devices by calling the ComputeAvailableDevices MSBuild target if it exists. /// + /// Whether restore should be skipped before computing devices /// List of available devices if the target exists, null otherwise + /// True if restore was performed, false otherwise /// True if the target was found and executed, false otherwise - public bool TryComputeAvailableDevices(out List? devices) + public bool TryComputeAvailableDevices(bool noRestore, out List? devices, out bool restoreWasPerformed) { devices = null; + restoreWasPerformed = false; if (!OpenProjectIfNeeded(out var projectInstance)) { @@ -234,10 +241,27 @@ public bool TryComputeAvailableDevices(out List? devices) return false; } + // If restore is allowed, run restore first so device computation sees the restored assets + if (!noRestore) + { + // Run the Restore target + var restoreResult = projectInstance.Build( + targets: ["Restore"], + loggers: GetLoggers(), + remoteLoggers: null, + out _); + if (!restoreResult) + { + return false; + } + + restoreWasPerformed = true; + } + // Build the target var buildResult = projectInstance.Build( targets: [Constants.ComputeAvailableDevices], - loggers: _binaryLogger is null ? null : [_binaryLogger], + loggers: GetLoggers(), remoteLoggers: null, out var targetOutputs); @@ -274,19 +298,24 @@ public bool TryComputeAvailableDevices(out List? devices) /// or shows an error (non-interactive mode). /// /// Whether to list devices and exit + /// Whether restore should be skipped /// The selected device, or null if not needed /// The RuntimeIdentifier for the selected device, or null if not provided + /// True if restore was performed, false otherwise /// True if we should continue, false if we should exit public bool TrySelectDevice( bool listDevices, + bool noRestore, out string? selectedDevice, - out string? runtimeIdentifier) + out string? runtimeIdentifier, + out bool restoreWasPerformed) { selectedDevice = null; runtimeIdentifier = null; + restoreWasPerformed = false; // Try to get available devices from the project - bool targetExists = TryComputeAvailableDevices(out var devices); + bool targetExists = TryComputeAvailableDevices(noRestore, out var devices, out restoreWasPerformed); // If the target doesn't exist, continue without device selection if (!targetExists) @@ -356,8 +385,6 @@ public bool TrySelectDevice( return true; } - - if (_isInteractive) { var deviceItem = PromptForDevice(devices); @@ -433,4 +460,15 @@ public bool TrySelectDevice( return null; } } + + /// + /// 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. + /// + private IEnumerable GetLoggers() + { + if (_binaryLogger is not null) + yield return _binaryLogger; + yield return CommonRunHelpers.GetConsoleLogger(_msbuildArgs); + } } diff --git a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj index c0f6c2109c0f..1214d95cdf76 100644 --- a/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj +++ b/test/TestAssets/TestProjects/DotnetRunDevices/DotnetRunDevices.csproj @@ -5,7 +5,7 @@ net9.0;$(CurrentTargetFramework) - +