Skip to content

Commit 4d032fb

Browse files
authored
Update Workspace.MSBuild to reference Microsoft.Build.Framework. (#78647)
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.
2 parents 1a3e783 + 016e4b0 commit 4d032fb

29 files changed

+218
-103
lines changed

eng/Directory.Packages.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@
146146
<PackageVersion Include="Microsoft.TestPlatform.TranslationLayer" Version="$(MicrosoftNETTestSdkVersion)" />
147147
<PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="$(MicrosoftNETTestSdkVersion)" />
148148

149+
<!--
150+
MSBuildWorkspace
151+
-->
152+
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
153+
149154
<!--
150155
Analyzers
151156
-->

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
5757
_logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
5858
ProjectFactory.SolutionPath = solutionFilePath;
5959

60-
// We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
61-
// and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
62-
await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
63-
var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
64-
65-
// If we don't have a .NET Core SDK on this machine at all, try .NET Framework
66-
if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
67-
{
68-
var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
69-
buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
70-
}
71-
72-
var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
60+
var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
7361
foreach (var (path, guid) in projects)
7462
{
7563
await BeginLoadingProjectAsync(path, guid);

src/LanguageServer/ProtocolUnitTests/Hover/HoverTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Linq;
88
using System.Threading;
99
using System.Threading.Tasks;
10-
using Microsoft.Build.Evaluation;
1110
using Microsoft.CodeAnalysis.PooledObjects;
1211
using Roslyn.Test.Utilities;
1312
using Roslyn.Text.Adornments;

src/Workspaces/MSBuild/BuildHost/BuildHost.cs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Runtime.CompilerServices;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12-
using Microsoft.Build.Construction;
1312
using Microsoft.Build.Locator;
1413
using Microsoft.Build.Logging;
1514
using Roslyn.Utilities;
@@ -133,31 +132,6 @@ private void EnsureMSBuildLoaded(string projectFilePath)
133132
Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(HasUsableMSBuild)} should have been called first to check.");
134133
}
135134

136-
public ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath)
137-
{
138-
EnsureMSBuildLoaded(solutionFilePath);
139-
return GetProjectsInSolutionCore(solutionFilePath);
140-
}
141-
142-
[MethodImpl(MethodImplOptions.NoInlining)] // Do not inline this, since this uses MSBuild types which are being loaded by the caller
143-
private static ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolutionCore(string solutionFilePath)
144-
{
145-
// WARNING: do not use a lambda in this function, as it internally will be put in a class that contains other lambdas used in
146-
// TryEnsureMSBuildLoaded; on Mono this causes type load errors.
147-
148-
var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
149-
150-
foreach (var project in SolutionFile.Parse(solutionFilePath).ProjectsInOrder)
151-
{
152-
if (project.ProjectType != SolutionProjectType.SolutionFolder)
153-
{
154-
builder.Add((project.AbsolutePath, project.ProjectGuid));
155-
}
156-
}
157-
158-
return builder.ToImmutableAndClear();
159-
}
160-
161135
/// <summary>
162136
/// Returns the target ID of the <see cref="ProjectFile"/> object created for this.
163137
/// </summary>

src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections.Immutable;
65
using System.Threading;
76
using System.Threading.Tasks;
87

@@ -14,7 +13,6 @@ namespace Microsoft.CodeAnalysis.MSBuild;
1413
internal interface IBuildHost
1514
{
1615
bool HasUsableMSBuild(string projectOrSolutionFilePath);
17-
ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath);
1816
Task<int> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken);
1917

2018
/// <summary>Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.</summary>

src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -167,51 +167,20 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
167167
throw new ArgumentNullException(nameof(solutionFilePath));
168168
}
169169

170-
if (!_pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
171-
{
172-
// TryGetAbsoluteSolutionPath should throw before we get here.
173-
return null!;
174-
}
175-
176-
var projectFilter = ImmutableHashSet<string>.Empty;
177-
if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
178-
!SolutionFilterReader.TryRead(absoluteSolutionPath, _pathResolver, out absoluteSolutionPath, out projectFilter))
179-
{
180-
throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
181-
}
170+
var (absoluteSolutionPath, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, _pathResolver, cancellationToken).ConfigureAwait(false);
171+
var projectPaths = projects.SelectAsArray(p => p.ProjectPath);
182172

183173
using (_dataGuard.DisposableWait(cancellationToken))
184174
{
185-
this.SetSolutionProperties(absoluteSolutionPath);
175+
SetSolutionProperties(absoluteSolutionPath);
186176
}
187177

188-
var solutionFile = MSB.Construction.SolutionFile.Parse(absoluteSolutionPath);
189178
var reportingMode = GetReportingModeForUnrecognizedProjects();
190179

191180
var reportingOptions = new DiagnosticReportingOptions(
192181
onPathFailure: reportingMode,
193182
onLoaderFailure: reportingMode);
194183

195-
var projectPaths = ImmutableArray.CreateBuilder<string>();
196-
197-
// load all the projects
198-
foreach (var project in solutionFile.ProjectsInOrder)
199-
{
200-
cancellationToken.ThrowIfCancellationRequested();
201-
202-
if (project.ProjectType == MSB.Construction.SolutionProjectType.SolutionFolder)
203-
{
204-
continue;
205-
}
206-
207-
// Load project if we have an empty project filter and the project path is present.
208-
if (projectFilter.IsEmpty ||
209-
projectFilter.Contains(project.AbsolutePath))
210-
{
211-
projectPaths.Add(project.RelativePath);
212-
}
213-
}
214-
215184
var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
216185
await using var _ = buildHostProcessManager.ConfigureAwait(false);
217186

@@ -221,7 +190,7 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
221190
_pathResolver,
222191
_projectFileExtensionRegistry,
223192
buildHostProcessManager,
224-
projectPaths.ToImmutable(),
193+
projectPaths,
225194
// TryGetAbsoluteSolutionPath should not return an invalid path
226195
baseDirectory: Path.GetDirectoryName(absoluteSolutionPath)!,
227196
Properties,
@@ -231,14 +200,14 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
231200
discoveredProjectOptions: reportingOptions,
232201
preferMetadataForReferencesOfDiscoveredProjects: false);
233202

234-
var projects = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
203+
var projectInfos = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
235204

236205
// construct workspace from loaded project infos
237206
return SolutionInfo.Create(
238207
SolutionId.CreateNewId(debugName: absoluteSolutionPath),
239208
version: default,
240209
absoluteSolutionPath,
241-
projects);
210+
projectInfos);
242211
}
243212

244213
/// <summary>

src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.MSBuild;
1111

1212
internal sealed class PathResolver
1313
{
14-
private readonly DiagnosticReporter _diagnosticReporter;
14+
private readonly DiagnosticReporter? _diagnosticReporter;
1515

16-
public PathResolver(DiagnosticReporter diagnosticReporter)
16+
public PathResolver(DiagnosticReporter? diagnosticReporter)
1717
{
1818
_diagnosticReporter = diagnosticReporter;
1919
}
@@ -26,14 +26,14 @@ public bool TryGetAbsoluteSolutionPath(string path, string baseDirectory, Diagno
2626
}
2727
catch (Exception)
2828
{
29-
_diagnosticReporter.Report(reportingMode, string.Format(WorkspacesResources.Invalid_solution_file_path_colon_0, path));
29+
_diagnosticReporter?.Report(reportingMode, string.Format(WorkspacesResources.Invalid_solution_file_path_colon_0, path));
3030
absolutePath = null;
3131
return false;
3232
}
3333

3434
if (!File.Exists(absolutePath))
3535
{
36-
_diagnosticReporter.Report(
36+
_diagnosticReporter?.Report(
3737
reportingMode,
3838
string.Format(WorkspacesResources.Solution_file_not_found_colon_0, absolutePath),
3939
msg => new FileNotFoundException(msg));
@@ -51,14 +51,14 @@ public bool TryGetAbsoluteProjectPath(string path, string baseDirectory, Diagnos
5151
}
5252
catch (Exception)
5353
{
54-
_diagnosticReporter.Report(reportingMode, string.Format(WorkspacesResources.Invalid_project_file_path_colon_0, path));
54+
_diagnosticReporter?.Report(reportingMode, string.Format(WorkspacesResources.Invalid_project_file_path_colon_0, path));
5555
absolutePath = null;
5656
return false;
5757
}
5858

5959
if (!File.Exists(absolutePath))
6060
{
61-
_diagnosticReporter.Report(
61+
_diagnosticReporter?.Report(
6262
reportingMode,
6363
string.Format(WorkspacesResources.Project_file_not_found_colon_0, absolutePath),
6464
msg => new FileNotFoundException(msg));

src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.SolutionFilterReader.cs renamed to src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Microsoft.CodeAnalysis.MSBuild;
1313

14-
public partial class MSBuildProjectLoader
14+
internal partial class SolutionFileReader
1515
{
1616
private static class SolutionFilterReader
1717
{
@@ -35,13 +35,7 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
3535
return false;
3636
}
3737

38-
if (!pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename))
39-
{
40-
// TryGetAbsoluteSolutionPath should throw before we get here.
41-
solutionFilename = string.Empty;
42-
projectFilter = [];
43-
return false;
44-
}
38+
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename));
4539

4640
if (!File.Exists(solutionFilename))
4741
{
@@ -64,10 +58,8 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
6458
}
6559

6660
// Fill the filter with the absolute project paths.
67-
if (pathResolver.TryGetAbsoluteProjectPath(projectPath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath))
68-
{
69-
filterProjects.Add(absoluteProjectPath);
70-
}
61+
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectPath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
62+
filterProjects.Add(absoluteProjectPath);
7163
}
7264

7365
projectFilter = filterProjects.ToImmutable();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Immutable;
7+
using System.IO;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
11+
using Roslyn.Utilities;
12+
13+
namespace Microsoft.CodeAnalysis.MSBuild;
14+
15+
internal partial class SolutionFileReader
16+
{
17+
public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
18+
{
19+
return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
20+
}
21+
22+
public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
23+
{
24+
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
25+
26+
// When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
27+
var projectFilter = ImmutableHashSet<string>.Empty;
28+
if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
29+
!SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
30+
{
31+
throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
32+
}
33+
34+
var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
35+
if (!projects.HasValue)
36+
{
37+
throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
38+
}
39+
40+
return (absoluteSolutionPath, projects.Value);
41+
}
42+
43+
private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
44+
{
45+
var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
46+
if (serializer == null)
47+
{
48+
return null;
49+
}
50+
51+
// The solution folder is the base directory for project paths.
52+
var baseDirectory = Path.GetDirectoryName(solutionFilePath);
53+
RoslynDebug.AssertNotNull(baseDirectory);
54+
55+
var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
56+
57+
var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
58+
foreach (var projectModel in solutionModel.SolutionProjects)
59+
{
60+
// If we are filtering based on a solution filter, then we need to verify the project is included.
61+
if (!projectFilter.IsEmpty)
62+
{
63+
Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
64+
if (!projectFilter.Contains(absoluteProjectPath))
65+
{
66+
continue;
67+
}
68+
}
69+
70+
builder.Add((projectModel.FilePath, projectModel.Id.ToString()));
71+
}
72+
73+
return builder.ToImmutable();
74+
}
75+
}

src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@
2222
</PackageDescription>
2323
</PropertyGroup>
2424
<ItemGroup>
25-
<PackageReference Include="Microsoft.Build" />
26-
<PackageReference Include="Microsoft.Build.Tasks.Core" />
25+
<PackageReference Include="Microsoft.Build.Framework" />
2726
<PackageReference Include="Microsoft.Extensions.Logging" />
27+
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
2828
<PackageReference Include="Newtonsoft.Json" />
29+
<!--
30+
Since System.Text.Json is part of the .NET BCL we do not want to add
31+
it as a package reference. Doing so will pin the version to what we use
32+
in our build instead of allowing it to use the version from the .NET Runtime
33+
we are running against.
34+
-->
35+
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
2936
</ItemGroup>
3037
<ItemGroup>
3138
<ProjectReference Include="..\..\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj" />

0 commit comments

Comments
 (0)