|  | 
| 3 | 3 | 
 | 
| 4 | 4 | using System.Collections.Immutable; | 
| 5 | 5 | using System.Diagnostics; | 
|  | 6 | +using Microsoft.Build.Execution; | 
|  | 7 | +using Microsoft.Build.Graph; | 
| 6 | 8 | using Microsoft.CodeAnalysis; | 
| 7 | 9 | 
 | 
| 8 | 10 | namespace Microsoft.DotNet.Watch | 
| @@ -240,7 +242,7 @@ void FileChangedCallback(ChangedPath change) | 
| 240 | 242 | 
 | 
| 241 | 243 |                         extendTimeout = false; | 
| 242 | 244 | 
 | 
| 243 |  | -                        var changedFiles = await CaptureChangedFilesSnapshot(rebuiltProjects: null); | 
|  | 245 | +                        var changedFiles = await CaptureChangedFilesSnapshot(rebuiltProjects: []); | 
| 244 | 246 |                         if (changedFiles is []) | 
| 245 | 247 |                         { | 
| 246 | 248 |                             continue; | 
| @@ -269,7 +271,7 @@ void FileChangedCallback(ChangedPath change) | 
| 269 | 271 | 
 | 
| 270 | 272 |                         HotReloadEventSource.Log.HotReloadStart(HotReloadEventSource.StartType.CompilationHandler); | 
| 271 | 273 | 
 | 
| 272 |  | -                        var (projectsToRebuild, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync( | 
|  | 274 | +                        var (managedCodeUpdates, projectsToRebuild, projectsToRedeploy, projectsToRestart) = await compilationHandler.HandleManagedCodeChangesAsync( | 
| 273 | 275 |                             autoRestart: _context.Options.NonInteractive || _rudeEditRestartPrompt?.AutoRestartPreference is true, | 
| 274 | 276 |                             restartPrompt: async (projectNames, cancellationToken) => | 
| 275 | 277 |                             { | 
| @@ -334,7 +336,7 @@ void FileChangedCallback(ChangedPath change) | 
| 334 | 336 |                                 try | 
| 335 | 337 |                                 { | 
| 336 | 338 |                                     var buildResults = await Task.WhenAll( | 
| 337 |  | -                                        projectsToRebuild.Values.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken))); | 
|  | 339 | +                                        projectsToRebuild.Select(projectPath => BuildProjectAsync(projectPath, rootProjectOptions.BuildArguments, iterationCancellationToken))); | 
| 338 | 340 | 
 | 
| 339 | 341 |                                     foreach (var (success, output, projectPath) in buildResults) | 
| 340 | 342 |                                     { | 
| @@ -363,7 +365,21 @@ void FileChangedCallback(ChangedPath change) | 
| 363 | 365 |                             // Apply them to the workspace. | 
| 364 | 366 |                             _ = await CaptureChangedFilesSnapshot(projectsToRebuild); | 
| 365 | 367 | 
 | 
| 366 |  | -                            _context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Count); | 
|  | 368 | +                            _context.Reporter.Report(MessageDescriptor.ProjectsRebuilt, projectsToRebuild.Length); | 
|  | 369 | +                        } | 
|  | 370 | + | 
|  | 371 | +                        // Deploy dependencies after rebuilding and before restarting. | 
|  | 372 | +                        if (!projectsToRedeploy.IsEmpty) | 
|  | 373 | +                        { | 
|  | 374 | +                            DeployProjectDependencies(evaluationResult.ProjectGraph, projectsToRedeploy, iterationCancellationToken); | 
|  | 375 | +                            _context.Reporter.Report(MessageDescriptor.ProjectDependenciesDeployed, projectsToRedeploy.Length); | 
|  | 376 | +                        } | 
|  | 377 | + | 
|  | 378 | +                        // Apply updates only after dependencies have been deployed, | 
|  | 379 | +                        // so that updated code doesn't attempt to access the dependency before it has been deployed. | 
|  | 380 | +                        if (!managedCodeUpdates.IsEmpty) | 
|  | 381 | +                        { | 
|  | 382 | +                            await compilationHandler.ApplyUpdatesAsync(managedCodeUpdates, iterationCancellationToken); | 
| 367 | 383 |                         } | 
| 368 | 384 | 
 | 
| 369 | 385 |                         if (!projectsToRestart.IsEmpty) | 
| @@ -393,7 +409,7 @@ await Task.WhenAll( | 
| 393 | 409 | 
 | 
| 394 | 410 |                         _context.Reporter.Report(MessageDescriptor.HotReloadChangeHandled, stopwatch.ElapsedMilliseconds); | 
| 395 | 411 | 
 | 
| 396 |  | -                        async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDictionary<ProjectId, string>? rebuiltProjects) | 
|  | 412 | +                        async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableArray<string> rebuiltProjects) | 
| 397 | 413 |                         { | 
| 398 | 414 |                             var changedPaths = Interlocked.Exchange(ref changedFilesAccumulator, []); | 
| 399 | 415 |                             if (changedPaths is []) | 
| @@ -464,12 +480,12 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict | 
| 464 | 480 |                                 _context.Reporter.Report(MessageDescriptor.ReEvaluationCompleted); | 
| 465 | 481 |                             } | 
| 466 | 482 | 
 | 
| 467 |  | -                            if (rebuiltProjects != null) | 
|  | 483 | +                            if (!rebuiltProjects.IsEmpty) | 
| 468 | 484 |                             { | 
| 469 | 485 |                                 // Filter changed files down to those contained in projects being rebuilt. | 
| 470 | 486 |                                 // File changes that affect projects that are not being rebuilt will stay in the accumulator | 
| 471 | 487 |                                 // and be included in the next Hot Reload change set. | 
| 472 |  | -                                var rebuiltProjectPaths = rebuiltProjects.Values.ToHashSet(); | 
|  | 488 | +                                var rebuiltProjectPaths = rebuiltProjects.ToHashSet(); | 
| 473 | 489 | 
 | 
| 474 | 490 |                                 var newAccumulator = ImmutableList<ChangedPath>.Empty; | 
| 475 | 491 |                                 var newChangedFiles = ImmutableList<ChangedFile>.Empty; | 
| @@ -555,6 +571,72 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict | 
| 555 | 571 |             } | 
| 556 | 572 |         } | 
| 557 | 573 | 
 | 
|  | 574 | +        private void DeployProjectDependencies(ProjectGraph graph, ImmutableArray<string> projectPaths, CancellationToken cancellationToken) | 
|  | 575 | +        { | 
|  | 576 | +            var projectPathSet = projectPaths.ToImmutableHashSet(PathUtilities.OSSpecificPathComparer); | 
|  | 577 | +            var buildReporter = new BuildReporter(_context.Reporter, _context.Options, _context.EnvironmentOptions); | 
|  | 578 | +            var targetName = TargetNames.ReferenceCopyLocalPathsOutputGroup; | 
|  | 579 | + | 
|  | 580 | +            foreach (var node in graph.ProjectNodes) | 
|  | 581 | +            { | 
|  | 582 | +                cancellationToken.ThrowIfCancellationRequested(); | 
|  | 583 | + | 
|  | 584 | +                var projectPath = node.ProjectInstance.FullPath; | 
|  | 585 | + | 
|  | 586 | +                if (!projectPathSet.Contains(projectPath)) | 
|  | 587 | +                { | 
|  | 588 | +                    continue; | 
|  | 589 | +                } | 
|  | 590 | + | 
|  | 591 | +                if (!node.ProjectInstance.Targets.ContainsKey(targetName)) | 
|  | 592 | +                { | 
|  | 593 | +                    continue; | 
|  | 594 | +                } | 
|  | 595 | + | 
|  | 596 | +                if (node.GetOutputDirectory() is not { } relativeOutputDir) | 
|  | 597 | +                { | 
|  | 598 | +                    continue; | 
|  | 599 | +                } | 
|  | 600 | + | 
|  | 601 | +                using var loggers = buildReporter.GetLoggers(projectPath, targetName); | 
|  | 602 | +                if (!node.ProjectInstance.Build([targetName], loggers, out var targetOutputs)) | 
|  | 603 | +                { | 
|  | 604 | +                    _context.Reporter.Verbose($"{targetName} target failed"); | 
|  | 605 | +                    loggers.ReportOutput(); | 
|  | 606 | +                    continue; | 
|  | 607 | +                } | 
|  | 608 | + | 
|  | 609 | +                var outputDir = Path.Combine(Path.GetDirectoryName(projectPath)!, relativeOutputDir); | 
|  | 610 | + | 
|  | 611 | +                foreach (var item in targetOutputs[targetName].Items) | 
|  | 612 | +                { | 
|  | 613 | +                    cancellationToken.ThrowIfCancellationRequested(); | 
|  | 614 | + | 
|  | 615 | +                    var sourcePath = item.ItemSpec; | 
|  | 616 | +                    var targetPath = Path.Combine(outputDir, item.GetMetadata(MetadataNames.TargetPath)); | 
|  | 617 | +                    if (!File.Exists(targetPath)) | 
|  | 618 | +                    { | 
|  | 619 | +                        _context.Reporter.Verbose($"Deploying project dependency '{targetPath}' from '{sourcePath}'"); | 
|  | 620 | + | 
|  | 621 | +                        try | 
|  | 622 | +                        { | 
|  | 623 | +                            var directory = Path.GetDirectoryName(targetPath); | 
|  | 624 | +                            if (directory != null) | 
|  | 625 | +                            { | 
|  | 626 | +                                Directory.CreateDirectory(directory); | 
|  | 627 | +                            } | 
|  | 628 | + | 
|  | 629 | +                            File.Copy(sourcePath, targetPath, overwrite: false); | 
|  | 630 | +                        } | 
|  | 631 | +                        catch (Exception e) | 
|  | 632 | +                        { | 
|  | 633 | +                            _context.Reporter.Verbose($"Copy failed: {e.Message}"); | 
|  | 634 | +                        } | 
|  | 635 | +                    } | 
|  | 636 | +                } | 
|  | 637 | +            } | 
|  | 638 | +        } | 
|  | 639 | + | 
| 558 | 640 |         private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatcher, EvaluationResult? evaluationResult, CancellationToken cancellationToken) | 
| 559 | 641 |         { | 
| 560 | 642 |             if (evaluationResult != null) | 
|  | 
0 commit comments