Skip to content

Commit

Permalink
Minimize CoreClrAssemblyLoader (all relevant logic is now pushed to M…
Browse files Browse the repository at this point in the history
…SBuildLoadContext/AssemblyDependencyResolver)

Simplify load rules: just use the loader

In .NET Core SDK scenarios, there are a ton of files dumped in the
'MSBuild bin folder', including NuGet and its Newtonsoft.Json
dependency. It's ok for tasks to use different versions of those,
so simplify to only the known-MSBuild-assembly rules (expressed
in CoreClrAssemblyLoader).
  • Loading branch information
rainersigwald committed Nov 14, 2019
1 parent 90ac0c3 commit 6bd5e48
Show file tree
Hide file tree
Showing 4 changed files with 8 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath, LoggingCont
#if FEATURE_ASSEMBLY_LOADFROM
return Assembly.LoadFrom(resolverPath);
#else
_loader.AddDependencyLocation(Path.GetDirectoryName(resolverPath));
return _loader.LoadFromPath(resolverPath);
#endif
}
Expand Down
132 changes: 6 additions & 126 deletions src/Shared/CoreCLRAssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Reflection;

#nullable enable
namespace Microsoft.Build.Shared
{
/// <summary>
Expand All @@ -14,148 +15,27 @@ namespace Microsoft.Build.Shared
internal sealed class CoreClrAssemblyLoader
{
private readonly Dictionary<string, Assembly> _pathsToAssemblies = new Dictionary<string, Assembly>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Assembly> _namesToAssemblies = new Dictionary<string, Assembly>();
private readonly HashSet<string> _dependencyPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly object _guard = new object();

private static readonly string[] _extensions = new[] { "ni.dll", "ni.exe", "dll", "exe" };
private static readonly Version _currentAssemblyVersion = new Version(Microsoft.Build.Shared.MSBuildConstants.CurrentAssemblyVersion);
private static readonly HashSet<string> _wellKnownAssemblyNames = new HashSet<string>(
new[]
{
"Microsoft.Build",
"Microsoft.Build.Framework",
"Microsoft.Build.Tasks.Core",
"Microsoft.Build.Utilities.Core"
});

public void AddDependencyLocation(string fullPath)
{
if (fullPath == null)
{
throw new ArgumentNullException(nameof(fullPath));
}

lock (_guard)
{
_dependencyPaths.Add(fullPath);
}
}

public Assembly LoadFromPath(string fullPath)
{
if (fullPath == null)
{
throw new ArgumentNullException(nameof(fullPath));
}

Debug.Assert(Path.IsPathRooted(fullPath));

lock (_guard)
{
Assembly assembly;
if (_pathsToAssemblies.TryGetValue(fullPath, out assembly))
if (_pathsToAssemblies.TryGetValue(fullPath, out Assembly? assembly))
{
return assembly;
}

return LoadAndCache(AssemblyLoadContext.Default, fullPath);
}
}
var contextForAssemblyPath = new MSBuildLoadContext(fullPath);

private Assembly TryGetWellKnownAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
{
if (!_wellKnownAssemblyNames.Contains(assemblyName.Name))
{
return null;
}

// Ensure we are attempting to load a matching version
// of the Microsoft.Build.* assembly.
assemblyName.Version = _currentAssemblyVersion;
assembly = contextForAssemblyPath.LoadFromAssemblyPath(fullPath);

var searchPaths = new[] { Assembly.GetExecutingAssembly().Location };
return TryResolveAssemblyFromPaths(context, assemblyName, searchPaths);
}
_pathsToAssemblies[fullPath] = assembly;

private Assembly TryResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
{
lock (_guard)
{
Assembly assembly = TryGetWellKnownAssembly(context, assemblyName);

if (assembly != null)
{
return assembly;
}

if (_namesToAssemblies.TryGetValue(assemblyName.FullName, out assembly))
{
return assembly;
}

return TryResolveAssemblyFromPaths(context, assemblyName, _dependencyPaths);
}
}

private Assembly TryResolveAssemblyFromPaths(AssemblyLoadContext context, AssemblyName assemblyName, IEnumerable<string> searchPaths)
{
foreach (var cultureSubfolder in string.IsNullOrEmpty(assemblyName.CultureName)
// If no culture is specified, attempt to load directly from
// the known dependency paths.
? new[] { string.Empty }
// Search for satellite assemblies in culture subdirectories
// of the assembly search directories, but fall back to the
// bare search directory if that fails.
: new[] { assemblyName.CultureName, string.Empty })
{
foreach (var searchPath in searchPaths)
{
foreach (var extension in _extensions)
{
var candidatePath = Path.Combine(searchPath,
cultureSubfolder,
$"{assemblyName.Name}.{extension}");

if (IsAssemblyAlreadyLoaded(candidatePath) ||
!FileSystems.Default.FileExists(candidatePath))
{
continue;
}

AssemblyName candidateAssemblyName = AssemblyLoadContext.GetAssemblyName(candidatePath);
if (candidateAssemblyName.Version != assemblyName.Version)
{
continue;
}

return LoadAndCache(context, candidatePath);
}
}
return assembly;
}

return null;
}

/// <remarks>
/// Assumes we have a lock on _guard
/// </remarks>
private Assembly LoadAndCache(AssemblyLoadContext _, string fullPath)
{
var cxt = new MSBuildLoadContext(fullPath);

var assembly = cxt.LoadFromAssemblyPath(fullPath);
var name = assembly.FullName;

_pathsToAssemblies[fullPath] = assembly;
_namesToAssemblies[name] = assembly;

return assembly;
}

private bool IsAssemblyAlreadyLoaded(string path)
{
return _pathsToAssemblies.ContainsKey(path);
}
}
}
1 change: 1 addition & 0 deletions src/Shared/MSBuildLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public MSBuildLoadContext(string assemblyPath) :
{
if (_wellKnownAssemblyNames.Contains(assemblyName.Name!))
{
// MSBuild assemblies should be loaded from the default context
return null;
}

Expand Down
23 changes: 1 addition & 22 deletions src/Shared/TypeLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,35 +151,14 @@ private static Assembly LoadAssembly(AssemblyLoadInfo assemblyLoadInfo)
{
if (assemblyLoadInfo.AssemblyName != null)
{
#if FEATURE_ASSEMBLY_LOADFROM
loadedAssembly = Assembly.Load(assemblyLoadInfo.AssemblyName);
#else
loadedAssembly = Assembly.Load(new AssemblyName(assemblyLoadInfo.AssemblyName));
#endif
}
else
{
#if FEATURE_ASSEMBLY_LOADFROM
loadedAssembly = Assembly.UnsafeLoadFrom(assemblyLoadInfo.AssemblyFile);
#else
// If the Assembly is provided via a file path, the following rules are used to load the assembly:
// - if the simple name of the assembly exists in the same folder as msbuild.exe, then that assembly gets loaded, indifferent of the user specified path
// - otherwise, the assembly from the user specified path is loaded, if it exists.

var assemblyNameInExecutableDirectory = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory,
Path.GetFileName(assemblyLoadInfo.AssemblyFile));

if (FileSystems.Default.FileExists(assemblyNameInExecutableDirectory))
{
var simpleName = Path.GetFileNameWithoutExtension(assemblyLoadInfo.AssemblyFile);
loadedAssembly = Assembly.Load(new AssemblyName(simpleName));
}
else
{
var baseDir = Path.GetDirectoryName(assemblyLoadInfo.AssemblyFile);
s_coreClrAssemblyLoader.AddDependencyLocation(baseDir);
loadedAssembly = s_coreClrAssemblyLoader.LoadFromPath(assemblyLoadInfo.AssemblyFile);
}
loadedAssembly = s_coreClrAssemblyLoader.LoadFromPath(assemblyLoadInfo.AssemblyFile);
#endif
}
}
Expand Down

0 comments on commit 6bd5e48

Please sign in to comment.