1 // Copyright (c) Microsoft. All rights reserved. 2 // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 4 using Microsoft.Build.Shared; 5 using Microsoft.Build.Utilities; 6 using System; 7 using System.Collections.Generic; 8 using System.IO; 9 using System.Reflection; 10 #if !FEATURE_APPDOMAIN 11 using System.Runtime.Loader; 12 #endif 13 14 using SdkReference = Microsoft.Build.Framework.SdkReference; 15 using SdkResolverBase = Microsoft.Build.Framework.SdkResolver; 16 using SdkResolverContextBase = Microsoft.Build.Framework.SdkResolverContext; 17 using SdkResultBase = Microsoft.Build.Framework.SdkResult; 18 using SdkResultFactoryBase = Microsoft.Build.Framework.SdkResultFactory; 19 20 namespace NuGet.MSBuildSdkResolver 21 { 22 /// <summary> 23 /// Acts as a base class for the NuGet-based SDK resolver and handles assembly resolution to dynamically locate NuGet assemblies. 24 /// </summary> 25 public abstract class NuGetSdkResolverBase : SdkResolverBase 26 { 27 /// <summary> 28 /// The sub-folder under the Visual Studio installation where the NuGet assemblies are located. 29 /// </summary> 30 public const string PathToNuGetUnderVisualStudioRoot = @"Common7\IDE\CommonExtensions\Microsoft\NuGet"; 31 32 /// <summary> 33 /// Attempts to locate the NuGet assemblies based on the current <see cref="BuildEnvironmentMode"/>. 34 /// </summary> 35 private static readonly Lazy<string> NuGetAssemblyPathLazy = new Lazy<string>(() => 36 { 37 // The environment variable overrides everything 38 string basePath = Environment.GetEnvironmentVariable(MSBuildConstants.NuGetAssemblyPathEnvironmentVariableName); 39 40 if (!String.IsNullOrWhiteSpace(basePath) && Directory.Exists(basePath)) 41 { 42 return basePath; 43 } 44 45 if (BuildEnvironmentHelper.Instance.Mode == BuildEnvironmentMode.VisualStudio) 46 { 47 // Return the path to NuGet under the Visual Studio installation 48 return Path.Combine(BuildEnvironmentHelper.Instance.VisualStudioInstallRootDirectory, PathToNuGetUnderVisualStudioRoot); 49 } 50 51 // Expect the NuGet assemblies to be next to MSBuild.exe, which is the case when running .NET CLI 52 return BuildEnvironmentHelper.Instance.MSBuildToolsDirectory32; 53 }); 54 55 /// <summary> 56 /// A list of NuGet assemblies that we have a dependency on but should load at runtime. This list is from dependencies of the 57 /// NuGet.Commands and NuGet.Protocol packages in project.json. This list should be updated if those dependencies change. 58 /// </summary> 59 internal static readonly HashSet<string> NuGetAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase) 60 { 61 "Newtonsoft.Json", 62 "NuGet.Commands", 63 "NuGet.Common", 64 "NuGet.Configuration", 65 "NuGet.Frameworks", 66 "NuGet.LibraryModel", 67 "NuGet.Packaging", 68 "NuGet.ProjectModel", 69 "NuGet.ProjectModel", 70 "NuGet.Protocol", 71 "NuGet.Versioning", 72 }; 73 74 /// <summary> 75 /// A custom assembly resolver used to locate NuGet dependencies. It is very important that we do not ship with 76 /// these dependencies because we need to load whatever version of NuGet is currently installed. If we loaded our 77 /// own NuGet assemblies, it would break NuGet functionality like Restore and Pack. 78 /// </summary> AssemblyResolve( object sender, ResolveEventArgs args)79 private static Assembly AssemblyResolve( 80 #if FEATURE_APPDOMAIN 81 object sender, 82 ResolveEventArgs args) 83 { 84 AssemblyName assemblyName = new AssemblyName(args.Name); 85 #else 86 AssemblyLoadContext assemblyLoadContext, 87 AssemblyName assemblyName) 88 { 89 #endif 90 if (NuGetAssemblies.Contains(assemblyName.Name)) 91 { 92 string assemblyPath = Path.Combine(NuGetAssemblyPathLazy.Value, $"{assemblyName.Name}.dll"); 93 94 if (File.Exists(assemblyPath)) 95 { 96 #if !FEATURE_APPDOMAIN 97 return assemblyLoadContext.LoadFromAssemblyPath(assemblyPath); 98 #elif !CLR2COMPATIBILITY 99 return Assembly.UnsafeLoadFrom(assemblyPath); 100 #else 101 return Assembly.LoadFrom(assemblyPath); 102 #endif 103 } 104 } 105 106 return null; 107 } 108 109 public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase context, SdkResultFactoryBase factory) 110 { 111 // Escape hatch to disable this resolver 112 if (Traits.Instance.EscapeHatches.DisableNuGetSdkResolver) 113 { 114 return null; 115 } 116 117 #if FEATURE_APPDOMAIN 118 AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve; 119 #else 120 AssemblyLoadContext.Default.Resolving += AssemblyResolve; 121 #endif 122 123 try 124 { 125 return ResolveSdk(sdk, context, factory); 126 } 127 finally 128 { 129 #if FEATURE_APPDOMAIN 130 AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve; 131 #else 132 AssemblyLoadContext.Default.Resolving -= AssemblyResolve; 133 #endif 134 } 135 } 136 137 protected abstract SdkResultBase ResolveSdk(SdkReference sdk, SdkResolverContextBase context, SdkResultFactoryBase factory); 138 } 139 } 140