File: GetReferenceAssemblyPaths.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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 Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;
 
#if FEATURE_GAC
using System.Threading;
using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture;
#endif
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Returns the reference assembly paths to the various frameworks
    /// </summary>
    [MSBuildMultiThreadableTask]
    public class GetReferenceAssemblyPaths : TaskExtension, IMultiThreadableTask
    {
        /// <inheritdoc />
        public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
 
        #region Data
#if FEATURE_GAC
        /// <summary>
        /// This is the sentinel assembly for .NET FX 3.5 SP1
        /// Used to determine if SP1 of 3.5 is installed
        /// </summary>
        private const string NET35SP1SentinelAssemblyName = "System.Data.Entity, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL";
 
        /// <summary>
        /// Cache in a static whether or not we have found the 35sp1sentinel assembly.
        /// </summary>
        private static readonly Lazy<bool> s_net35SP1SentinelAssemblyFound = new Lazy<bool>(() =>
        {
            // get an assemblyname from the string representation of the sentinel assembly name
            var sentinelAssemblyName = new AssemblyNameExtension(NET35SP1SentinelAssemblyName);
            string path = GlobalAssemblyCache.GetLocation(
                sentinelAssemblyName,
                SystemProcessorArchitecture.MSIL,
                runtimeVersion => "v2.0.50727",
                new Version("2.0.57027"),
                false,
                new FileExists(p => FileUtilities.FileExistsNoThrow(p)),
                GlobalAssemblyCache.pathFromFusionName,
                GlobalAssemblyCache.gacEnumerator,
                false);
 
            return !string.IsNullOrEmpty(path);
        }, LazyThreadSafetyMode.PublicationOnly);
#endif
 
        /// <summary>
        /// Hold the reference assembly paths based on the passed in targetframeworkmoniker.
        /// </summary>
        private IList<string> _tfmPaths;
 
        /// <summary>
        /// Hold the reference assembly paths based on the passed in targetframeworkmoniker without considering any profile passed in.
        /// </summary>
        private IList<string> _tfmPathsNoProfile;
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Returns the path based on the passed in TargetFrameworkMoniker. If the TargetFrameworkMoniker is null or empty
        /// this path will be empty.
        /// </summary>
        [Output]
        public string[] ReferenceAssemblyPaths
        {
            get
            {
                if (_tfmPaths != null)
                {
                    var pathsToReturn = new string[_tfmPaths.Count];
                    _tfmPaths.CopyTo(pathsToReturn, 0);
                    return pathsToReturn;
                }
                else
                {
                    return [];
                }
            }
        }
 
        /// <summary>
        /// Returns the path based on the passed in TargetFrameworkMoniker without considering the profile part of the moniker. If the TargetFrameworkMoniker is null or empty
        /// this path will be empty.
        /// </summary>
        [Output]
        public string[] FullFrameworkReferenceAssemblyPaths
        {
            get
            {
                if (_tfmPathsNoProfile != null)
                {
                    string[] pathsToReturn = new string[_tfmPathsNoProfile.Count];
                    _tfmPathsNoProfile.CopyTo(pathsToReturn, 0);
                    return pathsToReturn;
                }
                else
                {
                    return [];
                }
            }
        }
 
        /// <summary>
        /// The target framework moniker to get the reference assembly paths for
        /// </summary>
        public string TargetFrameworkMoniker { get; set; }
 
        /// <summary>
        /// The root path to use to generate the reference assembly path
        /// </summary>
        public string RootPath { get; set; }
 
        /// <summary>
        /// By default GetReferenceAssemblyPaths performs simple checks
        /// to ensure that certain runtime frameworks are installed depending on the
        /// target framework.
        /// set BypassFrameworkInstallChecks to true in order to bypass those checks.
        /// </summary>
        public bool BypassFrameworkInstallChecks { get; set; }
 
        /// <summary>
        /// If set to true, the task will not generate an error (or a warning) if the reference assemblies cannot be found.
        /// This allows the task to be used to check whether reference assemblies for a framework are available.
        /// </summary>
        public bool SuppressNotFoundError { get; set; }
 
        /// <summary>
        /// Gets the display name for the targetframeworkmoniker
        /// </summary>
        [Output]
        public string TargetFrameworkMonikerDisplayName { get; set; }
 
        /// <summary>
        /// Target frameworks are looked up in @RootPath. If it cannot be found
        /// there, then paths in @TargetFrameworkFallbackSearchPaths
        /// are used for the lookup, in order. This can have multiple paths, separated
        /// by ';'
        /// </summary>
        public string TargetFrameworkFallbackSearchPaths
        {
            get;
            set;
        }
 
        #endregion
 
        #region ITask Members
 
        /// <summary>
        /// If the target framework moniker is set, generate the correct Paths.
        /// </summary>
        public override bool Execute()
        {
            AbsolutePath? absoluteRootPath = !string.IsNullOrEmpty(RootPath)
                ? TaskEnvironment.GetAbsolutePath(RootPath)
                : new AbsolutePath(RootPath, ignoreRootedCheck: true);
            IList<AbsolutePath> absoluteFallbackSearchPaths = ResolveAbsoluteFallbackSearchPaths(TargetFrameworkFallbackSearchPaths);
 
            FrameworkNameVersioning moniker;
            FrameworkNameVersioning monikerWithNoProfile = null;
 
            // Are we targeting a profile.
            bool targetingProfile;
 
            try
            {
                moniker = new FrameworkNameVersioning(TargetFrameworkMoniker);
                targetingProfile = !String.IsNullOrEmpty(moniker.Profile);
 
                // If we are targeting a profile we need to generate a set of reference assembly paths which describe where the full framework
                //  exists, to do so we need to get the reference assembly location without the profile as part of the moniker.
                if (targetingProfile)
                {
                    monikerWithNoProfile = new FrameworkNameVersioning(moniker.Identifier, moniker.Version);
                }
 
#if FEATURE_GAC
                // This is a very specific "hack" to ensure that when we're targeting certain .NET Framework versions that
                // WPF gets to rely on .NET FX 3.5 SP1 being installed on the build machine.
                // This only needs to occur when we are targeting a .NET FX prior to v4.0
                if (!BypassFrameworkInstallChecks && moniker.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase) &&
                    moniker.Version.Major < 4)
                {
                    // We did not find the SP1 sentinel assembly in the GAC. Therefore we must assume that SP1 isn't installed
                    if (!s_net35SP1SentinelAssemblyFound.Value)
                    {
                        Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.NETFX35SP1NotIntstalled", TargetFrameworkMoniker);
                    }
                }
#endif
            }
            catch (ArgumentException e)
            {
                Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.InvalidTargetFrameworkMoniker", TargetFrameworkMoniker, e.Message);
                return false;
            }
 
            try
            {
                _tfmPaths = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, moniker);
 
                if (_tfmPaths?.Count > 0)
                {
                    TargetFrameworkMonikerDisplayName = ToolLocationHelper.GetDisplayNameForTargetFrameworkDirectory(_tfmPaths[0], moniker);
                }
 
                // If there is a profile get the paths without the profile.
                // There is no point in generating the full framework paths if profile path could not be found.
                if (targetingProfile && _tfmPaths != null)
                {
                    _tfmPathsNoProfile = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, monikerWithNoProfile);
                }
 
                // The path with out the profile is just the reference assembly paths.
                if (!targetingProfile)
                {
                    _tfmPathsNoProfile = _tfmPaths;
                }
            }
            catch (Exception e)
            {
                // The reason we need to do exception E here is because we are in a task and have the ability to log the message and give the user
                // feedback as to its cause, tasks if at all possible should not have exception leave them.
                Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.ProblemGeneratingReferencePaths", TargetFrameworkMoniker, e.Message);
 
                if (ExceptionHandling.IsCriticalException(e))
                {
                    throw;
                }
 
                _tfmPathsNoProfile = null;
                TargetFrameworkMonikerDisplayName = null;
            }
 
            return !Log.HasLoggedErrors;
        }
 
        /// <summary>
        /// Generate the set of chained reference assembly paths
        /// </summary>
        private IList<String> GetPaths(AbsolutePath? rootPath, IList<AbsolutePath> fallbackSearchPaths, FrameworkNameVersioning frameworkmoniker)
        {
            string fallbackSearchPathsJoined = string.Join(";", fallbackSearchPaths);
 
            IList<String> pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies(
                                                frameworkmoniker.Identifier,
                                                frameworkmoniker.Version.ToString(),
                                                frameworkmoniker.Profile,
                                                rootPath?.Value,
                                                fallbackSearchPathsJoined);
 
            if (!SuppressNotFoundError)
            {
                // No reference assembly paths could be found, log an error so an invalid build will not be produced.
                // 1/26/16: Note this was changed from a warning to an error (see GitHub #173).
                if (pathsToReturn.Count == 0)
                {
                    // Fixes bad error message when an old SDK assumes "net50" means ".NETFramework 5.0" instead of "netcoreapp 5.0"
                    // https://github.com/dotnet/msbuild/issues/5820
                    if (frameworkmoniker.Identifier == ".NETFramework" && frameworkmoniker.Version.Major >= 5)
                    {
                        Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.OutOfDateSDK", frameworkmoniker.ToString());
                    }
                    else
                    {
                        Log.LogErrorWithCodeFromResources("GetReferenceAssemblyPaths.NoReferenceAssemblyDirectoryFound", frameworkmoniker.ToString());
                    }
                }
            }
 
            return pathsToReturn;
        }
 
        /// <summary>
        /// Resolves each semicolon-separated fallback search path to absolute via TaskEnvironment.
        /// </summary>
        private IList<AbsolutePath> ResolveAbsoluteFallbackSearchPaths(string fallbackSearchPaths)
        {
            if (string.IsNullOrEmpty(fallbackSearchPaths))
            {
                return [];
            }
 
            string[] parts = fallbackSearchPaths.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
            var result = new AbsolutePath[parts.Length];
            for (int i = 0; i < parts.Length; i++)
            {
                result[i] = TaskEnvironment.GetAbsolutePath(parts[i]);
            }
 
            return result;
        }
 
        #endregion
    }
}