Skip to content

Commit adb4394

Browse files
authored
.SLNX format support (#10794)
* .slnx support - use the new parser to parse .slnx files
1 parent 2358769 commit adb4394

File tree

14 files changed

+932
-191
lines changed

14 files changed

+932
-191
lines changed

eng/SourceBuildPrebuiltBaseline.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<UsagePattern IdentityGlob="System.Security.Cryptography.Xml/*8.0.0*" />
1818
<UsagePattern IdentityGlob="System.Text.Json/*8.0.5*" />
1919
<UsagePattern IdentityGlob="System.Threading.Tasks.Dataflow/*8.0.0*" />
20+
<UsagePattern IdentityGlob="Microsoft.VisualStudio.SolutionPersistence/*1.0.9*" />
2021
</IgnorePatterns>
2122
<Usages>
2223
</Usages>

eng/Versions.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,8 @@
7676
<FileVersion>$(VersionPrefix).$(FileVersion.Split('.')[3])</FileVersion>
7777
</PropertyGroup>
7878
</Target>
79+
<!-- SolutionPersistence -->
80+
<PropertyGroup>
81+
<MicrosoftVisualStudioSolutionPersistenceVersion>1.0.9</MicrosoftVisualStudioSolutionPersistenceVersion>
82+
</PropertyGroup>
7983
</Project>

eng/dependabot/Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060

6161
<PackageVersion Include="Verify.Xunit" Version="19.14.1" />
6262
<PackageVersion Update="Verify.XUnit" Condition="'$(VerifyXUnitVersion)' != ''" Version="$(VerifyXUnitVersion)" />
63+
64+
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="$(MicrosoftVisualStudioSolutionPersistenceVersion)" />
6365
</ItemGroup>
6466

6567
<ItemGroup Condition="'$(DotNetBuildSourceOnly)' != 'true' AND $(ProjectIsDeprecated) != 'true'">

src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs

Lines changed: 258 additions & 77 deletions
Large diffs are not rendered by default.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Threading;
9+
using Microsoft.Build.Construction;
10+
using Microsoft.Build.Exceptions;
11+
using Microsoft.Build.Shared;
12+
using Microsoft.VisualStudio.SolutionPersistence;
13+
using Microsoft.VisualStudio.SolutionPersistence.Model;
14+
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
15+
using Shouldly;
16+
using Xunit;
17+
using Xunit.Abstractions;
18+
19+
#nullable disable
20+
21+
namespace Microsoft.Build.UnitTests.Construction
22+
{
23+
public class SolutionFile_NewParser_Tests
24+
{
25+
public ITestOutputHelper TestOutputHelper { get; }
26+
27+
public SolutionFile_NewParser_Tests(ITestOutputHelper testOutputHelper)
28+
{
29+
TestOutputHelper = testOutputHelper;
30+
}
31+
32+
/// <summary>
33+
/// Tests to see that all the data/properties are correctly parsed out of a Venus
34+
/// project in a .SLN. This can be checked only here because of AspNetConfigurations protection level.
35+
/// </summary>
36+
[Theory]
37+
[InlineData(false)]
38+
[InlineData(true)]
39+
public void ProjectWithWebsiteProperties(bool convertToSlnx)
40+
{
41+
string solutionFileContents =
42+
"""
43+
Microsoft Visual Studio Solution File, Format Version 9.00
44+
# Visual Studio 2005
45+
Project(`{E24C65DC-7377-472B-9ABA-BC803B73C61A}`) = `C:\WebSites\WebApplication3\`, `C:\WebSites\WebApplication3\`, `{464FD0B9-E335-4677-BE1E-6B2F982F4D86}`
46+
ProjectSection(WebsiteProperties) = preProject
47+
ProjectReferences = `{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSCla;ssLibra;ry1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;`
48+
Frontpage = false
49+
Debug.AspNetCompiler.VirtualPath = `/publishfirst`
50+
Debug.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite\`
51+
Debug.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst\`
52+
Debug.AspNetCompiler.ForceOverwrite = `true`
53+
Debug.AspNetCompiler.Updateable = `false`
54+
Debug.AspNetCompiler.Debug = `true`
55+
Debug.AspNetCompiler.KeyFile = `debugkeyfile.snk`
56+
Debug.AspNetCompiler.KeyContainer = `12345.container`
57+
Debug.AspNetCompiler.DelaySign = `true`
58+
Debug.AspNetCompiler.AllowPartiallyTrustedCallers = `false`
59+
Debug.AspNetCompiler.FixedNames = `debugfixednames`
60+
Release.AspNetCompiler.VirtualPath = `/publishfirst_release`
61+
Release.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite_release\`
62+
Release.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst_release\`
63+
Release.AspNetCompiler.ForceOverwrite = `true`
64+
Release.AspNetCompiler.Updateable = `true`
65+
Release.AspNetCompiler.Debug = `false`
66+
VWDPort = 63496
67+
EndProjectSection
68+
EndProject
69+
Global
70+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
71+
Debug|.NET = Debug|.NET
72+
EndGlobalSection
73+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
74+
{464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.ActiveCfg = Debug|.NET
75+
{464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.Build.0 = Debug|.NET
76+
EndGlobalSection
77+
GlobalSection(SolutionProperties) = preSolution
78+
HideSolutionNode = FALSE
79+
EndGlobalSection
80+
EndGlobal
81+
""";
82+
83+
SolutionFile solution = ParseSolutionHelper(solutionFileContents.Replace('`', '"'), convertToSlnx);
84+
85+
solution.ProjectsInOrder.ShouldHaveSingleItem();
86+
87+
solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject);
88+
solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\");
89+
solution.ProjectsInOrder[0].RelativePath.ShouldBe(ConvertToUnixPathIfNeeded(@"C:\WebSites\WebApplication3\"));
90+
solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2);
91+
solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull();
92+
solution.ProjectsInOrder[0].GetUniqueProjectName().ShouldBe(@"C:\WebSites\WebApplication3\");
93+
94+
Hashtable aspNetCompilerParameters = solution.ProjectsInOrder[0].AspNetConfigurations;
95+
AspNetCompilerParameters debugAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Debug"];
96+
AspNetCompilerParameters releaseAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Release"];
97+
98+
debugAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst");
99+
debugAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite\");
100+
debugAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst\");
101+
debugAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
102+
debugAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"false");
103+
debugAspNetCompilerParameters.aspNetDebug.ShouldBe(@"true");
104+
debugAspNetCompilerParameters.aspNetKeyFile.ShouldBe(@"debugkeyfile.snk");
105+
debugAspNetCompilerParameters.aspNetKeyContainer.ShouldBe(@"12345.container");
106+
debugAspNetCompilerParameters.aspNetDelaySign.ShouldBe(@"true");
107+
debugAspNetCompilerParameters.aspNetAPTCA.ShouldBe(@"false");
108+
debugAspNetCompilerParameters.aspNetFixedNames.ShouldBe(@"debugfixednames");
109+
110+
releaseAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst_release");
111+
releaseAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite_release\");
112+
releaseAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst_release\");
113+
releaseAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
114+
releaseAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"true");
115+
releaseAspNetCompilerParameters.aspNetDebug.ShouldBe(@"false");
116+
releaseAspNetCompilerParameters.aspNetKeyFile.ShouldBe("");
117+
releaseAspNetCompilerParameters.aspNetKeyContainer.ShouldBe("");
118+
releaseAspNetCompilerParameters.aspNetDelaySign.ShouldBe("");
119+
releaseAspNetCompilerParameters.aspNetAPTCA.ShouldBe("");
120+
releaseAspNetCompilerParameters.aspNetFixedNames.ShouldBe("");
121+
122+
List<string> aspNetProjectReferences = solution.ProjectsInOrder[0].ProjectReferences;
123+
aspNetProjectReferences.Count.ShouldBe(2);
124+
aspNetProjectReferences[0].ShouldBe("{FD705688-88D1-4C22-9BFF-86235D89C2FC}");
125+
aspNetProjectReferences[1].ShouldBe("{F0726D09-042B-4A7A-8A01-6BED2422BD5D}");
126+
}
127+
128+
/// <summary>
129+
/// Helper method to create a SolutionFile object, and call it to parse the SLN file
130+
/// represented by the string contents passed in. Optionally can convert the SLN to SLNX and then parse the solution.
131+
/// </summary>
132+
internal static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false)
133+
{
134+
solutionFileContents = solutionFileContents.Replace('\'', '"');
135+
136+
using (TestEnvironment testEnvironment = TestEnvironment.Create())
137+
{
138+
TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);
139+
140+
string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;
141+
142+
SolutionFile solutionFile = new SolutionFile { FullPath = solutionPath };
143+
solutionFile.ParseUsingNewParser();
144+
return solutionFile;
145+
}
146+
}
147+
148+
private static string ConvertToSlnx(string slnPath)
149+
{
150+
string slnxPath = slnPath + "x";
151+
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
152+
SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
153+
SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
154+
return slnxPath;
155+
}
156+
157+
private static string ConvertToUnixPathIfNeeded(string path)
158+
{
159+
// In the new parser, ProjectModel.FilePath is converted to Unix-style.
160+
return !NativeMethodsShared.IsWindows ? path.Replace('\\', '/') : path;
161+
}
162+
}
163+
}

src/Build.UnitTests/Construction/SolutionFilter_Tests.cs

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Threading;
89
using Microsoft.Build.BackEnd.Logging;
910
using Microsoft.Build.Construction;
1011
using Microsoft.Build.Evaluation;
@@ -13,6 +14,9 @@
1314
using Microsoft.Build.Framework;
1415
using Microsoft.Build.Graph;
1516
using Microsoft.Build.UnitTests;
17+
using Microsoft.VisualStudio.SolutionPersistence.Model;
18+
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
19+
using Microsoft.VisualStudio.SolutionPersistence;
1620
using Shouldly;
1721
using Xunit;
1822
using Xunit.Abstractions;
@@ -215,8 +219,10 @@ public void InvalidSolutionFilters(string slnfValue, string exceptionReason)
215219
/// <summary>
216220
/// Test that a solution filter file is parsed correctly, and it can accurately respond as to whether a project should be filtered out.
217221
/// </summary>
218-
[Fact]
219-
public void ParseSolutionFilter()
222+
[Theory]
223+
[InlineData(false)]
224+
[InlineData(true)]
225+
public void ParseSolutionFilter(bool convertToSlnx)
220226
{
221227
using (TestEnvironment testEnvironment = TestEnvironment.Create())
222228
{
@@ -229,35 +235,35 @@ public void ParseSolutionFilter()
229235
// The important part of this .sln is that it has references to each of the four projects we just created.
230236
TransientTestFile sln = testEnvironment.CreateFile(folder, "Microsoft.Build.Dev.sln",
231237
@"
232-
Microsoft Visual Studio Solution File, Format Version 12.00
233-
# Visual Studio 15
234-
VisualStudioVersion = 15.0.27004.2009
235-
MinimumVisualStudioVersion = 10.0.40219.1
236-
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build"", """ + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)) + @""", ""{69BE05E2-CBDA-4D27-9733-44E12B0F5627}""
237-
EndProject
238-
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""MSBuild"", """ + Path.Combine("src", Path.GetFileName(msbuild.Path)) + @""", ""{6F92CA55-1D15-4F34-B1FE-56C0B7EB455E}""
239-
EndProject
240-
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.CommandLine.UnitTests"", """ + Path.Combine("src", Path.GetFileName(commandLineUnitTests.Path)) + @""", ""{0ADDBC02-0076-4159-B351-2BF33FAA46B2}""
241-
EndProject
242-
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.Tasks.UnitTests"", """ + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)) + @""", ""{CF999BDE-02B3-431B-95E6-E88D621D9CBF}""
243-
EndProject
244-
Global
245-
GlobalSection(SolutionConfigurationPlatforms) = preSolution
246-
EndGlobalSection
247-
GlobalSection(ProjectConfigurationPlatforms) = postSolution
248-
EndGlobalSection
249-
GlobalSection(SolutionProperties) = preSolution
250-
HideSolutionNode = FALSE
251-
EndGlobalSection
252-
GlobalSection(ExtensibilityGlobals) = postSolution
253-
EndGlobalSection
254-
EndGlobal
238+
Microsoft Visual Studio Solution File, Format Version 12.00
239+
# Visual Studio 15
240+
VisualStudioVersion = 15.0.27004.2009
241+
MinimumVisualStudioVersion = 10.0.40219.1
242+
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build"", """ + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)) + @""", ""{69BE05E2-CBDA-4D27-9733-44E12B0F5627}""
243+
EndProject
244+
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""MSBuild"", """ + Path.Combine("src", Path.GetFileName(msbuild.Path)) + @""", ""{6F92CA55-1D15-4F34-B1FE-56C0B7EB455E}""
245+
EndProject
246+
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.CommandLine.UnitTests"", """ + Path.Combine("src", Path.GetFileName(commandLineUnitTests.Path)) + @""", ""{0ADDBC02-0076-4159-B351-2BF33FAA46B2}""
247+
EndProject
248+
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.Tasks.UnitTests"", """ + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)) + @""", ""{CF999BDE-02B3-431B-95E6-E88D621D9CBF}""
249+
EndProject
250+
Global
251+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
252+
EndGlobalSection
253+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
254+
EndGlobalSection
255+
GlobalSection(SolutionProperties) = preSolution
256+
HideSolutionNode = FALSE
257+
EndGlobalSection
258+
GlobalSection(ExtensibilityGlobals) = postSolution
259+
EndGlobalSection
260+
EndGlobal
255261
");
256262
TransientTestFile slnf = testEnvironment.CreateFile(folder, "Dev.slnf",
257263
@"
258264
{
259265
""solution"": {
260-
""path"": """ + sln.Path.Replace("\\", "\\\\") + @""",
266+
""path"": """ + (convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path).Replace("\\", "\\\\") + @""",
261267
""projects"": [
262268
""" + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)!).Replace("\\", "\\\\") + @""",
263269
""" + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)!).Replace("\\", "\\\\") + @"""
@@ -276,6 +282,15 @@ public void ParseSolutionFilter()
276282
}
277283
}
278284

285+
private static string ConvertToSlnx(string slnPath)
286+
{
287+
string slnxPath = slnPath + "x";
288+
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
289+
SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
290+
SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
291+
return slnxPath;
292+
}
293+
279294
private ILoggingService CreateMockLoggingService()
280295
{
281296
ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 0);

src/Build/Construction/Solution/ProjectInSolution.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,18 @@ internal string GetUniqueProjectName()
406406

407407
if (ParentProjectGuid != null)
408408
{
409-
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out ProjectInSolution proj))
409+
ProjectInSolution proj = null;
410+
ProjectInSolution solutionFolder = null;
411+
412+
// For the new parser, solution folders are not saved in ProjectsByGuid but in the SolutionFoldersByGuid.
413+
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
414+
!ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
410415
{
411-
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors",
416+
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
412417
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
413418
}
414419

415-
uniqueName = proj.GetUniqueProjectName() + "\\";
420+
uniqueName = (proj != null ? proj.GetUniqueProjectName() : solutionFolder.GetUniqueProjectName()) + "\\";
416421
}
417422

418423
// Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.
@@ -442,16 +447,19 @@ internal string GetOriginalProjectName()
442447
// If this project has a parent SLN folder, first get the full project name for the SLN folder,
443448
// and tack on trailing backslash.
444449
string projectName = String.Empty;
450+
ProjectInSolution proj = null;
451+
ProjectInSolution solutionFolder = null;
445452

446453
if (ParentProjectGuid != null)
447454
{
448-
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out ProjectInSolution parent))
455+
if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
456+
!ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
449457
{
450-
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(parent != null, "SubCategoryForSolutionParsingErrors",
458+
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
451459
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
452460
}
453461

454-
projectName = parent.GetOriginalProjectName() + "\\";
462+
projectName = (proj != null ? proj.GetOriginalProjectName() : solutionFolder.GetOriginalProjectName()) + "\\";
455463
}
456464

457465
// Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.

0 commit comments

Comments
 (0)