Skip to content

Commit

Permalink
.SLNX format support (#10794)
Browse files Browse the repository at this point in the history
* .slnx support - use the new parser to parse .slnx files
  • Loading branch information
surayya-MS authored Oct 17, 2024
1 parent 2358769 commit adb4394
Show file tree
Hide file tree
Showing 14 changed files with 932 additions and 191 deletions.
1 change: 1 addition & 0 deletions eng/SourceBuildPrebuiltBaseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<UsagePattern IdentityGlob="System.Security.Cryptography.Xml/*8.0.0*" />
<UsagePattern IdentityGlob="System.Text.Json/*8.0.5*" />
<UsagePattern IdentityGlob="System.Threading.Tasks.Dataflow/*8.0.0*" />
<UsagePattern IdentityGlob="Microsoft.VisualStudio.SolutionPersistence/*1.0.9*" />
</IgnorePatterns>
<Usages>
</Usages>
Expand Down
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.9</MicrosoftVisualStudioSolutionPersistenceVersion>
</PropertyGroup>
</Project>
2 changes: 2 additions & 0 deletions eng/dependabot/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@

<PackageVersion Include="Verify.Xunit" Version="19.14.1" />
<PackageVersion Update="Verify.XUnit" Condition="'$(VerifyXUnitVersion)' != ''" Version="$(VerifyXUnitVersion)" />

<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="$(MicrosoftVisualStudioSolutionPersistenceVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(DotNetBuildSourceOnly)' != 'true' AND $(ProjectIsDeprecated) != 'true'">
Expand Down
335 changes: 258 additions & 77 deletions src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Shared;
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
using Shouldly;
using Xunit;
using Xunit.Abstractions;

#nullable disable

namespace Microsoft.Build.UnitTests.Construction
{
public class SolutionFile_NewParser_Tests
{
public ITestOutputHelper TestOutputHelper { get; }

public SolutionFile_NewParser_Tests(ITestOutputHelper testOutputHelper)
{
TestOutputHelper = testOutputHelper;
}

/// <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\");
solution.ProjectsInOrder[0].RelativePath.ShouldBe(ConvertToUnixPathIfNeeded(@"C:\WebSites\WebApplication3\"));
solution.ProjectsInOrder[0].Dependencies.Count.ShouldBe(2);
solution.ProjectsInOrder[0].ParentProjectGuid.ShouldBeNull();
solution.ProjectsInOrder[0].GetUniqueProjectName().ShouldBe(@"C:\WebSites\WebApplication3\");

Hashtable aspNetCompilerParameters = solution.ProjectsInOrder[0].AspNetConfigurations;
AspNetCompilerParameters debugAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Debug"];
AspNetCompilerParameters releaseAspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParameters["Release"];

debugAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst");
debugAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite\");
debugAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst\");
debugAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
debugAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"false");
debugAspNetCompilerParameters.aspNetDebug.ShouldBe(@"true");
debugAspNetCompilerParameters.aspNetKeyFile.ShouldBe(@"debugkeyfile.snk");
debugAspNetCompilerParameters.aspNetKeyContainer.ShouldBe(@"12345.container");
debugAspNetCompilerParameters.aspNetDelaySign.ShouldBe(@"true");
debugAspNetCompilerParameters.aspNetAPTCA.ShouldBe(@"false");
debugAspNetCompilerParameters.aspNetFixedNames.ShouldBe(@"debugfixednames");

releaseAspNetCompilerParameters.aspNetVirtualPath.ShouldBe(@"/publishfirst_release");
releaseAspNetCompilerParameters.aspNetPhysicalPath.ShouldBe(@"..\rajeev\temp\websites\myfirstwebsite_release\");
releaseAspNetCompilerParameters.aspNetTargetPath.ShouldBe(@"..\rajeev\temp\publishfirst_release\");
releaseAspNetCompilerParameters.aspNetForce.ShouldBe(@"true");
releaseAspNetCompilerParameters.aspNetUpdateable.ShouldBe(@"true");
releaseAspNetCompilerParameters.aspNetDebug.ShouldBe(@"false");
releaseAspNetCompilerParameters.aspNetKeyFile.ShouldBe("");
releaseAspNetCompilerParameters.aspNetKeyContainer.ShouldBe("");
releaseAspNetCompilerParameters.aspNetDelaySign.ShouldBe("");
releaseAspNetCompilerParameters.aspNetAPTCA.ShouldBe("");
releaseAspNetCompilerParameters.aspNetFixedNames.ShouldBe("");

List<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('\'', '"');

using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);

string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;

SolutionFile solutionFile = new SolutionFile { FullPath = solutionPath };
solutionFile.ParseUsingNewParser();
return solutionFile;
}
}

private static string ConvertToSlnx(string slnPath)
{
string slnxPath = slnPath + "x";
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(slnPath).ShouldNotBeNull();
SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
return slnxPath;
}

private static string ConvertToUnixPathIfNeeded(string path)
{
// In the new parser, ProjectModel.FilePath is converted to Unix-style.
return !NativeMethodsShared.IsWindows ? path.Replace('\\', '/') : path;
}
}
}
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).ShouldNotBeNull();
SolutionModel solutionModel = serializer.OpenAsync(slnPath, CancellationToken.None).Result;
SolutionSerializers.SlnXml.SaveAsync(slnxPath, solutionModel, CancellationToken.None).Wait();
return slnxPath;
}

private ILoggingService CreateMockLoggingService()
{
ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 0);
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

0 comments on commit adb4394

Please sign in to comment.