File: Definition\ToolsetReader.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Microsoft.Build.Collections;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using ErrorUtils = Microsoft.Build.Shared.ErrorUtilities;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException;
using ReservedPropertyNames = Microsoft.Build.Internal.ReservedPropertyNames;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// The abstract base class for all Toolset readers.
    /// </summary>
    internal abstract class ToolsetReader
    {
        /// <summary>
        /// The global properties used to read the toolset.
        /// </summary>
        private PropertyDictionary<ProjectPropertyInstance> _globalProperties;
 
        /// <summary>
        /// The environment properties used to read the toolset.
        /// </summary>
        private readonly PropertyDictionary<ProjectPropertyInstance> _environmentProperties;
 
        /// <summary>
        /// Constructor
        /// </summary>
        protected ToolsetReader(
            PropertyDictionary<ProjectPropertyInstance> environmentProperties,
            PropertyDictionary<ProjectPropertyInstance> globalProperties)
        {
            _environmentProperties = environmentProperties;
            _globalProperties = globalProperties;
        }
 
        /// <summary>
        /// Returns the list of tools versions
        /// </summary>
        protected abstract IEnumerable<ToolsetPropertyDefinition> ToolsVersions
        {
            get;
        }
 
        /// <summary>
        /// Returns the default tools version, or null if none was specified
        /// </summary>
        protected abstract string DefaultToolsVersion
        {
            get;
        }
 
        /// <summary>
        /// Returns the path to find override tasks, or null if none was specified
        /// </summary>
        protected abstract string MSBuildOverrideTasksPath
        {
            get;
        }
 
        /// <summary>
        /// ToolsVersion to use as the default ToolsVersion for this version of MSBuild
        /// </summary>
        protected abstract string DefaultOverrideToolsVersion
        {
            get;
        }
 
        /// <summary>
        /// Gathers toolset data from the registry and configuration file, if any:
        /// allows you to specify which of the registry and configuration file to
        /// read from by providing ToolsetInitialization
        /// </summary>
        internal static string ReadAllToolsets(Dictionary<string, Toolset> toolsets, PropertyDictionary<ProjectPropertyInstance> environmentProperties, PropertyDictionary<ProjectPropertyInstance> globalProperties, ToolsetDefinitionLocations locations)
        {
            return ReadAllToolsets(toolsets,
#if FEATURE_WIN32_REGISTRY
                null,
#endif
                null,
                environmentProperties, globalProperties, locations);
        }
 
        /// <summary>
        /// Gathers toolset data from the registry and configuration file, if any.
        /// NOTE:  this method is internal for unit testing purposes only.
        /// </summary>
        internal static string ReadAllToolsets(
            Dictionary<string, Toolset> toolsets,
#if FEATURE_WIN32_REGISTRY
            ToolsetRegistryReader registryReader,
#endif
            ToolsetConfigurationReader configurationReader,
            PropertyDictionary<ProjectPropertyInstance> environmentProperties,
            PropertyDictionary<ProjectPropertyInstance> globalProperties,
            ToolsetDefinitionLocations locations)
        {
            var initialProperties =
                new PropertyDictionary<ProjectPropertyInstance>(environmentProperties);
 
            initialProperties.ImportProperties(globalProperties);
 
            // The ordering here is important because the configuration file should have greater precedence
            // than the registry, and we do a check and don't read in the new toolset if there's already one.
            string defaultToolsVersionFromConfiguration = null;
            string overrideTasksPathFromConfiguration = null;
            string defaultOverrideToolsVersionFromConfiguration = null;
 
            if ((locations & ToolsetDefinitionLocations.ConfigurationFile) == ToolsetDefinitionLocations.ConfigurationFile)
            {
                if (configurationReader == null)
                {
                    configurationReader = new ToolsetConfigurationReader(environmentProperties, globalProperties);
                }
 
                ReadConfigToolset();
 
                // This is isolated into its own function in order to isolate loading of
                // System.Configuration.ConfigurationManager.dll to codepaths that really
                // need it as a way of mitigating the need to update references to that
                // assembly in API consumers.
                //
                // https://github.com/microsoft/MSBuildLocator/issues/159
                [MethodImplAttribute(MethodImplOptions.NoInlining)]
                void ReadConfigToolset()
                {
                    // Accumulation of properties is okay in the config file because it's deterministically ordered
                    defaultToolsVersionFromConfiguration = configurationReader.ReadToolsets(toolsets, globalProperties,
                                    initialProperties, true /* accumulate properties */, out overrideTasksPathFromConfiguration,
                                    out defaultOverrideToolsVersionFromConfiguration);
                }
            }
 
            string defaultToolsVersionFromRegistry = null;
            string overrideTasksPathFromRegistry = null;
            string defaultOverrideToolsVersionFromRegistry = null;
 
            if ((locations & ToolsetDefinitionLocations.Registry) == ToolsetDefinitionLocations.Registry)
            {
#if FEATURE_WIN32_REGISTRY
                if (NativeMethodsShared.IsWindows || registryReader != null)
                {
                    // If we haven't been provided a registry reader (i.e. unit tests), create one
                    registryReader ??= new ToolsetRegistryReader(environmentProperties, globalProperties);
 
                    // We do not accumulate properties when reading them from the registry, because the order
                    // in which values are returned to us is essentially random: so we disallow one property
                    // in the registry to refer to another also in the registry
                    defaultToolsVersionFromRegistry = registryReader.ReadToolsets(toolsets, globalProperties,
                        initialProperties, false /* do not accumulate properties */, out overrideTasksPathFromRegistry,
                        out defaultOverrideToolsVersionFromRegistry);
                }
                else
#endif
                {
                    var currentDir = BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory.TrimEnd(Path.DirectorySeparatorChar);
                    var props = new PropertyDictionary<ProjectPropertyInstance>();
 
                    var libraryPath = NativeMethodsShared.FrameworkBasePath;
 
                    if (!string.IsNullOrEmpty(libraryPath))
                    {
                        // The 4.0 toolset is installed in the framework directory
                        var v4Dir =
                            FrameworkLocationHelper.GetPathToDotNetFrameworkV40(DotNetFrameworkArchitecture.Current);
                        if (v4Dir != null && !toolsets.ContainsKey("4.0"))
                        {
                            toolsets.Add(
                                "4.0",
                                new Toolset(
                                    "4.0",
                                    v4Dir,
                                    buildProperties: null,
                                    environmentProperties,
                                    globalProperties,
                                    null,
                                    currentDir,
                                    string.Empty));
                        }
 
                        // Other toolsets are installed in the xbuild directory
                        var xbuildToolsetsDir = Path.Combine(libraryPath, $"xbuild{Path.DirectorySeparatorChar}");
                        if (FileSystems.Default.DirectoryExists(xbuildToolsetsDir))
                        {
                            var r = new Regex(Regex.Escape(xbuildToolsetsDir) + @"\d+\.\d+");
                            foreach (var d in Directory.GetDirectories(xbuildToolsetsDir).Where(d => r.IsMatch(d)))
                            {
                                var version = Path.GetFileName(d);
                                var binPath = Path.Combine(d, "bin");
                                if (toolsets.ContainsKey(version))
                                {
                                    continue;
                                }
 
                                toolsets.Add(
                                    version,
                                    new Toolset(
                                        version,
                                        binPath,
                                        buildProperties: null,
                                        environmentProperties,
                                        globalProperties,
                                        null,
                                        currentDir,
                                        string.Empty));
                            }
                        }
                    }
                    if (!toolsets.ContainsKey(MSBuildConstants.CurrentToolsVersion))
                    {
                        toolsets.Add(
                            MSBuildConstants.CurrentToolsVersion,
                            new Toolset(
                                MSBuildConstants.CurrentToolsVersion,
                                currentDir,
                                props,
                                new PropertyDictionary<ProjectPropertyInstance>(),
                                currentDir,
                                string.Empty));
                    }
                }
            }
 
            // The 2.0 .NET Framework installer did not write a ToolsVersion key for itself in the registry.
            // The 3.5 installer writes one for 2.0, but 3.5 might not be installed.
            // The 4.0 and subsequent installers can't keep writing the 2.0 one, because (a) it causes SxS issues and (b) we
            // don't want it unless 2.0 is installed.
            // So if the 2.0 framework is actually installed, we're reading the registry, and either the registry or the config
            // file have not already created the 2.0 toolset, mock up a fake one.
            if (((locations & ToolsetDefinitionLocations.Registry) != 0) && !toolsets.ContainsKey("2.0")
                && FrameworkLocationHelper.PathToDotNetFrameworkV20 != null)
            {
                var synthetic20Toolset = new Toolset(
                    "2.0",
                    FrameworkLocationHelper.PathToDotNetFrameworkV20,
                    environmentProperties,
                    globalProperties,
                    null /* 2.0 did not have override tasks */,
                    null /* 2.0 did not have a default override toolsversion */);
                toolsets.Add("2.0", synthetic20Toolset);
            }
 
            string defaultToolsVersionFromLocal = null;
            string overrideTasksPathFromLocal = null;
            string defaultOverrideToolsVersionFromLocal = null;
 
            if ((locations & ToolsetDefinitionLocations.Local) == ToolsetDefinitionLocations.Local)
            {
                var localReader = new ToolsetLocalReader(environmentProperties, globalProperties);
 
                defaultToolsVersionFromLocal = localReader.ReadToolsets(
                        toolsets,
                        globalProperties,
                        initialProperties,
                        false /* accumulate properties */,
                        out overrideTasksPathFromLocal,
                        out defaultOverrideToolsVersionFromLocal);
            }
 
            // We'll use the path from the configuration file if it was specified, otherwise we'll try
            // the one from the registry.  It's possible (and valid) that neither the configuration file
            // nor the registry specify a override in which case we'll just return null.
            var overrideTasksPath = overrideTasksPathFromConfiguration ?? overrideTasksPathFromRegistry ?? overrideTasksPathFromLocal;
 
            // We'll use the path from the configuration file if it was specified, otherwise we'll try
            // the one from the registry.  It's possible (and valid) that neither the configuration file
            // nor the registry specify a override in which case we'll just return null.
            var defaultOverrideToolsVersion = defaultOverrideToolsVersionFromConfiguration
                                                 ?? defaultOverrideToolsVersionFromRegistry
                                                 ?? defaultOverrideToolsVersionFromLocal;
 
            // We'll use the default from the configuration file if it was specified, otherwise we'll try
            // the one from the registry.  It's possible (and valid) that neither the configuration file
            // nor the registry specify a default, in which case we'll just return null.
            var defaultToolsVersion = defaultToolsVersionFromConfiguration ?? defaultToolsVersionFromRegistry ?? defaultToolsVersionFromLocal;
 
            // If we got a default version from the registry or config file, and it
            // actually exists, fine.
            // Otherwise we have to come up with one.
            if (defaultToolsVersion != null && toolsets.ContainsKey(defaultToolsVersion))
            {
                return defaultToolsVersion;
            }
            // We're going to choose a hard coded default tools version of 2.0.
            defaultToolsVersion = Constants.defaultToolsVersion;
 
            // But don't overwrite any existing tools path for this default we're choosing.
            if (toolsets.ContainsKey(Constants.defaultToolsVersion))
            {
                return defaultToolsVersion;
            }
            // There's no tools path already for 2.0, so use the path to the v2.0 .NET Framework.
            // If an old-fashioned caller sets BinPath property, or passed a BinPath to the constructor,
            // that will overwrite what we're setting here.
            ErrorUtilities.VerifyThrow(
                Constants.defaultToolsVersion == "2.0",
                "Getting 2.0 FX path so default should be 2.0");
            var pathToFramework = FrameworkLocationHelper.PathToDotNetFrameworkV20;
 
            // We could not find the default toolsversion because it was not installed on the machine. Fallback to the
            // one we expect to always be there when running msbuild 4.0.
            if (pathToFramework == null)
            {
                pathToFramework = FrameworkLocationHelper.PathToDotNetFrameworkV40;
                defaultToolsVersion = Constants.defaultFallbackToolsVersion;
            }
 
            // Again don't overwrite any existing tools path for this default we're choosing.
            if (toolsets.ContainsKey(defaultToolsVersion))
            {
                return defaultToolsVersion;
            }
            var defaultToolset = new Toolset(
                defaultToolsVersion,
                pathToFramework,
                environmentProperties,
                globalProperties,
                overrideTasksPath,
                defaultOverrideToolsVersion);
            toolsets.Add(defaultToolsVersion, defaultToolset);
 
            return defaultToolsVersion;
        }
 
        /// <summary>
        /// Populates the toolset collection passed in with the toolsets read from some location.
        /// </summary>
        /// <remarks>Internal for unit testing only</remarks>
        /// <returns>the default tools version if available, or null otherwise</returns>
        internal string ReadToolsets(
            Dictionary<string, Toolset> toolsets,
            PropertyDictionary<ProjectPropertyInstance> globalProperties,
            PropertyDictionary<ProjectPropertyInstance> initialProperties,
            bool accumulateProperties,
            out string msBuildOverrideTasksPath,
            out string defaultOverrideToolsVersion)
        {
            ErrorUtils.VerifyThrowArgumentNull(toolsets, "Toolsets");
 
            ReadEachToolset(toolsets, globalProperties, initialProperties, accumulateProperties);
 
            string defaultToolsVersion = DefaultToolsVersion;
 
            msBuildOverrideTasksPath = MSBuildOverrideTasksPath;
 
            defaultOverrideToolsVersion = DefaultOverrideToolsVersion;
 
            // We don't check whether the default tools version actually
            // corresponds to a toolset definition. That's because our default for
            // the indefinite future is 2.0, and 2.0 might not be installed, which is fine.
            // If a project tries to use 2.0 (or whatever the default is) in these circumstances
            // they'll get a nice error saying that toolset isn't available and listing those that are.
            return defaultToolsVersion;
        }
 
        /// <summary>
        /// Provides an enumerator over property definitions for a specified tools version
        /// </summary>
        /// <param name="toolsVersion">The tools version.</param>
        /// <returns>An enumeration of property definitions.</returns>
        protected abstract IEnumerable<ToolsetPropertyDefinition> GetPropertyDefinitions(string toolsVersion);
 
        /// <summary>
        /// Provides an enumerator over the set of sub-toolset names available to a particular
        /// toolsversion
        /// </summary>
        /// <param name="toolsVersion">The tools version.</param>
        /// <returns>An enumeration of the sub-toolsets that belong to that toolsversion.</returns>
        protected abstract IEnumerable<string> GetSubToolsetVersions(string toolsVersion);
 
        /// <summary>
        /// Provides an enumerator over property definitions for a specified sub-toolset version
        /// under a specified toolset version.
        /// </summary>
        /// <param name="toolsVersion">The tools version.</param>
        /// <param name="subToolsetVersion">The sub-toolset version.</param>
        /// <returns>An enumeration of property definitions.</returns>
        protected abstract IEnumerable<ToolsetPropertyDefinition> GetSubToolsetPropertyDefinitions(string toolsVersion, string subToolsetVersion);
 
        /// <summary>
        /// Returns a map of MSBuildExtensionsPath* property names/kind to list of search paths
        /// </summary>
        protected abstract Dictionary<string, ProjectImportPathMatch> GetProjectImportSearchPathsTable(string toolsVersion, string os);
 
        /// <summary>
        /// Reads all the toolsets and populates the given ToolsetCollection with them
        /// </summary>
        private void ReadEachToolset(
            Dictionary<string, Toolset> toolsets,
            PropertyDictionary<ProjectPropertyInstance> globalProperties,
            PropertyDictionary<ProjectPropertyInstance> initialProperties,
            bool accumulateProperties)
        {
            foreach (ToolsetPropertyDefinition toolsVersion in ToolsVersions)
            {
                // If there's already an existing toolset, it's of higher precedence, so
                // don't even bother to read this toolset in.
                if (!toolsets.ContainsKey(toolsVersion.Name))
                {
                    // We clone here because we don't want to interfere with the evaluation
                    // of subsequent Toolsets; otherwise, properties found during the evaluation
                    // of this Toolset would be persisted in initialProperties and appear
                    // to later Toolsets as Global or Environment properties from the Engine.
                    PropertyDictionary<ProjectPropertyInstance> initialPropertiesClone = new PropertyDictionary<ProjectPropertyInstance>(initialProperties);
 
                    Toolset toolset = ReadToolset(toolsVersion, globalProperties, initialPropertiesClone, accumulateProperties);
 
                    // Register toolset paths into list of immutable directories
                    // example: C:\Windows\Microsoft.NET\Framework
                    string frameworksPathPrefix32 = existingRootOrNull(initialPropertiesClone.GetProperty("MSBuildFrameworkToolsPath32")?.EvaluatedValue?.Trim());
                    FileClassifier.Shared.RegisterImmutableDirectory(frameworksPathPrefix32);
                    // example: C:\Windows\Microsoft.NET\Framework64
                    string frameworksPathPrefix64 = existingRootOrNull(initialPropertiesClone.GetProperty("MSBuildFrameworkToolsPath64")?.EvaluatedValue?.Trim());
                    FileClassifier.Shared.RegisterImmutableDirectory(frameworksPathPrefix64);
                    // example: C:\Windows\Microsoft.NET\FrameworkArm64
                    string frameworksPathPrefixArm64 = existingRootOrNull(initialPropertiesClone.GetProperty("MSBuildFrameworkToolsPathArm64")?.EvaluatedValue?.Trim());
                    FileClassifier.Shared.RegisterImmutableDirectory(frameworksPathPrefixArm64);
 
                    if (toolset != null)
                    {
                        toolsets[toolset.ToolsVersion] = toolset;
                    }
                }
            }
 
            string existingRootOrNull(string path)
            {
                if (!string.IsNullOrEmpty(path))
                {
                    try
                    {
                        path = Directory.GetParent(FileUtilities.EnsureNoTrailingSlash(path))?.FullName;
 
                        if (!Directory.Exists(path))
                        {
                            path = null;
                        }
                    }
                    catch
                    {
                        path = null;
                    }
                }
 
                return path;
            }
        }
 
        /// <summary>
        /// Reads the settings for a specified tools version
        /// </summary>
        private Toolset ReadToolset(
            ToolsetPropertyDefinition toolsVersion,
            PropertyDictionary<ProjectPropertyInstance> globalProperties,
            PropertyDictionary<ProjectPropertyInstance> initialProperties,
            bool accumulateProperties)
        {
            // Initial properties is the set of properties we're going to use to expand property expressions like $(foo)
            // in the values we read out of the registry or config file. We'll add to it as we pick up properties (including binpath)
            // from the registry or config file, so that properties there can be referenced in values below them.
            // After processing all the properties, we don't need initialProperties anymore.
            string toolsPath = null;
            string binPath = null;
            PropertyDictionary<ProjectPropertyInstance> properties = new PropertyDictionary<ProjectPropertyInstance>();
 
            IEnumerable<ToolsetPropertyDefinition> rawProperties = GetPropertyDefinitions(toolsVersion.Name);
 
            Expander<ProjectPropertyInstance, ProjectItemInstance> expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(initialProperties, FileSystems.Default);
 
            foreach (ToolsetPropertyDefinition property in rawProperties)
            {
                EvaluateAndSetProperty(property, properties, globalProperties, initialProperties, accumulateProperties, ref toolsPath, ref binPath, ref expander);
            }
 
            Dictionary<string, SubToolset> subToolsets = new Dictionary<string, SubToolset>(StringComparer.OrdinalIgnoreCase);
            IEnumerable<string> subToolsetVersions = GetSubToolsetVersions(toolsVersion.Name);
 
            foreach (string subToolsetVersion in subToolsetVersions)
            {
                string subToolsetToolsPath = null;
                string subToolsetBinPath = null;
                IEnumerable<ToolsetPropertyDefinition> rawSubToolsetProperties = GetSubToolsetPropertyDefinitions(toolsVersion.Name, subToolsetVersion);
                PropertyDictionary<ProjectPropertyInstance> subToolsetProperties = new PropertyDictionary<ProjectPropertyInstance>();
 
                // If we have a sub-toolset, any values defined here will override the toolset properties.
                foreach (ToolsetPropertyDefinition property in rawSubToolsetProperties)
                {
                    EvaluateAndSetProperty(property, subToolsetProperties, globalProperties, initialProperties, false /* do not ever accumulate sub-toolset properties */, ref subToolsetToolsPath, ref subToolsetBinPath, ref expander);
                }
 
                if (subToolsetToolsPath != null || subToolsetBinPath != null)
                {
                    InvalidToolsetDefinitionException.Throw("MSBuildToolsPathNotSupportedInSubToolsets", toolsVersion.Name, toolsVersion.Source.LocationString, subToolsetVersion);
                }
 
                subToolsets[subToolsetVersion] = new SubToolset(subToolsetVersion, subToolsetProperties);
            }
 
            // All tools versions must specify a value for MSBuildToolsPath (or MSBuildBinPath)
            if (String.IsNullOrEmpty(toolsPath) && String.IsNullOrEmpty(binPath))
            {
                return null;
            }
 
            // If both MSBuildBinPath and MSBuildToolsPath are present, they must be the same
            if (toolsPath != null && binPath != null && !toolsPath.Equals(binPath, StringComparison.OrdinalIgnoreCase))
            {
                InvalidToolsetDefinitionException.Throw("ConflictingValuesOfMSBuildToolsPath", toolsVersion.Name, toolsVersion.Source.LocationString);
            }
 
            Toolset toolset = null;
 
            try
            {
                var importSearchPathsTable = GetProjectImportSearchPathsTable(toolsVersion.Name, NativeMethodsShared.GetOSNameForExtensionsPath());
                toolset = new Toolset(toolsVersion.Name, toolsPath ?? binPath, properties, _environmentProperties, globalProperties, subToolsets, MSBuildOverrideTasksPath, DefaultOverrideToolsVersion, importSearchPathsTable);
            }
            catch (ArgumentException e)
            {
                InvalidToolsetDefinitionException.Throw("ErrorCreatingToolset", toolsVersion.Name, e.Message);
            }
 
            return toolset;
        }
 
        /// <summary>
        /// Processes a particular ToolsetPropertyDefinition into the correct value and location in the initial and/or final property set.
        /// </summary>
        /// <param name="property">The ToolsetPropertyDefinition being analyzed.</param>
        /// <param name="properties">The final set of properties that we wish this toolset property to be added to. </param>
        /// <param name="globalProperties">The global properties, used for expansion and to make sure none are overridden.</param>
        /// <param name="initialProperties">The initial properties, used for expansion and added to if "accumulateProperties" is true.</param>
        /// <param name="accumulateProperties">If "true", we add this property to the initialProperties dictionary, as well, so that properties later in the toolset can use this value.</param>
        /// <param name="toolsPath">If this toolset property is the "MSBuildToolsPath" property, we will return the value in this parameter.</param>
        /// <param name="binPath">If this toolset property is the "MSBuildBinPath" property, we will return the value in this parameter.</param>
        /// <param name="expander">The expander used to expand the value of the properties.  Ref because if we are accumulating the properties, we need to re-create the expander to account for the new property value.</param>
        private void EvaluateAndSetProperty(ToolsetPropertyDefinition property, PropertyDictionary<ProjectPropertyInstance> properties, PropertyDictionary<ProjectPropertyInstance> globalProperties, PropertyDictionary<ProjectPropertyInstance> initialProperties, bool accumulateProperties, ref string toolsPath, ref string binPath, ref Expander<ProjectPropertyInstance, ProjectItemInstance> expander)
        {
            if (String.Equals(property.Name, ReservedPropertyNames.toolsPath, StringComparison.OrdinalIgnoreCase))
            {
                toolsPath = ExpandPropertyUnescaped(property, expander);
                toolsPath = ExpandRelativePathsRelativeToExeLocation(toolsPath);
 
                if (accumulateProperties)
                {
                    SetProperty(
                        new ToolsetPropertyDefinition(ReservedPropertyNames.toolsPath, toolsPath, property.Source),
                        initialProperties,
                        globalProperties);
                }
            }
            else if (String.Equals(property.Name, ReservedPropertyNames.binPath, StringComparison.OrdinalIgnoreCase))
            {
                binPath = ExpandPropertyUnescaped(property, expander);
                binPath = ExpandRelativePathsRelativeToExeLocation(binPath);
 
                if (accumulateProperties)
                {
                    SetProperty(
                        new ToolsetPropertyDefinition(ReservedPropertyNames.binPath, binPath, property.Source),
                        initialProperties,
                        globalProperties);
                }
            }
            else if (ReservedPropertyNames.IsReservedProperty(property.Name))
            {
                // We don't allow toolsets to define reserved properties
                string baseMessage = ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CannotModifyReservedProperty", property.Name);
                InvalidToolsetDefinitionException.Throw("InvalidPropertyNameInToolset", property.Name, property.Source.LocationString, baseMessage);
            }
            else
            {
                // It's an arbitrary property
                property.Value = ExpandPropertyUnescaped(property, expander);
 
                SetProperty(property, properties, globalProperties);
 
                if (accumulateProperties)
                {
                    SetProperty(property, initialProperties, globalProperties);
                }
            }
 
            if (accumulateProperties)
            {
                expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(initialProperties, FileSystems.Default);
            }
        }
 
        /// <summary>
        /// Expands the given unexpanded property expression using the properties in the
        /// given expander.
        /// </summary>
        private string ExpandPropertyUnescaped(ToolsetPropertyDefinition property, Expander<ProjectPropertyInstance, ProjectItemInstance> expander)
        {
            try
            {
                return expander.ExpandIntoStringAndUnescape(property.Value, ExpanderOptions.ExpandProperties, property.Source);
            }
            catch (InvalidProjectFileException ex)
            {
                InvalidToolsetDefinitionException.Throw(ex, "ErrorEvaluatingToolsetPropertyExpression", property.Value, property.Source.LocationString, ex.BaseMessage);
            }
 
            return string.Empty;
        }
 
        /// <summary>
        /// Sets the given property in the given property group.
        /// </summary>
        private void SetProperty(ToolsetPropertyDefinition property, PropertyDictionary<ProjectPropertyInstance> propertyGroup, PropertyDictionary<ProjectPropertyInstance> globalProperties)
        {
            try
            {
                // Global properties cannot be overwritten
                if (globalProperties[property.Name] == null)
                {
                    propertyGroup.Set(ProjectPropertyInstance.Create(property.Name, EscapingUtilities.UnescapeAll(property.Value), true /* may be reserved */, false /* not immutable */));
                }
            }
            catch (ArgumentException ex)
            {
                InvalidToolsetDefinitionException.Throw(ex, "InvalidPropertyNameInToolset", property.Name, property.Source.LocationString, ex.Message);
            }
        }
 
        /// <summary>
        /// Given a path, de-relativizes it using the location of the currently
        /// executing .exe as the base directory. For example, the path "..\foo"
        /// becomes "c:\windows\microsoft.net\framework\foo" if the current exe is
        /// "c:\windows\microsoft.net\framework\v3.5.1234\msbuild.exe".
        /// If the path is not relative, it is returned without modification.
        /// If the path is invalid, it is returned without modification.
        /// </summary>
        private string ExpandRelativePathsRelativeToExeLocation(string path)
        {
            try
            {
                // Trim, because we don't want to do anything with empty values
                // (those should cause an error)
                string trimmedValue = path.Trim();
                if (trimmedValue.Length > 0 && !Path.IsPathRooted(trimmedValue))
                {
                    path = Path.GetFullPath(
                        Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, trimmedValue));
                }
            }
            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
            {
                // This means that the path looked relative, but was an invalid path. In this case, we'll
                // just not expand it, and carry on - to be consistent with what happens when there's a
                // non-relative bin path with invalid characters. The problem will be detected later when
                // it's used in a project file.
            }
 
            return path;
        }
    }
 
    /// <summary>
    /// struct representing a reference to MSBuildExtensionsPath* property
    /// </summary>
    internal struct MSBuildExtensionsPathReferenceKind
    {
        /// <summary>
        /// MSBuildExtensionsPathReferenceKind instance for property named "MSBuildExtensionsPath"
        /// </summary>
        public static readonly MSBuildExtensionsPathReferenceKind Default = new MSBuildExtensionsPathReferenceKind("MSBuildExtensionsPath");
 
        /// <summary>
        /// MSBuildExtensionsPathReferenceKind instance for property named "MSBuildExtensionsPath32"
        /// </summary>
        public static readonly MSBuildExtensionsPathReferenceKind Path32 = new MSBuildExtensionsPathReferenceKind("MSBuildExtensionsPath32");
 
        /// <summary>
        /// MSBuildExtensionsPathReferenceKind instance for property named "MSBuildExtensionsPath64"
        /// </summary>
        public static readonly MSBuildExtensionsPathReferenceKind Path64 = new MSBuildExtensionsPathReferenceKind("MSBuildExtensionsPath64");
 
        /// <summary>
        /// MSBuildExtensionsPathReferenceKind instance representing no MSBuildExtensionsPath* property reference
        /// </summary>
        public static readonly MSBuildExtensionsPathReferenceKind None = new MSBuildExtensionsPathReferenceKind(String.Empty);
 
        private MSBuildExtensionsPathReferenceKind(string value)
        {
            StringRepresentation = value;
        }
 
        /// <summary>
        /// String representation of the property reference - eg. "MSBuildExtensionsPath32"
        /// </summary>
        public string StringRepresentation { get; private set; }
 
        /// <summary>
        /// Returns the corresponding property name - eg. "$(MSBuildExtensionsPath32)"
        /// </summary>
        public readonly string MSBuildPropertyName => String.Format($"$({StringRepresentation})");
 
        /// <summary>
        /// Tries to find a reference to MSBuildExtensionsPath* property in the given string
        /// </summary>
        public static MSBuildExtensionsPathReferenceKind FindIn(string expression)
        {
            if (expression.IndexOf("$(MSBuildExtensionsPath)") >= 0)
            {
                return MSBuildExtensionsPathReferenceKind.Default;
            }
 
            if (expression.IndexOf("$(MSBuildExtensionsPath32)") >= 0)
            {
                return MSBuildExtensionsPathReferenceKind.Path32;
            }
 
            if (expression.IndexOf("$(MSBuildExtensionsPath64)") >= 0)
            {
                return MSBuildExtensionsPathReferenceKind.Path64;
            }
 
            return MSBuildExtensionsPathReferenceKind.None;
        }
    }
}