Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,26 @@ public void WatchFiles(FileWatcher fileWatcher)
fileWatcher.WatchFiles(BuildFiles);
}

public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumerable<string> buildArguments, EnvironmentOptions environmentOptions)
{
// See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md

return CommandLineOptions.ParseBuildProperties(buildArguments)
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
.SetItem(PropertyNames.DotNetWatchBuild, "true")
.SetItem(PropertyNames.DesignTimeBuild, "true")
.SetItem(PropertyNames.SkipCompilerExecution, "true")
.SetItem(PropertyNames.ProvideCommandLineArgs, "true")
// F# targets depend on host path variable:
.SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath);
}

/// <summary>
/// Loads project graph and performs design-time build.
/// </summary>
public static EvaluationResult? TryCreate(
ProjectGraphFactory factory,
string rootProjectPath,
IEnumerable<string> buildArguments,
ILogger logger,
GlobalOptions options,
EnvironmentOptions environmentOptions,
Expand All @@ -46,20 +60,8 @@ public void WatchFiles(FileWatcher fileWatcher)
{
var buildReporter = new BuildReporter(logger, options, environmentOptions);

// See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md

var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value)
.SetItem(PropertyNames.DotNetWatchBuild, "true")
.SetItem(PropertyNames.DesignTimeBuild, "true")
.SetItem(PropertyNames.SkipCompilerExecution, "true")
.SetItem(PropertyNames.ProvideCommandLineArgs, "true")
// F# targets depend on host path variable:
.SetItem("DOTNET_HOST_PATH", environmentOptions.MuxerPath);

var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(
var projectGraph = factory.TryLoadProjectGraph(
rootProjectPath,
globalOptions,
logger,
projectGraphRequired: true,
cancellationToken);
Expand Down
80 changes: 80 additions & 0 deletions src/BuiltInTools/dotnet-watch/Build/ProjectGraphFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Microsoft.DotNet.Watch;

internal sealed class ProjectGraphFactory(ImmutableDictionary<string, string> globalOptions)
{
/// <summary>
/// Reuse <see cref="ProjectCollection"/> with XML element caching to improve performance.
///
/// The cache is automatically updated when build files change.
/// https://github.com/dotnet/msbuild/blob/b6f853defccd64ae1e9c7cf140e7e4de68bff07c/src/Build/Definition/ProjectCollection.cs#L343-L354
/// </summary>
private readonly ProjectCollection _collection = new(
globalProperties: globalOptions,
loggers: [],
remoteLoggers: [],
ToolsetDefinitionLocations.Default,
maxNodeCount: 1,
onlyLogCriticalEvents: false,
loadProjectsReadOnly: false,
useAsynchronousLogging: false,
reuseProjectRootElementCache: true);

/// <summary>
/// Tries to create a project graph by running the build evaluation phase on the <see cref="rootProjectFile"/>.
/// </summary>
public ProjectGraph? TryLoadProjectGraph(
string rootProjectFile,
ILogger logger,
bool projectGraphRequired,
CancellationToken cancellationToken)
{
var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
try
{
return new ProjectGraph([entryPoint], _collection, projectInstanceFactory: null, cancellationToken);
}
catch (Exception e) when (e is not OperationCanceledException)
{
// ProejctGraph aggregates OperationCanceledException exception,
// throw here to propagate the cancellation.
cancellationToken.ThrowIfCancellationRequested();

logger.LogDebug("Failed to load project graph.");

if (e is AggregateException { InnerExceptions: var innerExceptions })
{
foreach (var inner in innerExceptions)
{
Report(inner);
}
}
else
{
Report(e);
}

void Report(Exception e)
{
if (projectGraphRequired)
{
logger.LogError(e.Message);
}
else
{
logger.LogWarning(e.Message);
}
}
}

return null;
}
}
68 changes: 0 additions & 68 deletions src/BuiltInTools/dotnet-watch/Build/ProjectGraphUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,82 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Cli;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Microsoft.DotNet.Watch;

internal static class ProjectGraphUtilities
{
/// <summary>
/// Tries to create a project graph by running the build evaluation phase on the <see cref="rootProjectFile"/>.
/// </summary>
public static ProjectGraph? TryLoadProjectGraph(
string rootProjectFile,
ImmutableDictionary<string, string> globalOptions,
ILogger logger,
bool projectGraphRequired,
CancellationToken cancellationToken)
{
var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);
try
{
// Create a new project collection that does not reuse element cache
// to work around https://github.com/dotnet/msbuild/issues/12064:
var collection = new ProjectCollection(
globalProperties: globalOptions,
loggers: [],
remoteLoggers: [],
ToolsetDefinitionLocations.Default,
maxNodeCount: 1,
onlyLogCriticalEvents: false,
loadProjectsReadOnly: false,
useAsynchronousLogging: false,
reuseProjectRootElementCache: false);

return new ProjectGraph([entryPoint], collection, projectInstanceFactory: null, cancellationToken);
}
catch (Exception e) when (e is not OperationCanceledException)
{
// ProejctGraph aggregates OperationCanceledException exception,
// throw here to propagate the cancellation.
cancellationToken.ThrowIfCancellationRequested();

logger.LogDebug("Failed to load project graph.");

if (e is AggregateException { InnerExceptions: var innerExceptions })
{
foreach (var inner in innerExceptions)
{
Report(inner);
}
}
else
{
Report(e);
}

void Report(Exception e)
{
if (projectGraphRequired)
{
logger.LogError(e.Message);
}
else
{
logger.LogWarning(e.Message);
}
}
}

return null;
}

public static string GetDisplayName(this ProjectGraphNode projectNode)
=> $"{Path.GetFileNameWithoutExtension(projectNode.ProjectInstance.FullPath)} ({projectNode.GetTargetFramework()})";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal sealed class HotReloadDotNetWatcher
private readonly RestartPrompt? _rudeEditRestartPrompt;

private readonly DotNetWatchContext _context;
private readonly ProjectGraphFactory _designTimeBuildGraphFactory;

internal Task? Test_FileChangesCompletedTask { get; set; }

Expand All @@ -40,6 +41,11 @@ public HotReloadDotNetWatcher(DotNetWatchContext context, IConsole console, IRun

_rudeEditRestartPrompt = new RestartPrompt(context.Logger, consoleInput, noPrompt ? true : null);
}

_designTimeBuildGraphFactory = new ProjectGraphFactory(
EvaluationResult.GetGlobalBuildOptions(
context.RootProjectOptions.BuildArguments,
context.EnvironmentOptions));
}

public async Task WatchAsync(CancellationToken shutdownCancellationToken)
Expand Down Expand Up @@ -824,8 +830,8 @@ private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(bool restore,
var stopwatch = Stopwatch.StartNew();

var result = EvaluationResult.TryCreate(
_context.RootProjectOptions.ProjectPath,
_context.RootProjectOptions.BuildArguments,
_designTimeBuildGraphFactory,
_context.RootProjectOptions.ProjectPath,
_context.BuildLogger,
_context.Options,
_context.EnvironmentOptions,
Expand Down
8 changes: 4 additions & 4 deletions src/BuiltInTools/dotnet-watch/Watch/MsBuildFileSetFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ internal class MSBuildFileSetFactory(
private EnvironmentOptions EnvironmentOptions => buildReporter.EnvironmentOptions;
private ILogger Logger => buildReporter.Logger;

private readonly ProjectGraphFactory _designTimeBuildGraphFactory = new(
globalOptions: CommandLineOptions.ParseBuildProperties(buildArguments).ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value));

internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> files, ProjectGraph? projectGraph)
{
public readonly IReadOnlyDictionary<string, FileItem> Files = files;
Expand Down Expand Up @@ -124,10 +127,7 @@ void AddFile(string filePath, string? staticWebAssetPath)
ProjectGraph? projectGraph = null;
if (requireProjectGraph != null)
{
var globalOptions = CommandLineOptions.ParseBuildProperties(buildArguments)
.ToImmutableDictionary(keySelector: arg => arg.key, elementSelector: arg => arg.value);

projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(rootProjectFile, globalOptions, Logger, requireProjectGraph.Value, cancellationToken);
projectGraph = _designTimeBuildGraphFactory.TryLoadProjectGraph(rootProjectFile, Logger, requireProjectGraph.Value, cancellationToken);
if (projectGraph == null && requireProjectGraph == true)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public async Task ReferenceOutputAssembly_False()
var reporter = new TestReporter(Logger);
var loggerFactory = new LoggerFactory(reporter);
var logger = loggerFactory.CreateLogger("Test");
var projectGraph = ProjectGraphUtilities.TryLoadProjectGraph(options.ProjectPath, globalOptions: [], logger, projectGraphRequired: false, CancellationToken.None);
var factory = new ProjectGraphFactory(globalOptions: []);
var projectGraph = factory.TryLoadProjectGraph(options.ProjectPath, logger, projectGraphRequired: false, CancellationToken.None);
var handler = new CompilationHandler(logger, processRunner);

await handler.Workspace.UpdateProjectConeAsync(hostProject, CancellationToken.None);
Expand Down
Loading