diff --git a/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs b/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs index e95e07914929..ea8f75b1e53b 100644 --- a/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs +++ b/src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs @@ -40,7 +40,7 @@ protected async Task LaunchWatcherAsync( try { - var watcher = new HotReloadDotNetWatcher(context, Console, processLauncherFactory, targetFrameworkSelectionPrompt: null); + var watcher = new HotReloadDotNetWatcher(context, Console, processLauncherFactory, selectionPrompt: null); await watcher.WatchAsync(cancellationSource.Token); } catch (OperationCanceledException) when (shutdownHandler.CancellationToken.IsCancellationRequested) diff --git a/src/Dotnet.Watch/Watch/Build/BuildNames.cs b/src/Dotnet.Watch/Watch/Build/BuildNames.cs index d7ad8b1acbbd..69eeff0a97be 100644 --- a/src/Dotnet.Watch/Watch/Build/BuildNames.cs +++ b/src/Dotnet.Watch/Watch/Build/BuildNames.cs @@ -55,6 +55,7 @@ internal static class TargetNames public const string ResolveReferencedProjectsStaticWebAssets = nameof(ResolveReferencedProjectsStaticWebAssets); public const string GenerateComputedBuildStaticWebAssets = nameof(GenerateComputedBuildStaticWebAssets); public const string ReferenceCopyLocalPathsOutputGroup = nameof(ReferenceCopyLocalPathsOutputGroup); + public const string ComputeAvailableDevices = nameof(ComputeAvailableDevices); } internal static class ProjectCapability diff --git a/src/Dotnet.Watch/Watch/Build/EvaluationResult.cs b/src/Dotnet.Watch/Watch/Build/EvaluationResult.cs index c429d5681b4b..ebf115ff3c02 100644 --- a/src/Dotnet.Watch/Watch/Build/EvaluationResult.cs +++ b/src/Dotnet.Watch/Watch/Build/EvaluationResult.cs @@ -14,12 +14,11 @@ internal sealed class EvaluationResult( LoadedProjectGraph projectGraph, IReadOnlyDictionary restoredProjectInstances, IReadOnlyDictionary files, - IReadOnlyDictionary staticWebAssetsManifests, - ProjectBuildManager buildManager) + IReadOnlyDictionary staticWebAssetsManifests) { public IReadOnlyDictionary Files => files; public LoadedProjectGraph ProjectGraph => projectGraph; - public ProjectBuildManager BuildManager => buildManager; + public ProjectBuildManager BuildManager => projectGraph.BuildManager; public readonly FilePathExclusions ItemExclusions = projectGraph != null ? FilePathExclusions.Create(projectGraph.Graph) : FilePathExclusions.Empty; @@ -70,14 +69,11 @@ public static ImmutableDictionary GetGlobalBuildProperties(IEnum var projectLoadingStopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew(); - var buildReporter = new BuildReporter(projectGraph.Logger, globalOptions, environmentOptions); - var buildManager = new ProjectBuildManager(projectGraph.ProjectCollection, buildReporter); - if (restore) { var restoreRequests = projectGraph.Graph.GraphRoots.Select(node => BuildRequest.Create(node.ProjectInstance, [TargetNames.Restore])).ToArray(); - if (await buildManager.BuildAsync( + if (await projectGraph.BuildManager.BuildAsync( restoreRequests, onFailure: failedInstance => { @@ -108,7 +104,7 @@ public static ImmutableDictionary GetGlobalBuildProperties(IEnum var buildRequests = CreateDesignTimeBuildRequests(projectGraph.Graph, mainProjectTargetFramework, environmentOptions.SuppressHandlingStaticWebAssets).ToImmutableArray(); - var buildResults = await buildManager.BuildAsync( + var buildResults = await projectGraph.BuildManager.BuildAsync( buildRequests, onFailure: failedInstance => { @@ -132,7 +128,7 @@ public static ImmutableDictionary GetGlobalBuildProperties(IEnum BuildReporter.ReportWatchedFiles(logger, fileItems); - return new EvaluationResult(projectGraph, restoredProjectInstances, fileItems, staticWebAssetManifests, buildManager); + return new EvaluationResult(projectGraph, restoredProjectInstances, fileItems, staticWebAssetManifests); } // internal for testing diff --git a/src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs b/src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs index a05f0fa9873a..3378e758bb2b 100644 --- a/src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs +++ b/src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Watch; -internal sealed class LoadedProjectGraph(ProjectGraph graph, ProjectCollection collection, ILogger logger) +internal sealed class LoadedProjectGraph(ProjectGraph graph, ProjectCollection collection, ILogger logger, GlobalOptions globalOptions, EnvironmentOptions environmentOptions) { // full path of proj file to list of nodes representing all target frameworks of the project (excluding outer build nodes): private readonly IReadOnlyDictionary> _innerBuildNodes = @@ -20,6 +20,9 @@ internal sealed class LoadedProjectGraph(ProjectGraph graph, ProjectCollection c .Concat(graph.ProjectNodes.Select(p => p.ProjectInstance.FullPath)) .ToHashSet(PathUtilities.OSSpecificPathComparer)); + public readonly ProjectBuildManager BuildManager = + new(collection, new BuildReporter(logger, globalOptions, environmentOptions)); + public ProjectGraph Graph => graph; public ILogger Logger => logger; public ProjectCollection ProjectCollection => collection; diff --git a/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs b/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs index ec6583add8f8..0e98272acd92 100644 --- a/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs +++ b/src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs @@ -18,7 +18,9 @@ internal sealed class ProjectGraphFactory( ImmutableArray rootProjects, string? virtualProjectTargetFramework, ImmutableDictionary buildProperties, - ILogger logger) + ILogger logger, + GlobalOptions globalOptions, + EnvironmentOptions environmentOptions) { /// /// Reuse with XML element caching to improve performance. @@ -60,7 +62,9 @@ private static string GetProductTargetFramework() var graph = new LoadedProjectGraph( new ProjectGraph(entryPoints, _collection, (path, globalProperties, collection) => CreateProjectInstance(path, globalProperties, collection, logger), cancellationToken), _collection, - logger); + logger, + globalOptions, + environmentOptions); logger.LogDebug("Project graph loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0")); return graph; diff --git a/src/Dotnet.Watch/Watch/Context/ProjectOptions.cs b/src/Dotnet.Watch/Watch/Context/ProjectOptions.cs index 24c868c05798..2d37673aa282 100644 --- a/src/Dotnet.Watch/Watch/Context/ProjectOptions.cs +++ b/src/Dotnet.Watch/Watch/Context/ProjectOptions.cs @@ -22,6 +22,18 @@ internal sealed record ProjectOptions /// public string? TargetFramework { get; init; } + /// + /// Device identifier to use when launching the project. + /// If the project supports device selection and is null + /// the user will be prompted for a device in interactive mode. + /// + public string? Device { get; init; } + + /// + /// RuntimeIdentifier provided by the selected device, if any. + /// + public string? DeviceRuntimeIdentifier { get; init; } + /// /// No value indicates that no launch profile should be used. /// Null value indicates that the default launch profile should be used. diff --git a/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs b/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs index 9ac8aacf40c7..301a7adea0f9 100644 --- a/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs +++ b/src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Text.Encodings.Web; +using Microsoft.Build.Execution; using Microsoft.CodeAnalysis; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; @@ -18,18 +19,23 @@ internal sealed class HotReloadDotNetWatcher private readonly IConsole _console; private readonly IRuntimeProcessLauncherFactory? _runtimeProcessLauncherFactory; private readonly RestartPrompt? _rudeEditRestartPrompt; - private readonly TargetFrameworkSelectionPrompt? _targetFrameworkSelectionPrompt; + private readonly BuildParametersSelectionPrompt? _selectionPrompt; private readonly DotNetWatchContext _context; private readonly ProjectGraphFactory _designTimeBuildGraphFactory; internal Task? Test_FileChangesCompletedTask { get; set; } - public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRuntimeProcessLauncherFactory? runtimeProcessLauncherFactory, TargetFrameworkSelectionPrompt? targetFrameworkSelectionPrompt) + public HotReloadDotNetWatcher( + DotNetWatchContext context, + IConsole console, + IRuntimeProcessLauncherFactory? runtimeProcessLauncherFactory, + BuildParametersSelectionPrompt? selectionPrompt) { _context = context; _console = console; _runtimeProcessLauncherFactory = runtimeProcessLauncherFactory; + _selectionPrompt = selectionPrompt; if (!context.Options.NonInteractive) { var consoleInput = new ConsoleInputReader(_console, context.Options.LogLevel, context.EnvironmentOptions.SuppressEmojis); @@ -41,7 +47,6 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun } _rudeEditRestartPrompt = new RestartPrompt(context.Logger, consoleInput, noPrompt ? true : null); - _targetFrameworkSelectionPrompt = targetFrameworkSelectionPrompt; } _designTimeBuildGraphFactory = new ProjectGraphFactory( @@ -50,7 +55,9 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun buildProperties: EvaluationResult.GetGlobalBuildProperties( context.BuildArguments, context.EnvironmentOptions), - context.BuildLogger); + context.BuildLogger, + context.Options, + context.EnvironmentOptions); } public async Task WatchAsync(CancellationToken shutdownCancellationToken) @@ -95,7 +102,8 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) _context.RootProjects, fileWatcher, _context.MainProjectOptions, - frameworkSelector: _targetFrameworkSelectionPrompt != null ? _targetFrameworkSelectionPrompt.SelectAsync : null, + frameworkSelector: _selectionPrompt != null ? _selectionPrompt.SelectTargetFrameworkAsync : null, + deviceSelector: _selectionPrompt != null ? _selectionPrompt.SelectDeviceAsync : null, iterationCancellationToken); // Try load project graph and perform design-time build even if the build failed. @@ -142,7 +150,12 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken) var mainProjectOptions = _context.MainProjectOptions; if (mainProjectOptions != null) { - mainProjectOptions = mainProjectOptions with { TargetFramework = rootProjectsBuildResult.MainProjectTargetFramework }; + mainProjectOptions = mainProjectOptions with + { + TargetFramework = rootProjectsBuildResult.MainProjectTargetFramework, + Device = rootProjectsBuildResult.SelectedDevice, + DeviceRuntimeIdentifier = rootProjectsBuildResult.SelectedDeviceRuntimeIdentifier, + }; if (projectGraph.Graph.GraphRoots.Single()?.GetCapabilities().Contains(AspireServiceFactory.AppHostProjectCapability) == true) { @@ -272,6 +285,7 @@ [.. updates.ProjectsToRebuild.Select(ProjectRepresentation.FromProjectOrEntryPoi fileWatcher, mainProjectOptions, frameworkSelector: null, + deviceSelector: null, iterationCancellationToken); if (result.Success) @@ -939,9 +953,11 @@ private enum BuildAction } // internal for testing - internal sealed class BuildProjectsResult(string? mainProjectTargetFramework, LoadedProjectGraph? projectGraph, bool success) + internal sealed class BuildProjectsResult(string? mainProjectTargetFramework, string? selectedDevice, string? selectedDeviceRuntimeIdentifier, LoadedProjectGraph? projectGraph, bool success) { public string? MainProjectTargetFramework { get; } = mainProjectTargetFramework; + public string? SelectedDevice { get; } = selectedDevice; + public string? SelectedDeviceRuntimeIdentifier { get; } = selectedDeviceRuntimeIdentifier; public LoadedProjectGraph? ProjectGraph { get; } = projectGraph; public bool Success { get; } = success; } @@ -952,12 +968,15 @@ internal async Task BuildProjectsAsync( FileWatcher fileWatcher, ProjectOptions? mainProjectOptions, Func, CancellationToken, ValueTask>? frameworkSelector, + Func, CancellationToken, ValueTask>? deviceSelector, CancellationToken cancellationToken) { Debug.Assert(projects.Any()); LoadedProjectGraph? projectGraph = null; var targetFramework = mainProjectOptions?.TargetFramework; + var selectedDevice = mainProjectOptions?.Device; + var selectedDeviceRuntimeIdentifier = mainProjectOptions?.DeviceRuntimeIdentifier; _context.Logger.Log(MessageDescriptor.BuildStartedNotification, projects); @@ -965,20 +984,22 @@ internal async Task BuildProjectsAsync( fileWatcher.SuppressEvents = true; try { - var success = await BuildWithFrameworkSelectionAsync(); + var success = await BuildWithFrameworkAndDeviceSelectionAsync(); _context.Logger.Log(MessageDescriptor.BuildCompletedNotification, (projects, success)); - return new BuildProjectsResult(targetFramework, projectGraph, success); + return new BuildProjectsResult(targetFramework, selectedDevice, selectedDeviceRuntimeIdentifier, projectGraph, success); } finally { fileWatcher.SuppressEvents = false; } - async ValueTask BuildWithFrameworkSelectionAsync() + async ValueTask BuildWithFrameworkAndDeviceSelectionAsync() { + var needsFrameworkSelection = targetFramework == null && frameworkSelector != null; + var needsDeviceSelection = selectedDevice == null && deviceSelector != null; + if (mainProjectOptions == null || - frameworkSelector == null || - targetFramework != null || + (!needsFrameworkSelection && !needsDeviceSelection) || !mainProjectOptions.Representation.IsProjectFile) { return await BuildAsync(BuildAction.RestoreAndBuild, targetFramework); @@ -997,28 +1018,62 @@ async ValueTask BuildWithFrameworkSelectionAsync() } var rootProject = projectGraph.Graph.GraphRoots.Single().ProjectInstance; - if (rootProject.GetTargetFramework() is var framework and not "") - { - targetFramework = framework; - } - else if (rootProject.GetTargetFrameworks() is var frameworks and not []) + + // Select target framework if needed: + if (needsFrameworkSelection) { - targetFramework = await frameworkSelector(frameworks, cancellationToken); + Debug.Assert(frameworkSelector is not null); + + if (rootProject.GetTargetFramework() is var framework and not "") + { + targetFramework = framework; + } + else if (rootProject.GetTargetFrameworks() is var frameworks and not []) + { + targetFramework = await frameworkSelector(frameworks, cancellationToken); + } + else + { + _context.BuildLogger.LogError("Project '{Path}' does not specify a target framework.", rootProject.FullPath); + return false; + } } - else + + // Select device if needed: + if (needsDeviceSelection + && rootProject.Targets.ContainsKey(TargetNames.ComputeAvailableDevices)) { - _context.BuildLogger.LogError("Project '{Path}' does not specify a target framework.", rootProject.FullPath); - return false; + Debug.Assert(deviceSelector is not null); + + var deviceInfo = await TrySelectDeviceAsync(projectGraph, rootProject, targetFramework, deviceSelector, cancellationToken); + if (deviceInfo == null) + { + return false; + } + + selectedDevice = deviceInfo.Id; + selectedDeviceRuntimeIdentifier = deviceInfo.RuntimeIdentifier; + _context.Logger.LogDebug("Selected device: {DeviceId}", selectedDevice); + + // If the device provides a RuntimeIdentifier, re-restore so the assets file + // includes the RID target. This mirrors the dotnet-run behavior. + if (!string.IsNullOrEmpty(selectedDeviceRuntimeIdentifier)) + { + if (!await BuildAsync(BuildAction.RestoreOnly, targetFramework, deviceInfo)) + { + return false; + } + } } - return await BuildAsync(BuildAction.BuildOnly, targetFramework); + return await BuildAsync(BuildAction.BuildOnly, targetFramework, selectedDevice != null ? new DeviceInfo(selectedDevice, null, null, null, selectedDeviceRuntimeIdentifier) : null); } - async Task BuildAsync(BuildAction action, string? targetFramework) + async Task BuildAsync(BuildAction action, string? targetFramework, DeviceInfo? device = null) { if (projects is [var singleProject]) { - return await BuildFileOrProjectOrSolutionAsync(singleProject.ProjectOrEntryPointFilePath, targetFramework, action, cancellationToken); + return await BuildFileOrProjectOrSolutionAsync(singleProject.ProjectOrEntryPointFilePath, targetFramework, device, action, cancellationToken); } // TODO: workaround for https://github.com/dotnet/sdk/issues/51311 @@ -1027,7 +1082,7 @@ async Task BuildAsync(BuildAction action, string? targetFramework) if (projectPaths is [var singleProjectPath]) { - if (!await BuildFileOrProjectOrSolutionAsync(singleProjectPath, targetFramework, action, cancellationToken)) + if (!await BuildFileOrProjectOrSolutionAsync(singleProjectPath, targetFramework, device, action, cancellationToken)) { return false; } @@ -1047,7 +1102,7 @@ async Task BuildAsync(BuildAction action, string? targetFramework) try { - if (!await BuildFileOrProjectOrSolutionAsync(solutionFile, targetFramework, action, cancellationToken)) + if (!await BuildFileOrProjectOrSolutionAsync(solutionFile, targetFramework, device, action, cancellationToken)) { return false; } @@ -1068,7 +1123,7 @@ async Task BuildAsync(BuildAction action, string? targetFramework) // To maximize parallelism of building dependencies, build file-based projects after all physical projects: foreach (var file in projects.Where(p => p.EntryPointFilePath != null).Select(p => p.EntryPointFilePath!)) { - if (!await BuildFileOrProjectOrSolutionAsync(file, targetFramework, action, cancellationToken)) + if (!await BuildFileOrProjectOrSolutionAsync(file, targetFramework, device, action, cancellationToken)) { return false; } @@ -1078,7 +1133,67 @@ async Task BuildAsync(BuildAction action, string? targetFramework) } } - private async Task BuildFileOrProjectOrSolutionAsync(string path, string? targetFramework, BuildAction action, CancellationToken cancellationToken) + /// + /// Computes available devices and selects one. + /// Auto-selects a single device when only one is available; otherwise uses the provided device selector. + /// Returns null if no devices are available (error). + /// + private async Task TrySelectDeviceAsync( + LoadedProjectGraph projectGraph, + ProjectInstance rootProject, + string? targetFramework, + Func, CancellationToken, ValueTask> deviceSelector, + CancellationToken cancellationToken) + { + // Get the project node for the selected TFM so device computation is correct. + var projectNode = projectGraph.TryGetProjectNode(rootProject.FullPath, targetFramework); + if (projectNode == null) + { + return null; + } + + var projectInstance = projectNode.ProjectInstance.DeepCopy(); + + var results = await projectGraph.BuildManager.BuildAsync( + [BuildRequest.Create(projectInstance, [TargetNames.ComputeAvailableDevices])], + onFailure: _ => true, + operationName: TargetNames.ComputeAvailableDevices, + cancellationToken); + + if (results is not [var result] || !result.IsSuccess + || !result.TargetResults.TryGetValue(TargetNames.ComputeAvailableDevices, out var targetResult)) + { + _context.Logger.LogDebug("ComputeAvailableDevices target failed or returned no output."); + return null; + } + + var devices = new List(targetResult.Items.Length); + foreach (var item in targetResult.Items) + { + devices.Add(new DeviceInfo( + item.ItemSpec, + item.GetMetadata("Description"), + item.GetMetadata("Type"), + item.GetMetadata("Status"), + item.GetMetadata("RuntimeIdentifier"))); + } + + if (devices.Count == 0) + { + _context.Logger.Log(MessageDescriptor.NoDevicesAvailable); + return null; + } + + // Auto-select if only one device is available. + if (devices.Count == 1) + { + return devices[0]; + } + + return await deviceSelector(devices, cancellationToken); + } + + private async Task BuildFileOrProjectOrSolutionAsync(string path, string? targetFramework, DeviceInfo? device, BuildAction action, CancellationToken cancellationToken) { var arguments = new List { @@ -1094,6 +1209,17 @@ private async Task BuildFileOrProjectOrSolutionAsync(string path, string? arguments.Add(targetFramework); } + if (device != null) + { + arguments.Add($"-p:Device={device.Id}"); + + if (!string.IsNullOrEmpty(device.RuntimeIdentifier)) + { + arguments.Add("--runtime"); + arguments.Add(device.RuntimeIdentifier); + } + } + if (action == BuildAction.BuildOnly) { arguments.Add("--no-restore"); diff --git a/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs b/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs index 8b4b1707fefc..b905506370e9 100644 --- a/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs +++ b/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs @@ -111,6 +111,18 @@ private static IReadOnlyList GetProcessArguments(ProjectOptions projectO arguments.Add(projectOptions.TargetFramework); } + if (projectOptions.Device != null) + { + arguments.Add("--device"); + arguments.Add(projectOptions.Device); + + if (projectOptions.DeviceRuntimeIdentifier != null) + { + arguments.Add("--runtime"); + arguments.Add(projectOptions.DeviceRuntimeIdentifier); + } + } + foreach (var (name, value) in environmentBuilder) { arguments.Add("-e"); diff --git a/src/Dotnet.Watch/Watch/UI/BuildParametersSelectionPrompt.cs b/src/Dotnet.Watch/Watch/UI/BuildParametersSelectionPrompt.cs new file mode 100644 index 000000000000..68373e512213 --- /dev/null +++ b/src/Dotnet.Watch/Watch/UI/BuildParametersSelectionPrompt.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch; + +/// +/// Abstract base for interactive selection prompts in dotnet-watch. +/// Provides target framework and device selection with caching across watch restarts. +/// +internal abstract class BuildParametersSelectionPrompt : IDisposable +{ + public IReadOnlyList? PreviousTargetFrameworks { get; set; } + public string? PreviousFrameworkSelection { get; set; } + + public IReadOnlyList? PreviousDevices { get; set; } + public DeviceInfo? PreviousDeviceSelection { get; set; } + + public async ValueTask SelectTargetFrameworkAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken) + { + var orderedTargetFrameworks = targetFrameworks.Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + if (PreviousFrameworkSelection != null && PreviousTargetFrameworks?.SequenceEqual(orderedTargetFrameworks, StringComparer.OrdinalIgnoreCase) == true) + { + return PreviousFrameworkSelection; + } + + PreviousTargetFrameworks = orderedTargetFrameworks; + PreviousFrameworkSelection = await PromptForTargetFrameworkAsync(targetFrameworks, cancellationToken); + return PreviousFrameworkSelection; + } + + public async ValueTask SelectDeviceAsync(IReadOnlyList devices, CancellationToken cancellationToken) + { + if (PreviousDeviceSelection != null && PreviousDevices?.SequenceEqual(devices) == true) + { + return PreviousDeviceSelection; + } + + PreviousDevices = devices; + PreviousDeviceSelection = await PromptForDeviceAsync(devices, cancellationToken); + return PreviousDeviceSelection; + } + + protected abstract Task PromptForTargetFrameworkAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken); + + protected abstract Task PromptForDeviceAsync(IReadOnlyList devices, CancellationToken cancellationToken); + + public virtual void Dispose() { } +} diff --git a/src/Dotnet.Watch/Watch/UI/DeviceInfo.cs b/src/Dotnet.Watch/Watch/UI/DeviceInfo.cs new file mode 100644 index 000000000000..623e3cd25b76 --- /dev/null +++ b/src/Dotnet.Watch/Watch/UI/DeviceInfo.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch; + +/// +/// Represents a device item returned from the ComputeAvailableDevices MSBuild target. +/// +internal sealed record DeviceInfo(string Id, string? Description, string? Type, string? Status, string? RuntimeIdentifier); diff --git a/src/Dotnet.Watch/Watch/UI/IReporter.cs b/src/Dotnet.Watch/Watch/UI/IReporter.cs index bf72afd13e11..000ca3015327 100644 --- a/src/Dotnet.Watch/Watch/UI/IReporter.cs +++ b/src/Dotnet.Watch/Watch/UI/IReporter.cs @@ -282,6 +282,7 @@ public static MessageDescriptor GetDescriptor(EventId id) public static readonly MessageDescriptor> BuildStartedNotification = CreateNotification>(); public static readonly MessageDescriptor<(IEnumerable projects, bool success)> BuildCompletedNotification = CreateNotification<(IEnumerable projects, bool success)>(); public static readonly MessageDescriptor ManifestFileNotFound = Create(LogEvents.ManifestFileNotFound, Emoji.Default); + public static readonly MessageDescriptor NoDevicesAvailable = Create("No devices are available for this project.", Emoji.Error, LogLevel.Error); } internal sealed class MessageDescriptor(string? format, Emoji emoji, LogLevel level, EventId id) diff --git a/src/Dotnet.Watch/Watch/UI/TargetFrameworkSelectionPrompt.cs b/src/Dotnet.Watch/Watch/UI/TargetFrameworkSelectionPrompt.cs deleted file mode 100644 index 1d7a801ce347..000000000000 --- a/src/Dotnet.Watch/Watch/UI/TargetFrameworkSelectionPrompt.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.DotNet.Watch; - -internal abstract class TargetFrameworkSelectionPrompt : IDisposable -{ - public IReadOnlyList? PreviousTargetFrameworks { get; set; } - public string? PreviousSelection { get; set; } - - public async ValueTask SelectAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken) - { - var orderedTargetFrameworks = targetFrameworks.Order(StringComparer.OrdinalIgnoreCase).ToArray(); - - if (PreviousSelection != null && PreviousTargetFrameworks?.SequenceEqual(orderedTargetFrameworks, StringComparer.OrdinalIgnoreCase) == true) - { - return PreviousSelection; - } - - PreviousTargetFrameworks = orderedTargetFrameworks; - PreviousSelection = await PromptAsync(targetFrameworks, cancellationToken); - return PreviousSelection; - } - - protected abstract Task PromptAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken); - - public virtual void Dispose() { } -} diff --git a/src/Dotnet.Watch/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/Dotnet.Watch/dotnet-watch/CommandLine/CommandLineOptions.cs index 9d25af712486..6662b89fa9a1 100644 --- a/src/Dotnet.Watch/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/Dotnet.Watch/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -26,6 +26,7 @@ internal sealed class CommandLineOptions public string? FilePath { get; init; } public string? ProjectPath { get; init; } public string? TargetFramework { get; init; } + public string? Device { get; init; } public Optional LaunchProfileName { get; init; } /// @@ -162,6 +163,7 @@ internal sealed class CommandLineOptions LaunchProfileName = launchProfile, BuildArguments = buildArguments, TargetFramework = parseResult.GetValue(definition.FrameworkOption), + Device = parseResult.GetValue(definition.DeviceOption), }; } @@ -360,6 +362,7 @@ public ProjectOptions GetMainProjectOptions(ProjectRepresentation project, strin Representation = project, WorkingDirectory = workingDirectory, TargetFramework = TargetFramework, + Device = Device, Command = Command, CommandArguments = CommandArguments, LaunchEnvironmentVariables = [], diff --git a/src/Dotnet.Watch/dotnet-watch/CommandLine/DotnetWatchCommandDefinition.cs b/src/Dotnet.Watch/dotnet-watch/CommandLine/DotnetWatchCommandDefinition.cs index 71bbfa3bd255..41480589da5b 100644 --- a/src/Dotnet.Watch/dotnet-watch/CommandLine/DotnetWatchCommandDefinition.cs +++ b/src/Dotnet.Watch/dotnet-watch/CommandLine/DotnetWatchCommandDefinition.cs @@ -25,6 +25,15 @@ internal sealed class DotnetWatchCommandDefinition : RootCommand HelpName = CommandDefinitionStrings.FrameworkArgumentName, }; + /// + /// Specifies the device to run on. The watcher passes the value explicitly instead of forwarding the subcommand's --device option. + /// + public readonly Option DeviceOption = new("--device") + { + Description = Resources.Help_Device, + HelpName = "DEVICE_ID", + }; + // Options we need to know about. They are passed through to the subcommand if the subcommand supports them. public readonly Option ShortProjectOption = new("-p") @@ -80,6 +89,7 @@ public DotnetWatchCommandDefinition() Options.Add(NoHotReloadOption); Options.Add(NonInteractiveOption); Options.Add(FrameworkOption); + Options.Add(DeviceOption); Options.Add(LongProjectOption); Options.Add(ShortProjectOption); @@ -118,5 +128,6 @@ public bool IsWatchOption(Option option) option == ListOption || option == NoHotReloadOption || option == NonInteractiveOption || - option == FrameworkOption; + option == FrameworkOption || + option == DeviceOption; } diff --git a/src/Dotnet.Watch/dotnet-watch/Program.cs b/src/Dotnet.Watch/dotnet-watch/Program.cs index 9dc9b9c41d84..c4cd7ab0600c 100644 --- a/src/Dotnet.Watch/dotnet-watch/Program.cs +++ b/src/Dotnet.Watch/dotnet-watch/Program.cs @@ -265,9 +265,8 @@ internal async Task RunAsync() if (IsHotReloadEnabled()) { - using var tfmPrompt = context.Options.NonInteractive ? null - : new SpectreTargetFrameworkSelectionPrompt(console); - var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null, tfmPrompt); + using var selectionPrompt = context.Options.NonInteractive ? null : new SpectreBuildParametersSelectionPrompt(console); + var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: null, selectionPrompt); await watcher.WatchAsync(shutdownHandler.CancellationToken); } else if (mainProjectOptions.Representation.EntryPointFilePath != null) @@ -347,6 +346,7 @@ private async Task ListFilesAsync(ProcessRunner processRunner, Cancellation options.BuildArguments, processRunner, buildLogger, + options.GlobalOptions, environmentOptions); if (await fileSetFactory.TryCreateAsync(requireProjectGraph: null, cancellationToken) is not { } evaluationResult) diff --git a/src/Dotnet.Watch/dotnet-watch/Resources.resx b/src/Dotnet.Watch/dotnet-watch/Resources.resx index 9892790f4556..9ae47329057a 100644 --- a/src/Dotnet.Watch/dotnet-watch/Resources.resx +++ b/src/Dotnet.Watch/dotnet-watch/Resources.resx @@ -147,12 +147,21 @@ Type to search + + Select a device to run on: + + + Move up and down to reveal more devices + Runs dotnet-watch in non-interactive mode. This option is only supported when running with Hot Reload enabled. Use this option to prevent console input from being captured. + + The device identifier to run on (e.g. emulator, simulator, or physical device). + Suppress hot reload for supported apps. diff --git a/src/Dotnet.Watch/dotnet-watch/UI/SpectreTargetFrameworkSelectionPrompt.cs b/src/Dotnet.Watch/dotnet-watch/UI/SpectreBuildParametersSelectionPrompt.cs similarity index 64% rename from src/Dotnet.Watch/dotnet-watch/UI/SpectreTargetFrameworkSelectionPrompt.cs rename to src/Dotnet.Watch/dotnet-watch/UI/SpectreBuildParametersSelectionPrompt.cs index 6d68048d8413..7119266ef65b 100644 --- a/src/Dotnet.Watch/dotnet-watch/UI/SpectreTargetFrameworkSelectionPrompt.cs +++ b/src/Dotnet.Watch/dotnet-watch/UI/SpectreBuildParametersSelectionPrompt.cs @@ -6,9 +6,13 @@ namespace Microsoft.DotNet.Watch; -internal sealed class SpectreTargetFrameworkSelectionPrompt(IAnsiConsole console) : TargetFrameworkSelectionPrompt +internal sealed class SpectreBuildParametersSelectionPrompt(IAnsiConsole console) : BuildParametersSelectionPrompt { - public SpectreTargetFrameworkSelectionPrompt(IConsole watchConsole) + private const string CyanMarkup = "[cyan]"; + private const string GrayMarkup = "[gray]"; + private const string EndMarkup = "[/]"; + + public SpectreBuildParametersSelectionPrompt(IConsole watchConsole) : this(CreateConsole(watchConsole)) { } @@ -16,12 +20,12 @@ public SpectreTargetFrameworkSelectionPrompt(IConsole watchConsole) public override void Dispose() => (console as IDisposable)?.Dispose(); - protected override Task PromptAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken) + protected override Task PromptForTargetFrameworkAsync(IReadOnlyList targetFrameworks, CancellationToken cancellationToken) { var prompt = new SelectionPrompt() - .Title($"[cyan]{Markup.Escape(Resources.SelectTargetFrameworkPrompt)}[/]") + .Title($"{CyanMarkup}{Markup.Escape(Resources.SelectTargetFrameworkPrompt)}{EndMarkup}") .PageSize(10) - .MoreChoicesText($"[gray]({Markup.Escape(Resources.MoreFrameworksText)})[/]") + .MoreChoicesText($"{GrayMarkup}({Markup.Escape(Resources.MoreFrameworksText)}){EndMarkup}") .AddChoices(targetFrameworks) .EnableSearch() .SearchPlaceholderText(Resources.SearchPlaceholderText); @@ -29,6 +33,45 @@ protected override Task PromptAsync(IReadOnlyList targetFramewor return prompt.ShowAsync(console, cancellationToken); } + protected override Task PromptForDeviceAsync(IReadOnlyList devices, CancellationToken cancellationToken) + { + var prompt = new SelectionPrompt() + .Title($"{CyanMarkup}{Markup.Escape(Resources.SelectDevicePrompt)}{EndMarkup}") + .PageSize(10) + .MoreChoicesText($"{GrayMarkup}({Markup.Escape(Resources.MoreDevicesText)}){EndMarkup}") + .AddChoices(devices) + .UseConverter(FormatDevice) + .EnableSearch() + .SearchPlaceholderText(Resources.SearchPlaceholderText); + + return prompt.ShowAsync(console, cancellationToken); + } + + internal static string FormatDevice(DeviceInfo device) + { + var display = device.Id; + if (!string.IsNullOrWhiteSpace(device.Description)) + { + display += $" - {device.Description}"; + } + + if (!string.IsNullOrWhiteSpace(device.Type)) + { + display += $" ({device.Type}"; + if (!string.IsNullOrWhiteSpace(device.Status)) + { + display += $", {device.Status}"; + } + display += ")"; + } + else if (!string.IsNullOrWhiteSpace(device.Status)) + { + display += $" ({device.Status})"; + } + + return display; + } + private static IAnsiConsole CreateConsole(IConsole watchConsole) { if (!Console.IsInputRedirected) diff --git a/src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs b/src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs index eb76068b0812..4b0576dd3461 100644 --- a/src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs +++ b/src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs @@ -51,6 +51,7 @@ protected virtual MSBuildFileSetFactory CreateMSBuildFileSetFactory() _context.BuildArguments, _context.ProcessRunner, _context.BuildLogger, + _context.Options, _context.EnvironmentOptions); } @@ -88,6 +89,12 @@ public IReadOnlyList GetProcessArguments(int iteration) arguments.Add(MainProjectOptions.TargetFramework); } + if (MainProjectOptions.Device != null) + { + arguments.Add("--device"); + arguments.Add(MainProjectOptions.Device); + } + arguments.AddRange(MainProjectOptions.CommandArguments); return arguments; diff --git a/src/Dotnet.Watch/dotnet-watch/Watch/MsBuildFileSetFactory.cs b/src/Dotnet.Watch/dotnet-watch/Watch/MsBuildFileSetFactory.cs index 4f94e8535980..aa61f05d95e7 100644 --- a/src/Dotnet.Watch/dotnet-watch/Watch/MsBuildFileSetFactory.cs +++ b/src/Dotnet.Watch/dotnet-watch/Watch/MsBuildFileSetFactory.cs @@ -22,6 +22,7 @@ internal class MSBuildFileSetFactory( IEnumerable buildArguments, ProcessRunner processRunner, ILogger logger, + GlobalOptions globalOptions, EnvironmentOptions environmentOptions) { private const string TargetName = "GenerateWatchList"; @@ -33,7 +34,9 @@ internal class MSBuildFileSetFactory( [new ProjectRepresentation(rootProjectFile, entryPointFilePath: null)], targetFramework, buildProperties: BuildUtilities.ParseBuildProperties(buildArguments).ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value), - logger); + logger, + globalOptions, + environmentOptions); internal sealed class EvaluationResult(IReadOnlyDictionary files, LoadedProjectGraph? projectGraph) { diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf index d44e81ef9897..f06aeb552b1d 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf @@ -95,6 +95,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Posunutím nahoru a dolů zobrazíte další architektury. @@ -152,6 +162,11 @@ Examples: Zadejte hledaný text + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Vyberte cílovou architekturu, která se má spustit: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf index 1dd6e1c6c3ab..643eacbe4bf0 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf @@ -95,6 +95,11 @@ Beispiele: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Beispiele: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Nach oben und unten bewegen, um weitere Frameworks anzuzeigen @@ -152,6 +162,11 @@ Beispiele: Suchtext eingeben + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Wählen Sie das auszuführende Zielframework aus: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf index 8cd88128ff69..c1c2adc95b72 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf @@ -95,6 +95,11 @@ Ejemplos: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Ejemplos: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Subir y bajar para mostrar más marcos @@ -152,6 +162,11 @@ Ejemplos: Escriba para buscar + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Seleccione la plataforma de destino que se va a ejecutar: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf index 1e6fb5271f98..c379df1f5d20 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf @@ -95,6 +95,11 @@ Exemples : + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Exemples : + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Montez et descendez pour afficher plus de frameworks @@ -152,6 +162,11 @@ Exemples : Entrer le texte à rechercher + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Sélectionner le framework cible pour exécuter : diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.it.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.it.xlf index 6d6d78443c19..4510f1cb2753 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.it.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.it.xlf @@ -95,6 +95,11 @@ Esempi: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Esempi: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Spostarsi verso l'alto o verso il basso per visualizzare altri framework @@ -152,6 +162,11 @@ Esempi: Digita per cercare + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Selezionare il framework di destinazione da eseguire: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ja.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ja.xlf index 3b70dc72fdd9..b2f96c858610 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ja.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ja.xlf @@ -95,6 +95,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks 上下に移動して、さらに多くのフレームワークを表示 @@ -152,6 +162,11 @@ Examples: 入力して検索します + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: 実行するターゲット フレームワークを選択してください: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ko.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ko.xlf index c18faac8548f..3c3a704d8771 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ko.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ko.xlf @@ -95,6 +95,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks 더 많은 프레임워크를 보려면 위아래로 이동하세요. @@ -152,6 +162,11 @@ Examples: 검색할 형식 + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: 실행할 대상 프레임워크 선택: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pl.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pl.xlf index 11e12cce9089..9485c3e73f08 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pl.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pl.xlf @@ -95,6 +95,11 @@ Przykłady: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Przykłady: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Przesuń w górę lub w dół, aby zobaczyć więcej struktur @@ -152,6 +162,11 @@ Przykłady: Wpisz, aby wyszukać + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Wybierz docelową strukturę do uruchomienia: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pt-BR.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pt-BR.xlf index c2cf90dbc1f4..23b7b384d57d 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pt-BR.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.pt-BR.xlf @@ -95,6 +95,11 @@ Exemplos: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Exemplos: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Mover para cima e para baixo para ver mais estruturas @@ -152,6 +162,11 @@ Exemplos: Digitar para pesquisar + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Selecione a estrutura de destino para executar: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ru.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ru.xlf index 16b322cb7c73..db2a5678d77f 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ru.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.ru.xlf @@ -95,6 +95,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Прокрутите вверх и вниз, чтобы увидеть больше платформ @@ -152,6 +162,11 @@ Examples: Введите текст для поиска + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Выберите целевую платформу для запуска: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.tr.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.tr.xlf index 54425fd1bf8d..62d1c81aa127 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.tr.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.tr.xlf @@ -95,6 +95,11 @@ Açıklamalar: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Açıklamalar: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks Yukarı ve aşağı hareket ederek daha fazla çerçeve görüntüleyin @@ -152,6 +162,11 @@ Açıklamalar: Aramak için yazın + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: Çalıştırmak için hedef çerçeveyi seçin: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hans.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hans.xlf index fd7b9fd06974..6f0a97b938bb 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hans.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hans.xlf @@ -96,6 +96,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -143,6 +148,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks 上移或下移以显示更多框架 @@ -153,6 +163,11 @@ Examples: 键入以搜索 + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: 选择要运行的目标框架: diff --git a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hant.xlf b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hant.xlf index 7a132b3aaf28..f31ba66d99d2 100644 --- a/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hant.xlf +++ b/src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hant.xlf @@ -95,6 +95,11 @@ Examples: + + The device identifier to run on (e.g. emulator, simulator, or physical device). + The device identifier to run on (e.g. emulator, simulator, or physical device). + + Lists all discovered files without starting the watcher. @@ -142,6 +147,11 @@ Examples: + + Move up and down to reveal more devices + Move up and down to reveal more devices + + Move up and down to reveal more frameworks 上下移動以顯示更多的架構 @@ -152,6 +162,11 @@ Examples: 要搜尋的類型 + + Select a device to run on: + Select a device to run on: + + Select the target framework to run: 選取要執行的目標架構: diff --git a/test/dotnet-watch.Tests/Build/EvaluationTests.cs b/test/dotnet-watch.Tests/Build/EvaluationTests.cs index 3ca413fa1842..4b8f5c6f0652 100644 --- a/test/dotnet-watch.Tests/Build/EvaluationTests.cs +++ b/test/dotnet-watch.Tests/Build/EvaluationTests.cs @@ -442,7 +442,7 @@ public async Task ProjectReferences_Graph() var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDirectory); var processRunner = new ProcessRunner(processCleanupTimeout: TimeSpan.Zero); - var filesetFactory = new MSBuildFileSetFactory(projectA, targetFramework: null, buildArguments: ["/p:_DotNetWatchTraceOutput=true"], processRunner, _logger, options); + var filesetFactory = new MSBuildFileSetFactory(projectA, targetFramework: null, buildArguments: ["/p:_DotNetWatchTraceOutput=true"], processRunner, _logger, TestOptions.GlobalOptions, options); var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); Assert.NotNull(result); @@ -506,7 +506,7 @@ public async Task MsbuildOutput() var options = TestOptions.GetEnvironmentOptions(workingDirectory: Path.GetDirectoryName(project1Path)!); var processRunner = new ProcessRunner(processCleanupTimeout: TimeSpan.Zero); - var factory = new MSBuildFileSetFactory(project1Path, targetFramework: null, buildArguments: [], processRunner, _logger, options); + var factory = new MSBuildFileSetFactory(project1Path, targetFramework: null, buildArguments: [], processRunner, _logger, TestOptions.GlobalOptions, options); var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); Assert.Null(result); @@ -543,7 +543,7 @@ async Task VerifyTargetsEvaluation() var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDir) with { TestOutput = testDir }; var processRunner = new ProcessRunner(processCleanupTimeout: TimeSpan.Zero); var buildArguments = targetFramework != null ? new[] { "/p:TargetFramework=" + targetFramework } : []; - var factory = new MSBuildFileSetFactory(rootProjectPath, targetFramework: null, buildArguments, processRunner, _logger, options); + var factory = new MSBuildFileSetFactory(rootProjectPath, targetFramework: null, buildArguments, processRunner, _logger, TestOptions.GlobalOptions, options); var targetsResult = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None); Assert.NotNull(targetsResult); diff --git a/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs b/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs index fbcbb8404f8a..3f0322c2c0bb 100644 --- a/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs +++ b/test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs @@ -20,7 +20,7 @@ public void RegularProject() var projectPath = Path.Combine(testAsset.Path, "WatchNoDepsApp.csproj"); var projectRepr = new ProjectRepresentation(projectPath, entryPointFilePath: null); - var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger); + var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger, TestOptions.GlobalOptions, TestOptions.GetEnvironmentOptions(asset: testAsset)); var graph = factory.TryLoadProjectGraph(projectGraphRequired: true, CancellationToken.None); Assert.NotNull(graph); @@ -40,7 +40,7 @@ public void VirtualProject() """); var projectRepr = new ProjectRepresentation(projectPath: null, entryPointFilePath); - var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger); + var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger, TestOptions.GlobalOptions, TestOptions.GetEnvironmentOptions()); var graph = factory.TryLoadProjectGraph(projectGraphRequired: true, CancellationToken.None); Assert.NotNull(graph); @@ -60,7 +60,7 @@ public void VirtualProject_Error() """); var projectRepr = new ProjectRepresentation(projectPath: null, entryPointFilePath); - var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger); + var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger, TestOptions.GlobalOptions, TestOptions.GetEnvironmentOptions()); var graph = factory.TryLoadProjectGraph(projectGraphRequired: true, CancellationToken.None); Assert.Null(graph); @@ -92,7 +92,7 @@ public void VirtualProject_ProjectDirective() """); var projectRepr = new ProjectRepresentation(projectPath: null, entryPointFilePath); - var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger); + var factory = new ProjectGraphFactory([projectRepr], virtualProjectTargetFramework: null, buildProperties: [], _testLogger, TestOptions.GlobalOptions, TestOptions.GetEnvironmentOptions(asset: testAsset)); var graph = factory.TryLoadProjectGraph(projectGraphRequired: true, CancellationToken.None); Assert.NotNull(graph); diff --git a/test/dotnet-watch.Tests/HotReload/BuildParametersSelectionPromptTests.cs b/test/dotnet-watch.Tests/HotReload/BuildParametersSelectionPromptTests.cs new file mode 100644 index 000000000000..091146408807 --- /dev/null +++ b/test/dotnet-watch.Tests/HotReload/BuildParametersSelectionPromptTests.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SpectreTestConsole = Spectre.Console.Testing.TestConsole; + +namespace Microsoft.DotNet.Watch.UnitTests; + +public class BuildParametersSelectionPromptTests +{ + private static DeviceInfo[] CreateTestDevices() => + [ + new("emulator-5554", "Pixel 7 - API 35", "Emulator", "Online", "android-x64"), + new("emulator-5555", "Pixel 7 - API 36", "Emulator", "Online", "android-x64"), + new("0A041FDD400327", "Pixel 7 Pro", "Device", "Online", "android-arm64"), + ]; + + [Theory] + [CombinatorialData] + public async Task SelectsFrameworkByArrowKeysAndEnter([CombinatorialRange(0, count: 3)] int index) + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + for (var i = 0; i < index; i++) + { + console.Input.PushKey(ConsoleKey.DownArrow); + } + console.Input.PushKey(ConsoleKey.Enter); + + var frameworks = new[] { "net7.0", "net8.0", "net9.0" }; + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var result = await prompt.SelectTargetFrameworkAsync(frameworks, CancellationToken.None); + Assert.Equal(frameworks[index], result); + Assert.Equal(frameworks[index], prompt.PreviousFrameworkSelection); + } + + [Theory] + [CombinatorialData] + public async Task PreviousFrameworkSelectionIsReusedWhenUnchanged([CombinatorialRange(0, count: 3)] int index) + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + for (var i = 0; i < index; i++) + { + console.Input.PushKey(ConsoleKey.DownArrow); + } + console.Input.PushKey(ConsoleKey.Enter); + + var frameworks = new[] { "net7.0", "net8.0", "net9.0" }; + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var first = await prompt.SelectTargetFrameworkAsync(frameworks, CancellationToken.None); + Assert.Equal(frameworks[index], first); + + // Same frameworks (reordered, different casing) should reuse previous selection without prompting + var second = await prompt.SelectTargetFrameworkAsync(["NET9.0", "net7.0", "net8.0"], CancellationToken.None); + Assert.Equal(first, second); + } + + [Fact] + public async Task PromptsAgainWhenFrameworksChange() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushKey(ConsoleKey.Enter); + console.Input.PushKey(ConsoleKey.DownArrow); + console.Input.PushKey(ConsoleKey.Enter); + + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var first = await prompt.SelectTargetFrameworkAsync(["net7.0", "net8.0", "net9.0"], CancellationToken.None); + Assert.Equal("net7.0", first); + + var second = await prompt.SelectTargetFrameworkAsync(["net9.0", "net10.0"], CancellationToken.None); + Assert.Equal("net10.0", second); + } + + [Fact] + public async Task SelectsFrameworkBySearchText() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushText("net9.0"); + console.Input.PushKey(ConsoleKey.Enter); + + var frameworks = new[] { "net7.0", "net8.0", "net9.0", "net10.0" }; + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var result = await prompt.SelectTargetFrameworkAsync(frameworks, CancellationToken.None); + Assert.Equal("net9.0", result); + } + + [Fact] + public async Task SelectsFirstDeviceByEnter() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushKey(ConsoleKey.Enter); + + var devices = CreateTestDevices(); + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var result = await prompt.SelectDeviceAsync(devices, CancellationToken.None); + Assert.Equal(devices[0], result); + Assert.Equal(devices[0], prompt.PreviousDeviceSelection); + } + + [Fact] + public async Task SelectsDeviceBySearchText() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushText("Pixel 7 Pro"); + console.Input.PushKey(ConsoleKey.Enter); + + var devices = CreateTestDevices(); + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var result = await prompt.SelectDeviceAsync(devices, CancellationToken.None); + Assert.Equal("0A041FDD400327", result.Id); + Assert.Equal("android-arm64", result.RuntimeIdentifier); + } + + [Fact] + public async Task PreviousDeviceSelectionIsReusedWhenUnchanged() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushKey(ConsoleKey.Enter); + + var devices = CreateTestDevices(); + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var first = await prompt.SelectDeviceAsync(devices, CancellationToken.None); + Assert.Equal(devices[0], first); + + // Same devices should reuse previous selection without prompting + var second = await prompt.SelectDeviceAsync(devices, CancellationToken.None); + Assert.Equal(first, second); + } + + [Fact] + public async Task PromptsAgainWhenDevicesChange() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushKey(ConsoleKey.Enter); + console.Input.PushText("iPhone 15"); + console.Input.PushKey(ConsoleKey.Enter); + + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var first = await prompt.SelectDeviceAsync(CreateTestDevices(), CancellationToken.None); + Assert.Equal("emulator-5554", first.Id); + + DeviceInfo[] newDevices = + [ + new("sim-1", "iPhone 14 - iOS 18.6", "Simulator", "Booted", "iossimulator-arm64"), + new("sim-2", "iPhone 15 - iOS 26.0", "Simulator", "Shutdown", "iossimulator-arm64"), + ]; + var second = await prompt.SelectDeviceAsync(newDevices, CancellationToken.None); + Assert.Equal("sim-2", second.Id); + } + + [Fact] + public async Task SelectsDeviceBySearchingId() + { + var console = new SpectreTestConsole(); + console.Profile.Capabilities.Interactive = true; + + console.Input.PushText("emulator-5555"); + console.Input.PushKey(ConsoleKey.Enter); + + var devices = CreateTestDevices(); + var prompt = new SpectreBuildParametersSelectionPrompt(console); + + var result = await prompt.SelectDeviceAsync(devices, CancellationToken.None); + Assert.Equal("emulator-5555", result.Id); + Assert.Equal("android-x64", result.RuntimeIdentifier); + } + + [Fact] + public void FormatDevice_WithAllMetadata() + { + var device = new DeviceInfo("emulator-5554", "Pixel 7 - API 35", "Emulator", "Online", "android-x64"); + var formatted = SpectreBuildParametersSelectionPrompt.FormatDevice(device); + Assert.Equal("emulator-5554 - Pixel 7 - API 35 (Emulator, Online)", formatted); + } + + [Fact] + public void FormatDevice_WithoutType() + { + var device = new DeviceInfo("device-1", "My Phone", null, "Connected", null); + var formatted = SpectreBuildParametersSelectionPrompt.FormatDevice(device); + Assert.Equal("device-1 - My Phone (Connected)", formatted); + } + + [Fact] + public void FormatDevice_WithoutStatus() + { + var device = new DeviceInfo("device-1", "My Phone", "Device", null, null); + var formatted = SpectreBuildParametersSelectionPrompt.FormatDevice(device); + Assert.Equal("device-1 - My Phone (Device)", formatted); + } + + [Fact] + public void FormatDevice_IdOnly() + { + var device = new DeviceInfo("device-1", null, null, null, null); + var formatted = SpectreBuildParametersSelectionPrompt.FormatDevice(device); + Assert.Equal("device-1", formatted); + } +} diff --git a/test/dotnet-watch.Tests/HotReload/BuildProjectsTests.cs b/test/dotnet-watch.Tests/HotReload/BuildProjectsTests.cs index 3f73a014018a..15b0bcb6c3b6 100644 --- a/test/dotnet-watch.Tests/HotReload/BuildProjectsTests.cs +++ b/test/dotnet-watch.Tests/HotReload/BuildProjectsTests.cs @@ -50,7 +50,7 @@ public TestContext(ITestOutputHelper output, ImmutableArray + /// Tests device selection in dotnet-watch using the DotnetRunDevices test asset, + /// which provides ComputeAvailableDevices and DeployToDevice MSBuild targets. + /// + [Fact] + public async Task SelectsDevice() + { + var testAsset = TestAssets.CopyTestAsset("DotnetRunDevices") + .WithSource(); + + var tfm = ToolsetInfo.CurrentTargetFramework; + + // Start watch with ReadKeyFromStdin so we can interact with Spectre prompts. + // Pass --framework to skip TFM selection and focus on device selection. + App.Start(testAsset, ["-f", tfm], testFlags: TestFlags.ReadKeyFromStdin); + + // Wait for the device selection prompt + await App.WaitUntilOutputContains(Resources.SelectDevicePrompt); + + // Type to search for "test-device-1" and select it + foreach (var c in "test-device-1") + { + App.SendKey(c); + } + App.SendKey('\r'); + + // The app should launch and print the selected device + await App.WaitUntilOutputContains("Device: test-device-1"); + } + + [Fact] + public async Task AutoSelectsSingleDevice() + { + var testAsset = TestAssets.CopyTestAsset("DotnetRunDevices") + .WithSource(); + + var tfm = ToolsetInfo.CurrentTargetFramework; + + // SingleDevice=true makes ComputeAvailableDevices return only one device. + App.Start(testAsset, ["-f", tfm, "--property", "SingleDevice=true"], testFlags: TestFlags.ReadKeyFromStdin); + + // Should auto-select without prompting and launch the app + await App.WaitUntilOutputContains("Device: single-device"); + } } diff --git a/test/dotnet-watch.Tests/HotReload/TargetFrameworkSelectionPromptTests.cs b/test/dotnet-watch.Tests/HotReload/TargetFrameworkSelectionPromptTests.cs deleted file mode 100644 index a25cd6bc4a6b..000000000000 --- a/test/dotnet-watch.Tests/HotReload/TargetFrameworkSelectionPromptTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using SpectreTestConsole = Spectre.Console.Testing.TestConsole; - -namespace Microsoft.DotNet.Watch.UnitTests; - -public class TargetFrameworkSelectionPromptTests -{ - [Theory] - [CombinatorialData] - public async Task SelectsFrameworkByArrowKeysAndEnter([CombinatorialRange(0, count: 3)] int index) - { - var console = new SpectreTestConsole(); - console.Profile.Capabilities.Interactive = true; - - // Press DownArrow 'index' times to move to the desired item, then Enter to select - for (var i = 0; i < index; i++) - { - console.Input.PushKey(ConsoleKey.DownArrow); - } - console.Input.PushKey(ConsoleKey.Enter); - - var frameworks = new[] { "net7.0", "net8.0", "net9.0" }; - var prompt = new SpectreTargetFrameworkSelectionPrompt(console); - - var result = await prompt.SelectAsync(frameworks, CancellationToken.None); - Assert.Equal(frameworks[index], result); - Assert.Equal(frameworks[index], prompt.PreviousSelection); - } - - [Theory] - [CombinatorialData] - public async Task PreviousSelectionIsReusedWhenFrameworksUnchanged([CombinatorialRange(0, count: 3)] int index) - { - var console = new SpectreTestConsole(); - console.Profile.Capabilities.Interactive = true; - - // First selection via key presses - for (var i = 0; i < index; i++) - { - console.Input.PushKey(ConsoleKey.DownArrow); - } - console.Input.PushKey(ConsoleKey.Enter); - - var frameworks = new[] { "net7.0", "net8.0", "net9.0" }; - var prompt = new SpectreTargetFrameworkSelectionPrompt(console); - - var first = await prompt.SelectAsync(frameworks, CancellationToken.None); - Assert.Equal(frameworks[index], first); - - // Same frameworks (reordered, different casing) should reuse previous selection without prompting - var second = await prompt.SelectAsync(["NET9.0", "net7.0", "net8.0"], CancellationToken.None); - Assert.Equal(first, second); - } - - [Fact] - public async Task PromptsAgainWhenFrameworksChange() - { - var console = new SpectreTestConsole(); - console.Profile.Capabilities.Interactive = true; - - // First selection: pick first item - console.Input.PushKey(ConsoleKey.Enter); - // Second selection (after frameworks change): pick second item - console.Input.PushKey(ConsoleKey.DownArrow); - console.Input.PushKey(ConsoleKey.Enter); - - var prompt = new SpectreTargetFrameworkSelectionPrompt(console); - - var first = await prompt.SelectAsync(["net7.0", "net8.0", "net9.0"], CancellationToken.None); - Assert.Equal("net7.0", first); - - // Different set of frameworks — should prompt again - var second = await prompt.SelectAsync(["net9.0", "net10.0"], CancellationToken.None); - Assert.Equal("net10.0", second); - } - - [Fact] - public async Task SelectsFrameworkBySearchText() - { - var console = new SpectreTestConsole(); - console.Profile.Capabilities.Interactive = true; - - // Type "net9.0" to filter, then Enter to select the match - console.Input.PushText("net9.0"); - console.Input.PushKey(ConsoleKey.Enter); - - var frameworks = new[] { "net7.0", "net8.0", "net9.0", "net10.0" }; - var prompt = new SpectreTargetFrameworkSelectionPrompt(console); - - var result = await prompt.SelectAsync(frameworks, CancellationToken.None); - Assert.Equal("net9.0", result); - } -} diff --git a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs index 3bbbeddb34d3..4e95ae6e6fc3 100644 --- a/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs +++ b/test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs @@ -98,7 +98,7 @@ internal InProcTestWatcher CreateInProcWatcher(TestAsset testAsset, string[] arg }); var context = program.CreateContext(processRunner); - var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: factory, targetFrameworkSelectionPrompt: null); + var watcher = new HotReloadDotNetWatcher(context, console, runtimeProcessLauncherFactory: factory, selectionPrompt: null); var shutdownSource = new CancellationTokenSource(); return new InProcTestWatcher(Logger, watcher, context, eventObserver, reporter, console, serviceHolder, shutdownSource); diff --git a/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs b/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs index f5065d47dfb6..c057afa3829b 100644 --- a/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs +++ b/test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs @@ -11,6 +11,7 @@ internal class MockFileSetFactory() : MSBuildFileSetFactory( buildArguments: [], new ProcessRunner(processCleanupTimeout: TimeSpan.Zero), NullLogger.Instance, + TestOptions.GlobalOptions, TestOptions.GetEnvironmentOptions(Environment.CurrentDirectory) is var options ? options : options) { public Func? TryCreateImpl; diff --git a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs index 3744a59299b2..9e4d691b2975 100644 --- a/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs +++ b/test/dotnet-watch.Tests/TestUtilities/TestOptions.cs @@ -12,6 +12,7 @@ internal static class TestOptions public static int GetTestPort() => Interlocked.Increment(ref s_testPort); + public static readonly GlobalOptions GlobalOptions = new() { BinaryLogPath = "msbuild.binlog" }; public static readonly ProjectOptions ProjectOptions = GetProjectOptions(GetCommandLineOptions([])); public static EnvironmentOptions GetEnvironmentOptions(string workingDirectory = "", TestAsset? asset = null)