|
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