-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Implement support for changing project and package references #49611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
|
|
||
| using System.Collections.Immutable; | ||
| using System.Diagnostics; | ||
| using Microsoft.Build.Execution; | ||
| using Microsoft.Build.Graph; | ||
| using Microsoft.CodeAnalysis; | ||
|
|
||
| namespace Microsoft.DotNet.Watch | ||
|
|
@@ -240,7 +242,7 @@ void FileChangedCallback(ChangedPath change) | |
|
|
||
| extendTimeout = false; | ||
|
|
||
| var changedFiles = await CaptureChangedFilesSnapshot(rebuiltProjects: null); | ||
| var changedFiles = await CaptureChangedFilesSnapshot(rebuiltProjects: []); | ||
| if (changedFiles is []) | ||
| { | ||
| continue; | ||
|
|
@@ -269,7 +271,7 @@ void FileChangedCallback(ChangedPath change) | |
|
|
||
| HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler); | ||
|
|
||
| var (projectsToRebuild, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync( | ||
| var (managedCodeUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync( | ||
| autoRestart: _context.Options.NonInteractive || _rudeEditRestartPrompt?.AutoRestartPreference is true, | ||
| restartPrompt: async (projectNames, cancellationToken) => | ||
| { | ||
|
|
@@ -334,7 +336,7 @@ void FileChangedCallback(ChangedPath change) | |
| try | ||
| { | ||
| var buildResults = await Task.WhenAll( | ||
| projectsToRebuild.Values.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken))); | ||
| projectsToRebuild.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken))); | ||
|
|
||
| foreach (var (success, output, projectPath) in buildResults) | ||
| { | ||
|
|
@@ -363,7 +365,21 @@ void FileChangedCallback(ChangedPath change) | |
| // Apply them to the workspace. | ||
| _ = await CaptureChangedFilesSnapshot(projectsToRebuild); | ||
|
|
||
| _context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Count); | ||
| _context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Length); | ||
| } | ||
|
|
||
| // Deploy dependencies after rebuilding and before restarting. | ||
| if (!projectsToRedeploy.IsEmpty) | ||
| { | ||
| DeployProjectDependencies(evaluationResult.ProjectGraph, projectsToRedeploy, iterationCancellationToken); | ||
| _context.Reporter.Report(MessageDescriptor.ProjectDependenciesDeployed, projectsToRedeploy.Length); | ||
| } | ||
|
|
||
| // Apply updates only after dependencies have been deployed, | ||
| // so that updated code doesn't attempt to access the dependency before it has been deployed. | ||
| if (!managedCodeUpdates.IsEmpty) | ||
| { | ||
| await compilationHandler.ApplyUpdatesAsync(managedCodeUpdates, iterationCancellationToken); | ||
| } | ||
|
|
||
| if (!projectsToRestart.IsEmpty) | ||
|
|
@@ -393,7 +409,7 @@ await Task.WhenAll( | |
|
|
||
| _context.Reporter.Report(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds); | ||
|
|
||
| async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDictionary<ProjectId, string>? rebuiltProjects) | ||
| async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableArray<string> rebuiltProjects) | ||
| { | ||
| var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []); | ||
| if (changedPaths is []) | ||
|
|
@@ -464,12 +480,12 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict | |
| _context.Reporter.Report(MessageDescriptor.ReEvaluationCompleted); | ||
| } | ||
|
|
||
| if (rebuiltProjects != null) | ||
| if (!rebuiltProjects.IsEmpty) | ||
| { | ||
| // Filter changed files down to those contained in projects being rebuilt. | ||
| // File changes that affect projects that are not being rebuilt will stay in the accumulator | ||
| // and be included in the next Hot Reload change set. | ||
| var rebuiltProjectPaths = rebuiltProjects.Values.ToHashSet(); | ||
| var rebuiltProjectPaths = rebuiltProjects.ToHashSet(); | ||
|
|
||
| var newAccumulator = ImmutableList<ChangedPath>.Empty; | ||
| var newChangedFiles = ImmutableList<ChangedFile>.Empty; | ||
|
|
@@ -555,6 +571,72 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict | |
| } | ||
| } | ||
|
|
||
| private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray<string> projectPaths, CancellationToken cancellationToken) | ||
| { | ||
| var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer); | ||
| var buildReporter = new BuildReporter(_context.Reporter, _context.Options, _context.EnvironmentOptions); | ||
| var targetName = TargetNames.ReferenceCopyLocalPathsOutputGroup; | ||
|
|
||
| foreach (var node in graph.ProjectNodes) | ||
| { | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| var projectPath = node.ProjectInstance.FullPath; | ||
|
|
||
| if (!projectPathSet.Contains(projectPath)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (!node.ProjectInstance.Targets.ContainsKey(targetName)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (node.GetOutputDirectory() is not { } relativeOutputDir) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| using var loggers = buildReporter.GetLoggers(projectPath, targetName); | ||
| if (!node.ProjectInstance.Build([targetName], loggers, out var targetOutputs)) | ||
| { | ||
| _context.Reporter.Verbose($"{targetName} target failed"); | ||
| loggers.ReportOutput(); | ||
| continue; | ||
| } | ||
|
|
||
| var outputDir = Path.Combine(Path.GetDirectoryName(projectPath)!, relativeOutputDir); | ||
|
|
||
| foreach (var item in targetOutputs[targetName].Items) | ||
| { | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
|
|
||
| var sourcePath = item.ItemSpec; | ||
| var targetPath = Path.Combine(outputDir, item.GetMetadata(MetadataNames.TargetPath)); | ||
| if (!File.Exists(targetPath)) | ||
| { | ||
| _context.Reporter.Verbose($"Deploying project dependency '{targetPath}' from '{sourcePath}'"); | ||
|
|
||
| try | ||
| { | ||
| var directory = Path.GetDirectoryName(targetPath); | ||
| if (directory != null) | ||
| { | ||
| Directory.CreateDirectory(directory); | ||
| } | ||
|
|
||
| File.Copy(sourcePath, targetPath, overwrite: false); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the file is different? Is it expected that we use the old dependency? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, if the file exists it might have been loaded already. We could try to overwrite it and catch the exception, but I think that would result in less predictable behavior. The app might load the dependency at any point in time while it's running. Roslyn will report a rude edit if the assembly version changed. In such scenario we would not get here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense! Thanks for the explanation. |
||
| } | ||
| catch (Exception e) | ||
| { | ||
| _context.Reporter.Verbose($"Copy failed: {e.Message}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatcher, EvaluationResult? evaluationResult, CancellationToken cancellationToken) | ||
| { | ||
| if (evaluationResult != null) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.