Skip to content

Commit 72d9e94

Browse files
committed
Switch to HotReloadMSBuildWorkspace
1 parent 198b2c4 commit 72d9e94

File tree

12 files changed

+133
-291
lines changed

12 files changed

+133
-291
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
3838
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(MicrosoftCodeAnalysisPackageVersion)" />
3939
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion)" />
40+
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost" Version="$(MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion)" />
4041
<PackageVersion Include="Microsoft.CodeAnalysis.ExternalAccess.HotReload" Version="$(MicrosoftCodeAnalysisExternalAccessHotReloadPackageVersion)" />
4142

4243
<!-- roslyn-sdk dependencies-->

eng/Version.Details.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ This file should be imported by eng/Versions.props
112112
<MicrosoftCodeAnalysisPublicApiAnalyzersPackageVersion>5.3.0-2.25610.11</MicrosoftCodeAnalysisPublicApiAnalyzersPackageVersion>
113113
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>5.3.0-2.25610.11</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
114114
<MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>5.3.0-2.25610.11</MicrosoftCodeAnalysisWorkspacesMSBuildPackageVersion>
115+
<MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion>5.3.0-2.25610.11</MicrosoftCodeAnalysisWorkspacesMSBuildBuildHostPackageVersion>
115116
<MicrosoftNetCompilersToolsetPackageVersion>5.3.0-2.25610.11</MicrosoftNetCompilersToolsetPackageVersion>
116117
<MicrosoftNetCompilersToolsetFrameworkPackageVersion>5.3.0-2.25610.11</MicrosoftNetCompilersToolsetFrameworkPackageVersion>
117118
<!-- nuget/nuget.client dependencies -->

eng/Version.Details.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@
124124
<Uri>https://github.com/dotnet/roslyn</Uri>
125125
<Sha>46a48b8c1dfce7c35da115308bedd6a5954fd78a</Sha>
126126
</Dependency>
127+
<Dependency Name="Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost" Version="5.3.0-2.25610.11">
128+
<Uri>https://github.com/dotnet/roslyn</Uri>
129+
<Sha>46a48b8c1dfce7c35da115308bedd6a5954fd78a</Sha>
130+
</Dependency>
127131
<Dependency Name="Microsoft.Build.NuGetSdkResolver" Version="7.1.0-preview.1.42">
128132
<Uri>https://github.com/nuget/nuget.client</Uri>
129133
<Sha>b5efdd1f17df11700c9383def6ece79a40218ccd</Sha>

src/BuiltInTools/Watch/Build/EvaluationResult.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
namespace Microsoft.DotNet.Watch;
1111

12-
internal sealed class EvaluationResult(ProjectGraph projectGraph, IReadOnlyDictionary<string, FileItem> files, IReadOnlyDictionary<ProjectInstanceId, StaticWebAssetsManifest> staticWebAssetsManifests)
12+
internal sealed class EvaluationResult(
13+
ProjectGraph projectGraph,
14+
ImmutableArray<ProjectInstance> restoredProjectInstances,
15+
IReadOnlyDictionary<string, FileItem> files,
16+
IReadOnlyDictionary<ProjectInstanceId, StaticWebAssetsManifest> staticWebAssetsManifests)
1317
{
1418
public readonly IReadOnlyDictionary<string, FileItem> Files = files;
1519
public readonly ProjectGraph ProjectGraph = projectGraph;
@@ -31,6 +35,9 @@ public IReadOnlySet<string> BuildFiles
3135
public IReadOnlyDictionary<ProjectInstanceId, StaticWebAssetsManifest> StaticWebAssetsManifests
3236
=> staticWebAssetsManifests;
3337

38+
public ImmutableArray<ProjectInstance> RestoredProjectInstances
39+
=> restoredProjectInstances;
40+
3441
public void WatchFiles(FileWatcher fileWatcher)
3542
{
3643
fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true);
@@ -94,15 +101,19 @@ public static ImmutableDictionary<string, string> GetGlobalBuildOptions(IEnumera
94101
}
95102
}
96103

104+
// Capture the snapshot of original project instances after Restore target has been run.
105+
// These instances can be used to evaluate additional targets (e.g. deployment) if needed.
106+
var restoredProjectInstances = projectGraph.ProjectNodesTopologicallySorted.Select(node => node.ProjectInstance.DeepCopy()).ToImmutableArray();
107+
97108
var fileItems = new Dictionary<string, FileItem>();
98109
var staticWebAssetManifests = new Dictionary<ProjectInstanceId, StaticWebAssetsManifest>();
99110

111+
// Update the project instances of the graph with design-time build results.
112+
// The properties and items set by DTB will be used by the Workspace to create Roslyn representation of projects.
113+
100114
foreach (var project in projectGraph.ProjectNodesTopologicallySorted)
101115
{
102-
// Deep copy so that we can reuse the graph for building additional targets later on.
103-
// If we didn't copy the instance the targets might duplicate items that were already
104-
// populated by design-time build.
105-
var projectInstance = project.ProjectInstance.DeepCopy();
116+
var projectInstance = project.ProjectInstance;
106117

107118
// skip outer build project nodes:
108119
if (projectInstance.GetPropertyValue(PropertyNames.TargetFramework) == "")
@@ -189,7 +200,7 @@ void AddFile(string relativePath, string? staticWebAssetRelativeUrl)
189200

190201
buildReporter.ReportWatchedFiles(fileItems);
191202

192-
return new EvaluationResult(projectGraph, fileItems, staticWebAssetManifests);
203+
return new EvaluationResult(projectGraph, restoredProjectInstances, fileItems, staticWebAssetManifests);
193204
}
194205

195206
private static string[] GetBuildTargets(ProjectInstance projectInstance, EnvironmentOptions environmentOptions)

src/BuiltInTools/Watch/Build/FilePathExclusions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static FilePathExclusions Create(ProjectGraph projectGraph)
3939
{
4040
// If default items are not enabled exclude just the output directories.
4141

42-
TryAddOutputDir(projectNode.GetOutputDirectory());
42+
TryAddOutputDir(projectNode.ProjectInstance.GetOutputDirectory());
4343
TryAddOutputDir(projectNode.ProjectInstance.GetIntermediateOutputDirectory());
4444

4545
void TryAddOutputDir(string? dir)

src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ public static bool IsNetCoreApp(this ProjectGraphNode projectNode, Version minVe
5151
public static bool IsWebApp(this ProjectGraphNode projectNode)
5252
=> projectNode.GetCapabilities().Any(static value => value is ProjectCapability.AspNetCore or ProjectCapability.WebAssembly);
5353

54-
public static string? GetOutputDirectory(this ProjectGraphNode projectNode)
55-
=> projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetPath) is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(projectNode.ProjectInstance.Directory, path)) : null;
54+
public static string? GetOutputDirectory(this ProjectInstance project)
55+
=> project.GetPropertyValue(PropertyNames.TargetPath) is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(project.Directory, path)) : null;
5656

5757
public static string GetAssemblyName(this ProjectGraphNode projectNode)
5858
=> projectNode.ProjectInstance.GetPropertyValue(PropertyNames.TargetName);

src/BuiltInTools/Watch/FileWatcher/ChangeKind.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api;
5+
46
namespace Microsoft.DotNet.Watch;
57

68
internal enum ChangeKind
@@ -10,6 +12,18 @@ internal enum ChangeKind
1012
Delete
1113
}
1214

15+
internal static class ChangeKindExtensions
16+
{
17+
public static HotReloadFileChangeKind Convert(this ChangeKind changeKind) =>
18+
changeKind switch
19+
{
20+
ChangeKind.Update => HotReloadFileChangeKind.Update,
21+
ChangeKind.Add => HotReloadFileChangeKind.Add,
22+
ChangeKind.Delete => HotReloadFileChangeKind.Delete,
23+
_ => throw new InvalidOperationException()
24+
};
25+
}
26+
1327
internal readonly record struct ChangedFile(FileItem Item, ChangeKind Kind);
1428

1529
internal readonly record struct ChangedPath(string Path, ChangeKind Kind);

src/BuiltInTools/Watch/HotReload/CompilationHandler.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.DotNet.Watch
1515
{
1616
internal sealed class CompilationHandler : IDisposable
1717
{
18-
public readonly IncrementalMSBuildWorkspace Workspace;
18+
public readonly HotReloadMSBuildWorkspace Workspace;
1919
private readonly DotNetWatchContext _context;
2020
private readonly HotReloadService _hotReloadService;
2121

@@ -38,11 +38,19 @@ internal sealed class CompilationHandler : IDisposable
3838
private ImmutableList<HotReloadService.Update> _previousUpdates = [];
3939

4040
private bool _isDisposed;
41+
private int _solutionUpdateId;
42+
43+
/// <summary>
44+
/// Current set of project instances indexed by <see cref="ProjectInstance.FullPath"/>.
45+
/// Updated whenever the project graph changes.
46+
/// </summary>
47+
private ImmutableDictionary<string, ImmutableArray<ProjectInstance>> _projectInstances = [];
4148

4249
public CompilationHandler(DotNetWatchContext context)
4350
{
4451
_context = context;
45-
Workspace = new IncrementalMSBuildWorkspace(context.Logger);
52+
_processRunner = processRunner;
53+
Workspace = new HotReloadMSBuildWorkspace(logger, projectFile => (instances: _projectInstances.GetValueOrDefault(projectFile, []), project: null));
4654
_hotReloadService = new HotReloadService(Workspace.CurrentSolution.Services, () => ValueTask.FromResult(GetAggregateCapabilities()));
4755
}
4856

@@ -798,5 +806,70 @@ private static Task ForEachProjectAsync(ImmutableDictionary<string, ImmutableArr
798806

799807
private static ImmutableArray<HotReloadManagedCodeUpdate> ToManagedCodeUpdates(ImmutableArray<HotReloadService.Update> updates)
800808
=> [.. updates.Select(update => new HotReloadManagedCodeUpdate(update.ModuleId, update.MetadataDelta, update.ILDelta, update.PdbDelta, update.UpdatedTypes, update.RequiredCapabilities))];
809+
810+
private static ImmutableDictionary<string, ImmutableArray<ProjectInstance>> CreateProjectInstanceMap(ProjectGraph graph)
811+
=> graph.ProjectNodes
812+
.GroupBy(static node => node.ProjectInstance.FullPath)
813+
.ToImmutableDictionary(
814+
keySelector: static group => group.Key,
815+
elementSelector: static group => group.Select(static node => node.ProjectInstance).ToImmutableArray());
816+
817+
public async Task UpdateProjectConeAsync(ProjectGraph projectGraph, string projectPath, CancellationToken cancellationToken)
818+
{
819+
_logger.LogInformation("Loading projects ...");
820+
var stopwatch = Stopwatch.StartNew();
821+
822+
_projectInstances = CreateProjectInstanceMap(projectGraph);
823+
824+
var solution = await Workspace.UpdateProjectConeAsync(projectPath, cancellationToken);
825+
await SolutionUpdatedAsync(solution, "project update", cancellationToken);
826+
827+
_logger.LogInformation("Projects loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
828+
}
829+
830+
public async Task UpdateFileContentAsync(ImmutableList<ChangedFile> changedFiles, CancellationToken cancellationToken)
831+
{
832+
var solution = await Workspace.UpdateFileContentAsync(changedFiles.Select(static f => (f.Item.FilePath, f.Kind.Convert())), cancellationToken);
833+
await SolutionUpdatedAsync(solution, "document update", cancellationToken);
834+
}
835+
836+
private Task SolutionUpdatedAsync(Solution newSolution, string operationDisplayName, CancellationToken cancellationToken)
837+
=> ReportSolutionFilesAsync(newSolution, Interlocked.Increment(ref _solutionUpdateId), operationDisplayName, cancellationToken);
838+
839+
private async Task ReportSolutionFilesAsync(Solution solution, int updateId, string operationDisplayName, CancellationToken cancellationToken)
840+
{
841+
_logger.LogDebug("Solution after {Operation}: v{Version}", operationDisplayName, updateId);
842+
843+
if (!_logger.IsEnabled(LogLevel.Trace))
844+
{
845+
return;
846+
}
847+
848+
foreach (var project in solution.Projects)
849+
{
850+
_logger.LogDebug(" Project: {Path}", project.FilePath);
851+
852+
foreach (var document in project.Documents)
853+
{
854+
await InspectDocumentAsync(document, "Document").ConfigureAwait(false);
855+
}
856+
857+
foreach (var document in project.AdditionalDocuments)
858+
{
859+
await InspectDocumentAsync(document, "Additional").ConfigureAwait(false);
860+
}
861+
862+
foreach (var document in project.AnalyzerConfigDocuments)
863+
{
864+
await InspectDocumentAsync(document, "Config").ConfigureAwait(false);
865+
}
866+
}
867+
868+
async ValueTask InspectDocumentAsync(TextDocument document, string kind)
869+
{
870+
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
871+
_logger.LogDebug(" {Kind}: {FilePath} [{Checksum}]", kind, document.FilePath, Convert.ToBase64String(text.GetChecksum().ToArray()));
872+
}
873+
}
801874
}
802875
}

src/BuiltInTools/Watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public async Task WatchAsync(CancellationToken shutdownCancellationToken)
167167
return;
168168
}
169169

170-
await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
170+
await compilationHandler.UpdateProjectConeAsync(evaluationResult.ProjectGraph, rootProjectOptions.ProjectPath, iterationCancellationToken);
171171

172172
// Solution must be initialized after we load the solution but before we start watching for file changes to avoid race condition
173173
// when the EnC session captures content of the file after the changes has already been made.
@@ -370,7 +370,7 @@ void FileChangedCallback(ChangedPath change)
370370
// Deploy dependencies after rebuilding and before restarting.
371371
if (!projectsToRedeploy.IsEmpty)
372372
{
373-
DeployProjectDependencies(evaluationResult.ProjectGraph, projectsToRedeploy, iterationCancellationToken);
373+
DeployProjectDependencies(evaluationResult.RestoredProjectInstances, projectsToRedeploy, iterationCancellationToken);
374374
_context.Logger.Log(MessageDescriptor.ProjectDependenciesDeployed, projectsToRedeploy.Length);
375375
}
376376

@@ -443,7 +443,7 @@ async Task<ImmutableArray<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableArr
443443
// additional files/directories may have been added:
444444
evaluationResult.WatchFiles(fileWatcher);
445445

446-
await compilationHandler.Workspace.UpdateProjectConeAsync(rootProjectOptions.ProjectPath, iterationCancellationToken);
446+
await compilationHandler.UpdateProjectConeAsync(evaluationResult.ProjectGraph, rootProjectOptions.ProjectPath, iterationCancellationToken);
447447

448448
if (shutdownCancellationToken.IsCancellationRequested)
449449
{
@@ -495,7 +495,7 @@ async Task<ImmutableArray<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableArr
495495
{
496496
// Update the workspace to reflect changes in the file content:.
497497
// If the project was re-evaluated the Roslyn solution is already up to date.
498-
await compilationHandler.Workspace.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
498+
await compilationHandler.UpdateFileContentAsync(changedFiles, iterationCancellationToken);
499499
}
500500

501501
return [.. changedFiles];
@@ -554,6 +554,7 @@ async Task<ImmutableArray<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableArr
554554
}
555555
}
556556

557+
<<<<<<< HEAD
557558
private void AnalyzeFileChanges(
558559
List<ChangedFile> changedFiles,
559560
EvaluationResult evaluationResult,
@@ -652,35 +653,38 @@ private static bool MatchesStaticWebAssetFilePattern(EvaluationResult evaluation
652653
return false;
653654
}
654655

655-
private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray<string> projectPaths, CancellationToken cancellationToken)
656+
private void DeployProjectDependencies(ImmutableArray<ProjectInstance> restoredProjectInstances, ImmutableArray<string> projectPaths, CancellationToken cancellationToken)
656657
{
657658
var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer);
658659
var buildReporter = new BuildReporter(_context.Logger, _context.Options, _context.EnvironmentOptions);
659660
var targetName = TargetNames.ReferenceCopyLocalPathsOutputGroup;
660661

661-
foreach (var node in graph.ProjectNodes)
662+
foreach (var restoredProjectInstance in restoredProjectInstances)
662663
{
663664
cancellationToken.ThrowIfCancellationRequested();
664665

665-
var projectPath = node.ProjectInstance.FullPath;
666+
// Avoid modification of the restored snapshot.
667+
var projectInstance = restoredProjectInstance.DeepCopy();
668+
669+
var projectPath = projectInstance.FullPath;
666670

667671
if (!projectPathSet.Contains(projectPath))
668672
{
669673
continue;
670674
}
671675

672-
if (!node.ProjectInstance.Targets.ContainsKey(targetName))
676+
if (!projectInstance.Targets.ContainsKey(targetName))
673677
{
674678
continue;
675679
}
676680

677-
if (node.GetOutputDirectory() is not { } relativeOutputDir)
681+
if (projectInstance.GetOutputDirectory() is not { } relativeOutputDir)
678682
{
679683
continue;
680684
}
681685

682686
using var loggers = buildReporter.GetLoggers(projectPath, targetName);
683-
if (!node.ProjectInstance.Build([targetName], loggers, out var targetOutputs))
687+
if (!projectInstance.Build([targetName], loggers, out var targetOutputs))
684688
{
685689
_context.Logger.LogDebug("{TargetName} target failed", targetName);
686690
loggers.ReportOutput();

0 commit comments

Comments
 (0)