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

Merged
merged 25 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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