From 880d09a24af8a5322aec33934add9c881ccf16ec Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 10 Dec 2025 10:25:40 -0600 Subject: [PATCH 1/3] [dotnet-run] fix logging and `Restore` for device selection c164a9bc is mostly working with two issues I've discovered while testing: 1. If `ComputeAvailableDevices` requires `Restore` to have run (which is actually the case for iOS and Android), `dotnet run` would fail unless you did an explicit `dotnet restore` first: D:\src\helloandroid> D:\src\dotnet\sdk\artifacts\bin\redist\Debug\dotnet\dotnet.exe run -bl failed with 1 error(s) (0.1s) D:\src\dotnet\sdk\artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\Sdks\Microsoft.NET.Sdk\targets\Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1004: Assets file 'D:\src\helloandroid\obj\project.assets.json' not found. Run a NuGet package restore to generate this file. We can test this by changing `DotnetRunDevices.csproj`: Doing an explicit `Restore` step before `ComputeAvailableDevices` resolves this. 2. We were not passing loggers `ProjectInstance.Build()` calls in `RunCommandSelector`, so no logging output was shown during device computation. This is now fixed by creating a fresh console logger each time we call `Build()`. I tested this manually with a console app with a `ComputeAvailableDevices` target and a lengthy sleep: helloconsole ComputeAvailableDevices (9.1s) --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 14 +++-- .../dotnet/Commands/Run/RunCommandSelector.cs | 52 ++++++++++++++++--- .../DotnetRunDevices/DotnetRunDevices.csproj | 2 +- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index ba5801ff9b8c..45764da0cc2c 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) @@ -474,7 +482,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..3f072aa866bd 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 bool restorePerformed); // 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) - + From 7a34939dbcc00fda26d499af6e3d23e68f45246f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 10 Dec 2025 13:48:44 -0600 Subject: [PATCH 2/3] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Cli/dotnet/Commands/Run/RunCommandSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs index 3f072aa866bd..f10239673125 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommandSelector.cs @@ -315,7 +315,7 @@ public bool TrySelectDevice( restoreWasPerformed = false; // Try to get available devices from the project - bool targetExists = TryComputeAvailableDevices(noRestore, out var devices, out bool restorePerformed); + bool targetExists = TryComputeAvailableDevices(noRestore, out var devices, out restoreWasPerformed); // If the target doesn't exist, continue without device selection if (!targetExists) From 7b844f4ef8d0d689e955356895e57883a9097969 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 10 Dec 2025 13:59:23 -0600 Subject: [PATCH 3/3] Update RunCommand.cs --- src/Cli/dotnet/Commands/Run/RunCommand.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs index 45764da0cc2c..fb5dffd622f5 100644 --- a/src/Cli/dotnet/Commands/Run/RunCommand.cs +++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs @@ -304,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);