// 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.Text;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;
#nullable disable
namespace Microsoft.Build.Utilities;
public static partial class ToolLocationHelper
{
/// <summary>
/// Cache the results of reading the redist list so that we do not have to read the redist list over and over again to get the chains.
/// </summary>
private static Dictionary<string, string> s_chainedReferenceAssemblyPath;
/// <summary>
/// Cache the results of calling the GetPathToReferenceAssemblies so that we do not recalculate it every time we call the method
/// </summary>
private static Dictionary<string, IList<string>> s_cachedReferenceAssemblyPaths;
/// <summary>
/// Cache the display name for the TFM/FrameworkName, keyed by the target framework directory.
/// This is generated by the "Name" attribute on the root tag of the primary matching redist list.
/// Value is never an empty string or null: a name will be synthesized if necessary.
/// </summary>
private static Dictionary<string, string> s_cachedTargetFrameworkDisplayNames;
/// <summary>
/// Character used to separate search paths specified for MSBuildExtensionsPath* in
/// the config file
/// </summary>
private static readonly char[] _separatorForFallbackSearchPaths = MSBuildConstants.SemicolonChar;
/// <summary>
/// Delegate to a method which takes a version enumeration and return a string path
/// </summary>
internal delegate string VersionToPath(TargetDotNetFrameworkVersion version);
/// <summary>
/// Returns the path to the reference assemblies location for the given target framework's standard libraries (i.e. mscorlib).
/// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
/// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
/// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile)
{
IList<string> referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile);
// Check each returned reference assembly directory for one containing mscorlib.dll
// When we find it (most of the time it will be the first in the set) we'll
// return that directory.
foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories)
{
if (FileSystems.Default.FileExists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll")))
{
// We found the framework reference assembly directory with mscorlib in it
// that's our standard lib path, so return it, with no trailing slash.
return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory);
}
}
// We didn't find a standard library path in our set, return empty.
return string.Empty;
}
/// <summary>
/// Returns the path to mscorlib and system.dll
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget)
=> GetPathToStandardLibraries(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, platformTarget, null);
/// <summary>
/// Returns the path to mscorlib and system.dll
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
/// <param name="targetFrameworkRootPath">Root directory where the target framework will be looked for. Uses default path if this is null</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget, string targetFrameworkRootPath)
=> GetPathToStandardLibraries(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, platformTarget, targetFrameworkRootPath, null);
/// <summary>
/// Returns the path to mscorlib and system.dll
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
/// <param name="targetFrameworkRootPath">Root directory where the target framework will be looked for. Uses default path if this is null</param>
/// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in @targetFrameworkRootPath</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths)
{
ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier);
ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion);
Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion);
// For .net framework less than 4 the mscorlib should be found in the .net 2.0 directory
if (targetFrameworkIdentifier.Equals(FrameworkLocationHelper.dotNetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) && frameworkVersion.Major < 4 && string.IsNullOrEmpty(targetFrameworkProfile))
{
// The default
DotNetFrameworkArchitecture targetedArchitecture = DotNetFrameworkArchitecture.Current;
if (NativeMethods.IsWindows && platformTarget != null)
{
// If we are a 32 bit operating system the we should always return the 32 bit directory, or we are targeting x86, arm is also 32 bit
if (!Environment.Is64BitOperatingSystem || platformTarget.Equals("x86", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("arm", StringComparison.OrdinalIgnoreCase))
{
targetedArchitecture = DotNetFrameworkArchitecture.Bitness32;
}
else if (platformTarget.Equals("x64", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("Itanium", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("arm64", StringComparison.OrdinalIgnoreCase))
{
targetedArchitecture = DotNetFrameworkArchitecture.Bitness64;
}
}
string legacyMsCorlib20Path = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(targetedArchitecture);
if (legacyMsCorlib20Path != null && FileSystems.Default.FileExists(Path.Combine(legacyMsCorlib20Path, "mscorlib.dll")))
{
// We found the framework reference assembly directory with mscorlib in it
// that's our standard lib path, so return it, with no trailing slash.
return FileUtilities.EnsureNoTrailingSlash(legacyMsCorlib20Path);
}
// If for some reason the 2.0 framework is not installed in its default location then maybe someone is using the ".net 4.0" reference assembly
// location, if so then we can just use what ever version they passed in because it should be MSIL now and not bit specific.
}
IList<string> referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, targetFrameworkRootPath, targetFrameworkFallbackSearchPaths);
// Check each returned reference assembly directory for one containing mscorlib.dll
// When we find it (most of the time it will be the first in the set) we'll
// return that directory.
foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories)
{
if (FileSystems.Default.FileExists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll")))
{
// We found the framework reference assembly directory with mscorlib in it
// that's our standard lib path, so return it, with no trailing slash.
return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory);
}
}
// We didn't find a standard library path in our set, return empty.
return string.Empty;
}
/// <summary>
/// Returns the paths to the reference assemblies location for the given target framework.
/// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
/// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
/// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile)
=> GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, null);
/// <summary>
/// Returns the paths to the reference assemblies location for the given target framework.
/// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
/// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
/// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
/// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
/// Uses the default path if this is null.
/// </param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string targetFrameworkRootPath)
=> GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, targetFrameworkRootPath, null);
/// <summary>
/// Returns the paths to the reference assemblies location for the given target framework.
/// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
/// In addition when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
/// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
/// </summary>
/// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
/// <param name="targetFrameworkVersion">Version being targeted</param>
/// <param name="targetFrameworkProfile">Profile being targeted</param>
/// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
/// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
/// Uses the default path if this is null.
/// </param>
/// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in <paramref name="targetFrameworkRootPath"/></param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths)
{
ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion);
ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier);
ArgumentNullException.ThrowIfNull(targetFrameworkProfile);
Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion);
FrameworkNameVersioning targetFrameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, frameworkVersion, targetFrameworkProfile);
return GetPathToReferenceAssemblies(targetFrameworkRootPath, targetFrameworkFallbackSearchPaths, targetFrameworkName);
}
/// <summary>
/// Returns the paths to the reference assemblies location for the given target framework.
/// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
/// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
/// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
/// </summary>
/// <param name="frameworkName">Framework required</param>
/// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(FrameworkNameVersioning frameworkName)
{
// Verify the framework class passed in is not null. Other than being null the class will ensure the framework moniker is correct
ArgumentNullException.ThrowIfNull(frameworkName);
IList<string> paths =
GetPathToReferenceAssemblies(
FrameworkLocationHelper.programFilesReferenceAssemblyLocation,
frameworkName);
return paths;
}
/// <summary>
/// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since
/// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly.
/// </summary>
private static string VersionToDotNetFrameworkPath(VersionToPath PathToDotNetFramework, TargetDotNetFrameworkVersion version)
=> PathToDotNetFramework == null
? GetPathToDotNetFramework(version)
: PathToDotNetFramework(version);
/// <summary>
/// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since
/// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly.
/// </summary>
private static string VersionToDotNetReferenceAssemblies(VersionToPath PathToDotReferenceAssemblies, TargetDotNetFrameworkVersion version)
=> PathToDotReferenceAssemblies == null
? GetPathToDotNetFrameworkReferenceAssemblies(version)
: PathToDotReferenceAssemblies(version);
/// <summary>
/// Generate the list of reference assembly paths for well known .net framework versions
/// </summary>
/// <param name="frameworkName">Target framework moniker class which contains the targetframeworkVersion</param>
/// <param name="PathToDotNetFramework"></param>
/// <param name="PathToReferenceAssemblies"></param>
/// <returns>A collection of strings which list the chained reference assembly paths with the highest version being first</returns>
internal static IList<string> HandleLegacyDotNetFrameworkReferenceAssemblyPaths(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies, FrameworkNameVersioning frameworkName)
{
if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion20)
{
return HandleLegacy20(PathToDotNetFramework);
}
else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion30)
{
return HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies);
}
else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion35)
{
return HandleLegacy35(PathToDotNetFramework, PathToReferenceAssemblies);
}
// Don't know the framework send back an empty list because it does not exist
return new List<string>();
}
/// <summary>
/// Generate the key which will be used for the reference assembly cache so that multiple static methods will generate it in the same way.
/// </summary>
private static string GenerateReferenceAssemblyCacheKey(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName) => targetFrameworkRootPath + "|" + frameworkName.FullName;
/// <summary>
/// Create the shared cache if it is not null
/// </summary>
private static void CreateReferenceAssemblyPathsCache()
{
lock (s_locker)
{
s_cachedReferenceAssemblyPaths ??= new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
}
}
/// <summary>
/// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing
/// </summary>
private static IList<string> HandleLegacy35(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies)
{
var referencePaths = new List<string>();
string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version35);
string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version35);
if (referenceAssemblyPath != null && dotNetFrameworkPath != null)
{
referencePaths.Add(referenceAssemblyPath);
referencePaths.Add(dotNetFrameworkPath);
}
else
{
return referencePaths;
}
// This method will return either an empty list or a list with elements in the order 3.0 2.0
IList<string> referenceAssembly30Paths = HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies);
referencePaths.AddRange(referenceAssembly30Paths);
return referencePaths;
}
/// <summary>
/// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing
/// </summary>
private static IList<string> HandleLegacy30(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies)
{
var referencePaths = new List<string>();
string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version30);
string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version30);
if (referenceAssemblyPath != null && dotNetFrameworkPath != null)
{
referencePaths.Add(referenceAssemblyPath);
referencePaths.Add(dotNetFrameworkPath);
}
else
{
return referencePaths;
}
IList<string> referenceAssembly20Paths = HandleLegacy20(PathToDotNetFramework);
referencePaths.AddRange(referenceAssembly20Paths);
return referencePaths;
}
/// <summary>
/// Check to see if .net 2.0 is installed
/// </summary>
private static IList<string> HandleLegacy20(VersionToPath PathToDotNetFramework)
{
var referencePaths = new List<string>();
string referencePath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version20);
if (referencePath != null)
{
referencePaths.Add(referencePath);
}
return referencePaths;
}
/// <summary>
/// Returns the paths to the reference assemblies location for the given framework version relative to a given targetFrameworkRoot.
/// The method will not check to see if the path exists or not.
/// </summary>
/// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
/// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
/// </param>
/// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in @targetFrameworkRootPath</param>
/// <param name="frameworkName">A frameworkName class which represents a TargetFrameworkMoniker. This cannot be null.</param>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths, FrameworkNameVersioning frameworkName)
{
IList<string> pathsList = string.IsNullOrEmpty(targetFrameworkRootPath)
? GetPathToReferenceAssemblies(frameworkName)
: GetPathToReferenceAssemblies(targetFrameworkRootPath, frameworkName);
if (pathsList?.Count > 0)
{
return pathsList;
}
if (!string.IsNullOrEmpty(targetFrameworkFallbackSearchPaths))
{
foreach (string rootPath in targetFrameworkFallbackSearchPaths.Split(_separatorForFallbackSearchPaths, StringSplitOptions.RemoveEmptyEntries))
{
pathsList = GetPathToReferenceAssemblies(rootPath, frameworkName);
if (pathsList?.Count > 0)
{
return pathsList;
}
}
}
return pathsList ?? [];
}
/// <summary>
/// Returns the paths to the reference assemblies location for the given framework version relative to a given targetFrameworkRoot.
/// The method will not check to see if the path exists or not.
/// </summary>
/// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
/// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
/// </param>
/// <param name="frameworkName">A frameworkName class which represents a TargetFrameworkMoniker. This cannot be null.</param>
/// <returns>Collection of reference assembly locations.</returns>
public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName)
{
// Verify the root path is not null throw an ArgumentNullException if the given string parameter is null and ArgumentException if it has zero length.
ArgumentException.ThrowIfNullOrEmpty(targetFrameworkRootPath);
// Verify the framework class passed in is not null. Other than being null the class will ensure it is consistent and the internal state is correct
ArgumentNullException.ThrowIfNull(frameworkName);
string referenceAssemblyCacheKey = GenerateReferenceAssemblyCacheKey(targetFrameworkRootPath, frameworkName);
CreateReferenceAssemblyPathsCache();
lock (s_locker)
{
if (s_cachedReferenceAssemblyPaths.TryGetValue(referenceAssemblyCacheKey, out IList<string> referenceAssemblies))
{
return referenceAssemblies;
}
}
// Try and find the reference assemblies using the reference assembly path generation algorithm
IList<string> dotNetFrameworkReferenceAssemblies = GetPathAndChainReferenceAssemblyLocations(targetFrameworkRootPath, frameworkName, true);
// We have not found any reference assembly locations, if we are the .net framework we can try and fallback to the old legacy tool location helper methods
if (string.Equals(frameworkName.Identifier, ".NETFramework", StringComparison.OrdinalIgnoreCase)
&& dotNetFrameworkReferenceAssemblies.Count == 0)
{
if (string.IsNullOrEmpty(frameworkName.Profile)) // profiles are always in new locations
{
// If the identifier is ".NET Framework" and the version is a well know legacy version. Manually generate the list of reference assembly paths
// based on the known chaining order. Pass null in for the two delegates so we call the static methods rather than require the creation and calling
// of two delegates
dotNetFrameworkReferenceAssemblies = HandleLegacyDotNetFrameworkReferenceAssemblyPaths(
PathToDotNetFramework: null,
PathToReferenceAssemblies: null,
frameworkName);
}
}
for (int i = 0; i < dotNetFrameworkReferenceAssemblies.Count; i++)
{
if (
!dotNetFrameworkReferenceAssemblies[i].EndsWith(
Path.DirectorySeparatorChar.ToString(),
StringComparison.Ordinal))
{
dotNetFrameworkReferenceAssemblies[i] += Path.DirectorySeparatorChar;
}
}
lock (s_locker)
{
s_cachedReferenceAssemblyPaths[referenceAssemblyCacheKey] = dotNetFrameworkReferenceAssemblies;
}
return dotNetFrameworkReferenceAssemblies;
}
/// <summary>
/// Figures out a display name given the target framework details.
/// This is the equivalent of the target framework moniker, but for display.
/// If one cannot be found from the redist list file, a synthesized one is returned, so there is always a display name.
/// </summary>
public static string GetDisplayNameForTargetFrameworkDirectory(string targetFrameworkDirectory, FrameworkNameVersioning frameworkName)
{
string displayName;
lock (s_locker)
{
if (s_cachedTargetFrameworkDisplayNames != null
&& s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName))
{
return displayName;
}
}
// Not in the cache, try to find it and if so cache it
ChainReferenceAssemblyPath(targetFrameworkDirectory);
lock (s_locker)
{
if (s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName))
{
return displayName;
}
}
// Still don't have one.
// Probably it's 3.5 or earlier: make something reasonable.
// VS uses the same algorithm to find something to display
StringBuilder displayNameBuilder = new StringBuilder();
displayNameBuilder.Append(frameworkName.Identifier);
displayNameBuilder.Append(' ');
displayNameBuilder.Append('v').Append(frameworkName.Version.ToString());
if (!string.IsNullOrEmpty(frameworkName.Profile))
{
displayNameBuilder.Append(' ');
displayNameBuilder.Append(frameworkName.Profile);
}
displayName = displayNameBuilder.ToString();
// Cache it
lock (s_locker)
{
s_cachedTargetFrameworkDisplayNames[targetFrameworkDirectory] = displayName;
}
return displayName;
}
/// <summary>
/// Returns the paths to the reference assemblies location for the given framework version and properly chains the reference assemblies if required.
/// </summary>
/// <returns>Collection of reference assembly locations.</returns>
private static IList<string> GetPathAndChainReferenceAssemblyLocations(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName, bool chain)
{
var referencePaths = new List<string>();
string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName);
if (FileSystems.Default.DirectoryExists(path))
{
referencePaths.Add(path);
if (chain)
{
while (!string.IsNullOrEmpty(path))
{
// Will return String.Empty when there are no longer any paths to chain to
// We will return null if the chain is invalid and we need to return an empty chain.
path = ChainReferenceAssemblyPath(path);
if (!string.IsNullOrEmpty(path))
{
if (referencePaths.Contains(path))
{
break;
}
referencePaths.Add(path);
}
else if (path == null)
{
// We have an invalid chain, we need to clear out any reference paths we have already added.
referencePaths.Clear();
break;
}
}
}
}
return referencePaths;
}
/// <summary>
/// Take the path to a reference assembly directory which contains a RedistList folder which then contains a FrameworkList.xml file.
/// We will read in the xml file and determine if it has an IncludedFramework element in the redist list. If it does it will calculate
/// the path where the next link in the chain should be and return that path.
/// Also, when reading the redist list, if any display name is found it will be cached, keyed off the path passed in.
/// </summary>
/// <returns>Return null if we could not chain due to an error or the path not being found. return String.Empty if there is no next element in the chain</returns>
internal static string ChainReferenceAssemblyPath(string targetFrameworkDirectory)
{
string path = Path.GetFullPath(targetFrameworkDirectory);
lock (s_locker)
{
// Cache the results of the chain search so that we do not have to do an expensive read more than once per process per redist list.
s_chainedReferenceAssemblyPath ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
s_cachedTargetFrameworkDisplayNames ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (s_chainedReferenceAssemblyPath.TryGetValue(path, out string cachedPath))
{
return cachedPath;
}
}
// Read in the redist list at the specified path, and return
// the display name and the "include framework" value for chaining.
// If display name is not available, returns empty string.
// If include framework is not available, returns null.
// Caches the display name keyed by the path.
// Make sure we have a directory with a redist list folder and a FrameworkList.xml file in there as this is what we will use for chaining.
string redistListFolder = Path.Combine(path, "RedistList");
string redistFilePath = Path.Combine(redistListFolder, "FrameworkList.xml");
// If the redist list does not exist then the entire chain is incorrect.
if (!FileSystems.Default.FileExists(redistFilePath))
{
lock (s_locker)
{
s_chainedReferenceAssemblyPath[path] = null;
s_cachedTargetFrameworkDisplayNames[path] = null;
}
return null;
}
string includeFramework = null;
string displayName = null;
string redirectPath = null;
try
{
// Read in the xml file looking for the includeFramework inorder to chain.
XmlReaderSettings readerSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true };
FileStream fs = File.OpenRead(redistFilePath);
using (XmlReader reader = XmlReader.Create(fs, readerSettings))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (string.Equals(reader.Name, "FileList", StringComparison.OrdinalIgnoreCase))
{
reader.MoveToFirstAttribute();
do
{
if (string.Equals(reader.Name, "IncludeFramework", StringComparison.OrdinalIgnoreCase))
{
includeFramework = reader.Value;
continue;
}
if (string.Equals(reader.Name, "Name", StringComparison.OrdinalIgnoreCase))
{
displayName = reader.Value;
continue;
}
}
while (reader.MoveToNextAttribute());
reader.MoveToElement();
break;
}
}
}
}
}
catch (XmlException ex)
{
throw new InvalidOperationException(SR.FormatInvalidRedistFile(redistFilePath, ex.Message));
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
throw new InvalidOperationException(SR.FormatInvalidRedistFile(redistFilePath, ex.Message));
}
// Cache the display name if we have one
if (displayName != null)
{
lock (s_locker)
{
s_cachedTargetFrameworkDisplayNames[path] = displayName;
}
}
string pathToReturn = string.Empty;
try
{
// The IncludeFramework element could not be found so our chain is done.
if (!string.IsNullOrEmpty(includeFramework))
{
// Take the path which should point to something like c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.1
// We will take the path, to "up" a directory then append the name found in the redist. For example if the redist list had v4.0
// the path which would be expected would be c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.0
pathToReturn = path;
pathToReturn = Directory.GetParent(pathToReturn).FullName;
pathToReturn = Path.Combine(pathToReturn, includeFramework);
pathToReturn = Path.GetFullPath(pathToReturn);
// The directory which we are chaining to does not exist, return null indicating the chain is incorrect.
if (!FileSystems.Default.DirectoryExists(pathToReturn))
{
pathToReturn = null;
}
}
// We may also have a redirect path
else if (!string.IsNullOrEmpty(redirectPath) && FileSystems.Default.DirectoryExists(redirectPath))
{
pathToReturn = redirectPath;
}
lock (s_locker)
{
s_chainedReferenceAssemblyPath[path] = pathToReturn;
}
return pathToReturn;
}
catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
{
throw new InvalidOperationException(SR.FormatCouldNotCreateChain(path, pathToReturn, e.Message));
}
}
/// <summary>
/// Will return the root location for the reference assembly directory under the program files directory.
/// </summary>
/// <returns></returns>
public static string GetProgramFilesReferenceAssemblyRoot()
=> FrameworkLocationHelper.programFilesReferenceAssemblyLocation;
}