|
// 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.Configuration;
using System.IO;
using Microsoft.Build.Collections;
using Microsoft.Build.Shared;
#nullable disable
namespace Microsoft.Build.Evaluation
{
/// <summary>
/// Helper class for reading toolsets out of the configuration file.
/// </summary>
internal static class ToolsetConfigurationReaderHelpers
{
/// <summary>
/// Lock for process wide ToolsetConfigurationSection section cache
/// </summary>
private static readonly object s_syncLock = new();
/// <summary>
/// Process wide ToolsetConfigurationSection section cache
/// </summary>
private static ToolsetConfigurationSection s_toolsetConfigurationSectionCache;
private static Configuration s_configurationOfCachedSection;
internal static ToolsetConfigurationSection ReadToolsetConfigurationSection(Configuration configuration)
{
if (configuration == null)
{
return null;
}
lock (s_syncLock)
{
// Cache 1st requested configuration section. In unit tests, different Configuration is provided for particular test cases.
// During runtime, however, only MSBuild exe configuration file is provided to read toolset configuration from,
// and modifying MSBuild exe configuration during lifetime of msbuild nodes is neither expected nor supported.
if (s_toolsetConfigurationSectionCache == null)
{
s_toolsetConfigurationSectionCache = GetToolsetConfigurationSection(configuration);
s_configurationOfCachedSection = configuration;
}
return s_configurationOfCachedSection == configuration ?
s_toolsetConfigurationSectionCache :
GetToolsetConfigurationSection(configuration);
}
}
private static ToolsetConfigurationSection GetToolsetConfigurationSection(Configuration configuration)
{
ToolsetConfigurationSection configurationSection = null;
// This will be null if the application config file does not have the following section
// definition for the msbuildToolsets section as the first child element.
// <configSections>
// <section name=""msbuildToolsets"" type=""Microsoft.Build.Evaluation.ToolsetConfigurationSection, Microsoft.Build"" />
// </configSections>";
// Note that the application config file may or may not contain an msbuildToolsets element.
// For example:
// If section definition is present and section is not present, this value is not null
// If section definition is not present and section is also not present, this value is null
// If the section definition is not present and section is present, then this value is null
if (configuration != null)
{
ConfigurationSection msbuildSection = configuration.GetSection("msbuildToolsets");
configurationSection = msbuildSection as ToolsetConfigurationSection;
if (configurationSection == null && msbuildSection != null) // we found msbuildToolsets but the wrong type of handler
{
if (String.IsNullOrEmpty(msbuildSection.SectionInformation.Type) ||
msbuildSection.SectionInformation.Type.IndexOf("Microsoft.Build", StringComparison.OrdinalIgnoreCase) >= 0)
{
// Set the configuration type handler to the current ToolsetConfigurationSection type
msbuildSection.SectionInformation.Type = typeof(ToolsetConfigurationSection).AssemblyQualifiedName;
try
{
// fabricate a temporary config file with the correct section handler type in it
string tempFileName = FileUtilities.GetTemporaryFile();
// Save the modified config
configuration.SaveAs(tempFileName + ".config");
// Open the configuration again, the new type for the section handler will do its stuff
// Note that the OpenExeConfiguration call uses the config filename *without* the .config
// extension
configuration = ConfigurationManager.OpenExeConfiguration(tempFileName);
// Get the toolset information from the section using our real handler
configurationSection = configuration.GetSection("msbuildToolsets") as ToolsetConfigurationSection;
File.Delete(tempFileName + ".config");
File.Delete(tempFileName);
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
}
}
}
}
return configurationSection;
}
}
/// <summary>
/// Class representing the Toolset element
/// </summary>
/// <remarks>
/// Internal for unit testing only
/// </remarks>
internal sealed class ToolsetElement : ConfigurationElement
{
/// <summary>
/// ToolsVersion attribute of the element
/// </summary>
[ConfigurationProperty("toolsVersion", IsKey = true, IsRequired = true)]
public string toolsVersion
{
get
{
return (string)base[nameof(toolsVersion)];
}
set
{
base[nameof(toolsVersion)] = value;
}
}
/// <summary>
/// Property element collection
/// </summary>
[ConfigurationProperty("", IsDefaultCollection = true)]
public PropertyElementCollection PropertyElements
{
get
{
return (PropertyElementCollection)base[""];
}
}
/// <summary>
/// Collection of all the search paths for project imports, per OS
/// </summary>
[ConfigurationProperty("projectImportSearchPaths")]
public ExtensionsPathsElementCollection AllProjectImportSearchPaths
{
get
{
return (ExtensionsPathsElementCollection)base["projectImportSearchPaths"];
}
}
/// <summary>
/// Class representing all the per-OS search paths for MSBuildExtensionsPath*
/// </summary>
internal sealed class ExtensionsPathsElementCollection : ConfigurationElementCollection
{
/// <summary>
/// We use this dictionary to track whether or not we've seen a given
/// searchPaths definition before, since the .NET configuration classes
/// won't perform this check without respect for case.
/// </summary>
private Dictionary<string, string> _previouslySeenOS = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Type of the collection
/// This has to be public as cannot change access modifier when overriding
/// </summary>
public override ConfigurationElementCollectionType CollectionType
{
get
{
return ConfigurationElementCollectionType.BasicMap;
}
}
/// <summary>
/// Throw exception if an element with a duplicate key is added to the collection
/// </summary>
protected override bool ThrowOnDuplicate
{
get
{
return false;
}
}
/// <summary>
/// Name of the element
/// </summary>
protected override string ElementName
{
get
{
return "searchPaths";
}
}
/// <summary>
/// Gets an element with the specified name
/// </summary>
/// <param name="os">OS of the element</param>
/// <returns>element</returns>
public ExtensionsPathElement GetElement(string os)
{
return (ExtensionsPathElement)this.BaseGet(os);
}
/// <summary>
/// Gets an element based at the specified position
/// </summary>
/// <param name="index">position</param>
/// <returns>element</returns>
public ExtensionsPathElement GetElement(int index)
{
return (ExtensionsPathElement)this.BaseGet(index);
}
/// <summary>
/// Returns the key value for the given element
/// </summary>
/// <param name="element">element whose key is returned</param>
/// <returns>key</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((ExtensionsPathElement)element).OS;
}
/// <summary>
/// Creates a new element of the collection
/// </summary>
/// <returns>Created element</returns>
protected override ConfigurationElement CreateNewElement()
{
return new ExtensionsPathElement();
}
/// <summary>
/// overridden so we can track previously seen elements
/// </summary>
protected override void BaseAdd(int index, ConfigurationElement element)
{
UpdateOSMap(element);
base.BaseAdd(index, element);
}
/// <summary>
/// overridden so we can track previously seen elements
/// </summary>
protected override void BaseAdd(ConfigurationElement element)
{
UpdateOSMap(element);
base.BaseAdd(element);
}
/// <summary>
/// Stores the name of the OS in a case-insensitive map
/// so we can detect if it is specified more than once but with
/// different case
/// </summary>
private void UpdateOSMap(ConfigurationElement element)
{
string os = GetElementKey(element).ToString();
if (_previouslySeenOS.ContainsKey(os))
{
string locationString = String.Empty;
if (!String.IsNullOrEmpty(element.ElementInformation.Source))
{
if (element.ElementInformation.LineNumber != 0)
{
locationString = String.Format("{0} ({1})", element.ElementInformation.Source, element.ElementInformation.LineNumber);
}
else
{
locationString = element.ElementInformation.Source;
}
}
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MultipleDefinitionsForSameExtensionsPathOS", os, locationString);
throw new ConfigurationErrorsException(message, element.ElementInformation.Source, element.ElementInformation.LineNumber);
}
_previouslySeenOS.Add(os, string.Empty);
}
}
/// <summary>
/// Class representing searchPaths element for a single OS
/// </summary>
internal sealed class ExtensionsPathElement : ConfigurationElement
{
/// <summary>
/// OS attribute of the element
/// </summary>
[ConfigurationProperty("os", IsKey = true, IsRequired = true)]
public string OS
{
get
{
return (string)base["os"];
}
set
{
base["os"] = value;
}
}
/// <summary>
/// Property element collection
/// </summary>
[ConfigurationProperty("", IsDefaultCollection = true)]
public PropertyElementCollection PropertyElements
{
get
{
return (PropertyElementCollection)base[""];
}
}
}
/// <summary>
/// Class representing collection of property elements
/// </summary>
internal sealed class PropertyElementCollection : ConfigurationElementCollection
{
#region Private Fields
/// <summary>
/// We use this dictionary to track whether or not we've seen a given
/// property definition before, since the .NET configuration classes
/// won't perform this check without respect for case.
/// </summary>
private Dictionary<string, string> _previouslySeenPropertyNames = new Dictionary<string, string>(MSBuildNameIgnoreCaseComparer.Default);
#endregion
#region Properties
/// <summary>
/// Collection type
/// This has to be public as cannot change access modifier when overriding
/// </summary>
public override ConfigurationElementCollectionType CollectionType
{
get
{
return ConfigurationElementCollectionType.BasicMap;
}
}
/// <summary>
/// Throw exception if an element with a duplicate is added
/// </summary>
protected override bool ThrowOnDuplicate
{
get
{
return false;
}
}
/// <summary>
/// name of the element
/// </summary>
protected override string ElementName
{
get
{
return "property";
}
}
#endregion
#region Methods
/// <summary>
/// Gets an element with the specified name
/// </summary>
/// <param name="name">name of the element</param>
/// <returns>element</returns>
public PropertyElement GetElement(string name)
{
return (PropertyElement)this.BaseGet(name);
}
/// <summary>
/// Gets an element at the specified position
/// </summary>
/// <param name="index">position</param>
/// <returns>element</returns>
public PropertyElement GetElement(int index)
{
return (PropertyElement)this.BaseGet(index);
}
/// <summary>
/// Creates a new element
/// </summary>
/// <returns>element</returns>
protected override ConfigurationElement CreateNewElement()
{
return new PropertyElement();
}
/// <summary>
/// overridden so we can track previously seen property names
/// </summary>
protected override void BaseAdd(int index, ConfigurationElement element)
{
UpdatePropertyNameMap(element);
base.BaseAdd(index, element);
}
/// <summary>
/// overridden so we can track previously seen property names
/// </summary>
protected override void BaseAdd(ConfigurationElement element)
{
UpdatePropertyNameMap(element);
base.BaseAdd(element);
}
/// <summary>
/// Gets the key for the element
/// </summary>
/// <param name="element">element</param>
/// <returns>key</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((PropertyElement)element).Name;
}
/// <summary>
/// Stores the name of the tools version in a case-insensitive map
/// so we can detect if it is specified more than once but with
/// different case
/// </summary>
private void UpdatePropertyNameMap(ConfigurationElement element)
{
string propertyName = GetElementKey(element).ToString();
if (_previouslySeenPropertyNames.ContainsKey(propertyName))
{
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MultipleDefinitionsForSameProperty", propertyName);
throw new ConfigurationErrorsException(message, element.ElementInformation.Source, element.ElementInformation.LineNumber);
}
_previouslySeenPropertyNames.Add(propertyName, string.Empty);
}
#endregion
}
/// <summary>
/// This class represents property element
/// </summary>
internal sealed class PropertyElement : ConfigurationElement
{
/// <summary>
/// name attribute
/// </summary>
[ConfigurationProperty("name", IsKey = true, IsRequired = true)]
public string Name
{
get
{
return (string)base["name"];
}
set
{
base["name"] = value;
}
}
/// <summary>
/// value attribute
/// </summary>
[ConfigurationProperty("value", IsRequired = true)]
public string Value
{
get
{
return (string)base["value"];
}
set
{
base["value"] = value;
}
}
}
}
/// <summary>
/// Class representing the collection of toolset elements
/// </summary>
/// <remarks>
/// Internal for unit testing only
/// </remarks>
internal sealed class ToolsetElementCollection : ConfigurationElementCollection
{
/// <summary>
/// We use this dictionary to track whether or not we've seen a given
/// toolset definition before, since the .NET configuration classes
/// won't perform this check without respect for case.
/// </summary>
private Dictionary<string, string> _previouslySeenToolsVersions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Type of the collection
/// This has to be public as cannot change access modifier when overriding
/// </summary>
public override ConfigurationElementCollectionType CollectionType
{
get
{
return ConfigurationElementCollectionType.BasicMap;
}
}
/// <summary>
/// Throw exception if an element with a duplicate key is added to the collection
/// </summary>
protected override bool ThrowOnDuplicate
{
get
{
return false;
}
}
/// <summary>
/// Name of the element
/// </summary>
protected override string ElementName
{
get
{
return "toolset";
}
}
/// <summary>
/// Gets an element with the specified name
/// </summary>
/// <param name="toolsVersion">toolsVersion of the element</param>
/// <returns>element</returns>
public ToolsetElement GetElement(string toolsVersion)
{
return (ToolsetElement)this.BaseGet(toolsVersion);
}
/// <summary>
/// Gets an element based at the specified position
/// </summary>
/// <param name="index">position</param>
/// <returns>element</returns>
public ToolsetElement GetElement(int index)
{
return (ToolsetElement)this.BaseGet(index);
}
/// <summary>
/// Returns the key value for the given element
/// </summary>
/// <param name="element">element whose key is returned</param>
/// <returns>key</returns>
protected override object GetElementKey(ConfigurationElement element)
{
return ((ToolsetElement)element).toolsVersion;
}
/// <summary>
/// Creates a new element of the collection
/// </summary>
/// <returns>Created element</returns>
protected override ConfigurationElement CreateNewElement()
{
return new ToolsetElement();
}
/// <summary>
/// overridden so we can track previously seen tools versions
/// </summary>
protected override void BaseAdd(int index, ConfigurationElement element)
{
UpdateToolsVersionMap(element);
base.BaseAdd(index, element);
}
/// <summary>
/// overridden so we can track previously seen tools versions
/// </summary>
protected override void BaseAdd(ConfigurationElement element)
{
UpdateToolsVersionMap(element);
base.BaseAdd(element);
}
/// <summary>
/// Stores the name of the tools version in a case-insensitive map
/// so we can detect if it is specified more than once but with
/// different case
/// </summary>
private void UpdateToolsVersionMap(ConfigurationElement element)
{
string toolsVersion = GetElementKey(element).ToString();
if (_previouslySeenToolsVersions.ContainsKey(toolsVersion))
{
string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MultipleDefinitionsForSameToolset", toolsVersion);
throw new ConfigurationErrorsException(message, element.ElementInformation.Source, element.ElementInformation.LineNumber);
}
_previouslySeenToolsVersions.Add(toolsVersion, string.Empty);
}
}
/// <summary>
/// This class is used to programmatically read msbuildToolsets section
/// in from the configuration file. An example of application config file:
///
/// <configuration>
/// <msbuildToolsets default="2.0">
/// <toolset toolsVersion="2.0">
/// <property name="MSBuildBinPath" value="D:\windows\Microsoft.NET\Framework\v2.0.x86ret\"/>
/// <property name="SomeOtherProperty" value="SomeOtherPropertyValue"/>
/// </toolset>
/// <toolset toolsVersion="3.5">
/// <property name="MSBuildBinPath" value="D:\windows\Microsoft.NET\Framework\v3.5.x86ret\"/>
/// </toolset>
/// </msbuildToolsets>
/// </configuration>
///
/// </summary>
/// <remarks>
/// Internal for unit testing only
/// </remarks>
internal sealed class ToolsetConfigurationSection : ConfigurationSection
{
/// <summary>
/// toolsVersion element collection
/// </summary>
[ConfigurationProperty("", IsDefaultCollection = true)]
public ToolsetElementCollection Toolsets
{
get
{
return (ToolsetElementCollection)base[""];
}
}
/// <summary>
/// default attribute on msbuildToolsets element, specifying the default ToolsVersion
/// </summary>
[ConfigurationProperty("default")]
public string Default
{
get
{
// The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue"
// that doesn't seem to work if null is the desired default value. So here we return null
// whenever the base class gives us an empty string.
// Note this means we can't distinguish between the attribute being present but containing
// an empty string for its value and the attribute not being present at all.
string defaultValue = (string)base["default"];
return String.IsNullOrEmpty(defaultValue) ? null : defaultValue;
}
set
{
base["default"] = value;
}
}
/// <summary>
/// MsBuildOverrideTasksPath attribute on msbuildToolsets element, specifying the path to find msbuildOverrideTasks files
/// </summary>
[ConfigurationProperty("msbuildOverrideTasksPath")] // This string is case sensitive, can't change it
public string MSBuildOverrideTasksPath
{
get
{
// The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue"
// that doesn't seem to work if null is the desired default value. So here we return null
// whenever the base class gives us an empty string.
// Note this means we can't distinguish between the attribute being present but containing
// an empty string for its value and the attribute not being present at all.
string defaultValue = (string)base["msbuildOverrideTasksPath"];
return String.IsNullOrEmpty(defaultValue) ? null : defaultValue;
}
set
{
base["msbuildOverrideTasksPath"] = value;
}
}
/// <summary>
/// DefaultOverrideToolsVersion attribute on msbuildToolsets element, specifying the toolsversion that should be used by
/// default to build projects with this version of MSBuild.
/// </summary>
[ConfigurationProperty("DefaultOverrideToolsVersion")]
public string DefaultOverrideToolsVersion
{
get
{
// The ConfigurationPropertyAttribute constructor accepts a named parameter "DefaultValue"
// that doesn't seem to work if null is the desired default value. So here we return null
// whenever the base class gives us an empty string.
// Note this means we can't distinguish between the attribute being present but containing
// an empty string for its value and the attribute not being present at all.
string defaultValue = (string)base[nameof(DefaultOverrideToolsVersion)];
return String.IsNullOrEmpty(defaultValue) ? null : defaultValue;
}
set
{
base[nameof(DefaultOverrideToolsVersion)] = value;
}
}
}
}
|