Skip to content

Commit 365ff1f

Browse files
committed
Project and package references
1 parent 36a3998 commit 365ff1f

File tree

9 files changed

+268
-11
lines changed

9 files changed

+268
-11
lines changed

src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Diagnostics;
55
using System.IO.Pipes;
6+
using System.Reflection;
7+
using System.Runtime.Loader;
68
using Microsoft.DotNet.HotReload;
79

810
/// <summary>
@@ -19,12 +21,15 @@ internal sealed class StartupHook
1921
private static PosixSignalRegistration? s_signalRegistration;
2022
#endif
2123

24+
private static Func<AssemblyLoadContext, AssemblyName, Assembly?>? s_assemblyResolvingEventHandler;
25+
2226
/// <summary>
2327
/// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
2428
/// </summary>
2529
public static void Initialize()
2630
{
2731
var processPath = Environment.GetCommandLineArgs().FirstOrDefault();
32+
var processDir = Path.GetDirectoryName(processPath)!;
2833

2934
Log($"Loaded into process: {processPath} ({typeof(StartupHook).Assembly.Location})");
3035

@@ -60,6 +65,14 @@ public static void Initialize()
6065

6166
RegisterPosixSignalHandlers();
6267

68+
// prepare handler, it will be installed on first managed update:
69+
s_assemblyResolvingEventHandler = (_, args) =>
70+
{
71+
Log($"Resolving {args.Name}");
72+
var path = Path.Combine(processDir, args.Name + ".dll");
73+
return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
74+
};
75+
6376
var agent = new HotReloadAgent();
6477
try
6578
{
@@ -126,6 +139,14 @@ private static async Task ReceiveAndApplyUpdatesAsync(NamedPipeClientStream pipe
126139
// Shouldn't get initial managed code updates when the debugger is attached.
127140
// The debugger itself applies these updates when launching process with the debugger attached.
128141
Debug.Assert(!Debugger.IsAttached);
142+
143+
var handler = s_assemblyResolvingEventHandler;
144+
if (handler != null)
145+
{
146+
AssemblyLoadContext.Default.Resolving += handler;
147+
s_assemblyResolvingEventHandler = null;
148+
}
149+
129150
await ReadAndApplyManagedCodeUpdateAsync(pipeClient, agent, cancellationToken);
130151
break;
131152

src/BuiltInTools/dotnet-watch/Build/MsBuildFileSetFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@ private IReadOnlyList<string> GetMSBuildArguments(string watchListFilePath)
154154
"/t:" + TargetName
155155
};
156156

157-
#if !DEBUG
158157
if (environmentOptions.TestFlags.HasFlag(TestFlags.RunningAsTest))
159-
#endif
160158
{
161159
arguments.Add($"/bl:{Path.Combine(environmentOptions.TestOutput, "DotnetWatch.GenerateWatchList.binlog")}");
162160
}
@@ -215,7 +213,9 @@ private static string FindTargetsFile()
215213

216214
try
217215
{
218-
return new ProjectGraph([entryPoint], ProjectCollection.GlobalProjectCollection, projectInstanceFactory: null, cancellationToken);
216+
var graph = new ProjectGraph([entryPoint], ProjectCollection.GlobalProjectCollection, projectInstanceFactory: null, cancellationToken);
217+
reporter.Verbose($"Project graph loaded ({graph.ProjectNodes.Count} nodes)");
218+
return graph;
219219
}
220220
catch (Exception e)
221221
{

src/BuiltInTools/dotnet-watch/HotReload/HotReloadDotNetWatcher.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Collections.Immutable;
55
using System.Diagnostics;
6+
using Microsoft.Build.Execution;
7+
using Microsoft.Build.Graph;
68
using Microsoft.CodeAnalysis;
79

810
namespace Microsoft.DotNet.Watch
@@ -442,11 +444,12 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
442444

443445
// TODO: consider re-evaluating only affected projects instead of the whole graph.
444446
evaluationResult = await EvaluateRootProjectAsync(iterationCancellationToken);
447+
Debug.Assert(evaluationResult.ProjectGraph != null);
445448

446449
// additional directories may have been added:
447450
evaluationResult.WatchFiles(fileWatcher);
448451

449-
await compilationHandler.Workspace.UpdateProjectConeAsync(_fileSetFactory.RootProjectFile, iterationCancellationToken);
452+
var solutionChanges = await compilationHandler.Workspace.UpdateProjectConeAsync(_fileSetFactory.RootProjectFile, iterationCancellationToken);
450453

451454
if (shutdownCancellationToken.IsCancellationRequested)
452455
{
@@ -458,6 +461,8 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
458461
changedFiles = [.. changedFiles
459462
.Select(f => evaluationResult.Files.TryGetValue(f.Item.FilePath, out var evaluatedFile) ? f with { Item = evaluatedFile } : f)];
460463

464+
await DeployProjectDependenciesAsync(evaluationResult.ProjectGraph, solutionChanges, iterationCancellationToken);
465+
461466
_context.Reporter.Report(MessageDescriptor.ReEvaluationCompleted);
462467
}
463468

@@ -551,6 +556,64 @@ async Task<ImmutableList<ChangedFile>> CaptureChangedFilesSnapshot(ImmutableDict
551556
}
552557
}
553558

559+
private Task DeployProjectDependenciesAsync(ProjectGraph graph, SolutionChanges solutionChanges, CancellationToken cancellationToken)
560+
{
561+
// TODO: tfm
562+
563+
var affectedProjectFilePaths =
564+
(from projectChange in solutionChanges.GetProjectChanges()
565+
where projectChange.GetAddedProjectReferences().Any() || projectChange.GetAddedMetadataReferences().Any()
566+
select projectChange.NewProject.FilePath!).ToHashSet();
567+
568+
foreach (var node in graph.ProjectNodes)
569+
{
570+
if (affectedProjectFilePaths.Contains(node.ProjectInstance.FullPath))
571+
{
572+
// TODO: logger; GenerateBuildDependencyFile?
573+
if (!node.ProjectInstance.Build(["ReferenceCopyLocalPathsOutputGroup"], loggers: [], out var targetOutputs))
574+
{
575+
Context.Reporter.Verbose("ReferenceCopyLocalPathsOutputGroup target failed");
576+
continue;
577+
}
578+
579+
var copyLocalItems = targetOutputs.Values.Single();
580+
if (copyLocalItems.ResultCode != TargetResultCode.Success)
581+
{
582+
continue;
583+
}
584+
585+
var outDir = Path.Combine(Path.GetDirectoryName(node.ProjectInstance.FullPath)!, node.ProjectInstance.GetPropertyValue("OutDir"));
586+
587+
foreach (var item in copyLocalItems.Items)
588+
{
589+
var sourcePath = item.ItemSpec;
590+
var targetPath = Path.Combine(outDir, item.GetMetadata("TargetPath"));
591+
if (!File.Exists(targetPath))
592+
{
593+
Context.Reporter.Verbose($"Deploying project dependency '{targetPath}' from '{sourcePath}'");
594+
595+
try
596+
{
597+
var directory = Path.GetDirectoryName(targetPath);
598+
if (directory != null)
599+
{
600+
Directory.CreateDirectory(directory);
601+
}
602+
603+
File.Copy(sourcePath, targetPath, overwrite: false);
604+
}
605+
catch (Exception e)
606+
{
607+
Context.Reporter.Verbose($"Copy failed: {e.Message}");
608+
}
609+
}
610+
}
611+
}
612+
}
613+
614+
return Task.CompletedTask;
615+
}
616+
554617
private async ValueTask WaitForFileChangeBeforeRestarting(FileWatcher fileWatcher, EvaluationResult? evaluationResult, CancellationToken cancellationToken)
555618
{
556619
if (evaluationResult != null)

src/BuiltInTools/dotnet-watch/HotReload/IncrementalMSBuildWorkspace.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public IncrementalMSBuildWorkspace(IReporter reporter)
3030
_reporter = reporter;
3131
}
3232

33-
public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationToken cancellationToken)
33+
public async Task<SolutionChanges> UpdateProjectConeAsync(string rootProjectPath, CancellationToken cancellationToken)
3434
{
3535
var oldSolution = CurrentSolution;
3636

@@ -91,12 +91,16 @@ public async Task UpdateProjectConeAsync(string rootProjectPath, CancellationTok
9191
.WithCompilationOutputInfo(newProjectInfo.CompilationOutputInfo));
9292
}
9393

94-
await ReportSolutionFilesAsync(SetCurrentSolution(newSolution), cancellationToken);
94+
var finalNewSolution = SetCurrentSolution(newSolution);
95+
await ReportSolutionFilesAsync(finalNewSolution, cancellationToken);
9596
UpdateReferencesAfterAdd();
9697

98+
return finalNewSolution.GetChanges(oldSolution);
99+
97100
ProjectReference MapProjectReference(ProjectReference pr)
98-
// Only C# and VB projects are loaded by the MSBuildProjectLoader, so some references might be missing:
99-
=> new(projectIdMap.TryGetValue(pr.ProjectId, out var mappedId) ? mappedId : pr.ProjectId, pr.Aliases, pr.EmbedInteropTypes);
101+
// Only C# and VB projects are loaded by the MSBuildProjectLoader, so some references might be missing.
102+
// When a new project is added along with a new project reference the old project id is also null.
103+
=> new(projectIdMap.TryGetValue(pr.ProjectId, out var oldProjectId) && oldProjectId != null ? oldProjectId : pr.ProjectId, pr.Aliases, pr.EmbedInteropTypes);
100104

101105
ImmutableArray<DocumentInfo> MapDocuments(ProjectId mappedProjectId, IReadOnlyList<DocumentInfo> documents)
102106
=> documents.Select(docInfo =>

src/BuiltInTools/dotnet-watch/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"profiles": {
33
"dotnet-watch": {
44
"commandName": "Project",
5-
"commandLineArgs": "--verbose /bl:DotnetRun.binlog -lp http --non-interactive",
6-
"workingDirectory": "C:\\sdk1\\artifacts\\tmp\\Debug\\Aspire_BuildE---04F22750\\WatchAspire.AppHost",
5+
"commandLineArgs": "--verbose",
6+
"workingDirectory": "C:\\temp\\app\\dir",
77
"environmentVariables": {
88
"DOTNET_WATCH_DEBUG_SDK_DIRECTORY": "$(RepoRoot)artifacts\\bin\\redist\\$(Configuration)\\dotnet\\sdk\\$(Version)",
99
"DCP_IDE_REQUEST_TIMEOUT_SECONDS": "100000",

test/TestAssets/TestProjects/WatchAppWithProjectDeps/AppWithDeps/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ public static void Main(string[] args)
1919

2020
while (true)
2121
{
22-
Lib.Print();
22+
CallLib();
2323
Thread.Sleep(1000);
2424
}
2525
}
26+
27+
public static void CallLib()
28+
{
29+
Lib.Print();
30+
}
2631
}
2732
}

test/TestAssets/TestProjects/WatchAppWithProjectDeps/Dependency/Foo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public class Lib
44
{
55
public static void Print()
66
{
7+
System.Console.WriteLine("<Lib>");
78
}
89
}

test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,77 @@ public static void Print()
236236
await App.AssertOutputLineStartsWith("BUILD_CONST_IN_PROPS not set");
237237
}
238238

239+
[Fact]
240+
public async Task ProjectChange_AddProjectReference()
241+
{
242+
var testAsset = TestAssets.CopyTestAsset("WatchAppWithProjectDeps")
243+
.WithSource()
244+
.WithProjectChanges(project =>
245+
{
246+
foreach (var r in project.Root!.Descendants().Where(e => e.Name.LocalName == "ProjectReference").ToArray())
247+
{
248+
r.Remove();
249+
}
250+
});
251+
252+
var appProjDir = Path.Combine(testAsset.Path, "AppWithDeps");
253+
var appProjFile = Path.Combine(appProjDir, "App.WithDeps.csproj");
254+
var appFile = Path.Combine(appProjDir, "Program.cs");
255+
256+
UpdateSourceFile(appFile, code => code.Replace("Lib.Print();", "// Lib.Print();"));
257+
258+
App.Start(testAsset, [], "AppWithDeps");
259+
260+
await App.AssertWaitingForChanges();
261+
262+
UpdateSourceFile(appProjFile, src => src.Replace("""
263+
<ItemGroup />
264+
""", """
265+
<ItemGroup>
266+
<ProjectReference Include="..\Dependency\Dependency.csproj" />
267+
</ItemGroup>
268+
"""));
269+
270+
UpdateSourceFile(appFile, code => code.Replace("// Lib.Print();", "Lib.Print();"));
271+
272+
await App.WaitUntilOutputContains("<Lib>");
273+
274+
App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, $"AppWithDeps ({ToolsetInfo.CurrentTargetFramework})");
275+
App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation);
276+
}
277+
278+
[Fact]
279+
public async Task ProjectChange_AddPackageReference()
280+
{
281+
var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp")
282+
.WithSource();
283+
284+
var projFilePath = Path.Combine(testAsset.Path, "WatchHotReloadApp.csproj");
285+
var programFilePath = Path.Combine(testAsset.Path, "Program.cs");
286+
287+
App.Start(testAsset, []);
288+
289+
await App.AssertWaitingForChanges();
290+
App.Process.ClearOutput();
291+
292+
UpdateSourceFile(projFilePath, source => source.Replace("""
293+
<!-- items placeholder -->
294+
""", """
295+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
296+
"""));
297+
298+
UpdateSourceFile(programFilePath, source => source.Replace("Console.WriteLine(\".\");", "Console.WriteLine(typeof(Newtonsoft.Json.Linq.JToken));"));
299+
300+
await App.WaitUntilOutputContains("Newtonsoft.Json.Linq.JToken");
301+
302+
var binDirDll = Path.Combine(testAsset.Path, "bin", "Debug", ToolsetInfo.CurrentTargetFramework, "Newtonsoft.Json.dll");
303+
304+
App.AssertOutputContains($"Deploying project dependency '{binDirDll}'");
305+
App.AssertOutputContains(MessageDescriptor.HotReloadSucceeded, $"WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})");
306+
App.AssertOutputContains(MessageDescriptor.ProjectChangeTriggeredReEvaluation);
307+
App.AssertOutputContains("Resolving Newtonsoft.Json");
308+
}
309+
239310
[Fact]
240311
public async Task DefaultItemExcludes_DefaultItemsEnabled()
241312
{

0 commit comments

Comments
 (0)