aboutsummaryrefslogtreecommitdiff
path: root/third-party/DotNetCorePlugins/src
diff options
context:
space:
mode:
Diffstat (limited to 'third-party/DotNetCorePlugins/src')
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs73
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs10
-rw-r--r--third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs12
-rw-r--r--third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs73
-rw-r--r--third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs69
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs341
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs207
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs386
-rw-r--r--third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs129
-rw-r--r--third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj26
-rw-r--r--third-party/DotNetCorePlugins/src/PluginConfig.cs94
-rw-r--r--third-party/DotNetCorePlugins/src/PluginLoader.cs148
-rw-r--r--third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs6
13 files changed, 1574 insertions, 0 deletions
diff --git a/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs
new file mode 100644
index 0000000..0b4ac27
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/PlatformInformation.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Use the new .NET 8.0 collection syntax
+ * - Use the new OperatingSystem class for platform detection
+ * - Remove static constructor for new best practice guidlines
+ */
+
+using System;
+using System.Diagnostics;
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class PlatformInformation
+ {
+ public static readonly string[] NativeLibraryExtensions = GetNativeLibExtension();
+ public static readonly string[] NativeLibraryPrefixes = GetNativeLibPrefixs();
+
+ public static readonly string[] ManagedAssemblyExtensions =
+ [
+ ".dll",
+ ".ni.dll",
+ ".exe",
+ ".ni.exe"
+ ];
+
+ private static string[] GetNativeLibPrefixs()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return [""];
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return ["", "lib",];
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return ["", "lib"];
+ }
+ else
+ {
+ Debug.Fail("Unknown OS type");
+ return [];
+ }
+ }
+
+ private static string[] GetNativeLibExtension()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return [".dll"];
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return [".dylib"];
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return [".so", ".so.1"];
+ }
+ else
+ {
+ Debug.Fail("Unknown OS type");
+ return [];
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs
new file mode 100644
index 0000000..6da5dd4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeConfig.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class RuntimeConfig
+ {
+ public RuntimeOptions? runtimeOptions { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs
new file mode 100644
index 0000000..748aa38
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Internal/RuntimeOptions.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace McMaster.NETCore.Plugins
+{
+ internal class RuntimeOptions
+ {
+ public string? Tfm { get; set; }
+
+ public string[]? AdditionalProbingPaths { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs
new file mode 100644
index 0000000..ca45cc5
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/LibraryModel/ManagedLibrary.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+
+namespace McMaster.NETCore.Plugins.LibraryModel
+{
+ /// <summary>
+ /// Represents a managed, .NET assembly.
+ /// </summary>
+ [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
+ public class ManagedLibrary
+ {
+ private ManagedLibrary(AssemblyName name, string additionalProbingPath, string appLocalPath)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
+ AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
+ }
+
+ /// <summary>
+ /// Name of the managed library
+ /// </summary>
+ public AssemblyName Name { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within an additional probing path root. This is typically a combination
+ /// of the NuGet package ID (lowercased), version, and path within the package.
+ /// <para>
+ /// For example, <c>microsoft.data.sqlite/1.0.0/lib/netstandard1.3/Microsoft.Data.Sqlite.dll</c>
+ /// </para>
+ /// </summary>
+ public string AdditionalProbingPath { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within a deployed, framework-dependent application.
+ /// <para>
+ /// For most managed libraries, this will be the file name.
+ /// For example, <c>MyPlugin1.dll</c>.
+ /// </para>
+ /// <para>
+ /// For runtime-specific managed implementations, this may include a sub folder path.
+ /// For example, <c>runtimes/win/lib/netcoreapp2.0/System.Diagnostics.EventLog.dll</c>
+ /// </para>
+ /// </summary>
+ public string AppLocalPath { get; private set; }
+
+ /// <summary>
+ /// Create an instance of <see cref="ManagedLibrary" /> from a NuGet package.
+ /// </summary>
+ /// <param name="packageId">The name of the package.</param>
+ /// <param name="packageVersion">The version of the package.</param>
+ /// <param name="assetPath">The path within the NuGet package.</param>
+ /// <returns></returns>
+ public static ManagedLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
+ {
+ // When the asset comes from "lib/$tfm/", Microsoft.NET.Sdk will flatten this during publish based on the most compatible TFM.
+ // The SDK will not flatten managed libraries found under runtimes/
+ var appLocalPath = assetPath.StartsWith("lib/")
+ ? Path.GetFileName(assetPath)
+ : assetPath;
+
+ return new ManagedLibrary(
+ new AssemblyName(Path.GetFileNameWithoutExtension(assetPath)),
+ Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath),
+ appLocalPath
+ );
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs
new file mode 100644
index 0000000..ac5e061
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/LibraryModel/NativeLibrary.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+
+namespace McMaster.NETCore.Plugins.LibraryModel
+{
+ /// <summary>
+ /// Represents an unmanaged library, such as `libsqlite3`, which may need to be loaded
+ /// for P/Invoke to work.
+ /// </summary>
+ [DebuggerDisplay("{Name} = {AdditionalProbingPath}")]
+ public class NativeLibrary
+ {
+ private NativeLibrary(string name, string appLocalPath, string additionalProbingPath)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ AppLocalPath = appLocalPath ?? throw new ArgumentNullException(nameof(appLocalPath));
+ AdditionalProbingPath = additionalProbingPath ?? throw new ArgumentNullException(nameof(additionalProbingPath));
+ }
+
+ /// <summary>
+ /// Name of the native library. This should match the name of the P/Invoke call.
+ /// <para>
+ /// For example, if specifying `[DllImport("sqlite3")]`, <see cref="Name" /> should be <c>sqlite3</c>.
+ /// This may not match the exact file name as loading will attempt variations on the name according
+ /// to OS convention. On Windows, P/Invoke will attempt to load `sqlite3.dll`. On macOS, it will
+ /// attempt to find `sqlite3.dylib` and `libsqlite3.dylib`. On Linux, it will attempt to find
+ /// `sqlite3.so` and `libsqlite3.so`.
+ /// </para>
+ /// </summary>
+ public string Name { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within a deployed, framework-dependent application
+ /// <para>
+ /// For example, <c>runtimes/linux-x64/native/libsqlite.so</c>
+ /// </para>
+ /// </summary>
+ public string AppLocalPath { get; private set; }
+
+ /// <summary>
+ /// Contains path to file within an additional probing path root. This is typically a combination
+ /// of the NuGet package ID (lowercased), version, and path within the package.
+ /// <para>
+ /// For example, <c>sqlite/3.13.3/runtimes/linux-x64/native/libsqlite.so</c>
+ /// </para>
+ /// </summary>
+ public string AdditionalProbingPath { get; private set; }
+
+ /// <summary>
+ /// Create an instance of <see cref="NativeLibrary" /> from a NuGet package.
+ /// </summary>
+ /// <param name="packageId">The name of the package.</param>
+ /// <param name="packageVersion">The version of the package.</param>
+ /// <param name="assetPath">The path within the NuGet package.</param>
+ /// <returns></returns>
+ public static NativeLibrary CreateFromPackage(string packageId, string packageVersion, string assetPath)
+ {
+ return new NativeLibrary(
+ Path.GetFileNameWithoutExtension(assetPath),
+ assetPath,
+ Path.Combine(packageId.ToLowerInvariant(), packageVersion, assetPath)
+ );
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs
new file mode 100644
index 0000000..05d6238
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/AssemblyLoadContextBuilder.cs
@@ -0,0 +1,341 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed unused
+ */
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+using McMaster.NETCore.Plugins.LibraryModel;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// A builder for creating an instance of <see cref="AssemblyLoadContext" />.
+ /// </summary>
+ public class AssemblyLoadContextBuilder
+ {
+ private readonly List<string> _additionalProbingPaths = new();
+ private readonly List<string> _resourceProbingPaths = new();
+ private readonly List<string> _resourceProbingSubpaths = new();
+ private readonly Dictionary<string, ManagedLibrary> _managedLibraries = new(StringComparer.Ordinal);
+ private readonly Dictionary<string, NativeLibrary> _nativeLibraries = new(StringComparer.Ordinal);
+ private readonly HashSet<string> _privateAssemblies = new(StringComparer.Ordinal);
+ private readonly HashSet<string> _defaultAssemblies = new(StringComparer.Ordinal);
+ private AssemblyLoadContext _defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
+ private string? _mainAssemblyPath;
+ private bool _preferDefaultLoadContext;
+ private bool _isCollectible;
+ private bool _loadInMemory;
+ private bool _shadowCopyNativeLibraries;
+
+
+ /// <summary>
+ /// Creates an assembly load context using settings specified on the builder.
+ /// </summary>
+ /// <returns>A new ManagedLoadContext.</returns>
+ public AssemblyLoadContext Build()
+ {
+ var resourceProbingPaths = new List<string>(_resourceProbingPaths);
+ foreach (var additionalPath in _additionalProbingPaths)
+ {
+ foreach (var subPath in _resourceProbingSubpaths)
+ {
+ resourceProbingPaths.Add(Path.Combine(additionalPath, subPath));
+ }
+ }
+
+ if (_mainAssemblyPath == null)
+ {
+ throw new InvalidOperationException($"Missing required property. You must call '{nameof(SetMainAssemblyPath)}' to configure the default assembly.");
+ }
+
+ //Lots of arguments, make it clear
+ return new ManagedLoadContext(
+ mainAssemblyPath: _mainAssemblyPath,
+ managedAssemblies: _managedLibraries,
+ nativeLibraries: _nativeLibraries,
+ privateAssemblies: _privateAssemblies,
+ defaultAssemblies: _defaultAssemblies,
+ additionalProbingPaths: _additionalProbingPaths,
+ resourceProbingPaths: resourceProbingPaths,
+ defaultLoadContext: _defaultLoadContext,
+ preferDefaultLoadContext: _preferDefaultLoadContext,
+ isCollectible: _isCollectible,
+ loadInMemory: _loadInMemory,
+ shadowCopyNativeLibraries: _shadowCopyNativeLibraries
+ );
+ }
+
+ /// <summary>
+ /// Set the file path to the main assembly for the context. This is used as the starting point for loading
+ /// other assemblies. The directory that contains it is also known as the 'app local' directory.
+ /// </summary>
+ /// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Argument must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _mainAssemblyPath = path;
+ return this;
+ }
+
+ /// <summary>
+ /// Replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="AssemblyLoadContextBuilder"/>.
+ /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
+ /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
+ /// </summary>
+ /// <param name="context">The context to set.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder SetDefaultContext(AssemblyLoadContext context)
+ {
+ _defaultLoadContext = context ?? throw new ArgumentException($"Bad Argument: AssemblyLoadContext in {nameof(AssemblyLoadContextBuilder)}.{nameof(SetDefaultContext)} is null.");
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to prefer a private version of this assembly, even if that version is
+ /// different from the version used by the host application.
+ /// Use this when you do not need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// <para>
+ /// This may mean the types loaded from
+ /// this assembly will not match the types from an assembly with the same name, but different version,
+ /// in the host application.
+ /// </para>
+ /// <para>
+ /// For example, if the host application has a type named <c>Foo</c> from assembly <c>Banana, Version=1.0.0.0</c>
+ /// and the load context prefers a private version of <c>Banan, Version=2.0.0.0</c>, when comparing two objects,
+ /// one created by the host (Foo1) and one created from within the load context (Foo2), they will not have the same
+ /// type. <c>Foo1.GetType() != Foo2.GetType()</c>
+ /// </para>
+ /// </summary>
+ /// <param name="assemblyName">The name of the assembly.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name != null)
+ {
+ _privateAssemblies.Add(assemblyName.Name);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to first attempt to load assemblies by this name from the default app context, even
+ /// if other assemblies in this load context express a dependency on a higher or lower version.
+ /// Use this when you need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// </summary>
+ /// <param name="assemblyName">The name of the assembly.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
+ {
+ var names = new Queue<AssemblyName>();
+ names.Enqueue(assemblyName);
+ while (names.TryDequeue(out var name))
+ {
+ if (name.Name == null || _defaultAssemblies.Contains(name.Name))
+ {
+ // base cases
+ continue;
+ }
+
+ _defaultAssemblies.Add(name.Name);
+
+ // Load and find all dependencies of default assemblies.
+ // This sacrifices some performance for determinism in how transitive
+ // dependencies will be shared between host and plugin.
+ var assembly = _defaultLoadContext.LoadFromAssemblyName(name);
+
+ foreach (var reference in assembly.GetReferencedAssemblies())
+ {
+ names.Enqueue(reference);
+ }
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Instructs the load context to first search for binaries from the default app context, even
+ /// if other assemblies in this load context express a dependency on a higher or lower version.
+ /// Use this when you need to exchange types created from within the load context with other contexts
+ /// or the default app context.
+ /// <para>
+ /// This may mean the types loaded from within the context are force-downgraded to the version provided
+ /// by the host. <seealso cref="PreferLoadContextAssembly" /> can be used to selectively identify binaries
+ /// which should not be loaded from the default load context.
+ /// </para>
+ /// </summary>
+ /// <param name="preferDefaultLoadContext">When true, first attemp to load binaries from the default load context.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoadContext)
+ {
+ _preferDefaultLoadContext = preferDefaultLoadContext;
+ return this;
+ }
+
+
+ /// <summary>
+ /// Add a managed library to the load context.
+ /// </summary>
+ /// <param name="library">The managed library.</param>
+ /// <returns>The builder.</returns>
+ public AssemblyLoadContextBuilder AddManagedLibrary(ManagedLibrary library)
+ {
+ ValidateRelativePath(library.AdditionalProbingPath);
+
+ if (library.Name.Name != null)
+ {
+ _managedLibraries.Add(library.Name.Name, library);
+ }
+
+ return this;
+ }
+
+ /// <summary>
+ /// Add a native library to the load context.
+ /// </summary>
+ /// <param name="library"></param>
+ /// <returns></returns>
+ public AssemblyLoadContextBuilder AddNativeLibrary(NativeLibrary library)
+ {
+ ValidateRelativePath(library.AppLocalPath);
+ ValidateRelativePath(library.AdditionalProbingPath);
+
+ _nativeLibraries.Add(library.Name, library);
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be used to search for native and managed libraries.
+ /// </summary>
+ /// <param name="path">The file path. Must be a full file path.</param>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder AddProbingPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _additionalProbingPaths.Add(path);
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies).
+ /// </summary>
+ /// <param name="path">The file path. Must be a full file path.</param>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder AddResourceProbingPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (!Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be a full path.", nameof(path));
+ }
+
+ _resourceProbingPaths.Add(path);
+ return this;
+ }
+
+
+ /// <summary>
+ /// Enable unloading the assembly load context.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder EnableUnloading()
+ {
+ _isCollectible = true;
+ return this;
+ }
+
+ /// <summary>
+ /// Read .dll files into memory to avoid locking the files.
+ /// This is not as efficient, so is not enabled by default, but is required for scenarios
+ /// like hot reloading.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder PreloadAssembliesIntoMemory()
+ {
+ _loadInMemory = true; // required to prevent dotnet from locking loaded files
+ return this;
+ }
+
+ /// <summary>
+ /// Shadow copy native libraries (unmanaged DLLs) to avoid locking of these files.
+ /// This is not as efficient, so is not enabled by default, but is required for scenarios
+ /// like hot reloading of plugins dependent on native libraries.
+ /// </summary>
+ /// <returns>The builder</returns>
+ public AssemblyLoadContextBuilder ShadowCopyNativeLibraries()
+ {
+ _shadowCopyNativeLibraries = true;
+ return this;
+ }
+
+ /// <summary>
+ /// Add a <paramref name="path"/> that should be use to search for resource assemblies (aka satellite assemblies)
+ /// relative to any paths specified as <see cref="AddProbingPath"/>
+ /// </summary>
+ /// <param name="path">The file path. Must not be a full file path since it will be appended to additional probing path roots.</param>
+ /// <returns>The builder</returns>
+ internal AssemblyLoadContextBuilder AddResourceProbingSubpath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(path));
+ }
+
+ if (Path.IsPathRooted(path))
+ {
+ throw new ArgumentException("Argument must be not a full path.", nameof(path));
+ }
+
+ _resourceProbingSubpaths.Add(path);
+ return this;
+ }
+
+ private static void ValidateRelativePath(string probingPath)
+ {
+ if (string.IsNullOrEmpty(probingPath))
+ {
+ throw new ArgumentException("Value must not be null or empty.", nameof(probingPath));
+ }
+
+ if (Path.IsPathRooted(probingPath))
+ {
+ throw new ArgumentException("Argument must be a relative path.", nameof(probingPath));
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs
new file mode 100644
index 0000000..fc2d0a9
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/DependencyContextExtensions.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using McMaster.NETCore.Plugins.LibraryModel;
+using Microsoft.Extensions.DependencyModel;
+using NativeLibrary = McMaster.NETCore.Plugins.LibraryModel.NativeLibrary;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// Extensions for configuring a load context using .deps.json files.
+ /// </summary>
+ public static class DependencyContextExtensions
+ {
+ /// <summary>
+ /// Add dependency information to a load context from a .deps.json file.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="depsFilePath">The full path to the .deps.json file.</param>
+ /// <param name="error">An error, if one occurs while reading .deps.json</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder TryAddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath, out Exception? error)
+ {
+ error = null;
+ try
+ {
+ builder.AddDependencyContext(depsFilePath);
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+
+ return builder;
+ }
+
+ /// <summary>
+ /// Add dependency information to a load context from a .deps.json file.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="depsFilePath">The full path to the .deps.json file.</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, string depsFilePath)
+ {
+
+ var reader = new DependencyContextJsonReader();
+ using (var file = File.OpenRead(depsFilePath))
+ {
+ var deps = reader.Read(file);
+ builder.AddDependencyContext(deps);
+ }
+
+ return builder;
+ }
+
+ private static string GetFallbackRid()
+ {
+ // see https://github.com/dotnet/core-setup/blob/b64f7fffbd14a3517186b9a9d5cc001ab6e5bde6/src/corehost/common/pal.h#L53-L73
+
+ string ridBase;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ ridBase = "win10";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ ridBase = "linux";
+
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ ridBase = "osx.10.12";
+ }
+ else
+ {
+ return "any";
+ }
+
+ return RuntimeInformation.OSArchitecture switch
+ {
+ Architecture.X86 => ridBase + "-x86",
+ Architecture.X64 => ridBase + "-x64",
+ Architecture.Arm => ridBase + "-arm",
+ Architecture.Arm64 => ridBase + "-arm64",
+ _ => ridBase,
+ };
+ }
+
+ /// <summary>
+ /// Add a pre-parsed <see cref="DependencyContext" /> to the load context.
+ /// </summary>
+ /// <param name="builder">The builder.</param>
+ /// <param name="dependencyContext">The dependency context.</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder AddDependencyContext(this AssemblyLoadContextBuilder builder, DependencyContext dependencyContext)
+ {
+ var ridGraph = dependencyContext.RuntimeGraph.Any() || DependencyContext.Default == null
+ ? dependencyContext.RuntimeGraph
+ : DependencyContext.Default.RuntimeGraph;
+
+ var rid = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier();
+ var fallbackRid = GetFallbackRid();
+ var fallbackGraph = ridGraph.FirstOrDefault(g => g.Runtime == rid)
+ ?? ridGraph.FirstOrDefault(g => g.Runtime == fallbackRid)
+ ?? new RuntimeFallbacks("any");
+
+ foreach (var managed in dependencyContext.ResolveRuntimeAssemblies(fallbackGraph))
+ {
+ builder.AddManagedLibrary(managed);
+ }
+
+ foreach (var library in dependencyContext.ResolveResourceAssemblies())
+ {
+ foreach (var resource in library.ResourceAssemblies)
+ {
+ /*
+ * For resource assemblies, look in $packageRoot/$packageId/$version/$resourceGrandparent
+ *
+ * For example, a deps file may contain
+ *
+ * "Example/1.0.0": {
+ * "runtime": {
+ * "lib/netcoreapp2.0/Example.dll": { }
+ * },
+ * "resources": {
+ * "lib/netcoreapp2.0/es/Example.resources.dll": {
+ * "locale": "es"
+ * }
+ * }
+ * }
+ *
+ * In this case, probing should happen in $packageRoot/example/1.0.0/lib/netcoreapp2.0
+ */
+
+ var resourceDir = Path.GetDirectoryName(Path.GetDirectoryName(resource.Path));
+
+ if (resourceDir != null)
+ {
+ var path = Path.Combine(library.Name.ToLowerInvariant(),
+ library.Version,
+ resourceDir);
+
+ builder.AddResourceProbingSubpath(path);
+ }
+ }
+ }
+
+ foreach (var native in dependencyContext.ResolveNativeAssets(fallbackGraph))
+ {
+ builder.AddNativeLibrary(native);
+ }
+
+ return builder;
+ }
+
+ private static IEnumerable<ManagedLibrary> ResolveRuntimeAssemblies(this DependencyContext depContext, RuntimeFallbacks runtimeGraph)
+ {
+ var rids = GetRids(runtimeGraph);
+ return from library in depContext.RuntimeLibraries
+ from assetPath in SelectAssets(rids, library.RuntimeAssemblyGroups)
+ select ManagedLibrary.CreateFromPackage(library.Name, library.Version, assetPath);
+ }
+
+ private static IEnumerable<RuntimeLibrary> ResolveResourceAssemblies(this DependencyContext depContext)
+ {
+ return from library in depContext.RuntimeLibraries
+ where library.ResourceAssemblies != null && library.ResourceAssemblies.Count > 0
+ select library;
+ }
+
+ private static IEnumerable<NativeLibrary> ResolveNativeAssets(this DependencyContext depContext, RuntimeFallbacks runtimeGraph)
+ {
+ var rids = GetRids(runtimeGraph);
+ return from library in depContext.RuntimeLibraries
+ from assetPath in SelectAssets(rids, library.NativeLibraryGroups)
+ // some packages include symbols alongside native assets, such as System.Native.a or pwshplugin.pdb
+ where PlatformInformation.NativeLibraryExtensions.Contains(Path.GetExtension(assetPath), StringComparer.OrdinalIgnoreCase)
+ select NativeLibrary.CreateFromPackage(library.Name, library.Version, assetPath);
+ }
+
+ private static IEnumerable<string> GetRids(RuntimeFallbacks runtimeGraph)
+ {
+ return new[] { runtimeGraph.Runtime }.Concat(runtimeGraph?.Fallbacks ?? Enumerable.Empty<string>());
+ }
+
+ private static IEnumerable<string> SelectAssets(IEnumerable<string> rids, IEnumerable<RuntimeAssetGroup> groups)
+ {
+ foreach (var rid in rids)
+ {
+ var group = groups.FirstOrDefault(g => g.Runtime == rid);
+ if (group != null)
+ {
+ return group.AssetPaths;
+ }
+ }
+
+ // Return the RID-agnostic group
+ return groups.GetDefaultAssets();
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs
new file mode 100644
index 0000000..01b985e
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/ManagedLoadContext.cs
@@ -0,0 +1,386 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed lazy loading and hot-reload features
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using McMaster.NETCore.Plugins.LibraryModel;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// An implementation of <see cref="AssemblyLoadContext" /> which attempts to load managed and native
+ /// binaries at runtime immitating some of the behaviors of corehost.
+ /// </summary>
+ [DebuggerDisplay("'{Name}' ({_mainAssemblyPath})")]
+ internal class ManagedLoadContext : AssemblyLoadContext
+ {
+ private readonly string _basePath;
+ private readonly string _mainAssemblyPath;
+ private readonly IReadOnlyDictionary<string, ManagedLibrary> _managedAssemblies;
+ private readonly IReadOnlyDictionary<string, NativeLibrary> _nativeLibraries;
+ private readonly IReadOnlyCollection<string> _privateAssemblies;
+ private readonly ICollection<string> _defaultAssemblies;
+ private readonly IReadOnlyCollection<string> _additionalProbingPaths;
+ private readonly bool _preferDefaultLoadContext;
+ private readonly string[] _resourceRoots;
+ private readonly bool _loadInMemory;
+ private readonly AssemblyLoadContext _defaultLoadContext;
+ private readonly AssemblyDependencyResolver _dependencyResolver;
+ private readonly bool _shadowCopyNativeLibraries;
+ private readonly string _unmanagedDllShadowCopyDirectoryPath;
+
+ public ManagedLoadContext(string mainAssemblyPath,
+ IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
+ IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
+ IReadOnlyCollection<string> privateAssemblies,
+ IReadOnlyCollection<string> defaultAssemblies,
+ IReadOnlyCollection<string> additionalProbingPaths,
+ IReadOnlyCollection<string> resourceProbingPaths,
+ AssemblyLoadContext defaultLoadContext,
+ bool preferDefaultLoadContext,
+ bool isCollectible,
+ bool loadInMemory,
+ bool shadowCopyNativeLibraries)
+ : base(Path.GetFileNameWithoutExtension(mainAssemblyPath), isCollectible)
+
+ {
+ ArgumentNullException.ThrowIfNull(resourceProbingPaths);
+
+ _mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
+ _dependencyResolver = new AssemblyDependencyResolver(mainAssemblyPath);
+ _basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException(nameof(mainAssemblyPath));
+ _managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
+ _privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
+ _defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
+ _nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
+ _additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
+ _defaultLoadContext = defaultLoadContext;
+ _preferDefaultLoadContext = preferDefaultLoadContext;
+ _loadInMemory = loadInMemory;
+
+ _resourceRoots = new[] { _basePath }
+ .Concat(resourceProbingPaths)
+ .ToArray();
+
+ _shadowCopyNativeLibraries = shadowCopyNativeLibraries;
+ _unmanagedDllShadowCopyDirectoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+ if (shadowCopyNativeLibraries)
+ {
+ Unloading += _ => OnUnloaded();
+ }
+ }
+
+ /// <summary>
+ /// Load an assembly.
+ /// </summary>
+ /// <param name="assemblyName"></param>
+ /// <returns></returns>
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name == null)
+ {
+ // not sure how to handle this case. It's technically possible.
+ return null;
+ }
+
+ if ((_preferDefaultLoadContext || _defaultAssemblies.Contains(assemblyName.Name)) && !_privateAssemblies.Contains(assemblyName.Name))
+ {
+ // If default context is preferred, check first for types in the default context unless the dependency has been declared as private
+ try
+ {
+ var defaultAssembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
+ if (defaultAssembly != null)
+ {
+ // Older versions used to return null here such that returned assembly would be resolved from the default ALC.
+ // However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
+ // this context was built. As such, we simply return the Assembly from the user's chosen default load context.
+ return defaultAssembly;
+ }
+ }
+ catch
+ {
+ // Swallow errors in loading from the default context
+ }
+ }
+
+ var resolvedPath = _dependencyResolver.ResolveAssemblyToPath(assemblyName);
+ if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
+ {
+ return LoadAssemblyFromFilePath(resolvedPath);
+ }
+
+ // Resource assembly binding does not use the TPA. Instead, it probes PLATFORM_RESOURCE_ROOTS (a list of folders)
+ // for $folder/$culture/$assemblyName.dll
+ // See https://github.com/dotnet/coreclr/blob/3fca50a36e62a7433d7601d805d38de6baee7951/src/binder/assemblybinder.cpp#L1232-L1290
+
+ if (!string.IsNullOrEmpty(assemblyName.CultureName) && !string.Equals("neutral", assemblyName.CultureName))
+ {
+ foreach (var resourceRoot in _resourceRoots)
+ {
+ var resourcePath = Path.Combine(resourceRoot, assemblyName.CultureName, assemblyName.Name + ".dll");
+ if (File.Exists(resourcePath))
+ {
+ return LoadAssemblyFromFilePath(resourcePath);
+ }
+ }
+
+ return null;
+ }
+
+ if (_managedAssemblies.TryGetValue(assemblyName.Name, out var library) && library != null)
+ {
+ if (SearchForLibrary(library, out var path) && path != null)
+ {
+ return LoadAssemblyFromFilePath(path);
+ }
+ }
+ else
+ {
+ // if an assembly was not listed in the list of known assemblies,
+ // fallback to the load context base directory
+ var dllName = assemblyName.Name + ".dll";
+ foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath))
+ {
+ var localFile = Path.Combine(probingPath, dllName);
+ if (File.Exists(localFile))
+ {
+ return LoadAssemblyFromFilePath(localFile);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public Assembly LoadAssemblyFromFilePath(string path)
+ {
+ if (!_loadInMemory)
+ {
+ return LoadFromAssemblyPath(path);
+ }
+
+ using var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+ var pdbPath = Path.ChangeExtension(path, ".pdb");
+ if (File.Exists(pdbPath))
+ {
+ using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ return LoadFromStream(file, pdbFile);
+ }
+ return LoadFromStream(file);
+
+ }
+
+ /// <summary>
+ /// Loads the unmanaged binary using configured list of native libraries.
+ /// </summary>
+ /// <param name="unmanagedDllName"></param>
+ /// <returns></returns>
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ var resolvedPath = _dependencyResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (!string.IsNullOrEmpty(resolvedPath) && File.Exists(resolvedPath))
+ {
+ return LoadUnmanagedDllFromResolvedPath(resolvedPath, normalizePath: false);
+ }
+
+ foreach (var prefix in PlatformInformation.NativeLibraryPrefixes)
+ {
+ if (_nativeLibraries.TryGetValue(prefix + unmanagedDllName, out var library))
+ {
+ if (SearchForLibrary(library, prefix, out var path) && path != null)
+ {
+ return LoadUnmanagedDllFromResolvedPath(path);
+ }
+ }
+ else
+ {
+ // coreclr allows code to use [DllImport("sni")] or [DllImport("sni.dll")]
+ // This library treats the file name without the extension as the lookup name,
+ // so this loop is necessary to check if the unmanaged name matches a library
+ // when the file extension has been trimmed.
+ foreach (var suffix in PlatformInformation.NativeLibraryExtensions)
+ {
+ if (!unmanagedDllName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // check to see if there is a library entry for the library without the file extension
+ var trimmedName = unmanagedDllName.Substring(0, unmanagedDllName.Length - suffix.Length);
+
+ if (_nativeLibraries.TryGetValue(prefix + trimmedName, out library))
+ {
+ if (SearchForLibrary(library, prefix, out var path) && path != null)
+ {
+ return LoadUnmanagedDllFromResolvedPath(path);
+ }
+ }
+ else
+ {
+ // fallback to native assets which match the file name in the plugin base directory
+ var prefixSuffixDllName = prefix + unmanagedDllName + suffix;
+ var prefixDllName = prefix + unmanagedDllName;
+
+ foreach (var probingPath in _additionalProbingPaths.Prepend(_basePath))
+ {
+ var localFile = Path.Combine(probingPath, prefixSuffixDllName);
+ if (File.Exists(localFile))
+ {
+ return LoadUnmanagedDllFromResolvedPath(localFile);
+ }
+
+ var localFileWithoutSuffix = Path.Combine(probingPath, prefixDllName);
+ if (File.Exists(localFileWithoutSuffix))
+ {
+ return LoadUnmanagedDllFromResolvedPath(localFileWithoutSuffix);
+ }
+ }
+
+ }
+ }
+
+ }
+ }
+
+ return base.LoadUnmanagedDll(unmanagedDllName);
+ }
+
+ private bool SearchForLibrary(ManagedLibrary library, out string? path)
+ {
+ // 1. Check for in _basePath + app local path
+ var localFile = Path.Combine(_basePath, library.AppLocalPath);
+ if (File.Exists(localFile))
+ {
+ path = localFile;
+ return true;
+ }
+
+ // 2. Search additional probing paths
+ foreach (var searchPath in _additionalProbingPaths)
+ {
+ var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ // 3. Search in base path
+ foreach (var ext in PlatformInformation.ManagedAssemblyExtensions)
+ {
+ var local = Path.Combine(_basePath, library.Name.Name + ext);
+ if (File.Exists(local))
+ {
+ path = local;
+ return true;
+ }
+ }
+
+ path = null;
+ return false;
+ }
+
+ private bool SearchForLibrary(NativeLibrary library, string prefix, out string? path)
+ {
+ // 1. Search in base path
+ foreach (var ext in PlatformInformation.NativeLibraryExtensions)
+ {
+ var candidate = Path.Combine(_basePath, $"{prefix}{library.Name}{ext}");
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ // 2. Search in base path + app local (for portable deployments of netcoreapp)
+ var local = Path.Combine(_basePath, library.AppLocalPath);
+ if (File.Exists(local))
+ {
+ path = local;
+ return true;
+ }
+
+ // 3. Search additional probing paths
+ foreach (var searchPath in _additionalProbingPaths)
+ {
+ var candidate = Path.Combine(searchPath, library.AdditionalProbingPath);
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ return true;
+ }
+ }
+
+ path = null;
+ return false;
+ }
+
+ private IntPtr LoadUnmanagedDllFromResolvedPath(string unmanagedDllPath, bool normalizePath = true)
+ {
+ if (normalizePath)
+ {
+ unmanagedDllPath = Path.GetFullPath(unmanagedDllPath);
+ }
+
+ return _shadowCopyNativeLibraries
+ ? LoadUnmanagedDllFromShadowCopy(unmanagedDllPath)
+ : LoadUnmanagedDllFromPath(unmanagedDllPath);
+ }
+
+ private IntPtr LoadUnmanagedDllFromShadowCopy(string unmanagedDllPath)
+ {
+ var shadowCopyDllPath = CreateShadowCopy(unmanagedDllPath);
+
+ return LoadUnmanagedDllFromPath(shadowCopyDllPath);
+ }
+
+ private string CreateShadowCopy(string dllPath)
+ {
+ Directory.CreateDirectory(_unmanagedDllShadowCopyDirectoryPath);
+
+ var dllFileName = Path.GetFileName(dllPath);
+ var shadowCopyPath = Path.Combine(_unmanagedDllShadowCopyDirectoryPath, dllFileName);
+
+ if (!File.Exists(shadowCopyPath))
+ {
+ File.Copy(dllPath, shadowCopyPath);
+ }
+
+ return shadowCopyPath;
+ }
+
+ private void OnUnloaded()
+ {
+ if (!_shadowCopyNativeLibraries || !Directory.Exists(_unmanagedDllShadowCopyDirectoryPath))
+ {
+ return;
+ }
+
+ // Attempt to delete shadow copies
+ try
+ {
+ Directory.Delete(_unmanagedDllShadowCopyDirectoryPath, recursive: true);
+ }
+ catch (Exception)
+ {
+ // Files might be locked by host process. Nothing we can do about it, I guess.
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs
new file mode 100644
index 0000000..679c56a
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Loader/RuntimeConfigExtensions.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace McMaster.NETCore.Plugins.Loader
+{
+ /// <summary>
+ /// Extensions for creating a load context using settings from a runtimeconfig.json file
+ /// </summary>
+ public static class RuntimeConfigExtensions
+ {
+ private const string JsonExt = ".json";
+ private static readonly JsonSerializerOptions s_serializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ /// <summary>
+ /// Adds additional probing paths to a managed load context using settings found in the runtimeconfig.json
+ /// and runtimeconfig.dev.json files.
+ /// </summary>
+ /// <param name="builder">The context builder</param>
+ /// <param name="runtimeConfigPath">The path to the runtimeconfig.json file</param>
+ /// <param name="includeDevConfig">Also read runtimeconfig.dev.json file, if present.</param>
+ /// <param name="error">The error, if one occurs while parsing runtimeconfig.json</param>
+ /// <returns>The builder.</returns>
+ public static AssemblyLoadContextBuilder TryAddAdditionalProbingPathFromRuntimeConfig(
+ this AssemblyLoadContextBuilder builder,
+ string runtimeConfigPath,
+ bool includeDevConfig,
+ out Exception? error)
+ {
+ error = null;
+ try
+ {
+ var config = TryReadConfig(runtimeConfigPath);
+ if (config == null)
+ {
+ return builder;
+ }
+
+ RuntimeConfig? devConfig = null;
+ if (includeDevConfig)
+ {
+ var configDevPath = runtimeConfigPath.Substring(0, runtimeConfigPath.Length - JsonExt.Length) + ".dev.json";
+ devConfig = TryReadConfig(configDevPath);
+ }
+
+ var tfm = config.runtimeOptions?.Tfm ?? devConfig?.runtimeOptions?.Tfm;
+
+ if (config.runtimeOptions != null)
+ {
+ AddProbingPaths(builder, config.runtimeOptions, tfm);
+ }
+
+ if (devConfig?.runtimeOptions != null)
+ {
+ AddProbingPaths(builder, devConfig.runtimeOptions, tfm);
+ }
+
+ if (tfm != null)
+ {
+ var dotnet = Process.GetCurrentProcess().MainModule.FileName;
+ if (string.Equals(Path.GetFileNameWithoutExtension(dotnet), "dotnet", StringComparison.OrdinalIgnoreCase))
+ {
+ var dotnetHome = Path.GetDirectoryName(dotnet);
+ if (dotnetHome != null)
+ {
+ builder.AddProbingPath(Path.Combine(dotnetHome, "store", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), tfm));
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+ return builder;
+ }
+
+ private static void AddProbingPaths(AssemblyLoadContextBuilder builder, RuntimeOptions options, string? tfm)
+ {
+ if (options.AdditionalProbingPaths == null)
+ {
+ return;
+ }
+
+ foreach (var item in options.AdditionalProbingPaths)
+ {
+ var path = item;
+ if (path.Contains("|arch|"))
+ {
+ path = path.Replace("|arch|", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant());
+ }
+
+ if (path.Contains("|tfm|"))
+ {
+ if (tfm == null)
+ {
+ // We don't have enough information to parse this
+ continue;
+ }
+
+ path = path.Replace("|tfm|", tfm);
+ }
+
+ builder.AddProbingPath(path);
+ }
+ }
+
+ private static RuntimeConfig? TryReadConfig(string path)
+ {
+ try
+ {
+ var file = File.ReadAllBytes(path);
+ return JsonSerializer.Deserialize<RuntimeConfig>(file, s_serializerOptions);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj
new file mode 100644
index 0000000..fbc293c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/McMaster.NETCore.Plugins.csproj
@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFrameworks>net8.0</TargetFrameworks>
+ <OutputType>library</OutputType>
+ <Nullable>enable</Nullable>
+ <IsPublishable>False</IsPublishable>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <PackageDescription>Provides API for dynamically loading assemblies into a .NET application.
+
+This package should be used by the host application which needs to load plugins.
+See https://github.com/natemcmaster/DotNetCorePlugins/blob/main/README.md for more samples and documentation.
+ </PackageDescription>
+ <PackageTags>.NET Core;plugins</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
+ <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\lib\Utils\src\VNLib.Utils.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/third-party/DotNetCorePlugins/src/PluginConfig.cs b/third-party/DotNetCorePlugins/src/PluginConfig.cs
new file mode 100644
index 0000000..d9f781c
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/PluginConfig.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed hot reload as an option
+ * - Used net 8.0 auto properties instead of field indexers
+ * - Removed experimental lazy loading since it's not safe for my use cases
+ */
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+namespace McMaster.NETCore.Plugins
+{
+ /// <summary>
+ /// Represents the configuration for a .NET Core plugin.
+ /// </summary>
+ public class PluginConfig
+ {
+ /// <summary>
+ /// Initializes a new instance of <see cref="PluginConfig" />
+ /// </summary>
+ /// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
+ public PluginConfig(string mainAssemblyPath)
+ {
+ if (string.IsNullOrEmpty(mainAssemblyPath))
+ {
+ throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
+ }
+
+ if (!Path.IsPathRooted(mainAssemblyPath))
+ {
+ throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
+ }
+
+ MainAssemblyPath = mainAssemblyPath;
+ }
+
+ /// <summary>
+ /// The file path to the main assembly.
+ /// </summary>
+ public string MainAssemblyPath { get; }
+
+ /// <summary>
+ /// A list of assemblies which should be treated as private.
+ /// </summary>
+ public ICollection<AssemblyName> PrivateAssemblies { get; protected set; } = new List<AssemblyName>();
+
+ /// <summary>
+ /// A list of assemblies which should be unified between the host and the plugin.
+ /// </summary>
+ /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">
+ /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
+ /// </seealso>
+ public ICollection<AssemblyName> SharedAssemblies { get; protected set; } = new List<AssemblyName>();
+
+ /// <summary>
+ /// Attempt to unify all types from a plugin with the host.
+ /// <para>
+ /// This does not guarantee types will unify.
+ /// </para>
+ /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">
+ /// https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md
+ /// </seealso>
+ /// </summary>
+ public bool PreferSharedTypes { get; set; }
+
+ /// <summary>
+ /// If set, replaces the default <see cref="AssemblyLoadContext"/> used by the <see cref="PluginLoader"/>.
+ /// Use this feature if the <see cref="AssemblyLoadContext"/> of the <see cref="Assembly"/> is not the Runtime's default load context.
+ /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) != <see cref="AssemblyLoadContext.Default"/>
+ /// </summary>
+ public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
+
+ /// <summary>
+ /// The plugin can be unloaded from memory.
+ /// </summary>
+ public bool IsUnloadable { get; set; }
+
+ /// <summary>
+ /// Loads assemblies into memory in order to not lock files.
+ /// As example use case here would be: no hot reloading but able to
+ /// replace files and reload manually at later time
+ /// </summary>
+ public bool LoadInMemory { get; set; }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/PluginLoader.cs b/third-party/DotNetCorePlugins/src/PluginLoader.cs
new file mode 100644
index 0000000..cbe46f8
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/PluginLoader.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+/*
+ * Modifications Copyright (c) 2024 Vaughn Nugent
+ *
+ * Changes:
+ * - Removed unloadable feature as an optional pragma (aka always on)
+ * - Removed internal hot-reload since my plugin runtime handles this feature better
+ * - Removed all static creation functions as plugin config is far more direct and simple
+ * - Removed old/depricated features such as native deps resolution since thats handled now
+ * - Remove experimenal lazy loading
+ * - Only store the main asm file path instead of the mutable config instance
+ */
+
+using System;
+using System.Reflection;
+using System.Runtime.Loader;
+
+using McMaster.NETCore.Plugins.Loader;
+
+namespace McMaster.NETCore.Plugins
+{
+ /// <summary>
+ /// This loader attempts to load binaries for execution (both managed assemblies and native libraries)
+ /// in the same way that .NET Core would if they were originally part of the .NET Core application.
+ /// <para>
+ /// This loader reads configuration files produced by .NET Core (.deps.json and runtimeconfig.json)
+ /// as well as a custom file (*.config files). These files describe a list of .dlls and a set of dependencies.
+ /// The loader searches the plugin path, as well as any additionally specified paths, for binaries
+ /// which satisfy the plugin's requirements.
+ /// </para>
+ /// </summary>
+ public class PluginLoader
+ {
+
+ private readonly string _mainAssemblyPath;
+ private readonly AssemblyLoadContextBuilder _contextBuilder;
+
+ private ManagedLoadContext _context;
+
+ /// <summary>
+ /// Initialize an instance of <see cref="PluginLoader" />
+ /// </summary>
+ /// <param name="config">The configuration for the plugin.</param>
+ public PluginLoader(PluginConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ ArgumentException.ThrowIfNullOrWhiteSpace(config.MainAssemblyPath);
+
+ _mainAssemblyPath = config.MainAssemblyPath;
+ _contextBuilder = CreateLoadContextBuilder(config);
+ }
+
+ /// <summary>
+ /// True when this plugin is capable of being unloaded.
+ /// </summary>
+ public bool IsUnloadable => _context.IsCollectible;
+
+ /// <summary>
+ /// Initializes the new load context
+ /// </summary>
+ public void Load() => _context = (ManagedLoadContext)_contextBuilder.Build();
+
+ internal AssemblyLoadContext LoadContext => _context;
+
+ /// <summary>
+ /// Load the main assembly for the plugin.
+ /// </summary>
+ public Assembly LoadDefaultAssembly() => _context.LoadAssemblyFromFilePath(_mainAssemblyPath);
+
+ /// <summary>
+ /// Load an assembly by name.
+ /// </summary>
+ /// <param name="assemblyName">The assembly name.</param>
+ /// <returns>The assembly.</returns>
+ public Assembly LoadAssembly(AssemblyName assemblyName) => _context.LoadFromAssemblyName(assemblyName);
+
+ /// <summary>
+ /// Sets the scope used by some System.Reflection APIs which might trigger assembly loading.
+ /// <para>
+ /// See https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md for more details.
+ /// </para>
+ /// </summary>
+ /// <returns></returns>
+ public AssemblyLoadContext.ContextualReflectionScope EnterContextualReflection()
+ => _context.EnterContextualReflection();
+
+ /// <summary>
+ /// Unloads the internal assembly load context
+ /// </summary>
+ /// <param name="invokeGc">A value that indicates if a garbage collection should be run</param>
+ /// <exception cref="InvalidOperationException"></exception>
+ public void Destroy(bool invokeGc)
+ {
+ if (!IsUnloadable)
+ {
+ throw new InvalidOperationException("The current assembly context cannot be unloaded");
+ }
+
+ _context.Unload();
+
+ //Optionally wait for GC to finish
+ if (invokeGc)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+
+ private static AssemblyLoadContextBuilder CreateLoadContextBuilder(PluginConfig config)
+ {
+ var builder = new AssemblyLoadContextBuilder();
+
+ builder.SetMainAssemblyPath(config.MainAssemblyPath);
+ builder.SetDefaultContext(config.DefaultContext);
+
+ foreach (var ext in config.PrivateAssemblies)
+ {
+ builder.PreferLoadContextAssembly(ext);
+ }
+
+ if (config.PreferSharedTypes)
+ {
+ builder.PreferDefaultLoadContext(true);
+ }
+
+ if (config.IsUnloadable)
+ {
+ builder.EnableUnloading();
+ }
+
+ if (config.LoadInMemory)
+ {
+ builder.PreloadAssembliesIntoMemory();
+ builder.ShadowCopyNativeLibraries();
+ }
+
+ foreach (var assemblyName in config.SharedAssemblies)
+ {
+ builder.PreferDefaultLoadContextAssembly(assemblyName);
+ }
+
+ return builder;
+ }
+ }
+}
diff --git a/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..7ed81e4
--- /dev/null
+++ b/third-party/DotNetCorePlugins/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Nate McMaster.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("McMaster.NETCore.Plugins.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001df0eba4297c8ffdf114a13714ad787744619dfb18e29191703f6f782d6a09e4a4cac35b8c768cbbd9ade8197bc0f66ec66fabc9071a206c8060af8b7a332236968d3ee44b90bd2f30d0edcb6150555c6f8d988e48234debaf2d427a08d7c06ba1343411142dc8ac996f7f7dbe0e93d13f17a7624db5400510e6144b0fd683b9")]