From adb4394981ec9c44f59dfa9fbc903670eddf6be6 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada <114938397+surayya-MS@users.noreply.github.com> Date: Thu, 17 Oct 2024 08:12:01 +0200 Subject: [PATCH] .SLNX format support (#10794) * .slnx support - use the new parser to parse .slnx files --- eng/SourceBuildPrebuiltBaseline.xml | 1 + eng/Versions.props | 4 + eng/dependabot/Packages.props | 2 + .../Construction/SolutionFile_Tests.cs | 335 ++++++++++++++---- .../SolutionFile_NewParser_Tests.cs | 163 +++++++++ .../Construction/SolutionFilter_Tests.cs | 67 ++-- .../Solution/ProjectInSolution.cs | 20 +- .../Construction/Solution/SolutionFile.cs | 334 +++++++++++++++-- .../Solution/SolutionProjectGenerator.cs | 16 +- src/Build/Instance/ProjectInstance.cs | 103 ++++-- src/Build/Microsoft.Build.csproj | 3 +- src/MSBuild.UnitTests/XMake_Tests.cs | 62 +++- src/MSBuild/XMake.cs | 4 +- src/Shared/FileUtilities.cs | 9 +- 14 files changed, 932 insertions(+), 191 deletions(-) create mode 100644 src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs 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 ///