File: Utilities\NuGetFrameworkWrapper.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.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
 
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Wraps the NuGet.Frameworks assembly, which is referenced by reflection and optionally loaded into a separate AppDomain for performance.
    /// </summary>
    internal sealed partial class NuGetFrameworkWrapper
#if FEATURE_APPDOMAIN
        : MarshalByRefObject
#endif
    {
        private const string NuGetFrameworksAssemblyName = "NuGet.Frameworks";
        private const string NuGetFrameworksFileName = NuGetFrameworksAssemblyName + ".dll";
 
        /// <summary>
        /// Methods, properties, and objects used from the NuGet.Frameworks assembly.
        /// </summary>
        private MethodInfo ParseMethod;
        private MethodInfo IsCompatibleMethod;
        private object DefaultCompatibilityProvider;
        private PropertyInfo FrameworkProperty;
        private PropertyInfo VersionProperty;
        private PropertyInfo PlatformProperty;
        private PropertyInfo PlatformVersionProperty;
        private PropertyInfo AllFrameworkVersionsProperty;
 
        /// <summary>
        /// Public constructor for cross-domain activation only. Use <see cref="CreateInstance"/> to instantiate.
        /// </summary>
        public NuGetFrameworkWrapper()
        { }
 
        /// <summary>
        /// Initialized this instance. May run in a separate AppDomain.
        /// </summary>
        /// <param name="assemblyName">The NuGet.Frameworks to be loaded or null to load by path.</param>
        /// <param name="assemblyFilePath">The file path from which NuGet.Frameworks should be loaded of <paramref name="assemblyName"/> is null.</param>
        public void Initialize(AssemblyName assemblyName, string assemblyFilePath)
        {
            Assembly NuGetAssembly;
            if (assemblyName != null)
            {
                // This will load the assembly into the default load context if possible, and fall back to LoadFrom context.
                NuGetAssembly = Assembly.Load(assemblyName);
            }
            else
            {
                NuGetAssembly = Assembly.LoadFile(assemblyFilePath);
            }
 
            var NuGetFramework = NuGetAssembly.GetType("NuGet.Frameworks.NuGetFramework");
            var NuGetFrameworkCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.CompatibilityProvider");
            var NuGetFrameworkDefaultCompatibilityProvider = NuGetAssembly.GetType("NuGet.Frameworks.DefaultCompatibilityProvider");
            ParseMethod = NuGetFramework.GetMethod("Parse", [typeof(string)]);
            IsCompatibleMethod = NuGetFrameworkCompatibilityProvider.GetMethod("IsCompatible");
            DefaultCompatibilityProvider = NuGetFrameworkDefaultCompatibilityProvider.GetMethod("get_Instance").Invoke(null, Array.Empty<object>());
            FrameworkProperty = NuGetFramework.GetProperty("Framework");
            VersionProperty = NuGetFramework.GetProperty("Version");
            PlatformProperty = NuGetFramework.GetProperty("Platform");
            PlatformVersionProperty = NuGetFramework.GetProperty("PlatformVersion");
            AllFrameworkVersionsProperty = NuGetFramework.GetProperty("AllFrameworkVersions");
        }
 
        private object Parse(string tfm)
        {
            return ParseMethod.Invoke(null, [tfm]);
        }
 
        public string GetTargetFrameworkIdentifier(string tfm)
        {
            return FrameworkProperty.GetValue(Parse(tfm)) as string;
        }
 
        public string GetTargetFrameworkVersion(string tfm, int minVersionPartCount)
        {
            var version = VersionProperty.GetValue(Parse(tfm)) as Version;
            return GetNonZeroVersionParts(version, minVersionPartCount);
        }
 
        public string GetTargetPlatformIdentifier(string tfm)
        {
            return PlatformProperty.GetValue(Parse(tfm)) as string;
        }
 
        public string GetTargetPlatformVersion(string tfm, int minVersionPartCount)
        {
            var version = PlatformVersionProperty.GetValue(Parse(tfm)) as Version;
            return GetNonZeroVersionParts(version, minVersionPartCount);
        }
 
        public bool IsCompatible(string target, string candidate)
        {
            return Convert.ToBoolean(IsCompatibleMethod.Invoke(DefaultCompatibilityProvider, [Parse(target), Parse(candidate)]));
        }
 
        private string GetNonZeroVersionParts(Version version, int minVersionPartCount)
        {
            var nonZeroVersionParts = version.Revision == 0 ? version.Build == 0 ? version.Minor == 0 ? 1 : 2 : 3 : 4;
            return version.ToString(Math.Max(nonZeroVersionParts, minVersionPartCount));
        }
 
        public string FilterTargetFrameworks(string incoming, string filter)
        {
            IEnumerable<(string originalTfm, object parsedTfm)> incomingFrameworks = ParseTfms(incoming);
            IEnumerable<(string originalTfm, object parsedTfm)> filterFrameworks = ParseTfms(filter);
            StringBuilder tfmList = new StringBuilder();
 
            // An incoming target framework from 'incoming' is kept if it is compatible with any of the desired target frameworks on 'filter'
            foreach (var l in incomingFrameworks)
            {
                if (filterFrameworks.Any(r =>
                        (FrameworkProperty.GetValue(l.parsedTfm) as string).Equals(FrameworkProperty.GetValue(r.parsedTfm) as string, StringComparison.OrdinalIgnoreCase) &&
                        (((Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(l.parsedTfm))) && (Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(r.parsedTfm)))) ||
                         ((VersionProperty.GetValue(l.parsedTfm) as Version) == (VersionProperty.GetValue(r.parsedTfm) as Version)))))
                {
                    if (tfmList.Length == 0)
                    {
                        tfmList.Append(l.originalTfm);
                    }
                    else
                    {
                        tfmList.Append($";{l.originalTfm}");
                    }
                }
            }
 
            return tfmList.ToString();
 
            IEnumerable<(string originalTfm, object parsedTfm)> ParseTfms(string desiredTargetFrameworks)
            {
                return desiredTargetFrameworks.Split([';'], StringSplitOptions.RemoveEmptyEntries).Select(tfm =>
                {
                    (string originalTfm, object parsedTfm) parsed = (tfm, Parse(tfm));
                    return parsed;
                });
            }
        }
 
#if FEATURE_APPDOMAIN
        /// <summary>
        /// A null-returning InitializeLifetimeService to give the proxy an infinite lease time.
        /// </summary>
        public override object InitializeLifetimeService() => null;
 
        /// <summary>
        /// Creates <see cref="AppDomainSetup"/> suitable for loading Microsoft.Build, NuGet.Frameworks, and dependencies.
        /// See https://github.com/dotnet/msbuild/blob/main/documentation/NETFramework-NGEN.md#nugetframeworks for the motivation
        /// to use a separate AppDomain.
        /// </summary>
        private static AppDomainSetup CreateAppDomainSetup(AssemblyName assemblyName, string assemblyPath)
        {
            byte[] publicKeyToken = assemblyName.GetPublicKeyToken();
            StringBuilder publicKeyTokenString = new(publicKeyToken.Length * 2);
            for (int i = 0; i < publicKeyToken.Length; i++)
            {
                publicKeyTokenString.Append(publicKeyToken[i].ToString("x2", CultureInfo.InvariantCulture));
            }
 
            // Create an app.config for the AppDomain. We expect the AD to host the currently executing assembly Microsoft.Build,
            // NuGet.Frameworks, and Framework assemblies. It is important to use the same binding redirects that were used when
            // NGENing MSBuild for the native images to be used.
            string configuration = $"""
<?xml version="1.0" encoding="utf-8"?>
  <configuration>
    <runtime>
      <DisableFXClosureWalk enabled="true" />
      <DeferFXClosureWalk enabled="true" />
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        {(Environment.Is64BitProcess ? _bindingRedirects64 : _bindingRedirects32)}
        <dependentAssembly>
          <assemblyIdentity name="{NuGetFrameworksAssemblyName}" publicKeyToken="{publicKeyTokenString}" culture="{assemblyName.CultureName}" />
          <codeBase version="{assemblyName.Version}" href="{assemblyPath}" />
        </dependentAssembly>
      </assemblyBinding>
    </runtime>
  </configuration>
""";
 
            AppDomainSetup appDomainSetup = AppDomain.CurrentDomain.SetupInformation;
            appDomainSetup.SetConfigurationBytes(Encoding.UTF8.GetBytes(configuration));
            return appDomainSetup;
        }
#endif
 
        public static NuGetFrameworkWrapper CreateInstance()
        {
            // Resolve the location of the NuGet.Frameworks assembly
            string assemblyDirectory = BuildEnvironmentHelper.Instance.Mode == BuildEnvironmentMode.VisualStudio ?
                Path.Combine(BuildEnvironmentHelper.Instance.VisualStudioInstallRootDirectory, "Common7", "IDE", "CommonExtensions", "Microsoft", "NuGet") :
                BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory;
 
            string assemblyPath = Path.Combine(assemblyDirectory, NuGetFrameworksFileName);
 
            NuGetFrameworkWrapper instance = null;
            AssemblyName assemblyName = null;
#if FEATURE_APPDOMAIN
            if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10) &&
                (BuildEnvironmentHelper.Instance.RunningInMSBuildExe || BuildEnvironmentHelper.Instance.RunningInVisualStudio))
            {
                // If we are running in MSBuild.exe or VS, we can load the assembly with Assembly.Load, which enables
                // the runtime to bind to the native image, eliminating some non-trivial JITting cost. Devenv.exe knows how to
                // load the assembly by name. In MSBuild.exe, however, we don't know the version of the assembly statically so
                // we create a separate AppDomain with the right binding redirects.
                try
                {
                    assemblyName = AssemblyName.GetAssemblyName(assemblyPath);
                    if (assemblyName != null && BuildEnvironmentHelper.Instance.RunningInMSBuildExe)
                    {
                        AppDomainSetup appDomainSetup = CreateAppDomainSetup(assemblyName, assemblyPath);
                        if (appDomainSetup != null)
                        {
                            AppDomain appDomain = AppDomain.CreateDomain(nameof(NuGetFrameworkWrapper), null, appDomainSetup);
                            instance = (NuGetFrameworkWrapper)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(NuGetFrameworkWrapper).FullName);
                        }
                    }
                }
                catch
                {
                    // If anything goes wrong just fall back to loading into current AD by path.
                    instance = null;
                    assemblyName = null;
                }
            }
#endif
            try
            {
                instance ??= new NuGetFrameworkWrapper();
                instance.Initialize(assemblyName, assemblyPath);
 
                return instance;
            }
            catch (Exception ex)
            {
                throw new InternalErrorException(string.Format(AssemblyResources.GetString("NuGetAssemblyNotFound"), assemblyDirectory), ex);
            }
        }
    }
}