22// Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information.
33
44using System ;
5+ using System . Collections . Concurrent ;
56using System . Collections . Generic ;
67using System . IO ;
8+ using System . Linq ;
79using System . Reflection ;
8- using Microsoft . CodeAnalysis . CSharp . Syntax ;
10+ using System . Runtime . Loader ;
911
1012namespace Sharpmake
1113{
@@ -19,44 +21,14 @@ namespace Sharpmake
1921 /// </remarks>
2022 public class ExtensionLoader : IDisposable
2123 {
24+ public const string TempContextNamePrefix = "Temp extension check AssemblyLoadContext" ;
2225 public class ExtensionChecker : MarshalByRefObject
2326 {
24- private readonly Dictionary < string , bool > _loadedAssemblies = new Dictionary < string , bool > ( StringComparer . OrdinalIgnoreCase ) ;
25-
2627 public bool IsSharpmakeExtension ( string assemblyPath )
2728 {
28- if ( assemblyPath == null )
29- throw new ArgumentNullException ( nameof ( assemblyPath ) ) ;
30- if ( ! File . Exists ( assemblyPath ) )
31- throw new FileNotFoundException ( "Cannot find the assembly DLL." , assemblyPath ) ;
32-
33- lock ( _loadedAssemblies )
34- {
35- // If the assembly has already been loaded, check if it is.
36- bool result ;
37- if ( _loadedAssemblies . TryGetValue ( assemblyPath , out result ) )
38- return result ;
39-
40- try
41- {
42- Assembly assembly = Assembly . LoadFrom ( assemblyPath ) ;
43- result = IsSharpmakeExtension ( assembly ) ;
44- }
45- catch ( BadImageFormatException )
46- {
47- // This is either a native C/C++ assembly, or there is a x86/x64 mismatch
48- // that prevents it to load. Sharpmake platforms have no reason to not be
49- // AnyCPU so just assume that it's not a Sharpmake extension.
50- result = false ;
51- }
52- catch ( Exception ex )
53- {
54- throw new Error ( "An unexpected error has occurred while loading a potential Sharpmake extension assembly: {0}" , ex . Message ) ;
55- }
56-
57- _loadedAssemblies . Add ( assemblyPath , result ) ;
58- return result ;
59- }
29+ ExtensionLoader extensionLoader = new ( ) ;
30+ bool result = extensionLoader . LoadExtension ( assemblyPath ) != null ;
31+ return result ;
6032 }
6133
6234 public static bool IsSharpmakeExtension ( Assembly assembly )
@@ -68,6 +40,12 @@ public static bool IsSharpmakeExtension(Assembly assembly)
6840 }
6941 }
7042
43+ public static bool IsTempAssembly ( Assembly assembly )
44+ {
45+ return AssemblyLoadContext . GetLoadContext ( assembly ) ? . Name ? . StartsWith ( ExtensionLoader . TempContextNamePrefix ) ?? false ;
46+ }
47+
48+ private static ConcurrentDictionary < string , byte > _nonExtensionAssemblyPaths = new ( ) ;
7149 /// <summary>
7250 /// Releases the remote <see cref="AppDomain"/> if one was created.
7351 /// </summary>
@@ -92,23 +70,75 @@ public Assembly LoadExtension(string assemblyPath, bool fastLoad)
9270 /// Loads a Sharpmake extension assembly.
9371 /// </summary>
9472 /// <param name="assemblyPath">The path of the assembly that contains the Sharpmake extension.</param>
95- /// <returns>The loaded extension's <see cref="Assembly"/>.</returns>
73+ /// <returns>The loaded extension's <see cref="Assembly"/> or null if it's not a Sharpmake extension .</returns>
9674 public Assembly LoadExtension ( string assemblyPath )
9775 {
9876 if ( assemblyPath == null )
9977 throw new ArgumentNullException ( nameof ( assemblyPath ) ) ;
10078
101- try
79+ if ( ! File . Exists ( assemblyPath ) )
80+ throw new FileNotFoundException ( "Cannot find the assembly DLL." , assemblyPath ) ;
81+
82+ if ( _nonExtensionAssemblyPaths . Keys . Contains ( assemblyPath ) )
83+ return null ;
84+
85+ // If we've already loaded the assembly in some loading context, check the attributes without loading it again
86+ foreach ( AssemblyLoadContext alc in AssemblyLoadContext . All )
10287 {
103- Assembly assembly = Assembly . LoadFrom ( assemblyPath ) ;
104- if ( ! ExtensionChecker . IsSharpmakeExtension ( assembly ) )
88+ Assembly assembly = alc . Assemblies . FirstOrDefault ( a => AssemblyHasSamePath ( a , assemblyPath ) ) ;
89+ if ( assembly != null )
90+ {
91+ if ( ExtensionChecker . IsSharpmakeExtension ( assembly ) )
92+ return assembly ;
93+
94+ _nonExtensionAssemblyPaths . TryAdd ( assemblyPath , 0 ) ;
95+
10596 return null ;
106- return assembly ;
97+ }
98+ }
99+
100+ // Check the attributes via an unloadable context (reflexion only is not a thing anymore in .net 6)
101+ var contextName = $ "{ TempContextNamePrefix } - { Path . GetFileNameWithoutExtension ( assemblyPath ) } ";
102+ AssemblyLoadContext tempLoadContext = new ( contextName , true ) ;
103+
104+ bool isSharpmakeExtension ;
105+
106+ try
107+ {
108+ var tempAssembly = tempLoadContext . LoadFromAssemblyPath ( assemblyPath ) ;
109+ isSharpmakeExtension = ExtensionChecker . IsSharpmakeExtension ( tempAssembly ) ;
110+ // log unloading of contexts, only log info for Sharpmake extensions
111+ Action < string , object [ ] > logger = isSharpmakeExtension ? Builder . Instance . LogWriteLine : Builder . Instance . DebugWriteLine ;
112+ tempLoadContext . Unloading += ( context ) => logger ( $ " [ExtensionLoader] Attempting to unload temporary context { context . Name } ", new object [ ] { } ) ;
107113 }
108114 catch ( BadImageFormatException )
109115 {
116+ // This is either a native C/C++ assembly, or there is a x86/x64 mismatch
117+ // that prevents it to load. Sharpmake platforms have no reason to not be
118+ // AnyCPU so just assume that it's not a Sharpmake extension.
110119 return null ;
111120 }
121+ finally
122+ {
123+ // Unload the AssemblyLoadContext
124+ // NOTE: the unloading event is fired on calling unload for the context, the assembly might not be unloaded
125+ // if there are references to it (make sure it is not used on temp assembly loading)
126+ tempLoadContext . Unload ( ) ;
127+ }
128+
129+ if ( isSharpmakeExtension )
130+ return AssemblyLoadContext . Default . LoadFromAssemblyPath ( assemblyPath ) ;
131+
132+ _nonExtensionAssemblyPaths . TryAdd ( assemblyPath , 0 ) ;
133+
134+ return null ;
135+
136+ static bool AssemblyHasSamePath ( Assembly assembly , string assemblyPath )
137+ {
138+ return assembly != null
139+ && ! string . IsNullOrEmpty ( assemblyPath ) && ! assembly . IsDynamic &&
140+ string . Equals ( assembly . Location , assemblyPath , StringComparison . Ordinal ) ;
141+ }
112142 }
113143
114144 [ Obsolete ( "This can't work on .net 6. Dead code" , error : true ) ]
0 commit comments