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