From 3d1897d01f866859663b27166f378612677da74f Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 10 Oct 2024 14:19:41 +0200 Subject: [PATCH 01/23] include SolutionPersistence package --- NuGet.config | 1 + eng/Versions.props | 4 ++++ src/Build/Microsoft.Build.csproj | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/NuGet.config b/NuGet.config index 659ab421680..c82f938bd58 100644 --- a/NuGet.config +++ b/NuGet.config @@ -19,6 +19,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index 7df9b218345..91741b441c8 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -76,4 +76,8 @@ $(VersionPrefix).$(FileVersion.Split('.')[3]) + + + 0.5.26-beta + diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 24471d364ba..127f2a85480 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -30,6 +30,7 @@ + @@ -39,6 +40,10 @@ + + + + From 86f4847d83c2c486d1139845455e514d05299010 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 10 Oct 2024 15:01:42 +0200 Subject: [PATCH 02/23] add slnx in XMake.ProcessProjectSwitch --- src/MSBuild.UnitTests/XMake_Tests.cs | 59 +++++++++++++++++++++++++++- src/MSBuild/XMake.cs | 4 +- src/Shared/FileUtilities.cs | 9 ++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index ee6eb6219fb..c3002f6d502 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,20 @@ 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() + { + Should.Throw(() => + { + string[] projects = { "test.proj", "Different.slnx" }; + string[] extensionsToIgnore = null; + 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 +1870,31 @@ public void TestProcessProjectSwitchTwoSolutions() }); } /// + /// Test when there are two solutions in the same directory - .sln and .slnx + /// + [Fact] + public void TestProcessProjectSwitchSlnAndSlnx() + { + Should.Throw(() => + { + string[] projects = { "test.slnx", "Different.sln" }; + string[] extensionsToIgnore = null; + IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); + MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles); + }); + } + [Fact] + public void TestProcessProjectSwitchTwoSlnx() + { + Should.Throw(() => + { + string[] projects = { "test.slnx", "Different.slnx" }; + string[] extensionsToIgnore = null; + 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 +1954,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..469bd3a5376 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 or slnx 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 /// From 8a60719fbd240228022c962e12ddc9d8d87aa55e Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 10 Oct 2024 16:34:11 +0200 Subject: [PATCH 03/23] support for .slnx format --- .../Construction/SolutionFile_Tests.cs | 309 ++++++++++++---- .../SolutionFile_NewParser_Tests.cs | 166 +++++++++ .../Construction/SolutionFilter_Tests.cs | 67 ++-- .../Solution/ProjectInSolution.cs | 20 +- .../Construction/Solution/SolutionFile.cs | 340 ++++++++++++++++-- .../Solution/SolutionProjectGenerator.cs | 16 +- src/Build/Instance/ProjectInstance.cs | 103 ++++-- 7 files changed, 846 insertions(+), 175 deletions(-) create mode 100644 src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 84d703d22e8..4e1377c535d 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -5,11 +5,16 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using Microsoft.Build.Construction; using Microsoft.Build.Exceptions; using Microsoft.Build.Shared; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; +using Microsoft.VisualStudio.SolutionPersistence; using Shouldly; using Xunit; +using System.Linq; #nullable disable @@ -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); + string expectedProjectName = convertToSlnx ? "Project name" : "Project name.myvctype"; + Assert.Equal(expectedProjectName, 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); + 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(@"ConsoleApplication1\ConsoleApplication1.vbproj", 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(@"vbClassLibrary\vbClassLibrary.vbproj", 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(@"ClassLibrary1\ClassLibrary1.csproj", 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() @@ -420,6 +440,78 @@ public void SolutionFolders() 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.First(p => p.RelativePath == @"ClassLibrary1\ClassLibrary1.csproj"); + Assert.Empty(classLibrary1.Dependencies); + Assert.Null(classLibrary1.ParentProjectGuid); + + var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder.First(p => p.RelativePath == @"MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj"); + Assert.Empty(myPhysicalFolderClassLibrary1.Dependencies); + + var classLibrary2 = solution.ProjectsInOrder.First(p => p.RelativePath == @"ClassLibrary2\ClassLibrary2.csproj"); + 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 +648,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 +695,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(@"ClassLibrary1\ClassLibrary1.csproj", 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(@"ClassLibrary2\ClassLibrary2.csproj", 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.Empty(solution.ProjectsInOrder[2].Dependencies); Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid); } @@ -629,11 +725,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 +776,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 +802,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 +833,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 +939,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 +992,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 +1101,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,22 +1215,37 @@ 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"); - + string slnxPath = solutionPath + "x"; try { File.WriteAllText(solutionPath, solutionFileContents); - SolutionFile sp = SolutionFile.Parse(solutionPath); - return sp; + if (convertToSlnx) + { + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solutionModel = serializer.OpenAsync(solutionPath, CancellationToken.None).Result; + SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); + + SolutionFile slnx = SolutionFile.Parse(slnxPath); + return slnx; + } + + SolutionFile sln = SolutionFile.Parse(solutionPath); + return sln; } finally { File.Delete(solutionPath); + + if (convertToSlnx) + { + File.Delete(slnxPath); + } } } } 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..db5dd71db6c --- /dev/null +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -0,0 +1,166 @@ +// 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\"); + // TODO: try set Relative path with a port http://localhost:8080/WebSites/WebApplication3/ + solution.ProjectsInOrder[0].RelativePath.ShouldBe(@"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('\'', '"'); + string solutionPath = FileUtilities.GetTemporaryFileName(".sln"); + string slnxPath = solutionPath + "x"; + try + { + File.WriteAllText(solutionPath, solutionFileContents); + if (convertToSlnx) + { + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + SolutionModel solutionModel = serializer.OpenAsync(solutionPath, CancellationToken.None).Result; + SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); + + SolutionFile slnx = new SolutionFile { FullPath = slnxPath }; + slnx.ParseUsingNewParser(); + return slnx; + } + + SolutionFile sln = SolutionFile.Parse(solutionPath); + return sln; + } + finally + { + File.Delete(solutionPath); + + if (convertToSlnx) + { + File.Delete(slnxPath); + } + } + } + } +} diff --git a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs index 400c3f6af52..43a074de6ad 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); + 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..475ddb9df67 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 /// + /// Solution folders are no longer for the new parser. 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); + /// Solution folders are no longer included for the new parser. + 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,233 @@ 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 (AggregateException aggregateException) + { + ProjectFileErrorUtilities.ThrowInvalidProjectFile( + new BuildEventFileInfo(FullPath), + $"InvalidProjectFile", + string.Join(" ", aggregateException.InnerExceptions.Select(inner => inner.Message))); + } + 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 . + /// + /// + private void ReadSolutionModel(SolutionModel solutionModel) + { + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ParseSolutionFile() got a null 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 "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}". + /// + 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 +673,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 +743,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 +790,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 +798,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 +880,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 +1259,7 @@ private void AddProjectToSolution(ProjectInSolution proj) { if (!String.IsNullOrEmpty(proj.ProjectGuid)) { - _projects[proj.ProjectGuid] = proj; + _projectsByGuid[proj.ProjectGuid] = proj; } _projectsInOrder.Add(proj); } @@ -1264,6 +1523,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 +1611,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 +1671,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 +1759,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 +1874,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 +1890,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; From e734a1bd46afb0ea10623a0d6ada12b79b7dc316 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 10 Oct 2024 20:05:09 +0200 Subject: [PATCH 04/23] small fix --- NuGet.config | 1 - src/Build.UnitTests/Construction/SolutionFilter_Tests.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/NuGet.config b/NuGet.config index c82f938bd58..659ab421680 100644 --- a/NuGet.config +++ b/NuGet.config @@ -19,7 +19,6 @@ - diff --git a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs index 43a074de6ad..b5e169aa70d 100644 --- a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs @@ -285,7 +285,7 @@ public void ParseSolutionFilter(bool convertToSlnx) private static string ConvertToSlnx(string slnPath) { string slnxPath = slnPath + "x"; - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath); + ISolutionSerializer? serializer = SolutionSerializers.GetSerializerByMoniker(slnPath); SolutionModel solutionModel = serializer!.OpenAsync(slnPath, CancellationToken.None).Result; SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); return slnxPath; From 32bc09f06c580cd521fa8df93b879a5c5915e0b6 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Thu, 10 Oct 2024 20:09:58 +0200 Subject: [PATCH 05/23] update package --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 91741b441c8..a87b4aa149e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -78,6 +78,6 @@ - 0.5.26-beta + 1.0.4 From 9fd5e8cdeb366bfd45a967b688f5bb6d22f44c24 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 14:49:30 +0200 Subject: [PATCH 06/23] fix comment --- src/MSBuild/XMake.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 469bd3a5376..6ced0b3e006 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -3552,7 +3552,7 @@ internal static string ProcessProjectSwitch( } } - // Get all files in the current directory that have a sln or slnx extension + // 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(); From 221fb65b09784fa5fca88afda58d730d427ee2ef Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada <114938397+surayya-MS@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:27:33 +0200 Subject: [PATCH 07/23] Apply suggestions from code review commit suggestions Co-authored-by: Rainer Sigwald Co-authored-by: Jan Krivanek --- src/Build.UnitTests/Construction/SolutionFilter_Tests.cs | 4 ++-- src/Build/Construction/Solution/SolutionFile.cs | 4 ++-- src/MSBuild.UnitTests/XMake_Tests.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs index b5e169aa70d..e173c47c640 100644 --- a/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFilter_Tests.cs @@ -285,8 +285,8 @@ public void ParseSolutionFilter(bool convertToSlnx) private static string ConvertToSlnx(string slnPath) { string slnxPath = slnPath + "x"; - ISolutionSerializer? serializer = SolutionSerializers.GetSerializerByMoniker(slnPath); - SolutionModel solutionModel = serializer!.OpenAsync(slnPath, CancellationToken.None).Result; + ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull(); + SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result; SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); return slnxPath; } diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs index 475ddb9df67..3121cb0bbb5 100644 --- a/src/Build/Construction/Solution/SolutionFile.cs +++ b/src/Build/Construction/Solution/SolutionFile.cs @@ -335,7 +335,7 @@ internal void ParseUsingNewParser() /// private void ReadSolutionModel(SolutionModel solutionModel) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ParseSolutionFile() got a null solution file!"); + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ParseSolutionFile() got a null or empty solution file."); ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); _projectsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -409,7 +409,7 @@ private string GetProjectName(SolutionProjectModel projectModel) => !string.IsNullOrEmpty(projectModel.DisplayName) ? projectModel.DisplayName : projectModel.ActualDisplayName; /// - /// Returns a string from Guid in the format "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}". + /// 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(); diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index c3002f6d502..56b3811f249 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -1819,10 +1819,11 @@ public void TestProcessProjectSwitchSlnProjDifferentNames() [Fact] public void TestProcessProjectSwitchSlnxProjDifferentNames() { + string[] projects = { "test.proj", "Different.slnx" }; + string[] extensionsToIgnore = null; + Should.Throw(() => { - string[] projects = { "test.proj", "Different.slnx" }; - string[] extensionsToIgnore = null; IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles); }); From 62ed51578b236b41c764e92ed480bcb93cc57631 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 15:28:15 +0200 Subject: [PATCH 08/23] fix error message --- src/Build/Construction/Solution/SolutionFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs index 475ddb9df67..68fefb4044f 100644 --- a/src/Build/Construction/Solution/SolutionFile.cs +++ b/src/Build/Construction/Solution/SolutionFile.cs @@ -335,7 +335,7 @@ internal void ParseUsingNewParser() /// private void ReadSolutionModel(SolutionModel solutionModel) { - ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ParseSolutionFile() got a null solution file!"); + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(_solutionFile), "ReadSolutionModel() got a null solution file!"); ErrorUtilities.VerifyThrowInternalRooted(_solutionFile); _projectsByGuid = new Dictionary(StringComparer.OrdinalIgnoreCase); From 9c2002fd00e52744de87fa51998ac2d99c3d36fa Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 15:28:25 +0200 Subject: [PATCH 09/23] move package version --- eng/dependabot/Packages.props | 2 ++ src/Build/Microsoft.Build.csproj | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) 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/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 127f2a85480..9a39ec6bad7 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -39,10 +39,6 @@ - - - - From 94130b93e5ae82332b17243fa6cb9a050f9ff17e Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 15:52:27 +0200 Subject: [PATCH 10/23] remove comment --- src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs index db5dd71db6c..1aa7ab49834 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -86,7 +86,6 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject); solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\"); - // TODO: try set Relative path with a port http://localhost:8080/WebSites/WebApplication3/ solution.ProjectsInOrder[0].RelativePath.ShouldBe(@"C:\WebSites\WebApplication3\"); solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2); solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull(); From 09a5a7b6b036605abee3e4226ea25653d5cb0f2a Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 15:57:28 +0200 Subject: [PATCH 11/23] small fix --- src/Build/Construction/Solution/SolutionFile.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs index fb2c9f9d757..dd9497fe1be 100644 --- a/src/Build/Construction/Solution/SolutionFile.cs +++ b/src/Build/Construction/Solution/SolutionFile.cs @@ -305,13 +305,6 @@ internal void ParseUsingNewParser() SolutionModel solutionModel = serializer.OpenAsync(FullPath, CancellationToken.None).Result; ReadSolutionModel(solutionModel); } - catch (AggregateException aggregateException) - { - ProjectFileErrorUtilities.ThrowInvalidProjectFile( - new BuildEventFileInfo(FullPath), - $"InvalidProjectFile", - string.Join(" ", aggregateException.InnerExceptions.Select(inner => inner.Message))); - } catch (Exception ex) { ProjectFileErrorUtilities.ThrowInvalidProjectFile( From 63be4f4700b20ae5c8c82d94537e1b3f75fbd581 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 16:49:03 +0200 Subject: [PATCH 12/23] small change in xmake tests --- src/MSBuild.UnitTests/XMake_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 56b3811f249..6d2eddab546 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -1819,7 +1819,7 @@ public void TestProcessProjectSwitchSlnProjDifferentNames() [Fact] public void TestProcessProjectSwitchSlnxProjDifferentNames() { - string[] projects = { "test.proj", "Different.slnx" }; + string[] projects = ["test.proj", "Different.slnx"]; string[] extensionsToIgnore = null; Should.Throw(() => From 73b84435047ad8b5b66ad5ebc484dc01a1be426d Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 16:51:57 +0200 Subject: [PATCH 13/23] apply same suggestion to the other xmake tests --- src/MSBuild.UnitTests/XMake_Tests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 6d2eddab546..fb588b1615b 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -1876,10 +1876,11 @@ public void TestProcessProjectSwitchTwoSolutions() [Fact] public void TestProcessProjectSwitchSlnAndSlnx() { + string[] projects = ["test.slnx", "Different.sln"]; + string[] extensionsToIgnore = null; + Should.Throw(() => - { - string[] projects = { "test.slnx", "Different.sln" }; - string[] extensionsToIgnore = null; + { IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles); }); @@ -1887,10 +1888,11 @@ public void TestProcessProjectSwitchSlnAndSlnx() [Fact] public void TestProcessProjectSwitchTwoSlnx() { + string[] projects = ["test.slnx", "Different.slnx"]; + string[] extensionsToIgnore = null; + Should.Throw(() => { - string[] projects = { "test.slnx", "Different.slnx" }; - string[] extensionsToIgnore = null; IgnoreProjectExtensionsHelper projectHelper = new IgnoreProjectExtensionsHelper(projects); MSBuildApp.ProcessProjectSwitch(Array.Empty(), extensionsToIgnore, projectHelper.GetFiles); }); From 1bd2a25da9a9526a757562fa3ce260d0a6f38419 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Fri, 11 Oct 2024 17:18:02 +0200 Subject: [PATCH 14/23] refactor ParseSolutionHelper --- .../Construction/SolutionFile_Tests.cs | 39 +++++++---------- .../SolutionFile_NewParser_Tests.cs | 42 ++++++++----------- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 4e1377c535d..64588c540b4 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -1220,33 +1220,24 @@ public void ParseSolutionWithComments() private static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false) { solutionFileContents = solutionFileContents.Replace('\'', '"'); - string solutionPath = FileUtilities.GetTemporaryFileName(".sln"); - string slnxPath = solutionPath + "x"; - try - { - File.WriteAllText(solutionPath, solutionFileContents); - if (convertToSlnx) - { - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); - SolutionModel solutionModel = serializer.OpenAsync(solutionPath, CancellationToken.None).Result; - SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); - - SolutionFile slnx = SolutionFile.Parse(slnxPath); - return slnx; - } - - SolutionFile sln = SolutionFile.Parse(solutionPath); - return sln; - } - finally + + using (TestEnvironment testEnvironment = TestEnvironment.Create()) { - File.Delete(solutionPath); + TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents); - if (convertToSlnx) - { - File.Delete(slnxPath); - } + 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; + } } } diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs index 1aa7ab49834..6db6e939e56 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -132,34 +132,26 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) internal static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false) { solutionFileContents = solutionFileContents.Replace('\'', '"'); - string solutionPath = FileUtilities.GetTemporaryFileName(".sln"); - string slnxPath = solutionPath + "x"; - try - { - File.WriteAllText(solutionPath, solutionFileContents); - if (convertToSlnx) - { - ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); - SolutionModel solutionModel = serializer.OpenAsync(solutionPath, CancellationToken.None).Result; - SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); - - SolutionFile slnx = new SolutionFile { FullPath = slnxPath }; - slnx.ParseUsingNewParser(); - return slnx; - } - - SolutionFile sln = SolutionFile.Parse(solutionPath); - return sln; - } - 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; - if (convertToSlnx) - { - File.Delete(slnxPath); - } + 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; + } } } From cf14602c2758be0487794e4add4554ab8c4df382 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Mon, 14 Oct 2024 17:05:17 -0500 Subject: [PATCH 15/23] Bump to first nuget.org package version --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index a87b4aa149e..7bda52158f2 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -78,6 +78,6 @@ - 1.0.4 + 1.0.9 From 415125b8acbdafcf0373a258db07dae7636bdcd5 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 14:17:24 +0200 Subject: [PATCH 16/23] fix tests for unix --- .../Construction/SolutionFile_Tests.cs | 32 ++++++++++++------- .../SolutionFile_NewParser_Tests.cs | 7 +++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 64588c540b4..547d43c5f10 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -4,17 +4,17 @@ 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 Microsoft.VisualStudio.SolutionPersistence; using Shouldly; using Xunit; -using System.Linq; #nullable disable @@ -96,7 +96,7 @@ public void ParseSolution_VC2(bool convertToSlnx) string expectedProjectName = convertToSlnx ? "Project name" : "Project name.myvctype"; Assert.Equal(expectedProjectName, solution.ProjectsInOrder[0].ProjectName); - Assert.Equal("Relative path\\to\\Project name.myvctype", solution.ProjectsInOrder[0].RelativePath); + Assert.Equal(GetPathForCurrentOs("Relative path\\to\\Project name.myvctype"), solution.ProjectsInOrder[0].RelativePath); if (!convertToSlnx) { // When converting to SLNX, the project GUID is not preserved. @@ -416,7 +416,7 @@ public void SolutionFolders() Assert.Equal(5, solution.ProjectsInOrder.Count); - Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", solution.ProjectsInOrder[0].RelativePath); + Assert.Equal(GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj"), 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); @@ -425,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(GetPathForCurrentOs("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj"), 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); @@ -434,7 +434,7 @@ 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(GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj"), 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); @@ -496,14 +496,17 @@ public void SolutionFoldersSlnx() Assert.Equal(3, solution.ProjectsInOrder.Count); - var classLibrary1 = solution.ProjectsInOrder.First(p => p.RelativePath == @"ClassLibrary1\ClassLibrary1.csproj"); + var classLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj")); + Assert.NotNull(classLibrary1); Assert.Empty(classLibrary1.Dependencies); Assert.Null(classLibrary1.ParentProjectGuid); - var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder.First(p => p.RelativePath == @"MyPhysicalFolder\ClassLibrary1\ClassLibrary1.csproj"); + var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj")); + Assert.NotNull(myPhysicalFolderClassLibrary1); Assert.Empty(myPhysicalFolderClassLibrary1.Dependencies); - var classLibrary2 = solution.ProjectsInOrder.First(p => p.RelativePath == @"ClassLibrary2\ClassLibrary2.csproj"); + var classLibrary2 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj")); + Assert.NotNull(classLibrary2); Assert.Empty(classLibrary2.Dependencies); // When converting to slnx, the guids are not preserved. @@ -705,19 +708,19 @@ public void SolutionDependencies(bool convertToSlnx) var classLibrary2 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary2"); var classLibrary3 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary3"); - Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", classLibrary1.RelativePath); + Assert.Equal(GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj"), classLibrary1.RelativePath); Assert.Single(classLibrary1.Dependencies); Assert.Equal(classLibrary3.ProjectGuid, classLibrary1.Dependencies[0]); Assert.Null(solution.ProjectsInOrder[0].ParentProjectGuid); - Assert.Equal(@"ClassLibrary2\ClassLibrary2.csproj", classLibrary2.RelativePath); + Assert.Equal(GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj"), 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(GetPathForCurrentOs("ClassLibrary3\\ClassLibrary3.csproj"), solution.ProjectsInOrder[2].RelativePath); Assert.Empty(solution.ProjectsInOrder[2].Dependencies); Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid); } @@ -1239,5 +1242,10 @@ private static string ConvertToSlnx(string slnPath) SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); return slnxPath; } + + private static string GetPathForCurrentOs(string path) + { + return NativeMethodsShared.IsWindows ? path : Path.GetFullPath(path).Replace('\\', '/'); + } } } diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs index 6db6e939e56..2726fc38344 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -86,7 +86,7 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject); solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\"); - solution.ProjectsInOrder[0].RelativePath.ShouldBe(@"C:\WebSites\WebApplication3\"); + solution.ProjectsInOrder[0].RelativePath.ShouldBe(GetPathForCurrentOs(@"C:\WebSites\WebApplication3\")); solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2); solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull(); solution.ProjectsInOrder[0].GetUniqueProjectName().ShouldBe(@"C:\WebSites\WebApplication3\"); @@ -153,5 +153,10 @@ private static string ConvertToSlnx(string slnPath) SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait(); return slnxPath; } + + private static string GetPathForCurrentOs(string path) + { + return NativeMethodsShared.IsWindows ? path : Path.GetFullPath(path).Replace('\\', '/'); + } } } From ccc12e1d3992a4ea0b84371ba4aa47142bc08a36 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 14:40:03 +0200 Subject: [PATCH 17/23] small fix --- src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs | 2 +- .../Construction/SolutionFile_NewParser_Tests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 547d43c5f10..49cea2f3682 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -1245,7 +1245,7 @@ private static string ConvertToSlnx(string slnPath) private static string GetPathForCurrentOs(string path) { - return NativeMethodsShared.IsWindows ? path : Path.GetFullPath(path).Replace('\\', '/'); + return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); } } } diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs index 2726fc38344..c21f164d0b5 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -156,7 +156,7 @@ private static string ConvertToSlnx(string slnPath) private static string GetPathForCurrentOs(string path) { - return NativeMethodsShared.IsWindows ? path : Path.GetFullPath(path).Replace('\\', '/'); + return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); } } } From 07d3c3563d1544f2d9e7457a7541db3c4c3c20b6 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 14:53:16 +0200 Subject: [PATCH 18/23] rename test method --- .../Construction/SolutionFile_Tests.cs | 22 +++++++++---------- .../SolutionFile_NewParser_Tests.cs | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 49cea2f3682..3ddd79b7418 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -96,7 +96,7 @@ public void ParseSolution_VC2(bool convertToSlnx) string expectedProjectName = convertToSlnx ? "Project name" : "Project name.myvctype"; Assert.Equal(expectedProjectName, solution.ProjectsInOrder[0].ProjectName); - Assert.Equal(GetPathForCurrentOs("Relative path\\to\\Project name.myvctype"), solution.ProjectsInOrder[0].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("Relative path\\to\\Project name.myvctype"), solution.ProjectsInOrder[0].RelativePath); if (!convertToSlnx) { // When converting to SLNX, the project GUID is not preserved. @@ -416,7 +416,7 @@ public void SolutionFolders() Assert.Equal(5, solution.ProjectsInOrder.Count); - Assert.Equal(GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj"), solution.ProjectsInOrder[0].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj"), 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); @@ -425,7 +425,7 @@ public void SolutionFolders() Assert.Empty(solution.ProjectsInOrder[1].Dependencies); Assert.Null(solution.ProjectsInOrder[1].ParentProjectGuid); - Assert.Equal(GetPathForCurrentOs("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj"), solution.ProjectsInOrder[2].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj"), 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); @@ -434,7 +434,7 @@ public void SolutionFolders() Assert.Empty(solution.ProjectsInOrder[3].Dependencies); Assert.Equal("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[3].ParentProjectGuid); - Assert.Equal(GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj"), solution.ProjectsInOrder[4].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj"), 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); @@ -496,16 +496,16 @@ public void SolutionFoldersSlnx() Assert.Equal(3, solution.ProjectsInOrder.Count); - var classLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj")); + var classLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj")); Assert.NotNull(classLibrary1); Assert.Empty(classLibrary1.Dependencies); Assert.Null(classLibrary1.ParentProjectGuid); - var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj")); + var myPhysicalFolderClassLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("MyPhysicalFolder\\ClassLibrary1\\ClassLibrary1.csproj")); Assert.NotNull(myPhysicalFolderClassLibrary1); Assert.Empty(myPhysicalFolderClassLibrary1.Dependencies); - var classLibrary2 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj")); + var classLibrary2 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj")); Assert.NotNull(classLibrary2); Assert.Empty(classLibrary2.Dependencies); @@ -708,19 +708,19 @@ public void SolutionDependencies(bool convertToSlnx) var classLibrary2 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary2"); var classLibrary3 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary3"); - Assert.Equal(GetPathForCurrentOs("ClassLibrary1\\ClassLibrary1.csproj"), classLibrary1.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj"), classLibrary1.RelativePath); Assert.Single(classLibrary1.Dependencies); Assert.Equal(classLibrary3.ProjectGuid, classLibrary1.Dependencies[0]); Assert.Null(solution.ProjectsInOrder[0].ParentProjectGuid); - Assert.Equal(GetPathForCurrentOs("ClassLibrary2\\ClassLibrary2.csproj"), classLibrary2.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj"), 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(GetPathForCurrentOs("ClassLibrary3\\ClassLibrary3.csproj"), solution.ProjectsInOrder[2].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary3\\ClassLibrary3.csproj"), solution.ProjectsInOrder[2].RelativePath); Assert.Empty(solution.ProjectsInOrder[2].Dependencies); Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid); } @@ -1243,7 +1243,7 @@ private static string ConvertToSlnx(string slnPath) return slnxPath; } - private static string GetPathForCurrentOs(string path) + private static string ConvertToUnixPathIfNeeded(string path) { return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); } diff --git a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs index c21f164d0b5..e69fac5c555 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -86,7 +86,7 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject); solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\"); - solution.ProjectsInOrder[0].RelativePath.ShouldBe(GetPathForCurrentOs(@"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\"); @@ -154,7 +154,7 @@ private static string ConvertToSlnx(string slnPath) return slnxPath; } - private static string GetPathForCurrentOs(string path) + private static string ConvertToUnixPathIfNeeded(string path) { return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); } From 8a67d7a35356756fcb79cf84f6845897683119ab Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 16:55:09 +0200 Subject: [PATCH 19/23] fix unix tests --- .../Construction/SolutionFile_Tests.cs | 28 +++++++++++-------- .../SolutionFile_NewParser_Tests.cs | 7 +++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 3ddd79b7418..079bc81d5c0 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -96,7 +96,7 @@ public void ParseSolution_VC2(bool convertToSlnx) string expectedProjectName = convertToSlnx ? "Project name" : "Project name.myvctype"; Assert.Equal(expectedProjectName, solution.ProjectsInOrder[0].ProjectName); - Assert.Equal(ConvertToUnixPathIfNeeded("Relative path\\to\\Project name.myvctype"), solution.ProjectsInOrder[0].RelativePath); + 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. @@ -416,7 +416,7 @@ public void SolutionFolders() Assert.Equal(5, solution.ProjectsInOrder.Count); - Assert.Equal(ConvertToUnixPathIfNeeded("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); @@ -425,7 +425,7 @@ public void SolutionFolders() Assert.Empty(solution.ProjectsInOrder[1].Dependencies); Assert.Null(solution.ProjectsInOrder[1].ParentProjectGuid); - Assert.Equal(ConvertToUnixPathIfNeeded("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); @@ -434,7 +434,7 @@ public void SolutionFolders() Assert.Empty(solution.ProjectsInOrder[3].Dependencies); Assert.Equal("{E0F97730-25D2-418A-A7BD-02CAFDC6E470}", solution.ProjectsInOrder[3].ParentProjectGuid); - Assert.Equal(ConvertToUnixPathIfNeeded("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); @@ -496,16 +496,19 @@ public void SolutionFoldersSlnx() Assert.Equal(3, solution.ProjectsInOrder.Count); - var classLibrary1 = solution.ProjectsInOrder.FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj")); + 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")); + 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")); + var classLibrary2 = solution.ProjectsInOrder + .FirstOrDefault(p => p.RelativePath == ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj", true)); Assert.NotNull(classLibrary2); Assert.Empty(classLibrary2.Dependencies); @@ -708,19 +711,19 @@ public void SolutionDependencies(bool convertToSlnx) var classLibrary2 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary2"); var classLibrary3 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary3"); - Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj"), classLibrary1.RelativePath); + 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(ConvertToUnixPathIfNeeded("ClassLibrary2\\ClassLibrary2.csproj"), classLibrary2.RelativePath); + 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(ConvertToUnixPathIfNeeded("ClassLibrary3\\ClassLibrary3.csproj"), solution.ProjectsInOrder[2].RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary3\\ClassLibrary3.csproj", convertToSlnx), solution.ProjectsInOrder[2].RelativePath); Assert.Empty(solution.ProjectsInOrder[2].Dependencies); Assert.Null(solution.ProjectsInOrder[2].ParentProjectGuid); } @@ -1243,9 +1246,10 @@ private static string ConvertToSlnx(string slnPath) return slnxPath; } - private static string ConvertToUnixPathIfNeeded(string path) + private static string ConvertToUnixPathIfNeeded(string path, bool isConvertedToSlnx) { - return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); + // When converting to SLNX, the paths are converted to Unix style + 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 index e69fac5c555..857a3776def 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -86,7 +86,7 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) 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].RelativePath.ShouldBe(ConvertToUnixPathIfNeeded(@"C:\WebSites\WebApplication3\", convertToSlnx)); solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2); solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull(); solution.ProjectsInOrder[0].GetUniqueProjectName().ShouldBe(@"C:\WebSites\WebApplication3\"); @@ -154,9 +154,10 @@ private static string ConvertToSlnx(string slnPath) return slnxPath; } - private static string ConvertToUnixPathIfNeeded(string path) + private static string ConvertToUnixPathIfNeeded(string path, bool isConvertedToSlnx) { - return NativeMethodsShared.IsWindows ? path : path.Replace('\\', '/'); + // When converting to SLNX, the paths are converted to Unix style + return !NativeMethodsShared.IsWindows && isConvertedToSlnx ? path.Replace('\\', '/') : path; } } } From 7594edfc5d4074cf3100552389e2becd8b3057fe Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 17:30:11 +0200 Subject: [PATCH 20/23] fix --- .../Construction/SolutionFile_Tests.cs | 9 +++++---- .../Construction/SolutionFile_NewParser_Tests.cs | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index 079bc81d5c0..cf3bd65a31e 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -338,17 +338,17 @@ public void BasicSolution(bool convertToSlnx) // When converting to slnx, the order of the projects is not preserved. ProjectInSolution consoleApplication1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ConsoleApplication1"); - Assert.Equal(@"ConsoleApplication1\ConsoleApplication1.vbproj", consoleApplication1.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ConsoleApplication1\\ConsoleApplication1.vbproj", convertToSlnx), consoleApplication1.RelativePath); Assert.Empty(consoleApplication1.Dependencies); Assert.Null(consoleApplication1.ParentProjectGuid); ProjectInSolution vbClassLibrary = solution.ProjectsInOrder.First(p => p.ProjectName == "vbClassLibrary"); - Assert.Equal(@"vbClassLibrary\vbClassLibrary.vbproj", vbClassLibrary.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("vbClassLibrary\vbClassLibrary.vbproj", convertToSlnx), vbClassLibrary.RelativePath); Assert.Empty(vbClassLibrary.Dependencies); Assert.Null(vbClassLibrary.ParentProjectGuid); ProjectInSolution classLibrary1 = solution.ProjectsInOrder.First(p => p.ProjectName == "ClassLibrary1"); - Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", classLibrary1.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("ClassLibrary1\\ClassLibrary1.csproj", convertToSlnx), classLibrary1.RelativePath); Assert.Empty(classLibrary1.Dependencies); Assert.Null(classLibrary1.ParentProjectGuid); @@ -1248,7 +1248,8 @@ private static string ConvertToSlnx(string slnPath) private static string ConvertToUnixPathIfNeeded(string path, bool isConvertedToSlnx) { - // When converting to SLNX, the paths are converted to Unix style + // 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 index 857a3776def..7f56b600dca 100644 --- a/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs +++ b/src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs @@ -86,7 +86,7 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx) solution.ProjectsInOrder[0].ProjectType.ShouldBe(SolutionProjectType.WebProject); solution.ProjectsInOrder[0].ProjectName.ShouldBe(@"C:\WebSites\WebApplication3\"); - solution.ProjectsInOrder[0].RelativePath.ShouldBe(ConvertToUnixPathIfNeeded(@"C:\WebSites\WebApplication3\", convertToSlnx)); + 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\"); @@ -154,10 +154,10 @@ private static string ConvertToSlnx(string slnPath) return slnxPath; } - private static string ConvertToUnixPathIfNeeded(string path, bool isConvertedToSlnx) + private static string ConvertToUnixPathIfNeeded(string path) { - // When converting to SLNX, the paths are converted to Unix style - return !NativeMethodsShared.IsWindows && isConvertedToSlnx ? path.Replace('\\', '/') : path; + // In the new parser, ProjectModel.FilePath is converted to Unix-style. + return !NativeMethodsShared.IsWindows ? path.Replace('\\', '/') : path; } } } From 674e2572dc4a28b2599c519b6c5b25f17f67fbed Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Tue, 15 Oct 2024 17:54:32 +0200 Subject: [PATCH 21/23] fix --- src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs index cf3bd65a31e..d6abd900521 100644 --- a/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs @@ -343,7 +343,7 @@ public void BasicSolution(bool convertToSlnx) Assert.Null(consoleApplication1.ParentProjectGuid); ProjectInSolution vbClassLibrary = solution.ProjectsInOrder.First(p => p.ProjectName == "vbClassLibrary"); - Assert.Equal(ConvertToUnixPathIfNeeded("vbClassLibrary\vbClassLibrary.vbproj", convertToSlnx), vbClassLibrary.RelativePath); + Assert.Equal(ConvertToUnixPathIfNeeded("vbClassLibrary\\vbClassLibrary.vbproj", convertToSlnx), vbClassLibrary.RelativePath); Assert.Empty(vbClassLibrary.Dependencies); Assert.Null(vbClassLibrary.ParentProjectGuid); From b3ea2750a74a522bc448dbf4f69a6956887b2d57 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Wed, 16 Oct 2024 15:48:53 +0200 Subject: [PATCH 22/23] add Microsoft.VisualStudio.SolutionPersistence to SourceBuildPrebuiltBaseline.xml --- eng/SourceBuildPrebuiltBaseline.xml | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 57169dba90e96d1d85ff49e896c7a5ba0e6c7769 Mon Sep 17 00:00:00 2001 From: Surayya Huseyn Zada Date: Wed, 16 Oct 2024 17:25:37 +0200 Subject: [PATCH 23/23] add more details to docs comments --- src/Build/Construction/Solution/SolutionFile.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Build/Construction/Solution/SolutionFile.cs b/src/Build/Construction/Solution/SolutionFile.cs index dd9497fe1be..983cd691d0d 100644 --- a/src/Build/Construction/Solution/SolutionFile.cs +++ b/src/Build/Construction/Solution/SolutionFile.cs @@ -198,14 +198,14 @@ internal int VisualStudioVersion /// /// All projects in this solution, in the order they appeared in the solution file /// - /// Solution folders are no longer for the new parser. + /// 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 /// - /// Solution folders are no longer included for the new parser. + /// For the new parser, solution folders are no longer included. public IReadOnlyDictionary ProjectsByGuid => new ReadOnlyDictionary(_projectsByGuid); internal IReadOnlyDictionary SolutionFoldersByGuid => new ReadOnlyDictionary(_solutionFoldersByGuid); @@ -324,6 +324,7 @@ internal void ParseUsingNewParser() /// /// Maps to . + /// is a result of parsing solution using the new parser. /// /// private void ReadSolutionModel(SolutionModel solutionModel)