Skip to content

Conversation

JoeRobich
Copy link
Member

@JoeRobich JoeRobich commented May 19, 2025

Workspace.MSBuild requires a MSBuild dependency because our API references their ILogger interface. This PR changes the MSBuild references to be Microsoft.Build.Framework, which is where the interface is defined.

In order to fully drop the Microsoft.Build dependency I also pulled in the VS-SolutionPersister changes which bring solution parsing back in-proc.

@JoeRobich JoeRobich requested a review from a team as a code owner May 19, 2025 20:41
@RikkiGibson
Copy link
Member

What is the effect of the change? Is it reducing the net msbuild surface we are depending on / making it harder to inadvertently use msbuild APIs that we should not be using?

@JoeRobich
Copy link
Member Author

What is the effect of the change?

Currently our package brings in a broad set of MSBuild dependencies (see below). This change limits it to only the dependency that is necessary.

image

@RikkiGibson
Copy link
Member

It looks like possibly a subset of the transitive dependencies need to be brought back to fix the build

JoeRobich added 2 commits May 19, 2025 21:42
Removes the GetProjectsInSolution BuildHost API. Instead we can use the SolutionPersister library in processes to parse the projects from a solution file.
@JoeRobich JoeRobich requested a review from a team as a code owner May 20, 2025 06:33
@jasonmalinowski jasonmalinowski self-requested a review May 20, 2025 06:35
}

var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case that solutionFilePath points to a .slnf ReadSolutionFileAsync returns the filtered solution's file path. Should we be assigning that path to the ProjectFactory.SolutionPath?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you basically asking whether to use the path to the slnf or the path to the associated sln? I think we would want to use the slnf, which this PR appears to be doing right now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, it is working today without this change and is currently using the .slnf path.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that's probably reasonable anyways; the only real thing the solution path is used for is caches, and I could imagine what we have to cache is different for different filters over the same solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(if that's bad, maybe the fix is we should be passing around the workspace path or something else for that...)

throw new ArgumentNullException(nameof(solutionFilePath));
}

if (!_pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this bit into a new SolutionFileReader class.

private readonly DiagnosticReporter? _diagnosticReporter;

public PathResolver(DiagnosticReporter diagnosticReporter)
public PathResolver(DiagnosticReporter? diagnosticReporter)
Copy link
Member Author

@JoeRobich JoeRobich May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the DiagnosticReporter optional since this will also be indirectly used by the LanguageServerProjectSystem which does not use a reporter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be though?

@RikkiGibson RikkiGibson self-assigned this May 21, 2025
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally brought in by Microsoft.Build and is already available as part of .NET.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we only need this for net472?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Text.Json is part of the .NET BCL.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it as a PackageReference for .NET TFM will pin the version to whatever we use in the build instead of allowing it to use the version included with newer runtimes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add all that as a comment.

var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
if (serializer == null)
{
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does the serializer being null here mean? Are there different serializers for sln vs slnx?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! And I imagine when solution filter support is add it will be a separate serializer.

if (!projectFilter.IsEmpty)
{
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
if (!projectFilter.Contains(absoluteProjectPath))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, does the solution parsing library not give us a way to get the subset of projects out of a solution filter? We have to do that ourselves?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not presently. However, there is an open feature request microsoft/vs-solutionpersistence#85

<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we only need this for net472?

@JoeRobich JoeRobich merged commit 4d032fb into main May 23, 2025
28 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone May 23, 2025

public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect this Directory.GetCurrentDirectory() code is only used in the classic MSBuildWorkspace path; we shouldn't have relative paths bouncing around in the language server at all, and I'd actually be a bit worried what it might resolve to at all...

var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
if (!projects.HasValue)
{
throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the only case this can happen is the format of the solution file isn't recognized? Should we give a better message in this case?

var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
if (serializer == null)
{
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have this just throw? Otherwise we're having to deal with the null return at the caller which isn't really helping much since it makes me think the rest of the method isn't supposed to throw (but absolutely would if something went wrong?)

foreach (var projectModel in solutionModel.SolutionProjects)
{
// If we are filtering based on a solution filter, then we need to verify the project is included.
if (!projectFilter.IsEmpty)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have an empty filter, what happens? Should this use a nullable set to make it more clear?

Comment on lines +30 to +33
Since System.Text.Json is part of the .NET BCL we do not want to add
it as a package reference. Doing so will pin the version to what we use
in our build instead of allowing it to use the version from the .NET Runtime
we are running against.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is System.Text.Json extra special here, or should we be doing this for other references too?

@@ -35,13 +35,7 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
return false;
}

if (!pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename))
{
// TryGetAbsoluteSolutionPath should throw before we get here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to bring this comment back -- since if you're not aware of the DiagnosticReportingMode bit this might seem a bit odd...

@johnkors
Copy link

johnkors commented Jun 4, 2025

Can someone tell me how to read a slnx-file programatically after this change?

Using Microsoft.CodeAnalysis.CSharp.Workspaces/4.14.0 &

await workspace.OpenSolutionAsync("some.slnx");

still throws exceptions.

Is there another way?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants