Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.SLNX format support #10794

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,8 @@
<FileVersion>$(VersionPrefix).$(FileVersion.Split('.')[3])</FileVersion>
</PropertyGroup>
</Target>
<!-- SolutionPersistence -->
<PropertyGroup>
<MicrosoftVisualStudioSolutionPersistenceVersion>1.0.4</MicrosoftVisualStudioSolutionPersistenceVersion>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not on nuget.org yet so we'll have to either wait for that (@richardstanton?) or get a more manual push going.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/microsoft/vs-solutionpersistence/blob/main/README.md has a badge linked to nuget.org, but currently it's returning 404. dotnet nuget push missing somewhere :)

</PropertyGroup>
</Project>
309 changes: 243 additions & 66 deletions src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs
Original file line number Diff line number Diff line change
@@ -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;
}

/// <summary>
/// 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.
/// </summary>
[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/
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
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<string> 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}");
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
}
}
67 changes: 41 additions & 26 deletions src/Build.UnitTests/Construction/SolutionFilter_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -215,8 +219,10 @@ public void InvalidSolutionFilters(string slnfValue, string exceptionReason)
/// <summary>
/// Test that a solution filter file is parsed correctly, and it can accurately respond as to whether a project should be filtered out.
/// </summary>
[Fact]
public void ParseSolutionFilter()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ParseSolutionFilter(bool convertToSlnx)
{
using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
Expand All @@ -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("\\", "\\\\") + @"""
Expand All @@ -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;
surayya-MS marked this conversation as resolved.
Show resolved Hide resolved
SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
return slnxPath;
}

private ILoggingService CreateMockLoggingService()
{
ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 0);
Expand Down
20 changes: 14 additions & 6 deletions src/Build/Construction/Solution/ProjectInSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading