Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
53 changes: 22 additions & 31 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();
logger.Log(MessageDescriptor.LoadingProjects);

var projectGraph = factory.TryLoadProjectGraph(projectGraphRequired: true, cancellationToken);

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 @@ -128,7 +108,17 @@ public static ImmutableDictionary<string, string> GetGlobalBuildProperties(IEnum

var buildRequests =
(from node in projectGraph.Graph.ProjectNodesTopologicallySorted
where node.ProjectInstance.GetPropertyValue(PropertyNames.TargetFramework) != ""
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 ||
node.ReferencingProjects.Count != 1 ||
!node.ReferencingProjects.First().IsRoot

let targets = GetBuildTargets(node.ProjectInstance, environmentOptions)
where targets is not []
select BuildRequest.Create(node.ProjectInstance, [.. targets])).ToArray();
Expand All @@ -151,6 +141,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 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
35 changes: 28 additions & 7 deletions src/Dotnet.Watch/Watch/Build/ProjectGraphUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@ 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 bool IsRoot
=> projectNode.ReferencingProjects.Count == 0;
}

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 +75,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 +93,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 +131,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