Consolidate solution configuration xml parsing logic (#8590)

Fixes #6751

This change combines the solution configuration xml parsing which the project caching code and the tasks code uses. I did not add `SolutionProjectGenerator.AddPropertyGroupForSolutionConfiguration` into the mix as that's a write scenario and the other two are read scenarios. Maybe at some point the object model could be made more robust and support both the read and write scenarios, but whatever, this is incrementally better.

Note that the main motivation here is that the graph construction code currently does not support the sln-defined configurations, ie what the `AssignProjectConfiguration` target does, so I'll be working on a follow-up change for that. And instead of creating a third copy of this parsing logic, I thought I'd send this PR to consolidate the logic first.

Basically, `SolutionConfiguration.cs` is just ripped directly from `ResolveProjectBase.cs`, and then `ProjectCacheService` just uses `SolutionConfiguration` instead of parsing itself.
This commit is contained in:
David Federman 2023-03-28 11:47:13 -07:00 коммит произвёл GitHub
Родитель cb58a18c1f
Коммит 3cea3e9651
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 162 добавлений и 122 удалений

Просмотреть файл

@ -543,22 +543,19 @@ namespace Microsoft.Build.Experimental.ProjectCache
string definingProjectPath,
Dictionary<string, string> templateGlobalProperties)
{
// TODO: fix code clone for parsing CurrentSolutionConfiguration xml: https://github.com/dotnet/msbuild/issues/6751
var doc = new XmlDocument();
doc.LoadXml(solutionConfigurationXml);
var root = doc.DocumentElement!;
var projectConfigurationNodes = root.GetElementsByTagName("ProjectConfiguration");
ErrorUtilities.VerifyThrow(projectConfigurationNodes.Count > 0, "Expected at least one project in solution");
var graphEntryPoints = new List<ProjectGraphEntryPoint>(projectConfigurationNodes.Count);
foreach (XmlNode node in projectConfigurationNodes)
XmlNodeList? projectConfigurations = SolutionConfiguration.GetProjectConfigurations(solutionConfigurationXml);
if (projectConfigurations == null || projectConfigurations.Count == 0)
{
ErrorUtilities.VerifyThrowInternalNull(node.Attributes, nameof(node.Attributes));
return Array.Empty<ProjectGraphEntryPoint>();
}
var buildProjectInSolution = node.Attributes!["BuildProjectInSolution"];
var graphEntryPoints = new List<ProjectGraphEntryPoint>(projectConfigurations.Count);
foreach (XmlElement projectConfiguration in projectConfigurations)
{
ErrorUtilities.VerifyThrowInternalNull(projectConfiguration.Attributes, nameof(projectConfiguration.Attributes));
var buildProjectInSolution = projectConfiguration.Attributes![SolutionConfiguration.BuildProjectInSolutionAttribute];
if (buildProjectInSolution is not null &&
string.IsNullOrWhiteSpace(buildProjectInSolution.Value) is false &&
bool.TryParse(buildProjectInSolution.Value, out var buildProject) &&
@ -567,12 +564,12 @@ namespace Microsoft.Build.Experimental.ProjectCache
continue;
}
var projectPathAttribute = node.Attributes!["AbsolutePath"];
XmlAttribute? projectPathAttribute = projectConfiguration.Attributes![SolutionConfiguration.AbsolutePathAttribute];
ErrorUtilities.VerifyThrow(projectPathAttribute is not null, "Expected VS to set the project path on each ProjectConfiguration element.");
var projectPath = projectPathAttribute!.Value;
string projectPath = projectPathAttribute!.Value;
var (configuration, platform) = SolutionFile.ParseConfigurationName(node.InnerText, definingProjectPath, 0, solutionConfigurationXml);
(string configuration, string platform) = SolutionFile.ParseConfigurationName(projectConfiguration.InnerText, definingProjectPath, 0, solutionConfigurationXml);
// Take the defining project global properties and override the configuration and platform.
// It's sufficient to only set Configuration and Platform.

Просмотреть файл

@ -247,7 +247,7 @@ namespace Microsoft.Build.Construction
};
using (XmlWriter xw = XmlWriter.Create(solutionConfigurationContents, settings))
{
// TODO: fix code clone for parsing CurrentSolutionConfiguration xml: https://github.com/dotnet/msbuild/issues/6751
// TODO: Consider augmenting SolutionConfiguration with this code
xw.WriteStartElement("SolutionConfiguration");
// add a project configuration entry for each project in the solution

Просмотреть файл

@ -126,6 +126,7 @@
<Link>PlatformNegotiation.cs</Link>
<ExcludeFromStyleCop>true</ExcludeFromStyleCop>
</Compile>
<Compile Include="..\Shared\SolutionConfiguration.cs" />
<Compile Include="..\Shared\TaskLoggingHelper.cs">
<Link>BackEnd\Components\RequestBuilder\IntrinsicTasks\TaskLoggingHelper.cs</Link>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>

Просмотреть файл

@ -0,0 +1,135 @@
// 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.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Xml;
namespace Microsoft.Build.Shared
{
internal sealed class SolutionConfiguration
{
public const string ProjectAttribute = "Project";
public const string AbsolutePathAttribute = "AbsolutePath";
public const string BuildProjectInSolutionAttribute = "BuildProjectInSolution";
// This field stores pre-cached project elements for project guids for quicker access by project guid
private readonly Dictionary<string, XmlElement> _cachedProjectElements = new Dictionary<string, XmlElement>(StringComparer.OrdinalIgnoreCase);
// This field stores pre-cached project elements for project guids for quicker access by project absolute path
private readonly Dictionary<string, XmlElement> _cachedProjectElementsByAbsolutePath = new Dictionary<string, XmlElement>(StringComparer.OrdinalIgnoreCase);
// This field stores the project absolute path for quicker access by project guid
private readonly Dictionary<string, string> _cachedProjectAbsolutePathsByGuid = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// This field stores the project guid for quicker access by project absolute path
private readonly Dictionary<string, string> _cachedProjectGuidsByAbsolutePath = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// This field stores the list of dependency project guids by depending project guid
private readonly Dictionary<string, List<string>> _cachedDependencyProjectGuidsByDependingProjectGuid = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
public SolutionConfiguration(string xmlString)
{
// Example:
//
// <SolutionConfiguration>
// <ProjectConfiguration Project="{786E302A-96CE-43DC-B640-D6B6CC9BF6C0}" AbsolutePath="c:foo\Project1\A.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
// <ProjectConfiguration Project="{881C1674-4ECA-451D-85B6-D7C59B7F16FA}" AbsolutePath="c:foo\Project2\B.csproj" BuildProjectInSolution="True">Debug|AnyCPU<ProjectDependency Project="{4A727FF8-65F2-401E-95AD-7C8BBFBE3167}" /></ProjectConfiguration>
// <ProjectConfiguration Project="{4A727FF8-65F2-401E-95AD-7C8BBFBE3167}" AbsolutePath="c:foo\Project3\C.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
// </SolutionConfiguration>
//
XmlNodeList? projectConfigurationElements = GetProjectConfigurations(xmlString);
if (projectConfigurationElements != null)
{
foreach (XmlElement xmlElement in projectConfigurationElements)
{
string projectGuid = xmlElement.GetAttribute(ProjectAttribute);
string projectAbsolutePath = xmlElement.GetAttribute(AbsolutePathAttribute);
// What we really want here is the normalized path, like we'd get with an item's "FullPath" metadata. However,
// if there's some bogus full path in the solution configuration (e.g. a website with a "full path" of c:\solutiondirectory\http://localhost)
// we do NOT want to throw -- chances are extremely high that that's information that will never actually be used. So resolve the full path
// but just swallow any IO-related exceptions that result. If the path is bogus, the method will return null, so we'll just quietly fail
// to cache it below.
projectAbsolutePath = FileUtilities.GetFullPathNoThrow(projectAbsolutePath);
if (!string.IsNullOrEmpty(projectGuid))
{
_cachedProjectElements[projectGuid] = xmlElement;
if (!string.IsNullOrEmpty(projectAbsolutePath))
{
_cachedProjectElementsByAbsolutePath[projectAbsolutePath] = xmlElement;
_cachedProjectAbsolutePathsByGuid[projectGuid] = projectAbsolutePath;
_cachedProjectGuidsByAbsolutePath[projectAbsolutePath] = projectGuid;
}
foreach (XmlNode dependencyNode in xmlElement.ChildNodes)
{
if (dependencyNode.NodeType != XmlNodeType.Element)
{
continue;
}
XmlElement dependencyElement = ((XmlElement)dependencyNode);
if (!String.Equals(dependencyElement.Name, "ProjectDependency", StringComparison.Ordinal))
{
continue;
}
string dependencyGuid = dependencyElement.GetAttribute("Project");
if (dependencyGuid.Length == 0)
{
continue;
}
if (!_cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out List<string>? list))
{
list = new List<string>();
_cachedDependencyProjectGuidsByDependingProjectGuid.Add(projectGuid, list);
}
list.Add(dependencyGuid);
}
}
}
}
}
public static SolutionConfiguration Empty { get; } = new SolutionConfiguration(string.Empty);
public ICollection<XmlElement> ProjectConfigurations => _cachedProjectElements.Values;
public static XmlNodeList? GetProjectConfigurations(string xmlString)
{
XmlDocument? doc = null;
if (!string.IsNullOrEmpty(xmlString))
{
doc = new XmlDocument();
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), settings))
{
doc.Load(reader);
}
}
return doc?.DocumentElement?.ChildNodes;
}
public bool TryGetProjectByGuid(string projectGuid, [NotNullWhen(true)] out XmlElement? projectElement) => _cachedProjectElements.TryGetValue(projectGuid, out projectElement);
public bool TryGetProjectByAbsolutePath(string projectFullPath, [NotNullWhen(true)] out XmlElement? projectElement) => _cachedProjectElementsByAbsolutePath.TryGetValue(projectFullPath, out projectElement);
public bool TryGetProjectGuidByAbsolutePath(string projectFullPath, [NotNullWhen(true)] out string? projectGuid) => _cachedProjectGuidsByAbsolutePath.TryGetValue(projectFullPath, out projectGuid);
public bool TryGetProjectDependencies(string projectGuid, [NotNullWhen(true)] out List<string>? dependencyProjectGuids) => _cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out dependencyProjectGuids);
public bool TryGetProjectPathByGuid(string projectGuid, [NotNullWhen(true)] out string? projectPath) => _cachedProjectAbsolutePathsByGuid.TryGetValue(projectGuid, out projectPath);
}
}

Просмотреть файл

@ -152,7 +152,6 @@ namespace Microsoft.Build.Tasks
private const string attrFullConfiguration = "FullConfiguration";
private const string buildReferenceMetadataName = "BuildReference";
private const string referenceOutputAssemblyMetadataName = "ReferenceOutputAssembly";
private const string buildProjectInSolutionAttribute = "BuildProjectInSolution";
private const string attrConfiguration = "Configuration";
private const string attrPlatform = "Platform";
private const string attrSetConfiguration = "SetConfiguration";
@ -337,7 +336,7 @@ namespace Microsoft.Build.Tasks
if (projectConfigurationElement != null && resolvedProjectWithConfiguration != null && onlyReferenceAndBuildProjectsEnabledInSolutionConfiguration)
{
// The value of the specified attribute. An empty string is returned if a matching attribute is not found or if the attribute does not have a specified or default value.
string buildProjectInSolution = projectConfigurationElement.GetAttribute(buildProjectInSolutionAttribute);
string buildProjectInSolution = projectConfigurationElement.GetAttribute(SolutionConfiguration.BuildProjectInSolutionAttribute);
// We could not parse out what was in the attribute, act as if it was not set in the first place.
if (bool.TryParse(buildProjectInSolution, out bool buildProject))

Просмотреть файл

@ -102,6 +102,9 @@
<Compile Include="..\Shared\StrongNameHelpers.cs">
<Link>StrongNameHelpers.cs</Link>
</Compile>
<Compile Include="..\Shared\SolutionConfiguration.cs">
<Link>SolutionConfiguration.cs</Link>
</Compile>
<Compile Include="..\Shared\TaskLoggingHelperExtension.cs">
<Link>TaskLoggingHelperExtension.cs</Link>
<ExcludeFromStyleCop>True</ExcludeFromStyleCop>

Просмотреть файл

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Xml;
using Microsoft.Build.Framework;
@ -42,25 +41,10 @@ namespace Microsoft.Build.Tasks
// This field stores all the distinct project references by project absolute path
private readonly HashSet<string> _cachedProjectReferencesByAbsolutePath = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// This field stores pre-cached project elements for project guids for quicker access by project guid
private readonly Dictionary<string, XmlElement> _cachedProjectElements = new Dictionary<string, XmlElement>(StringComparer.OrdinalIgnoreCase);
// This field stores pre-cached project elements for project guids for quicker access by project absolute path
private readonly Dictionary<string, XmlElement> _cachedProjectElementsByAbsolutePath = new Dictionary<string, XmlElement>(StringComparer.OrdinalIgnoreCase);
// This field stores the project absolute path for quicker access by project guid
private readonly Dictionary<string, string> _cachedProjectAbsolutePathsByGuid = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// This field stores the project guid for quicker access by project absolute path
private readonly Dictionary<string, string> _cachedProjectGuidsByAbsolutePath = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// This field stores the list of dependency project guids by depending project guid
private readonly Dictionary<string, List<string>> _cachedDependencyProjectGuidsByDependingProjectGuid = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
private SolutionConfiguration _solutionConfiguration = SolutionConfiguration.Empty;
private const string attributeProject = "Project";
private const string attributeAbsolutePath = "AbsolutePath";
#endregion
#region Methods
@ -121,86 +105,7 @@ namespace Microsoft.Build.Tasks
/// <summary>
/// Pre-cache individual project elements from the XML string in a hashtable for quicker access.
/// </summary>
internal void CacheProjectElementsFromXml(string xmlString)
{
// TODO: fix code clone for parsing CurrentSolutionConfiguration xml: https://github.com/dotnet/msbuild/issues/6751
XmlDocument doc = null;
if (!string.IsNullOrEmpty(xmlString))
{
doc = new XmlDocument();
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), settings))
{
doc.Load(reader);
}
}
// Example:
//
// <SolutionConfiguration>
// <ProjectConfiguration Project="{786E302A-96CE-43DC-B640-D6B6CC9BF6C0}" AbsolutePath="c:foo\Project1\A.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
// <ProjectConfiguration Project="{881C1674-4ECA-451D-85B6-D7C59B7F16FA}" AbsolutePath="c:foo\Project2\B.csproj" BuildProjectInSolution="True">Debug|AnyCPU<ProjectDependency Project="{4A727FF8-65F2-401E-95AD-7C8BBFBE3167}" /></ProjectConfiguration>
// <ProjectConfiguration Project="{4A727FF8-65F2-401E-95AD-7C8BBFBE3167}" AbsolutePath="c:foo\Project3\C.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
// </SolutionConfiguration>
//
if (doc?.DocumentElement != null)
{
foreach (XmlElement xmlElement in doc.DocumentElement.ChildNodes)
{
string projectGuid = xmlElement.GetAttribute(attributeProject);
string projectAbsolutePath = xmlElement.GetAttribute(attributeAbsolutePath);
// What we really want here is the normalized path, like we'd get with an item's "FullPath" metadata. However,
// if there's some bogus full path in the solution configuration (e.g. a website with a "full path" of c:\solutiondirectory\http://localhost)
// we do NOT want to throw -- chances are extremely high that that's information that will never actually be used. So resolve the full path
// but just swallow any IO-related exceptions that result. If the path is bogus, the method will return null, so we'll just quietly fail
// to cache it below.
projectAbsolutePath = FileUtilities.GetFullPathNoThrow(projectAbsolutePath);
if (!string.IsNullOrEmpty(projectGuid))
{
_cachedProjectElements[projectGuid] = xmlElement;
if (!string.IsNullOrEmpty(projectAbsolutePath))
{
_cachedProjectElementsByAbsolutePath[projectAbsolutePath] = xmlElement;
_cachedProjectAbsolutePathsByGuid[projectGuid] = projectAbsolutePath;
_cachedProjectGuidsByAbsolutePath[projectAbsolutePath] = projectGuid;
}
foreach (XmlNode dependencyNode in xmlElement.ChildNodes)
{
if (dependencyNode.NodeType != XmlNodeType.Element)
{
continue;
}
XmlElement dependencyElement = ((XmlElement)dependencyNode);
if (!String.Equals(dependencyElement.Name, "ProjectDependency", StringComparison.Ordinal))
{
continue;
}
string dependencyGuid = dependencyElement.GetAttribute("Project");
if (dependencyGuid.Length == 0)
{
continue;
}
if (!_cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out List<string> list))
{
list = new List<string>();
_cachedDependencyProjectGuidsByDependingProjectGuid.Add(projectGuid, list);
}
list.Add(dependencyGuid);
}
}
}
}
}
internal void CacheProjectElementsFromXml(string xmlString) => _solutionConfiguration = new SolutionConfiguration(xmlString);
/// <summary>
/// Helper method for retrieving whatever was stored in the XML string for the given project
@ -219,7 +124,7 @@ namespace Microsoft.Build.Tasks
{
string projectGuid = projectRef.GetMetadata(attributeProject);
if ((_cachedProjectElements.TryGetValue(projectGuid, out XmlElement projectElement)) && (projectElement != null))
if (_solutionConfiguration.TryGetProjectByGuid(projectGuid, out XmlElement projectElement))
{
return projectElement;
}
@ -228,7 +133,7 @@ namespace Microsoft.Build.Tasks
// next we'll try a lookup by the absolute path of the project
string projectFullPath = projectRef.GetMetadata("FullPath"); // reserved metadata "FullPath" is used at it will cache the value
if ((_cachedProjectElementsByAbsolutePath.TryGetValue(projectFullPath, out projectElement)) && (projectElement != null))
if (_solutionConfiguration.TryGetProjectByAbsolutePath(projectFullPath, out projectElement))
{
return projectElement;
}
@ -243,14 +148,14 @@ namespace Microsoft.Build.Tasks
protected void AddSyntheticProjectReferences(string currentProjectAbsolutePath)
{
// Get the guid for this project
if (!_cachedProjectGuidsByAbsolutePath.TryGetValue(currentProjectAbsolutePath, out string projectGuid))
if (!_solutionConfiguration.TryGetProjectGuidByAbsolutePath(currentProjectAbsolutePath, out string projectGuid))
{
// We were passed a blob, but we weren't listed in it. Odd. Return.
return;
}
// Use the guid to look up the dependencies for it
if (!_cachedDependencyProjectGuidsByDependingProjectGuid.TryGetValue(projectGuid, out List<string> guids))
if (!_solutionConfiguration.TryGetProjectDependencies(projectGuid, out List<string> guids))
{
// We didn't have dependencies listed in the blob
return;
@ -262,7 +167,7 @@ namespace Microsoft.Build.Tasks
foreach (string guid in guids)
{
// Get the absolute path of the dependency, using the blob
if (!_cachedProjectAbsolutePathsByGuid.TryGetValue(guid, out string path))
if (!_solutionConfiguration.TryGetProjectPathByGuid(guid, out string path))
{
// We had a dependency listed in the blob that wasn't itself in the blob. Odd. Return.
continue;