Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/Dotnet.Watch/HotReloadClient/Logging/LogEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@ public static void Log<TArg1, TArg2, TArg3>(this ILogger logger, LogEvent<(TArg1
public static readonly LogEvent<string> SendingStaticAssetUpdateRequest = Create<string>(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'.");
public static readonly LogEvent<string> RefreshServerRunningAt = Create<string>(LogLevel.Debug, "Refresh server running at {0}.");
public static readonly LogEvent<None> ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server.");
public static readonly LogEvent<string> ManifestFileNotFound = Create<string>(LogLevel.Debug, "Manifest file '{0}' not found.");
}

Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public bool TryGetBundleFilePath(string bundleFileName, [NotNullWhen(true)] out
}
catch (FileNotFoundException)
{
logger.LogDebug("File '{FilePath}' does not exist.", path);
logger.Log(LogEvents.ManifestFileNotFound, path);
return null;
}
catch (Exception e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ protected async Task<int> LaunchWatcherAsync(
EnvironmentOptions = EnvironmentOptions,
MainProjectOptions = null,
BuildArguments = [],
TargetFramework = null,
RootProjects = rootProjects,
BrowserRefreshServerFactory = new BrowserRefreshServerFactory(),
BrowserLauncher = new BrowserLauncher(logger, Reporter, EnvironmentOptions),
Expand Down
74 changes: 36 additions & 38 deletions src/Dotnet.Watch/Watch/Build/EvaluationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,26 @@ internal sealed class EvaluationResult(
IReadOnlyDictionary<ProjectInstanceId, StaticWebAssetsManifest> staticWebAssetsManifests,
ProjectBuildManager buildManager)
{
public readonly IReadOnlyDictionary<string, FileItem> Files = files;
public readonly LoadedProjectGraph ProjectGraph = projectGraph;
public readonly ProjectBuildManager BuildManager = buildManager;
public IReadOnlyDictionary<string, FileItem> Files => files;
public LoadedProjectGraph ProjectGraph => projectGraph;
public ProjectBuildManager BuildManager => buildManager;

public readonly FilePathExclusions ItemExclusions
= projectGraph != null ? FilePathExclusions.Create(projectGraph.Graph) : FilePathExclusions.Empty;

private readonly Lazy<IReadOnlySet<string>> _lazyBuildFiles
= new(() => projectGraph != null ? CreateBuildFileSet(projectGraph.Graph) : new HashSet<string>());

private static IReadOnlySet<string> CreateBuildFileSet(ProjectGraph projectGraph)
=> projectGraph.ProjectNodes.SelectMany(p => p.ProjectInstance.ImportPaths)
.Concat(projectGraph.ProjectNodes.Select(p => p.ProjectInstance.FullPath))
.ToHashSet(PathUtilities.OSSpecificPathComparer);

public IReadOnlySet<string> BuildFiles
=> _lazyBuildFiles.Value;

public IReadOnlyDictionary<ProjectInstanceId, StaticWebAssetsManifest> StaticWebAssetsManifests
=> staticWebAssetsManifests;

public IReadOnlyDictionary<ProjectInstanceId, ProjectInstance> RestoredProjectInstances
=> restoredProjectInstances;

public void WatchFiles(FileWatcher fileWatcher)
public void WatchFileItems(FileWatcher fileWatcher)
{
fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true);

fileWatcher.WatchContainingDirectories(
StaticWebAssetsManifests.Values.SelectMany(static manifest => manifest.DiscoveryPatterns.Select(static pattern => pattern.Directory)),
includeSubdirectories: true);

fileWatcher.WatchFiles(BuildFiles);
}

public static ImmutableDictionary<string, string> GetGlobalBuildProperties(IEnumerable<string> buildArguments, EnvironmentOptions environmentOptions)
Expand All @@ -70,31 +57,24 @@ public static ImmutableDictionary<string, string> GetGlobalBuildProperties(IEnum
/// Loads project graph and performs design-time build.
/// </summary>
public static async ValueTask<EvaluationResult?> TryCreateAsync(
ProjectGraphFactory factory,
LoadedProjectGraph projectGraph,
ILogger logger,
GlobalOptions globalOptions,
EnvironmentOptions environmentOptions,
string? mainProjectTargetFramework,
bool restore,
CancellationToken cancellationToken)
{
var logger = factory.Logger;
var stopwatch = Stopwatch.StartNew();

var projectGraph = factory.TryLoadProjectGraph(projectGraphRequired: true, cancellationToken);
logger.Log(MessageDescriptor.LoadingProjects);

if (projectGraph == null)
{
return null;
}
var projectLoadingStopwatch = Stopwatch.StartNew();
var stopwatch = Stopwatch.StartNew();

var buildReporter = new BuildReporter(projectGraph.Logger, globalOptions, environmentOptions);
var buildManager = new ProjectBuildManager(projectGraph.ProjectCollection, buildReporter);

logger.LogDebug("Project graph loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));

if (restore)
{
stopwatch.Restart();

var restoreRequests = projectGraph.Graph.GraphRoots.Select(node => BuildRequest.Create(node.ProjectInstance, [TargetNames.Restore])).ToArray();

if (await buildManager.BuildAsync(
Expand Down Expand Up @@ -126,12 +106,7 @@ public static ImmutableDictionary<string, string> GetGlobalBuildProperties(IEnum
// Update the project instances of the graph with design-time build results.
// The properties and items set by DTB will be used by the Workspace to create Roslyn representation of projects.

var buildRequests =
(from node in projectGraph.Graph.ProjectNodesTopologicallySorted
where node.ProjectInstance.GetPropertyValue(PropertyNames.TargetFramework) != ""
let targets = GetBuildTargets(node.ProjectInstance, environmentOptions)
where targets is not []
select BuildRequest.Create(node.ProjectInstance, [.. targets])).ToArray();
var buildRequests = CreateDesignTimeBuildRequests(projectGraph.Graph, mainProjectTargetFramework, environmentOptions.SuppressHandlingStaticWebAssets).ToImmutableArray();

var buildResults = await buildManager.BuildAsync(
buildRequests,
Expand All @@ -151,6 +126,7 @@ where targets is not []
}

logger.LogDebug("Design-time build completed in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
logger.Log(MessageDescriptor.LoadedProjects, projectGraph.Graph.ProjectNodes.Count, projectLoadingStopwatch.Elapsed.TotalSeconds);

ProcessBuildResults(buildResults, logger, out var fileItems, out var staticWebAssetManifests);

Expand All @@ -159,6 +135,28 @@ where targets is not []
return new EvaluationResult(projectGraph, restoredProjectInstances, fileItems, staticWebAssetManifests, buildManager);
}

// internal for testing
internal static IEnumerable<BuildRequest<object?>> CreateDesignTimeBuildRequests(ProjectGraph graph, string? mainProjectTargetFramework, bool suppressStaticWebAssets)
{
return from node in graph.ProjectNodesTopologicallySorted
let targetFramework = node.ProjectInstance.GetTargetFramework()

// skip outer-build projects
where targetFramework != ""

// skip root projects that do not match main project TFM, if specified:
where mainProjectTargetFramework == null ||
targetFramework == mainProjectTargetFramework ||
HasParentWithTargetFramework(node)

let targets = GetBuildTargets(node.ProjectInstance, suppressStaticWebAssets)
where targets is not []
select BuildRequest.Create(node.ProjectInstance, [.. targets]);

static bool HasParentWithTargetFramework(ProjectGraphNode node)
=> node.ReferencingProjects.Any(p => p.ProjectInstance.GetTargetFramework() != "");
}

private static void ProcessBuildResults(
ImmutableArray<BuildResult<object?>> buildResults,
ILogger logger,
Expand Down Expand Up @@ -245,7 +243,7 @@ void AddFile(string relativePath, string? staticWebAssetRelativeUrl)
staticWebAssetManifests = staticWebAssetManifestsBuilder;
}

private static string[] GetBuildTargets(ProjectInstance projectInstance, EnvironmentOptions environmentOptions)
private static string[] GetBuildTargets(ProjectInstance projectInstance, bool suppressStaticWebAssets)
{
var compileTarget = projectInstance.Targets.ContainsKey(TargetNames.CompileDesignTime)
? TargetNames.CompileDesignTime
Expand All @@ -263,7 +261,7 @@ private static string[] GetBuildTargets(ProjectInstance projectInstance, Environ
compileTarget
};

if (!environmentOptions.SuppressHandlingStaticWebAssets)
if (!suppressStaticWebAssets)
{
// generates static file asset manifest
if (projectInstance.Targets.ContainsKey(TargetNames.GenerateComputedBuildStaticWebAssets))
Expand Down
20 changes: 14 additions & 6 deletions src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Graph;
using Microsoft.Extensions.Logging;
Expand All @@ -9,21 +10,28 @@ namespace Microsoft.DotNet.Watch
{
internal sealed class LoadedProjectGraph(ProjectGraph graph, ProjectCollection collection, ILogger logger)
{
// full path of proj file to list of nodes representing all target frameworks of the project:
public readonly IReadOnlyDictionary<string, IReadOnlyList<ProjectGraphNode>> Map =
graph.ProjectNodes.GroupBy(n => n.ProjectInstance.FullPath).ToDictionary(
// full path of proj file to list of nodes representing all target frameworks of the project (excluding outer build nodes):
private readonly IReadOnlyDictionary<string, IReadOnlyList<ProjectGraphNode>> _innerBuildNodes =
graph.ProjectNodes.Where(n => n.ProjectInstance.GetTargetFramework() != "").GroupBy(n => n.ProjectInstance.FullPath).ToDictionary(
keySelector: static g => g.Key,
elementSelector: static g => (IReadOnlyList<ProjectGraphNode>)[.. g]);

private readonly Lazy<IReadOnlySet<string>> _lazyBuildFiles = new(() =>
graph.ProjectNodes.SelectMany(p => p.ProjectInstance.ImportPaths)
.Concat(graph.ProjectNodes.Select(p => p.ProjectInstance.FullPath))
.ToHashSet(PathUtilities.OSSpecificPathComparer));

public ProjectGraph Graph => graph;
public ILogger Logger => logger;
public ProjectCollection ProjectCollection => collection;

public IReadOnlySet<string> BuildFiles => _lazyBuildFiles.Value;

public IReadOnlyList<ProjectGraphNode> GetProjectNodes(string projectPath)
{
if (Map.TryGetValue(projectPath, out var rootProjectNodes))
if (_innerBuildNodes.TryGetValue(projectPath, out var nodes))
{
return rootProjectNodes;
return nodes;
}

logger.LogError("Project '{ProjectPath}' not found in the project graph.", projectPath);
Expand Down Expand Up @@ -52,7 +60,7 @@ public IReadOnlyList<ProjectGraphNode> GetProjectNodes(string projectPath)
ProjectGraphNode? candidate = null;
foreach (var node in projectNodes)
{
if (node.ProjectInstance.GetPropertyValue("TargetFramework") == targetFramework)
if (node.ProjectInstance.GetTargetFramework() == targetFramework)
{
if (candidate != null)
{
Expand Down
13 changes: 9 additions & 4 deletions src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Build.Evaluation;
Expand All @@ -15,7 +16,7 @@ namespace Microsoft.DotNet.Watch;

internal sealed class ProjectGraphFactory(
ImmutableArray<ProjectRepresentation> rootProjects,
string? targetFramework,
string? virtualProjectTargetFramework,
ImmutableDictionary<string, string> buildProperties,
ILogger logger)
{
Expand All @@ -36,7 +37,7 @@ internal sealed class ProjectGraphFactory(
useAsynchronousLogging: false,
reuseProjectRootElementCache: true);

private readonly string _targetFramework = targetFramework ?? GetProductTargetFramework();
private readonly string _virtualProjectTargetFramework = virtualProjectTargetFramework ?? GetProductTargetFramework();

public ILogger Logger => logger;

Expand All @@ -55,10 +56,14 @@ private static string GetProductTargetFramework()
var entryPoints = rootProjects.Select(p => new ProjectGraphEntryPoint(p.ProjectGraphPath, buildProperties));
try
{
return new LoadedProjectGraph(
var stopwatch = Stopwatch.StartNew();
var graph = new LoadedProjectGraph(
new ProjectGraph(entryPoints, _collection, (path, globalProperties, collection) => CreateProjectInstance(path, globalProperties, collection, logger), cancellationToken),
_collection,
logger);

logger.LogDebug("Project graph loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
return graph;
}
catch (ProjectCreationFailedException)
{
Expand Down Expand Up @@ -120,7 +125,7 @@ private ProjectInstance CreateProjectInstance(string projectPath, Dictionary<str

var projectInstance = VirtualProjectBuilder.CreateProjectInstance(
entryPointFilePath,
_targetFramework,
_virtualProjectTargetFramework,
projectCollection,
(path, line, message) =>
{
Expand Down
33 changes: 26 additions & 7 deletions src/Dotnet.Watch/Watch/Build/ProjectGraphUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@
using System.Runtime.Versioning;
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.DotNet.HotReload;

namespace Microsoft.DotNet.Watch;

internal static class ProjectGraphUtilities
{
public static string GetDisplayName(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetDisplayName();
extension(ProjectGraphNode projectNode)
{
public string GetDisplayName()
=> projectNode.ProjectInstance.GetDisplayName();
}

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

public static string GetTargetFramework(this ProjectInstance project)
=> project.GetPropertyValue(PropertyNames.TargetFramework);

public static IEnumerable<string> GetTargetFrameworks(this ProjectInstance project)
public static IReadOnlyList<string> GetTargetFrameworks(this ProjectInstance project)
=> project.GetStringListPropertyValue(PropertyNames.TargetFrameworks);

public static Version? GetTargetFrameworkVersion(this ProjectGraphNode projectNode)
Expand Down Expand Up @@ -69,13 +73,13 @@ public static bool IsAutoRestartEnabled(this ProjectGraphNode projectNode)
public static bool AreDefaultItemsEnabled(this ProjectGraphNode projectNode)
=> projectNode.GetBooleanPropertyValue(PropertyNames.EnableDefaultItems);

public static IEnumerable<string> GetDefaultItemExcludes(this ProjectGraphNode projectNode)
public static IReadOnlyList<string> GetDefaultItemExcludes(this ProjectGraphNode projectNode)
=> projectNode.GetStringListPropertyValue(PropertyNames.DefaultItemExcludes);

public static IEnumerable<string> GetStringListPropertyValue(this ProjectGraphNode projectNode, string propertyName)
public static IReadOnlyList<string> GetStringListPropertyValue(this ProjectGraphNode projectNode, string propertyName)
=> projectNode.ProjectInstance.GetStringListPropertyValue(propertyName);

public static IEnumerable<string> GetStringListPropertyValue(this ProjectInstance project, string propertyName)
public static IReadOnlyList<string> GetStringListPropertyValue(this ProjectInstance project, string propertyName)
=> project.GetPropertyValue(propertyName).Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);

public static bool GetBooleanPropertyValue(this ProjectGraphNode projectNode, string propertyName, bool defaultValue = false)
Expand All @@ -87,15 +91,27 @@ public static bool GetBooleanPropertyValue(this ProjectInstance project, string
public static bool GetBooleanMetadataValue(this ProjectItemInstance item, string metadataName, bool defaultValue = false)
=> item.GetMetadataValue(metadataName) is { Length: > 0 } value ? bool.TryParse(value, out var result) && result : defaultValue;

/// <summary>
/// Yields the project itself and all its ancestors, excluding outer build nodes.
/// </summary>
public static IEnumerable<ProjectGraphNode> GetAncestorsAndSelf(this ProjectGraphNode project)
=> GetAncestorsAndSelf([project]);

/// <summary>
/// Yields the given projects and all their ancestors, excluding outer build nodes.
/// </summary>
public static IEnumerable<ProjectGraphNode> GetAncestorsAndSelf(this IEnumerable<ProjectGraphNode> projects)
=> GetTransitiveProjects(projects, static project => project.ReferencingProjects);

/// <summary>
/// Yields the project itself and all transitively referenced projects, excluding outer build nodes.
/// </summary>
public static IEnumerable<ProjectGraphNode> GetDescendantsAndSelf(this ProjectGraphNode project)
=> GetDescendantsAndSelf([project]);

/// <summary>
/// Yields the given projects and all transitively referenced projects, excluding outer build nodes.
/// </summary>
public static IEnumerable<ProjectGraphNode> GetDescendantsAndSelf(this IEnumerable<ProjectGraphNode> projects)
=> GetTransitiveProjects(projects, static project => project.ProjectReferences);

Expand All @@ -113,7 +129,10 @@ private static IEnumerable<ProjectGraphNode> GetTransitiveProjects(IEnumerable<P
var project = queue.Dequeue();
if (visited.Add(project))
{
yield return project;
if (project.ProjectInstance.GetTargetFramework() != "")
{
yield return project;
}

foreach (var referencingProject in getEdges(project))
{
Expand Down
5 changes: 0 additions & 5 deletions src/Dotnet.Watch/Watch/Context/DotNetWatchContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ internal sealed class DotNetWatchContext : IDisposable
/// </summary>
public required ProjectOptions? MainProjectOptions { get; init; }

/// <summary>
/// Default target framework.
/// </summary>
public required string? TargetFramework { get; init; }

/// <summary>
/// Additional arguments passed to `dotnet build` when building projects.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Dotnet.Watch/Watch/Context/ProjectOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ internal sealed record ProjectOptions

public required string WorkingDirectory { get; init; }

/// <summary>
/// Target framework to use to launch the project.
/// If the project multi-targets and <see cref="TargetFramework"/> is null
/// the user will be prompted for the framework in interactive mode
/// or an error is reported in non-interactive mode.
/// </summary>
public string? TargetFramework { get; init; }

/// <summary>
/// No value indicates that no launch profile should be used.
/// Null value indicates that the default launch profile should be used.
Expand Down
Loading
Loading