-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Allow dotnet-watch to use MSBuildProjectLoader without launching out-of-process build hosts #80359
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
base: main
Are you sure you want to change the base?
Conversation
- Convert PathResolver to a readonly struct that optionally holds a DiagnosticReporter and a string representing a base directory. - Remove _pathResolver fields from MSBuildProjectLoader and MSBuildProjectLoader.Worker - Add GetPathResolver(...) helper in MSBuildProjectLoader.Worker. - Refactor how PathResolver is used in SolutionFileReader. - Update various 'path' parameter names for consistency.
Pass projectFilePaths, baseDirectory and reportingOptions to Worker.LoadAsync rather than the constructor.
Add ProjectLoadOperationRunner struct that wraps an `IProgress<ProjectLoadProgress>?` and provides a DoOperationAndReportProgressAsync method.
This change extracts the logic from MSBuildProjectLoader.Worker that depend on BuildHostProcessManager into a separate BuildHostProjectFileInfoLoader class.
Introduce an IProjectFileInfoLoaderFactory abstraction that can be optionally passed to an MSBuildProjectLoader to allow customization of ProjectFileInfo loading. If an IProjectFileInfoLoaderFactory isn't provided, the default BuildHostProjectFileInfoLoaderFactory instance is used. This abstraction is intended to allow dotnet-watch to provide a custom IProjectFileLoaderFactory that can produce ProjectFileInfos without using an out-of-proc build host.
Add interfaces and wrappers for dotnet-watch.
|
@DustinCampbell @tmat A very quick scroll through the code makes me realize I'm missing the big picture here: once you've run much of this code are you directly consuming the resulting workspace? Or are you still doing other steps to apply them to your workspace? The reason I'm asking is I would have expected you were either:
It looks like it's a bit of a mix here which means I'm not seeing the full picture. |
|
@jasonmalinowski dotnet-watch has its own Workspace impl, we only use MSBuildWorkspace to load project info. |
| { | ||
| Task<LoadProjectFileInfosResult> LoadProjectFileInfosAsync( | ||
| string projectFilePath, | ||
| string languageName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't language name implied by project file path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but there's a ProjectFileExtensionRegistry that gets used in MSBuild workspace. I'd prefer to leave that as an implementation detail and not expose it via the external access. So, by the time this method is called, the language name has already been retrieved from the project path.
| } | ||
|
|
||
| internal MSBuildProjectLoader( | ||
| Workspace workspace, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the only two things that are needed from workspace are SolutionServices and OnWorkspaceFailed. Can we remove the dependency on Workspace from DiagnosticReporter and here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taking it a step further, maybe we can also remove dependency on SolutionServices by defining an interface with the few methods that are used, such as GetDefaultParseOptions(language), GetDefaultCompilationOptions(language) and ParseCommandLine(...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are good suggestions, but I think that's probably outside of the scope of this refactoring. The goal of this change is to introduce a way for dotnet-watch to provide an alternative means of creating ProjectFileInfo instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into this at some point doing the C# extension work and yes, there's definitely cleanup opportunities. But I agree with out of scope here...
|
@tmat: how do you get the info applied to the actual workspace? Reusing some of the existing code somewhere or just doing your own thing? |
@jasonmalinowski: dotnet-watch doesn't use |
|
|
||
| internal static class WatchMSBuildProjectLoader | ||
| { | ||
| public static MSBuildProjectLoader Create( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove this overload.
|
Not sure I understand how would dotnet-watch consume the API. Do you have a draft PR in dotnet-watch that shows that? |
jasonmalinowski
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes generally all make sense to me up until the last commit where we're adding the external access API and wrappers, because it seems like we're spending a huge amount of effort passing around data in ways I wouldn't expect. Specifically dotnet watch is expected to fill out a bunch of WatchProjectFileInfos, which we then convert to ProjectFileInfos, and then we convert to ProjectInfos, only to then convert them to another ProjectInfo here, which we then hand back to a Roslyn API to do an internal call. That's...a lot of wrapping and unwrapping the same data.
But how is dotnet watch even expected to fill that out without either consuming the stuff that lives in the BuildHost today, or reimplementing that? If I recall from an earlier conversation the concern was dotnet watch already had MSBuild project instances in-proc from the builds it had already done, and the desire was to reuse those. But we could then make a much more direct replacement of that WatchHotReloadService.WithProjectInfo() that takes the MSBuild types and does everything you need internally, which would simplify the UpdateProjectConeAsync() code as it is.
I love the first commits no matter what doing some of the miscellaneous cleanups, and thank you for breaking apart the commits since that made this much easier to review that bit.
| public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cancellationToken) | ||
| { | ||
| var results = ImmutableArray.CreateBuilder<ProjectInfo>(); | ||
| var processedPaths = new HashSet<string>(PathUtilities.Comparer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if you wanted to rename this processedFilePaths since you're already doing the other updates.
| /// Loads the <see cref="ProjectInfo"/> from the given list of project files and all referenced projects. | ||
| /// </summary> | ||
| /// <param name="projectFilePaths">An ordered list of paths to project files that should be loaded.</param> | ||
| /// /// <param name="baseDirectory">An optional base directory to use when resolving project file paths |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra ///.
|
|
||
| namespace Microsoft.CodeAnalysis.MSBuild; | ||
|
|
||
| internal abstract class ProjectFileInfoLoader( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider calling this AbstractProjectFileInfoLoader, simply because right now there are so many classes with "file" "loader" and similar that it's getting very hard to keep these apart.
| } | ||
|
|
||
| internal MSBuildProjectLoader( | ||
| Workspace workspace, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into this at some point doing the C# extension work and yes, there's definitely cleanup opportunities. But I agree with out of scope here...
This change refactors
MSBuildProjectLoaderto allow dotnet-watch to load projects without running a design-time build out-of-proc. Because dotnet-watch runs as a built-in dotent tool, the correct MSBuild is already loaded in process. So, launching build host processes is unnecessary overhead.The main refactoring is to extract an abstraction for loading MSBuild projects as
ProjectFileInfoinstances thatMSBuildProjectLoaderturns intoProjectInfos. This consists of an abstractProjectFileInfoLoaderclass and anIProjectFileInfoLoaderFactoryinterface that can optionally be passed into anMSBuildProjectLoaderconstructor. By default, theBuildHostProjectInfoLoaderFactoryis used if anotherIProjectFileInfoLoaderFactoryisn't provided. So, the default behavior ofMSBuildProjectLoadershould remain intact.In addition, types have been added to expose an external access API to dotnet-watch. These external access APIs allow dotnet-watch to plug-in a custom
IProjectFileInfoLoaderFactory.I've kept the commit history consistent so that it's easy to review commit-by-commit.
cc @jasonmalinowski and @tmat