diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml
index 41e59576f29..f4674a72703 100644
--- a/eng/SourceBuildPrebuiltBaseline.xml
+++ b/eng/SourceBuildPrebuiltBaseline.xml
@@ -17,6 +17,7 @@
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 526059cbc0f..1cc7aab7f14 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -76,4 +76,8 @@
$(VersionPrefix).$(FileVersion.Split('.')[3])
+
+
+ 1.0.9
+
diff --git a/eng/dependabot/Packages.props b/eng/dependabot/Packages.props
index 1672382b7c3..4aab28833bb 100644
--- a/eng/dependabot/Packages.props
+++ b/eng/dependabot/Packages.props
@@ -60,6 +60,8 @@
+
+
diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs
index 84d703d22e8..d6abd900521 100644
--- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs
+++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs
@@ -4,10 +4,15 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text;
+using System.Threading;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Shared;
+using Microsoft.VisualStudio.SolutionPersistence;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
using Shouldly;
using Xunit;
@@ -59,11 +64,13 @@ public void ParseSolution_VC()
/// Test that a project with the C++ project guid and an arbitrary extension is seen as valid --
/// we assume that all C++ projects except .vcproj are MSBuild format.
///
- [Fact]
- public void ParseSolution_VC2()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ParseSolution_VC2(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}') = 'Project name.myvctype', 'Relative path\to\Project name.myvctype', '{0ABED153-9451-483C-8140-9E8D7306B216}'
@@ -83,13 +90,18 @@ public void ParseSolution_VC2()
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
- Assert.Equal("Project name.myvctype", solution.ProjectsInOrder[0].ProjectName);
- Assert.Equal("Relative path\\to\\Project name.myvctype", solution.ProjectsInOrder[0].RelativePath);
- Assert.Equal("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid);
+ string expectedProjectName = convertToSlnx ? "Project name" : "Project name.myvctype";
+ Assert.Equal(expectedProjectName, solution.ProjectsInOrder[0].ProjectName);
+ Assert.Equal(ConvertToUnixPathIfNeeded("Relative path\\to\\Project name.myvctype", convertToSlnx), solution.ProjectsInOrder[0].RelativePath);
+ if (!convertToSlnx)
+ {
+ // When converting to SLNX, the project GUID is not preserved.
+ Assert.Equal("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid);
+ }
}
///
@@ -280,11 +292,13 @@ public void ParseSolutionFileWithDescriptionInformation()
///
/// Tests the parsing of a very basic .SLN file with three independent projects.
///
- [Fact]
- public void BasicSolution()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void BasicSolution(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{F184B08F-C81C-45F6-A57F-5ABD9991F28F}') = 'ConsoleApplication1', 'ConsoleApplication1\ConsoleApplication1.vbproj', '{AB3413A6-D689-486D-B7F0-A095371B3F13}'
@@ -316,34 +330,40 @@ public void BasicSolution()
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
Assert.Equal(3, solution.ProjectsInOrder.Count);
- Assert.Equal("ConsoleApplication1", solution.ProjectsInOrder[0].ProjectName);
- Assert.Equal(@"ConsoleApplication1\ConsoleApplication1.vbproj", solution.ProjectsInOrder[0].RelativePath);
- Assert.Equal("{AB3413A6-D689-486D-B7F0-A095371B3F13}", solution.ProjectsInOrder[0].ProjectGuid);
- Assert.Empty(solution.ProjectsInOrder[0].Dependencies);
- Assert.Null(solution.ProjectsInOrder[0].ParentProjectGuid);
+ // When converting to slnx, the order of the projects is not preserved.
+ ProjectInSolution consoleApplication1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ConsoleApplication1");
+ Assert.Equal(ConvertToUnixPathIfNeeded("ConsoleApplication1\\ConsoleApplication1.vbproj", convertToSlnx), consoleApplication1.RelativePath);
+ Assert.Empty(consoleApplication1.Dependencies);
+ Assert.Null(consoleApplication1.ParentProjectGuid);
- Assert.Equal("vbClassLibrary", solution.ProjectsInOrder[1].ProjectName);
- Assert.Equal(@"vbClassLibrary\vbClassLibrary.vbproj", solution.ProjectsInOrder[1].RelativePath);
- Assert.Equal("{BA333A76-4511-47B8-8DF4-CA51C303AD0B}", solution.ProjectsInOrder[1].ProjectGuid);
- Assert.Empty(solution.ProjectsInOrder[1].Dependencies);
- Assert.Null(solution.ProjectsInOrder[1].ParentProjectGuid);
+ ProjectInSolution vbClassLibrary = solution.ProjectsInOrder.First(p => p.ProjectName == "vbClassLibrary");
+ Assert.Equal(ConvertToUnixPathIfNeeded("vbClassLibrary\\vbClassLibrary.vbproj", convertToSlnx), vbClassLibrary.RelativePath);
+ Assert.Empty(vbClassLibrary.Dependencies);
+ Assert.Null(vbClassLibrary.ParentProjectGuid);
- Assert.Equal("ClassLibrary1", solution.ProjectsInOrder[2].ProjectName);
- Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath);
- Assert.Equal("{DEBCE986-61B9-435E-8018-44B9EF751655}", solution.ProjectsInOrder[2].ProjectGuid);
- Assert.Empty(solution.ProjectsInOrder[2].Dependencies);
- Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid);
+ ProjectInSolution classLibrary1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary1");
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj", convertToSlnx), classLibrary1.RelativePath);
+ Assert.Empty(classLibrary1.Dependencies);
+ Assert.Null(classLibrary1.ParentProjectGuid);
+
+ if (!convertToSlnx)
+ {
+ Assert.Equal("{AB3413A6-D689-486D-B7F0-A095371B3F13}", consoleApplication1.ProjectGuid);
+ Assert.Equal("{BA333A76-4511-47B8-8DF4-CA51C303AD0B}", vbClassLibrary.ProjectGuid);
+ Assert.Equal("{DEBCE986-61B9-435E-8018-44B9EF751655}", classLibrary1.ProjectGuid);
+ }
}
///
/// Exercises solution folders, and makes sure that samely named projects in different
/// solution folders will get correctly uniquified.
+ /// For the new parser, solution folders are not included to ProjectsInOrder or ProjectsByGuid.
///
[Fact]
public void SolutionFolders()
@@ -396,7 +416,7 @@ public void SolutionFolders()
Assert.Equal(5, solution.ProjectsInOrder.Count);
- Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath);
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj", false), solution.ProjectsInOrder[0].RelativePath);
Assert.Equal("{34E0D07D-CF8F-459D-9449-C4188D8C5564}", solution.ProjectsInOrder[0].ProjectGuid);
Assert.Empty(solution.ProjectsInOrder[0].Dependencies);
Assert.Null(solution.ProjectsInOrder[0].ParentProjectGuid);
@@ -405,7 +425,7 @@ public void SolutionFolders()
Assert.Empty(solution.ProjectsInOrder[1].Dependencies);
Assert.Null(solution.ProjectsInOrder[1].ParentProjectGuid);
- Assert.Equal(@"MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[2].RelativePath);
+ Assert.Equal(ConvertToUnixPathIfNeeded("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj", false), solution.ProjectsInOrder[2].RelativePath);
Assert.Equal("{A5EE8128-B08E-4533-86C5-E46714981680}", solution.ProjectsInOrder[2].ProjectGuid);
Assert.Empty(solution.ProjectsInOrder[2].Dependencies);
Assert.Equal("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[2].ParentProjectGuid);
@@ -414,12 +434,90 @@ public void SolutionFolders()
Assert.Empty(solution.ProjectsInOrder[3].Dependencies);
Assert.Equal("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[3].ParentProjectGuid);
- Assert.Equal(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[4].RelativePath);
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj", false), solution.ProjectsInOrder[4].RelativePath);
Assert.Equal("{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}", solution.ProjectsInOrder[4].ProjectGuid);
Assert.Empty(solution.ProjectsInOrder[4].Dependencies);
Assert.Equal("{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}", solution.ProjectsInOrder[4].ParentProjectGuid);
}
+ ///
+ /// Exercises solution folders, and makes sure that samely named projects in different
+ /// solution folders will get correctly uniquified.
+ /// For the new parser, solution folders are not included to ProjectsInOrder or ProjectsByGuid.
+ ///
+ [Fact]
+ public void SolutionFoldersSlnx()
+ {
+ string solutionFileContents =
+ """
+ Microsoft Visual Studio Solution File, Format Version 9.00
+ # Visual Studio 2005
+ Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{34E0D07D-CF8F-459D-9449-C4188D8C5564}'
+ EndProject
+ Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySlnFolder', 'MySlnFolder', '{E0F97730-25D2-418A-A7BD-02CAFDC6E470}'
+ EndProject
+ Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj', '{A5EE8128-B08E-4533-86C5-E46714981680}'
+ EndProject
+ Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'MySubSlnFolder', 'MySubSlnFolder', '{2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}'
+ EndProject
+ Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary2', 'ClassLibrary2\ClassLibrary2.csproj', '{6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}'
+ EndProject
+ Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {34E0D07D-CF8F-459D-9449-C4188D8C5564}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A5EE8128-B08E-4533-86C5-E46714981680}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A5EE8128-B08E-4533-86C5-E46714981680}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A5EE8128-B08E-4533-86C5-E46714981680} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470}
+ {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B} = {E0F97730-25D2-418A-A7BD-02CAFDC6E470}
+ {6DB98C35-FDCC-4818-B5D4-1F0A385FDFD4} = {2AE8D6C4-FB43-430C-8AEB-15E5EEDAAE4B}
+ EndGlobalSection
+ EndGlobal
+ """;
+
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, true);
+
+ Assert.Equal(3, solution.ProjectsInOrder.Count);
+
+ var classLibrary1 = solution.ProjectsInOrder
+ .FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj", true));
+ Assert.NotNull(classLibrary1);
+ Assert.Empty(classLibrary1.Dependencies);
+ Assert.Null(classLibrary1.ParentProjectGuid);
+
+ var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder
+ .FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj", true));
+ Assert.NotNull(myPhysicalFolderClassLibrary1);
+ Assert.Empty(myPhysicalFolderClassLibrary1.Dependencies);
+
+ var classLibrary2 = solution.ProjectsInOrder
+ .FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj", true));
+ Assert.NotNull(classLibrary2);
+ Assert.Empty(classLibrary2.Dependencies);
+
+ // When converting to slnx, the guids are not preserved.
+ // try at list assert not null
+ Assert.NotNull(myPhysicalFolderClassLibrary1.ParentProjectGuid);
+ Assert.NotNull(classLibrary2.ParentProjectGuid);
+ }
+
///
/// Exercises shared projects.
///
@@ -556,13 +654,15 @@ public void MissingNestedProject()
///
/// Verifies that hand-coded project-to-project dependencies listed in the .SLN file
- /// are correctly recognized by our solution parser.
+ /// are correctly recognized by the solution parser.
///
- [Fact]
- public void SolutionDependencies()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void SolutionDependencies(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{05A5AD00-71B5-4612-AF2F-9EA9121C4111}'
@@ -601,27 +701,29 @@ public void SolutionDependencies()
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
Assert.Equal(3, solution.ProjectsInOrder.Count);
- Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath);
- Assert.Equal("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", solution.ProjectsInOrder[0].ProjectGuid);
- Assert.Single(solution.ProjectsInOrder[0].Dependencies);
- Assert.Equal("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string)solution.ProjectsInOrder[0].Dependencies[0]);
+ var classLibrary1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary1");
+ var classLibrary2 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary2");
+ var classLibrary3 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary3");
+
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj", convertToSlnx), classLibrary1.RelativePath);
+ Assert.Single(classLibrary1.Dependencies);
+ Assert.Equal(classLibrary3.ProjectGuid, classLibrary1.Dependencies[0]);
Assert.Null(solution.ProjectsInOrder[0].ParentProjectGuid);
- Assert.Equal(@"ClassLibrary2\ClassLibrary2.csproj", solution.ProjectsInOrder[1].RelativePath);
- Assert.Equal("{7F316407-AE3E-4F26-BE61-2C50D30DA158}", solution.ProjectsInOrder[1].ProjectGuid);
- Assert.Equal(2, solution.ProjectsInOrder[1].Dependencies.Count);
- Assert.Equal("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", (string)solution.ProjectsInOrder[1].Dependencies[0]);
- Assert.Equal("{05A5AD00-71B5-4612-AF2F-9EA9121C4111}", (string)solution.ProjectsInOrder[1].Dependencies[1]);
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj", convertToSlnx), classLibrary2.RelativePath);
+ Assert.Equal(2, classLibrary2.Dependencies.Count);
+ // When converting to SLNX, the projects dependencies order is not preserved.
+ Assert.Contains(classLibrary3.ProjectGuid, classLibrary2.Dependencies);
+ Assert.Contains(classLibrary1.ProjectGuid, classLibrary2.Dependencies);
Assert.Null(solution.ProjectsInOrder[1].ParentProjectGuid);
- Assert.Equal(@"ClassLibrary3\ClassLibrary3.csproj", solution.ProjectsInOrder[2].RelativePath);
- Assert.Equal("{FAB4EE06-6E01-495A-8926-5514599E3DD9}", solution.ProjectsInOrder[2].ProjectGuid);
+ Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary3\\ClassLibrary3.csproj", convertToSlnx), solution.ProjectsInOrder[2].RelativePath);
Assert.Empty(solution.ProjectsInOrder[2].Dependencies);
Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid);
}
@@ -629,11 +731,13 @@ public void SolutionDependencies()
///
/// Make sure the solution configurations get parsed correctly for a simple mixed C#/VC solution
///
- [Fact]
- public void ParseSolutionConfigurations()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ParseSolutionConfigurations(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}'
@@ -678,9 +782,9 @@ public void ParseSolutionConfigurations()
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
Assert.Equal(7, solution.SolutionConfigurations.Count);
@@ -704,11 +808,13 @@ public void ParseSolutionConfigurations()
///
/// Make sure the solution configurations get parsed correctly for a simple C# application
///
- [Fact]
- public void ParseSolutionConfigurationsNoMixedPlatform()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ParseSolutionConfigurationsNoMixedPlatform(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}'
@@ -733,14 +839,14 @@ public void ParseSolutionConfigurationsNoMixedPlatform()
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.ActiveCfg = Release|Any CPU
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|ARM.Build.0 = Release|Any CPU
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|x86.ActiveCfg = Release|Any CPU
- EndGlobalSection
+ EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
Assert.Equal(6, solution.SolutionConfigurations.Count);
@@ -839,15 +945,18 @@ public void ParseInvalidSolutionConfigurations3()
ParseSolutionHelper(solutionFileContents);
});
}
+
///
/// Make sure the project configurations in solution configurations get parsed correctly
/// for a simple mixed C#/VC solution
///
- [Fact]
- public void ParseProjectConfigurationsInSolutionConfigurations1()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ParseProjectConfigurationsInSolutionConfigurations1(bool convertToSlnx)
{
string solutionFileContents =
- @"
+ """
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}') = 'ClassLibrary1', 'ClassLibrary1\ClassLibrary1.csproj', '{6185CC21-BE89-448A-B3C0-D1C27112E595}'
@@ -889,12 +998,12 @@ public void ParseProjectConfigurationsInSolutionConfigurations1()
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
- ";
+ """;
- SolutionFile solution = ParseSolutionHelper(solutionFileContents);
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, convertToSlnx);
- ProjectInSolution csharpProject = (ProjectInSolution)solution.ProjectsByGuid["{6185CC21-BE89-448A-B3C0-D1C27112E595}"];
- ProjectInSolution vcProject = (ProjectInSolution)solution.ProjectsByGuid["{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}"];
+ ProjectInSolution csharpProject = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary1");
+ ProjectInSolution vcProject = solution.ProjectsInOrder.First(p => p.ProjectName == "MainApp");
Assert.Equal(6, csharpProject.ProjectConfigurations.Count);
@@ -998,6 +1107,65 @@ public void ParseProjectConfigurationsInSolutionConfigurations2()
Assert.Equal(".NET", solution.GetDefaultPlatformName()); // "Default solution platform"
}
+ [Fact]
+ public void ParseProjectConfigurationsInSolutionConfigurationsSlnx()
+ {
+ string solutionFileContents =
+ """
+ Microsoft Visual Studio Solution File, Format Version 12.00
+ # Visual Studio Version 17
+ VisualStudioVersion = 17.11.35111.106
+ MinimumVisualStudioVersion = 10.0.40219.1
+ Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""WinFormsApp1"", ""WinFormsApp1\WinFormsApp1.csproj"", ""{3B592A6A-6215-4675-9237-7FEB36BDB4F1}""
+ EndProject
+ Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary1"", ""ClassLibrary1\ClassLibrary1.csproj"", ""{C25056E0-405C-4476-9B22-839264A8530C}""
+ EndProject
+ Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Win32 = Debug|Win32
+ Release|Win32 = Release|Win32
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3B592A6A-6215-4675-9237-7FEB36BDB4F1}.Debug|Win32.ActiveCfg = Debug|x86
+ {3B592A6A-6215-4675-9237-7FEB36BDB4F1}.Debug|Win32.Build.0 = Debug|x86
+ {3B592A6A-6215-4675-9237-7FEB36BDB4F1}.Release|Win32.ActiveCfg = Release|x86
+ {3B592A6A-6215-4675-9237-7FEB36BDB4F1}.Release|Win32.Build.0 = Release|x86
+ {C25056E0-405C-4476-9B22-839264A8530C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C25056E0-405C-4476-9B22-839264A8530C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AA62B7C4-C703-4DBC-A7AD-D183666ECC20}
+ EndGlobalSection
+ EndGlobal
+ """;
+
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents, true);
+
+ ProjectInSolution winFormsApp1 = solution.ProjectsInOrder.First(p => p.ProjectName == "WinFormsApp1");
+ ProjectInSolution classLibrary1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary1");
+
+ Assert.Equal(2, winFormsApp1.ProjectConfigurations.Count);
+
+ Assert.Equal("Debug|x86", winFormsApp1.ProjectConfigurations["Debug|Win32"].FullName);
+ Assert.True(winFormsApp1.ProjectConfigurations["Debug|Win32"].IncludeInBuild);
+
+ Assert.Equal("Release|x86", winFormsApp1.ProjectConfigurations["Release|Win32"].FullName);
+ Assert.True(winFormsApp1.ProjectConfigurations["Debug|Win32"].IncludeInBuild);
+
+ Assert.Equal(2, classLibrary1.ProjectConfigurations.Count);
+
+ Assert.Equal("Debug|AnyCPU", classLibrary1.ProjectConfigurations["Debug|Any CPU"].FullName);
+ Assert.False(classLibrary1.ProjectConfigurations["Debug|Any CPU"].IncludeInBuild);
+
+ Assert.Equal("Release|AnyCPU", classLibrary1.ProjectConfigurations["Release|Any CPU"].FullName);
+ Assert.False(classLibrary1.ProjectConfigurations["Release|Any CPU"].IncludeInBuild);
+ }
+
///
/// Parse solution file with comments
///
@@ -1053,23 +1221,36 @@ public void ParseSolutionWithComments()
///
/// Helper method to create a SolutionFile object, and call it to parse the SLN file
- /// represented by the string contents passed in.
+ /// represented by the string contents passed in. Optionally can convert the SLN to SLNX and then parse the solution.
///
- private static SolutionFile ParseSolutionHelper(string solutionFileContents)
+ private static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false)
{
solutionFileContents = solutionFileContents.Replace('\'', '"');
- string solutionPath = FileUtilities.GetTemporaryFileName(".sln");
- try
- {
- File.WriteAllText(solutionPath, solutionFileContents);
- SolutionFile sp = SolutionFile.Parse(solutionPath);
- return sp;
- }
- finally
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
- File.Delete(solutionPath);
+ TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);
+
+ string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;
+
+ return SolutionFile.Parse(solutionPath);
}
}
+
+ private static string ConvertToSlnx(string slnPath)
+ {
+ string slnxPath = slnPath + "x";
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
+ SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
+ SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
+ return slnxPath;
+ }
+
+ private static string ConvertToUnixPathIfNeeded(string path, bool isConvertedToSlnx)
+ {
+ // In the new parser, ProjectModel.FilePath is converted to Unix-style.
+ // we are using the new parser only for slnx files.
+ return !NativeMethodsShared.IsWindows && isConvertedToSlnx ? path.Replace('\\', '/') : path;
+ }
}
}
diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs
new file mode 100644
index 00000000000..7f56b600dca
--- /dev/null
+++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs
@@ -0,0 +1,163 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Microsoft.Build.Construction;
+using Microsoft.Build.Exceptions;
+using Microsoft.Build.Shared;
+using Microsoft.VisualStudio.SolutionPersistence;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+
+#nullable disable
+
+namespace Microsoft.Build.UnitTests.Construction
+{
+ public class SolutionFile_NewParser_Tests
+ {
+ public ITestOutputHelper TestOutputHelper { get; }
+
+ public SolutionFile_NewParser_Tests(ITestOutputHelper testOutputHelper)
+ {
+ TestOutputHelper = testOutputHelper;
+ }
+
+ ///
+ /// Tests to see that all the data/properties are correctly parsed out of a Venus
+ /// project in a .SLN. This can be checked only here because of AspNetConfigurations protection level.
+ ///
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ProjectWithWebsiteProperties(bool convertToSlnx)
+ {
+ string solutionFileContents =
+ """
+ Microsoft Visual Studio Solution File, Format Version 9.00
+ # Visual Studio 2005
+ Project(`{E24C65DC-7377-472B-9ABA-BC803B73C61A}`) = `C:\WebSites\WebApplication3\`, `C:\WebSites\WebApplication3\`, `{464FD0B9-E335-4677-BE1E-6B2F982F4D86}`
+ ProjectSection(WebsiteProperties) = preProject
+ ProjectReferences = `{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSCla;ssLibra;ry1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;`
+ Frontpage = false
+ Debug.AspNetCompiler.VirtualPath = `/publishfirst`
+ Debug.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite\`
+ Debug.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst\`
+ Debug.AspNetCompiler.ForceOverwrite = `true`
+ Debug.AspNetCompiler.Updateable = `false`
+ Debug.AspNetCompiler.Debug = `true`
+ Debug.AspNetCompiler.KeyFile = `debugkeyfile.snk`
+ Debug.AspNetCompiler.KeyContainer = `12345.container`
+ Debug.AspNetCompiler.DelaySign = `true`
+ Debug.AspNetCompiler.AllowPartiallyTrustedCallers = `false`
+ Debug.AspNetCompiler.FixedNames = `debugfixednames`
+ Release.AspNetCompiler.VirtualPath = `/publishfirst_release`
+ Release.AspNetCompiler.PhysicalPath = `..\rajeev\temp\websites\myfirstwebsite_release\`
+ Release.AspNetCompiler.TargetPath = `..\rajeev\temp\publishfirst_release\`
+ Release.AspNetCompiler.ForceOverwrite = `true`
+ Release.AspNetCompiler.Updateable = `true`
+ Release.AspNetCompiler.Debug = `false`
+ VWDPort = 63496
+ EndProjectSection
+ EndProject
+ Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|.NET = Debug|.NET
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.ActiveCfg = Debug|.NET
+ {464FD0B9-E335-4677-BE1E-6B2F982F4D86}.Debug|.NET.Build.0 = Debug|.NET
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ EndGlobal
+ """;
+
+ SolutionFile solution = ParseSolutionHelper(solutionFileContents.Replace('`', '"'), convertToSlnx);
+
+ solution.ProjectsInOrder.ShouldHaveSingleItem();
+
+ solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject);
+ solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\");
+ solution.ProjectsInOrder[0].RelativePath.ShouldBe(ConvertToUnixPathIfNeeded(@"C:\WebSites\WebApplication3\"));
+ solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2);
+ solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull();
+ solution.ProjectsInOrder[0].GetUniqueProjectName().ShouldBe(@"C:\WebSites\WebApplication3\");
+
+ Hashtable aspNetCompilerParameters = solution.ProjectsInOrder[0].AspNetConfigurations;
+ AspNetCompilerParameters debugAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Debug"];
+ AspNetCompilerParameters releaseAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Release"];
+
+ debugAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst");
+ debugAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite\");
+ debugAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst\");
+ debugAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
+ debugAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"false");
+ debugAspNetCompilerParameters.aspNetDebug.ShouldBe(@"true");
+ debugAspNetCompilerParameters.aspNetKeyFile.ShouldBe(@"debugkeyfile.snk");
+ debugAspNetCompilerParameters.aspNetKeyContainer.ShouldBe(@"12345.container");
+ debugAspNetCompilerParameters.aspNetDelaySign.ShouldBe(@"true");
+ debugAspNetCompilerParameters.aspNetAPTCA.ShouldBe(@"false");
+ debugAspNetCompilerParameters.aspNetFixedNames.ShouldBe(@"debugfixednames");
+
+ releaseAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst_release");
+ releaseAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite_release\");
+ releaseAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst_release\");
+ releaseAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
+ releaseAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"true");
+ releaseAspNetCompilerParameters.aspNetDebug.ShouldBe(@"false");
+ releaseAspNetCompilerParameters.aspNetKeyFile.ShouldBe("");
+ releaseAspNetCompilerParameters.aspNetKeyContainer.ShouldBe("");
+ releaseAspNetCompilerParameters.aspNetDelaySign.ShouldBe("");
+ releaseAspNetCompilerParameters.aspNetAPTCA.ShouldBe("");
+ releaseAspNetCompilerParameters.aspNetFixedNames.ShouldBe("");
+
+ List aspNetProjectReferences = solution.ProjectsInOrder[0].ProjectReferences;
+ aspNetProjectReferences.Count.ShouldBe(2);
+ aspNetProjectReferences[0].ShouldBe("{FD705688-88D1-4C22-9BFF-86235D89C2FC}");
+ aspNetProjectReferences[1].ShouldBe("{F0726D09-042B-4A7A-8A01-6BED2422BD5D}");
+ }
+
+ ///
+ /// Helper method to create a SolutionFile object, and call it to parse the SLN file
+ /// represented by the string contents passed in. Optionally can convert the SLN to SLNX and then parse the solution.
+ ///
+ internal static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false)
+ {
+ solutionFileContents = solutionFileContents.Replace('\'', '"');
+
+ using (TestEnvironment testEnvironment = TestEnvironment.Create())
+ {
+ TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);
+
+ string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;
+
+ SolutionFile solutionFile = new SolutionFile { FullPath = solutionPath };
+ solutionFile.ParseUsingNewParser();
+ return solutionFile;
+ }
+ }
+
+ private static string ConvertToSlnx(string slnPath)
+ {
+ string slnxPath = slnPath + "x";
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
+ SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
+ SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
+ return slnxPath;
+ }
+
+ private static string ConvertToUnixPathIfNeeded(string path)
+ {
+ // In the new parser, ProjectModel.FilePath is converted to Unix-style.
+ return !NativeMethodsShared.IsWindows ? path.Replace('\\', '/') : path;
+ }
+ }
+}
diff --git a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs
index 400c3f6af52..e173c47c640 100644
--- a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs
+++ b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
@@ -13,6 +14,9 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
using Microsoft.Build.UnitTests;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Microsoft.VisualStudio.SolutionPersistence;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
@@ -215,8 +219,10 @@ public void InvalidSolutionFilters(string slnfValue, string exceptionReason)
///
/// Test that a solution filter file is parsed correctly, and it can accurately respond as to whether a project should be filtered out.
///
- [Fact]
- public void ParseSolutionFilter()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ParseSolutionFilter(bool convertToSlnx)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
@@ -229,35 +235,35 @@ public void ParseSolutionFilter()
// The important part of this .sln is that it has references to each of the four projects we just created.
TransientTestFile sln = testEnvironment.CreateFile(folder, "Microsoft.Build.Dev.sln",
@"
- Microsoft Visual Studio Solution File, Format Version 12.00
- # Visual Studio 15
- VisualStudioVersion = 15.0.27004.2009
- MinimumVisualStudioVersion = 10.0.40219.1
- Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build"", """ + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)) + @""", ""{69BE05E2-CBDA-4D27-9733-44E12B0F5627}""
- EndProject
- Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""MSBuild"", """ + Path.Combine("src", Path.GetFileName(msbuild.Path)) + @""", ""{6F92CA55-1D15-4F34-B1FE-56C0B7EB455E}""
- EndProject
- Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.CommandLine.UnitTests"", """ + Path.Combine("src", Path.GetFileName(commandLineUnitTests.Path)) + @""", ""{0ADDBC02-0076-4159-B351-2BF33FAA46B2}""
- EndProject
- Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.Tasks.UnitTests"", """ + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)) + @""", ""{CF999BDE-02B3-431B-95E6-E88D621D9CBF}""
- EndProject
- Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- EndGlobalSection
- EndGlobal
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27004.2009
+MinimumVisualStudioVersion = 10.0.40219.1
+Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build"", """ + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)) + @""", ""{69BE05E2-CBDA-4D27-9733-44E12B0F5627}""
+EndProject
+Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""MSBuild"", """ + Path.Combine("src", Path.GetFileName(msbuild.Path)) + @""", ""{6F92CA55-1D15-4F34-B1FE-56C0B7EB455E}""
+EndProject
+Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.CommandLine.UnitTests"", """ + Path.Combine("src", Path.GetFileName(commandLineUnitTests.Path)) + @""", ""{0ADDBC02-0076-4159-B351-2BF33FAA46B2}""
+EndProject
+Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""Microsoft.Build.Tasks.UnitTests"", """ + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)) + @""", ""{CF999BDE-02B3-431B-95E6-E88D621D9CBF}""
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+EndGlobalSection
+GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+EndGlobalSection
+GlobalSection(ExtensibilityGlobals) = postSolution
+EndGlobalSection
+EndGlobal
");
TransientTestFile slnf = testEnvironment.CreateFile(folder, "Dev.slnf",
@"
{
""solution"": {
- ""path"": """ + sln.Path.Replace("\\", "\\\\") + @""",
+ ""path"": """ + (convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path).Replace("\\", "\\\\") + @""",
""projects"": [
""" + Path.Combine("src", Path.GetFileName(microsoftBuild.Path)!).Replace("\\", "\\\\") + @""",
""" + Path.Combine("src", Path.GetFileName(tasksUnitTests.Path)!).Replace("\\", "\\\\") + @"""
@@ -276,6 +282,15 @@ public void ParseSolutionFilter()
}
}
+ private static string ConvertToSlnx(string slnPath)
+ {
+ string slnxPath = slnPath + "x";
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
+ SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
+ SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
+ return slnxPath;
+ }
+
private ILoggingService CreateMockLoggingService()
{
ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 0);
diff --git a/src/Build/Construction/Solution/ProjectInSolution.cs b/src/Build/Construction/Solution/ProjectInSolution.cs
index a73df401565..1343cf51914 100644
--- a/src/Build/Construction/Solution/ProjectInSolution.cs
+++ b/src/Build/Construction/Solution/ProjectInSolution.cs
@@ -406,13 +406,18 @@ internal string GetUniqueProjectName()
if (ParentProjectGuid != null)
{
- if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out ProjectInSolution proj))
+ ProjectInSolution proj = null;
+ ProjectInSolution solutionFolder = null;
+
+ // For the new parser, solution folders are not saved in ProjectsByGuid but in the SolutionFoldersByGuid.
+ if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
+ !ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
{
- ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors",
+ ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
}
- uniqueName = proj.GetUniqueProjectName() + "\\";
+ uniqueName = (proj != null ? proj.GetUniqueProjectName() : solutionFolder.GetUniqueProjectName()) + "\\";
}
// 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()
// If this project has a parent SLN folder, first get the full project name for the SLN folder,
// and tack on trailing backslash.
string projectName = String.Empty;
+ ProjectInSolution proj = null;
+ ProjectInSolution solutionFolder = null;
if (ParentProjectGuid != null)
{
- if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out ProjectInSolution parent))
+ if (!ParentSolution.ProjectsByGuid.TryGetValue(ParentProjectGuid, out proj) &&
+ !ParentSolution.SolutionFoldersByGuid.TryGetValue(ParentProjectGuid, out solutionFolder))
{
- ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(parent != null, "SubCategoryForSolutionParsingErrors",
+ ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null || solutionFolder != null, "SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(ParentSolution.FullPath), "SolutionParseNestedProjectErrorWithNameAndGuid", ProjectName, ProjectGuid, ParentProjectGuid);
}
- projectName = parent.GetOriginalProjectName() + "\\";
+ projectName = (proj != null ? proj.GetOriginalProjectName() : solutionFolder.GetOriginalProjectName()) + "\\";
}
// Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.
diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs
index 4676638ed9f..983cd691d0d 100644
--- a/src/Build/Construction/Solution/SolutionFile.cs
+++ b/src/Build/Construction/Solution/SolutionFile.cs
@@ -6,14 +6,20 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
+using System.Threading;
using System.Xml;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
+using Microsoft.VisualStudio.SolutionPersistence;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo;
using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities;
using ExceptionUtilities = Microsoft.Build.Shared.ExceptionHandling;
@@ -92,13 +98,16 @@ public sealed class SolutionFile
// conversion, or in preparation for actually building the solution?
// The list of projects in this SLN, keyed by the project GUID.
- private Dictionary _projects;
+ private Dictionary _projectsByGuid;
+
+ // The list of solution folders in this SLN, keyed by the folder's GUID.
+ private Dictionary _solutionFoldersByGuid;
// The list of projects in the SLN, in order of their appearance in the SLN.
private List _projectsInOrder;
// The list of solution configurations in the solution
- private List _solutionConfigurations;
+ private Dictionary _solutionConfigurationsByFullName;
// cached default configuration name for GetDefaultConfigurationName
private string _defaultConfigurationName;
@@ -147,13 +156,15 @@ internal SolutionFile()
internal List SolutionParserErrorCodes { get; } = new List();
///
- /// Returns the actual major version of the parsed solution file
+ /// Returns the actual major version of the parsed solution file.
///
+ /// This will return 0 for the new parser because Version is not available.
internal int Version { get; private set; }
///
- /// Returns Visual Studio major version
+ /// Returns Visual Studio major version.
///
+ /// This might not be available for the new parser and returns -1.
internal int VisualStudioVersion
{
get
@@ -180,16 +191,24 @@ internal int VisualStudioVersion
///
internal bool ContainsWebDeploymentProjects { get; private set; }
+ internal bool UseNewParser => ShouldUseNewParser(_solutionFile);
+
+ internal static bool ShouldUseNewParser(string solutionFile) => FileUtilities.IsSolutionXFilename(solutionFile);
+
///
/// All projects in this solution, in the order they appeared in the solution file
///
+ /// For the new parser, solution folders are no longer included.
public IReadOnlyList ProjectsInOrder => _projectsInOrder.AsReadOnly();
///
/// The collection of projects in this solution, accessible by their guids as a
/// string in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form
///
- public IReadOnlyDictionary ProjectsByGuid => new ReadOnlyDictionary(_projects);
+ /// For the new parser, solution folders are no longer included.
+ public IReadOnlyDictionary ProjectsByGuid => new ReadOnlyDictionary(_projectsByGuid);
+
+ internal IReadOnlyDictionary SolutionFoldersByGuid => new ReadOnlyDictionary(_solutionFoldersByGuid);
///
/// This is the read/write accessor for the solution file which we will parse. This
@@ -239,7 +258,7 @@ internal string SolutionFileDirectory
///
/// The list of all full solution configurations (configuration + platform) in this solution
///
- public IReadOnlyList SolutionConfigurations => _solutionConfigurations.AsReadOnly();
+ public IReadOnlyList SolutionConfigurations => _solutionConfigurationsByFullName.Values.ToList().AsReadOnly();
#endregion
@@ -257,11 +276,227 @@ internal bool ProjectShouldBuild(string projectFile)
///
public static SolutionFile Parse(string solutionFile)
{
- var parser = new SolutionFile { FullPath = solutionFile };
- parser.ParseSolutionFile();
- return parser;
+ var solution = new SolutionFile { FullPath = solutionFile };
+
+ if (solution.UseNewParser)
+ {
+ solution.ParseUsingNewParser();
+ }
+ else
+ {
+ // Parse the solution file using the old parser
+ solution.ParseSolutionFile();
+ }
+
+ return solution;
+ }
+
+ ///
+ /// Parses .sln, .slnx and .slnf files using Microsoft.VisualStudio.SolutionPersistence.
+ ///
+ internal void ParseUsingNewParser()
+ {
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(FullPath);
+
+ if (serializer != null)
+ {
+ try
+ {
+ SolutionModel solutionModel = serializer.OpenAsync(FullPath, CancellationToken.None).Result;
+ ReadSolutionModel(solutionModel);
+ }
+ catch (Exception ex)
+ {
+ ProjectFileErrorUtilities.ThrowInvalidProjectFile(
+ new BuildEventFileInfo(FullPath),
+ $"InvalidProjectFile",
+ ex.ToString());
+ }
+ }
+ else if (serializer == null)
+ {
+ ProjectFileErrorUtilities.ThrowInvalidProjectFile(
+ new BuildEventFileInfo(FullPath),
+ $"InvalidProjectFile",
+ $"No solution serializer was found for {FullPath}");
+ }
+ }
+
+ ///
+ /// Maps to .
+ /// is a result of parsing solution using the new parser.
+ ///
+ ///
+ private void ReadSolutionModel(SolutionModel solutionModel)
+ {
+ ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ReadSolutionModel() got a null or empty solution file.");
+ ErrorUtilities.VerifyThrowInternalRooted(_solutionFile);
+
+ _projectsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _solutionFoldersByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _projectsInOrder = new List();
+ ContainsWebProjects = false;
+ Version = 0;
+ _currentLineNumber = 0;
+ _solutionConfigurationsByFullName = new Dictionary();
+ _defaultConfigurationName = null;
+ _defaultPlatformName = null;
+
+ _currentVisualStudioVersion = solutionModel.VisualStudioProperties.Version;
+
+ ReadProjects(solutionModel);
+
+ // We need to save the solution folders in order to cache the unique project names and check for duplicates.
+ ReadSolutionFolders(solutionModel);
+
+ if (_solutionFilter != null)
+ {
+ ValidateProjectsInSolutionFilter();
+ }
+
+ CacheUniqueProjectNamesAndCheckForDuplicates();
+ }
+
+ private void ReadProjects(SolutionModel solutionModel)
+ {
+ foreach (SolutionProjectModel projectModel in solutionModel.SolutionProjects)
+ {
+ var proj = new ProjectInSolution(this)
+ {
+ ProjectName = GetProjectName(projectModel),
+ RelativePath = projectModel.FilePath,
+ ProjectGuid = ToProjectGuidFormat(projectModel.Id),
+ };
+
+ // If the project name is empty the new parser throws an error.
+
+ // Validate project relative path
+ ValidateProjectRelativePath(proj);
+
+ SetProjectType(proj, ToProjectGuidFormat(projectModel.TypeId));
+
+ SetProjectDependencies(proj, projectModel);
+
+ SetWebsiteProperties(proj, projectModel);
+
+ // Note: This is corresponds to GlobalSection(NestedProjects) section in sln files.
+ if (projectModel.Parent != null)
+ {
+ proj.ParentProjectGuid = ToProjectGuidFormat(projectModel.Parent.Id);
+ }
+
+ SetProjectConfigurations(proj, projectModel, solutionModel.BuildTypes, solutionModel.Platforms);
+
+ // Add the project to the collection
+ AddProjectToSolution(proj);
+
+ // If the project is an etp project then parse the etp project file
+ // to get the projects contained in it.
+ if (IsEtpProjectFile(proj.RelativePath))
+ {
+ ParseEtpProject(proj);
+ }
+ }
+ }
+
+ private string GetProjectName(SolutionProjectModel projectModel)
+ => !string.IsNullOrEmpty(projectModel.DisplayName) ? projectModel.DisplayName : projectModel.ActualDisplayName;
+
+ ///
+ /// Returns a string from Guid in the format that the old MSBuild solution parser returned.
+ ///
+ private static string ToProjectGuidFormat(Guid id) => id.ToString("B").ToUpper();
+
+ private void SetProjectDependencies(ProjectInSolution proj, SolutionProjectModel projectModel)
+ {
+ if (projectModel.Dependencies == null)
+ {
+ return;
+ }
+
+ foreach (var dependency in projectModel.Dependencies)
+ {
+ proj.AddDependency(ToProjectGuidFormat(dependency.Id));
+ }
+ }
+
+ private void SetWebsiteProperties(ProjectInSolution proj, SolutionProjectModel projectModel)
+ {
+ SolutionPropertyBag websiteProperties = projectModel?.Properties.FirstOrDefault(p => p.Id == "WebsiteProperties");
+
+ if (websiteProperties is null)
+ {
+ return;
+ }
+
+ foreach (var property in websiteProperties)
+ {
+ ParseAspNetCompilerProperty(proj, property.Key, property.Value);
+ }
+ }
+
+ private void SetProjectConfigurations(
+ ProjectInSolution proj,
+ SolutionProjectModel projectModel,
+ IReadOnlyList buildTypes,
+ IReadOnlyList platforms)
+ {
+ foreach (string solutionBuildType in buildTypes)
+ {
+ foreach (string solutionPlatform in platforms)
+ {
+ // isBuild represents Build.0. The "Build.0" entry tells us whether to build the project configuration in the given solution configuration
+ // _ argument represents Deploy.0 which we do not use in the old parser
+ (string projectBuildType, string projectPlatform, bool isBuild, bool _) = projectModel.GetProjectConfiguration(solutionBuildType, solutionPlatform);
+
+ if (projectBuildType == null || projectPlatform == null)
+ {
+ continue;
+ }
+
+ var projectConfiguration = new ProjectConfigurationInSolution(
+ projectBuildType,
+ projectPlatform,
+ isBuild);
+
+ string configurationName = SolutionConfigurationInSolution.ComputeFullName(solutionBuildType, solutionPlatform);
+
+ proj.SetProjectConfiguration(configurationName, projectConfiguration);
+
+ // There are no solution configurations in the new parser. Instead we collect them from each project's configurations.
+ AddSolutionConfiguration(solutionBuildType, solutionPlatform);
+ }
+ }
}
+ private void ReadSolutionFolders(SolutionModel solutionModel)
+ {
+ foreach (SolutionFolderModel solutionFolderModel in solutionModel.SolutionFolders)
+ {
+ var proj = new ProjectInSolution(this)
+ {
+ ProjectName = GetSolutionFolderName(solutionFolderModel),
+ ProjectGuid = ToProjectGuidFormat(solutionFolderModel.Id),
+ ProjectType = SolutionProjectType.SolutionFolder,
+ };
+
+ // If the project name is empty the new parser throws an error.
+
+ if (solutionFolderModel.Parent != null)
+ {
+ proj.ParentProjectGuid = ToProjectGuidFormat(solutionFolderModel.Parent.Id);
+ }
+
+ if (!string.IsNullOrEmpty(proj.ProjectGuid))
+ {
+ _solutionFoldersByGuid[proj.ProjectGuid] = proj;
+ }
+ }
+ }
+
+ private string GetSolutionFolderName(SolutionFolderModel solutionFolderModel)
+ => !string.IsNullOrEmpty(solutionFolderModel.Name) ? solutionFolderModel.Name : solutionFolderModel.ActualDisplayName;
+
///
/// Returns "true" if it's a project that's expected to be buildable, or false if it's
/// not (e.g. a solution folder)
@@ -432,7 +667,12 @@ internal static string ParseSolutionFromSolutionFilter(string solutionFilterFile
///
internal void AddSolutionConfiguration(string configurationName, string platformName)
{
- _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationName, platformName));
+ var solutionConfiguration = new SolutionConfigurationInSolution(configurationName, platformName);
+
+ if (!_solutionConfigurationsByFullName.ContainsKey(solutionConfiguration.FullName))
+ {
+ _solutionConfigurationsByFullName[solutionConfiguration.FullName] = solutionConfiguration;
+ }
}
///
@@ -497,12 +737,13 @@ internal void ParseSolutionFile()
///
internal void ParseSolution()
{
- _projects = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _projectsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _solutionFoldersByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase);
_projectsInOrder = new List();
ContainsWebProjects = false;
Version = 0;
_currentLineNumber = 0;
- _solutionConfigurations = new List();
+ _solutionConfigurationsByFullName = new Dictionary();
_defaultConfigurationName = null;
_defaultPlatformName = null;
@@ -543,24 +784,7 @@ internal void ParseSolution()
if (_solutionFilter != null)
{
- HashSet projectPaths = new HashSet(_projectsInOrder.Count, _pathComparer);
- foreach (ProjectInSolution project in _projectsInOrder)
- {
- projectPaths.Add(FileUtilities.FixFilePath(project.RelativePath));
- }
- foreach (string project in _solutionFilter)
- {
- if (!projectPaths.Contains(project))
- {
- ProjectFileErrorUtilities.ThrowInvalidProjectFile(
- "SubCategoryForSolutionParsingErrors",
- new BuildEventFileInfo(FileUtilities.GetFullPath(project, Path.GetDirectoryName(_solutionFile))),
- "SolutionFilterFilterContainsProjectNotInSolution",
- _solutionFilterFile,
- project,
- _solutionFile);
- }
- }
+ ValidateProjectsInSolutionFilter();
}
if (rawProjectConfigurationsEntries != null)
@@ -568,13 +792,18 @@ internal void ParseSolution()
ProcessProjectConfigurationSection(rawProjectConfigurationsEntries);
}
+ CacheUniqueProjectNamesAndCheckForDuplicates();
+ }
+
+ private void CacheUniqueProjectNamesAndCheckForDuplicates()
+ {
// Cache the unique name of each project, and check that we don't have any duplicates.
var projectsByUniqueName = new Dictionary(StringComparer.OrdinalIgnoreCase);
var projectsByOriginalName = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (ProjectInSolution proj in _projectsInOrder)
{
- // Find the unique name for the project. This method also caches the unique name,
+ // Find the unique name for the project. This method also caches the unique name,
// so it doesn't have to be recomputed later.
string uniqueName = proj.GetUniqueProjectName();
@@ -645,7 +874,31 @@ internal void ParseSolution()
"SolutionParseDuplicateProject",
uniqueNameExists ? uniqueName : proj.ProjectName);
}
- } // ParseSolutionFile()
+ }
+
+ private void ValidateProjectsInSolutionFilter()
+ {
+ HashSet projectPaths = new HashSet(_projectsInOrder.Count, _pathComparer);
+
+ foreach (ProjectInSolution project in _projectsInOrder)
+ {
+ projectPaths.Add(FileUtilities.FixFilePath(project.RelativePath));
+ }
+
+ foreach (string project in _solutionFilter)
+ {
+ if (!projectPaths.Contains(project))
+ {
+ ProjectFileErrorUtilities.ThrowInvalidProjectFile(
+ "SubCategoryForSolutionParsingErrors",
+ new BuildEventFileInfo(FileUtilities.GetFullPath(project, Path.GetDirectoryName(_solutionFile))),
+ "SolutionFilterFilterContainsProjectNotInSolution",
+ _solutionFilterFile,
+ project,
+ _solutionFile);
+ }
+ }
+ }
///
/// This method searches the first two lines of the solution file opened by the specified
@@ -1000,7 +1253,7 @@ private void AddProjectToSolution(ProjectInSolution proj)
{
if (!String.IsNullOrEmpty(proj.ProjectGuid))
{
- _projects[proj.ProjectGuid] = proj;
+ _projectsByGuid[proj.ProjectGuid] = proj;
}
_projectsInOrder.Add(proj);
}
@@ -1264,6 +1517,11 @@ internal void ParseFirstProjectLine(
// Validate project relative path
ValidateProjectRelativePath(proj);
+ SetProjectType(proj, projectTypeGuid);
+ }
+
+ private void SetProjectType(ProjectInSolution proj, string projectTypeGuid)
+ {
// Figure out what type of project this is.
if ((String.Equals(projectTypeGuid, vbProjectGuid, StringComparison.OrdinalIgnoreCase)) ||
(String.Equals(projectTypeGuid, csProjectGuid, StringComparison.OrdinalIgnoreCase)) ||
@@ -1347,7 +1605,7 @@ internal void ParseNestedProjects()
string projectGuid = match.Groups["PROPERTYNAME"].Value.Trim();
string parentProjectGuid = match.Groups["PROPERTYVALUE"].Value.Trim();
- if (!_projects.TryGetValue(projectGuid, out ProjectInSolution proj))
+ if (!_projectsByGuid.TryGetValue(projectGuid, out ProjectInSolution proj))
{
ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectUndefinedError", projectGuid, parentProjectGuid);
@@ -1407,7 +1665,7 @@ internal void ParseSolutionConfigurations()
var (configuration, platform) = ParseConfigurationName(fullConfigurationName, FullPath, _currentLineNumber, str);
- _solutionConfigurations.Add(new SolutionConfigurationInSolution(configuration, platform));
+ AddSolutionConfiguration(configuration, platform);
} while (true);
}
@@ -1495,7 +1753,7 @@ internal void ProcessProjectConfigurationSection(Dictionary rawP
// Solution folders don't have configurations
if (project.ProjectType != SolutionProjectType.SolutionFolder)
{
- foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionConfigurations)
+ foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionConfigurationsByFullName.Values)
{
// The "ActiveCfg" entry defines the active project configuration in the given solution configuration
// This entry must be present for every possible solution configuration/project combination.
@@ -1610,7 +1868,7 @@ public string GetDefaultPlatformName()
///
internal string GetProjectUniqueNameByGuid(string projectGuid)
{
- if (_projects.TryGetValue(projectGuid, out ProjectInSolution proj))
+ if (_projectsByGuid.TryGetValue(projectGuid, out ProjectInSolution proj))
{
return proj.GetUniqueProjectName();
}
@@ -1626,7 +1884,7 @@ internal string GetProjectUniqueNameByGuid(string projectGuid)
///
internal string GetProjectRelativePathByGuid(string projectGuid)
{
- if (_projects.TryGetValue(projectGuid, out ProjectInSolution proj))
+ if (_projectsByGuid.TryGetValue(projectGuid, out ProjectInSolution proj))
{
return proj.RelativePath;
}
diff --git a/src/Build/Construction/Solution/SolutionProjectGenerator.cs b/src/Build/Construction/Solution/SolutionProjectGenerator.cs
index 1cbb076827b..760fcb390f3 100644
--- a/src/Build/Construction/Solution/SolutionProjectGenerator.cs
+++ b/src/Build/Construction/Solution/SolutionProjectGenerator.cs
@@ -691,12 +691,16 @@ internal static bool WouldProjectBuild(SolutionFile solutionFile, string selecte
///
private ProjectInstance[] Generate()
{
- // Validate against our minimum for upgradable projects
- ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
- _solutionFile.Version >= SolutionFile.slnFileMinVersion,
- "SubCategoryForSolutionParsingErrors",
- new BuildEventFileInfo(_solutionFile.FullPath),
- "SolutionParseUpgradeNeeded");
+ // The Version is not available in the new parser.
+ if (!_solutionFile.UseNewParser)
+ {
+ // Validate against our minimum for upgradable projects
+ ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
+ _solutionFile.Version >= SolutionFile.slnFileMinVersion,
+ "SubCategoryForSolutionParsingErrors",
+ new BuildEventFileInfo(_solutionFile.FullPath),
+ "SolutionParseUpgradeNeeded");
+ }
// This is needed in order to make decisions about tools versions such as whether to put a
// ToolsVersion parameter on task tags and what MSBuildToolsPath to use when
diff --git a/src/Build/Instance/ProjectInstance.cs b/src/Build/Instance/ProjectInstance.cs
index 1419a53c6db..1eacb69a5d0 100644
--- a/src/Build/Instance/ProjectInstance.cs
+++ b/src/Build/Instance/ProjectInstance.cs
@@ -2574,45 +2574,82 @@ internal static ProjectInstance[] LoadSolutionForBuild(
// we should be generating a 4.0+ or a 3.5-style wrapper project based on the version of the solution.
else
{
- string solutionFile = projectFile;
- if (FileUtilities.IsSolutionFilterFilename(projectFile))
- {
- solutionFile = SolutionFile.ParseSolutionFromSolutionFilter(projectFile, out _);
- }
- SolutionFile.GetSolutionFileAndVisualStudioMajorVersions(solutionFile, out int solutionVersion, out int visualStudioVersion);
+ projectInstances = CalculateToolsVersionAndGenerateSolutionWrapper(
+ projectFile,
+ buildParameters,
+ loggingService,
+ projectBuildEventContext,
+ globalProperties,
+ isExplicitlyLoaded,
+ targetNames,
+ sdkResolverService,
+ submissionId);
+ }
+
+ return projectInstances;
+ }
+
+ private static ProjectInstance[] CalculateToolsVersionAndGenerateSolutionWrapper(
+ string projectFile,
+ BuildParameters buildParameters,
+ ILoggingService loggingService,
+ BuildEventContext projectBuildEventContext,
+ Dictionary globalProperties,
+ bool isExplicitlyLoaded,
+ IReadOnlyCollection targetNames,
+ ISdkResolverService sdkResolverService,
+ int submissionId)
+ {
+ string solutionFileName = projectFile;
+
+ if (FileUtilities.IsSolutionFilterFilename(projectFile))
+ {
+ solutionFileName = SolutionFile.ParseSolutionFromSolutionFilter(projectFile, out _);
+ }
- // If we get to this point, it's because it's a valid version. Map the solution version
- // to the equivalent MSBuild ToolsVersion, and unless it's Dev10 or newer, spawn the old
- // engine to generate the solution wrapper.
- if (solutionVersion <= 9) /* Whidbey or before */
+ if (SolutionFile.ShouldUseNewParser(solutionFileName))
+ {
+ // For the new parser we use Current tools version.
+ return GenerateSolutionWrapper(projectFile, globalProperties, "Current", loggingService, projectBuildEventContext, targetNames, sdkResolverService, submissionId);
+ }
+
+ // For the old parser we try to make a best-effort guess based on the version of the solution.
+ string toolsVersion = null;
+ ProjectInstance[] projectInstances = null;
+
+ SolutionFile.GetSolutionFileAndVisualStudioMajorVersions(solutionFileName, out int solutionVersion, out int visualStudioVersion);
+
+ // If we get to this point, it's because it's a valid version. Map the solution version
+ // to the equivalent MSBuild ToolsVersion, and unless it's Dev10 or newer, spawn the old
+ // engine to generate the solution wrapper.
+ if (solutionVersion <= 9) /* Whidbey or before */
+ {
+ loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "2.0", solutionVersion);
+ projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "2.0", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded, sdkResolverService, submissionId);
+ }
+ else if (solutionVersion == 10) /* Orcas */
+ {
+ loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "3.5", solutionVersion);
+ projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "3.5", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded, sdkResolverService, submissionId);
+ }
+ else
+ {
+ if ((solutionVersion == 11) || (solutionVersion == 12 && visualStudioVersion == 0)) /* Dev 10 and Dev 11 */
{
- loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "2.0", solutionVersion);
- projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "2.0", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded, sdkResolverService, submissionId);
+ toolsVersion = "4.0";
}
- else if (solutionVersion == 10) /* Orcas */
+ else /* Dev 12 and above */
{
- loggingService.LogComment(projectBuildEventContext, MessageImportance.Low, "OldWrapperGeneratedOldSolutionVersion", "3.5", solutionVersion);
- projectInstances = GenerateSolutionWrapperUsingOldOM(projectFile, globalProperties, "3.5", buildParameters.ProjectRootElementCache, buildParameters, loggingService, projectBuildEventContext, isExplicitlyLoaded, sdkResolverService, submissionId);
+ toolsVersion = visualStudioVersion.ToString(CultureInfo.InvariantCulture) + ".0";
}
- else
- {
- if ((solutionVersion == 11) || (solutionVersion == 12 && visualStudioVersion == 0)) /* Dev 10 and Dev 11 */
- {
- toolsVersion = "4.0";
- }
- else /* Dev 12 and above */
- {
- toolsVersion = visualStudioVersion.ToString(CultureInfo.InvariantCulture) + ".0";
- }
- string toolsVersionToUse = Utilities.GenerateToolsVersionToUse(
- explicitToolsVersion: null,
- toolsVersionFromProject: FileUtilities.IsSolutionFilterFilename(projectFile) ? "Current" : toolsVersion,
- getToolset: buildParameters.GetToolset,
- defaultToolsVersion: Constants.defaultSolutionWrapperProjectToolsVersion,
- usingDifferentToolsVersionFromProjectFile: out _);
- projectInstances = GenerateSolutionWrapper(projectFile, globalProperties, toolsVersionToUse, loggingService, projectBuildEventContext, targetNames, sdkResolverService, submissionId);
- }
+ string toolsVersionToUse = Utilities.GenerateToolsVersionToUse(
+ explicitToolsVersion: null,
+ toolsVersionFromProject: FileUtilities.IsSolutionFilterFilename(projectFile) ? "Current" : toolsVersion,
+ getToolset: buildParameters.GetToolset,
+ defaultToolsVersion: Constants.defaultSolutionWrapperProjectToolsVersion,
+ usingDifferentToolsVersionFromProjectFile: out _);
+ projectInstances = GenerateSolutionWrapper(projectFile, globalProperties, toolsVersionToUse, loggingService, projectBuildEventContext, targetNames, sdkResolverService, submissionId);
}
return projectInstances;
diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj
index 24471d364ba..9a39ec6bad7 100644
--- a/src/Build/Microsoft.Build.csproj
+++ b/src/Build/Microsoft.Build.csproj
@@ -30,6 +30,7 @@
+
@@ -38,7 +39,7 @@
-
+
diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs
index ee6eb6219fb..fb588b1615b 100644
--- a/src/MSBuild.UnitTests/XMake_Tests.cs
+++ b/src/MSBuild.UnitTests/XMake_Tests.cs
@@ -1577,8 +1577,10 @@ private void RunPriorityBuildTest(ProcessPriorityClass expectedPrority, params s
///
[Theory]
[InlineData(new[] { "my.proj", "my.sln", "my.slnf" }, "my.sln")]
+ [InlineData(new[] { "my.proj", "my.slnx", "my.slnf" }, "my.slnx")]
[InlineData(new[] { "abc.proj", "bcd.csproj", "slnf.slnf", "other.slnf" }, "abc.proj")]
[InlineData(new[] { "abc.sln", "slnf.slnf", "abc.slnf" }, "abc.sln")]
+ [InlineData(new[] { "abc.slnx", "slnf.slnf", "abc.slnf" }, "abc.slnx")]
[InlineData(new[] { "abc.csproj", "abc.slnf", "not.slnf" }, "abc.csproj")]
[InlineData(new[] { "abc.slnf" }, "abc.slnf")]
public void TestDefaultBuildWithSolutionFilter(string[] projects, string answer)
@@ -1724,11 +1726,21 @@ public void TestProcessProjectSwitch()
projectHelper = new IgnoreProjectExtensionsHelper(projects);
MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.sln", StringCompareShould.IgnoreCase); // "Expected test.sln to be only solution found"
+ projects = new[] { "test.proj", "test.slnx" };
+ extensionsToIgnore = new[] { ".vcproj" };
+ projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.slnx", StringCompareShould.IgnoreCase); // "Expected test.slnx to be only solution found"
+
projects = new[] { "test.proj", "test.sln", "test.proj~", "test.sln~" };
extensionsToIgnore = Array.Empty();
projectHelper = new IgnoreProjectExtensionsHelper(projects);
MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.sln", StringCompareShould.IgnoreCase); // "Expected test.sln to be only solution found"
+ projects = new[] { "test.proj", "test.slnx", "test.proj~", "test.sln~" };
+ extensionsToIgnore = Array.Empty();
+ projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.slnx", StringCompareShould.IgnoreCase); // "Expected test.slnx to be only solution found"
+
projects = new[] { "test.proj" };
extensionsToIgnore = Array.Empty();
projectHelper = new IgnoreProjectExtensionsHelper(projects);
@@ -1744,6 +1756,12 @@ public void TestProcessProjectSwitch()
projectHelper = new IgnoreProjectExtensionsHelper(projects);
MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.sln", StringCompareShould.IgnoreCase); // "Expected test.sln to be only solution found"
+ projects = new[] { "test.slnx" };
+ extensionsToIgnore = Array.Empty();
+ projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles).ShouldBe("test.slnx", StringCompareShould.IgnoreCase); // "Expected test.slnx to be only solution found"
+
+
projects = new[] { "test.sln", "test.sln~" };
extensionsToIgnore = Array.Empty();
projectHelper = new IgnoreProjectExtensionsHelper(projects);
@@ -1796,6 +1814,21 @@ public void TestProcessProjectSwitchSlnProjDifferentNames()
});
}
///
+ /// Test the case where there is a .slnx and a project in the same directory but they have different names
+ ///
+ [Fact]
+ public void TestProcessProjectSwitchSlnxProjDifferentNames()
+ {
+ string[] projects = ["test.proj", "Different.slnx"];
+ string[] extensionsToIgnore = null;
+
+ Should.Throw(() =>
+ {
+ IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles);
+ });
+ }
+ ///
/// Test the case where we have two proj files in the same directory
///
[Fact]
@@ -1838,6 +1871,33 @@ public void TestProcessProjectSwitchTwoSolutions()
});
}
///
+ /// Test when there are two solutions in the same directory - .sln and .slnx
+ ///
+ [Fact]
+ public void TestProcessProjectSwitchSlnAndSlnx()
+ {
+ string[] projects = ["test.slnx", "Different.sln"];
+ string[] extensionsToIgnore = null;
+
+ Should.Throw(() =>
+ {
+ IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles);
+ });
+ }
+ [Fact]
+ public void TestProcessProjectSwitchTwoSlnx()
+ {
+ string[] projects = ["test.slnx", "Different.slnx"];
+ string[] extensionsToIgnore = null;
+
+ Should.Throw(() =>
+ {
+ IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects);
+ MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles);
+ });
+ }
+ ///
/// Check the case where there are more than two projects in the directory and one is a proj file
///
[Fact]
@@ -1897,7 +1957,7 @@ internal string[] GetFiles(string path, string searchPattern)
List fileNamesToReturn = new List();
foreach (string file in _directoryFileNameList)
{
- if (string.Equals(searchPattern, "*.sln", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(searchPattern, "*.sln?", StringComparison.OrdinalIgnoreCase))
{
if (FileUtilities.IsSolutionFilename(file))
{
diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs
index d850697a06f..6ced0b3e006 100644
--- a/src/MSBuild/XMake.cs
+++ b/src/MSBuild/XMake.cs
@@ -3552,8 +3552,8 @@ internal static string ProcessProjectSwitch(
}
}
- // Get all files in the current directory that have a sln extension
- string[] potentialSolutionFiles = getFiles(projectDirectory ?? ".", "*.sln");
+ // Get all files in the current directory that have a sln-like extension
+ string[] potentialSolutionFiles = getFiles(projectDirectory ?? ".", "*.sln?");
List actualSolutionFiles = new List();
List solutionFilterFiles = new List();
if (potentialSolutionFiles != null)
diff --git a/src/Shared/FileUtilities.cs b/src/Shared/FileUtilities.cs
index d2d6108add8..76dd5ee1f2d 100644
--- a/src/Shared/FileUtilities.cs
+++ b/src/Shared/FileUtilities.cs
@@ -1065,7 +1065,9 @@ internal static bool FileOrDirectoryExistsNoThrow(string fullPath, IFileSystem f
///
internal static bool IsSolutionFilename(string filename)
{
- return HasExtension(filename, ".sln") || HasExtension(filename, ".slnf");
+ return HasExtension(filename, ".sln") ||
+ HasExtension(filename, ".slnf") ||
+ HasExtension(filename, ".slnx");
}
internal static bool IsSolutionFilterFilename(string filename)
@@ -1073,6 +1075,11 @@ internal static bool IsSolutionFilterFilename(string filename)
return HasExtension(filename, ".slnf");
}
+ internal static bool IsSolutionXFilename(string filename)
+ {
+ return HasExtension(filename, ".slnx");
+ }
+
///
/// Returns true if the specified filename is a VC++ project file, otherwise returns false
///