diff --git a/LibGit2Sharp.Tests/GlobalSettingsFixture.cs b/LibGit2Sharp.Tests/GlobalSettingsFixture.cs index 76b2c2ad3..381d13d65 100644 --- a/LibGit2Sharp.Tests/GlobalSettingsFixture.cs +++ b/LibGit2Sharp.Tests/GlobalSettingsFixture.cs @@ -1,4 +1,7 @@ -using System.Text.RegularExpressions; +using System; +using System.IO; +using System.Text.RegularExpressions; +using LibGit2Sharp.Core; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -49,5 +52,34 @@ public void TryingToResetNativeLibraryPathAfterLoadedThrows() Assert.Throws(() => { GlobalSettings.NativeLibraryPath = "C:/Foo"; }); } + + [SkippableTheory] + [InlineData("x86")] + [InlineData("x64")] + public void LoadFromSpecifiedPath(string architecture) + { + Skip.IfNot(Platform.IsRunningOnNetFramework(), ".NET Framework only test."); + + var nativeDllFileName = NativeDllName.Name + ".dll"; + var testDir = Path.GetDirectoryName(typeof(GlobalSettingsFixture).Assembly.Location); + var testAppExe = Path.Combine(testDir, $"NativeLibraryLoadTestApp.{architecture}.exe"); + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var platformDir = Path.Combine(tempDir, "plat"); + + try + { + Directory.CreateDirectory(Path.Combine(platformDir, architecture)); + File.Copy(Path.Combine(GlobalSettings.NativeLibraryPath, architecture, nativeDllFileName), Path.Combine(platformDir, architecture, nativeDllFileName)); + + var (output, exitCode) = ProcessHelper.RunProcess(testAppExe, arguments: $@"{NativeDllName.Name} ""{platformDir}""", workingDirectory: tempDir); + + Assert.Empty(output); + Assert.Equal(0, exitCode); + } + finally + { + DirectoryHelper.DeleteDirectory(tempDir); + } + } } } diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 29cc60403..6837e02eb 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -7,6 +7,8 @@ + + @@ -24,4 +26,18 @@ + + + <_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).exe')" /> + <_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).exe.config')" /> + <_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).pdb')" /> + + + + + + + + + diff --git a/LibGit2Sharp.Tests/TestHelpers/ProcessHelper.cs b/LibGit2Sharp.Tests/TestHelpers/ProcessHelper.cs new file mode 100644 index 000000000..7c2855528 --- /dev/null +++ b/LibGit2Sharp.Tests/TestHelpers/ProcessHelper.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace LibGit2Sharp.Tests +{ + public static class ProcessHelper + { + public static (string, int) RunProcess(string fileName, string arguments, string workingDirectory = null) + { + var process = new Process + { + StartInfo = new ProcessStartInfo(fileName, arguments) + { + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory ?? string.Empty + } + }; + + var output = new StringBuilder(); + + process.OutputDataReceived += (_, e) => output.AppendLine(e.Data); + process.ErrorDataReceived += (_, e) => output.AppendLine(e.Data); + + process.Start(); + + process.WaitForExit(); + + return (output.ToString(), process.ExitCode); + } + } +} diff --git a/LibGit2Sharp.sln b/LibGit2Sharp.sln index fb8a7101b..921aee2d4 100644 --- a/LibGit2Sharp.sln +++ b/LibGit2Sharp.sln @@ -9,12 +9,18 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0CA739FD-DA4D-4F64-9834-DA14A3ECD04B}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore + Targets\CodeGenerator.targets = Targets\CodeGenerator.targets Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + Targets\GenerateNativeDllName.targets = Targets\GenerateNativeDllName.targets nuget.config = nuget.config version.json = version.json EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeLibraryLoadTestApp.x86", "NativeLibraryLoadTestApp\x86\NativeLibraryLoadTestApp.x86.csproj", "{86453D2C-4953-4DF4-B12A-ADE579608BAA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeLibraryLoadTestApp.x64", "NativeLibraryLoadTestApp\x64\NativeLibraryLoadTestApp.x64.csproj", "{5C55175D-6A1F-4C51-B791-BF7DD00124EE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +35,14 @@ Global {286E63EB-04DD-4ADE-88D6-041B57800761}.Debug|Any CPU.Build.0 = Debug|Any CPU {286E63EB-04DD-4ADE-88D6-041B57800761}.Release|Any CPU.ActiveCfg = Release|Any CPU {286E63EB-04DD-4ADE-88D6-041B57800761}.Release|Any CPU.Build.0 = Release|Any CPU + {86453D2C-4953-4DF4-B12A-ADE579608BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86453D2C-4953-4DF4-B12A-ADE579608BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86453D2C-4953-4DF4-B12A-ADE579608BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86453D2C-4953-4DF4-B12A-ADE579608BAA}.Release|Any CPU.Build.0 = Release|Any CPU + {5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index ddcb0cdd1..b5aa8097e 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -1,10 +1,15 @@ using System; -using System.Globalization; using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using LibGit2Sharp.Core.Handles; +// Restrict the set of directories where the native library is loaded from to safe directories. +[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.ApplicationDirectory | DllImportSearchPath.SafeDirectories)] + +#pragma warning disable IDE1006 // Naming Styles + // ReSharper disable InconsistentNaming namespace LibGit2Sharp.Core { @@ -17,41 +22,68 @@ internal static class NativeMethods // This will handle initialization and shutdown of the underlying // native library. #pragma warning disable 0414 - private static readonly NativeShutdownObject shutdownObject; - #pragma warning restore 0414 + private static NativeShutdownObject shutdownObject; +#pragma warning restore 0414 static NativeMethods() { - if (Platform.OperatingSystem == OperatingSystemType.Windows) + if (Platform.IsRunningOnNetFramework() || Platform.IsRunningOnNetCore()) { - string nativeLibraryPath = GlobalSettings.GetAndLockNativeLibraryPath(); - - string path = Path.Combine(nativeLibraryPath, Platform.ProcessorArchitecture); - - const string pathEnvVariable = "PATH"; - Environment.SetEnvironmentVariable(pathEnvVariable, - String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", path, Path.PathSeparator, Environment.GetEnvironmentVariable(pathEnvVariable))); + string nativeLibraryDir = GlobalSettings.GetAndLockNativeLibraryPath(); + if (nativeLibraryDir != null) + { + string nativeLibraryPath = Path.Combine(nativeLibraryDir, libgit2 + Platform.GetNativeLibraryExtension()); + + // Try to load the .dll from the path explicitly. + // If this call succeeds further DllImports will find the library loaded and not attempt to load it again. + // If it fails the next DllImport will load the library from safe directories. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + LoadWindowsLibrary(nativeLibraryPath); + } + else + { + LoadUnixLibrary(nativeLibraryPath, RTLD_NOW); + } + } } - LoadNativeLibrary(); - shutdownObject = new NativeShutdownObject(); + InitializeNativeLibrary(); } + public const int RTLD_NOW = 0x002; + + [DllImport("libdl", EntryPoint = "dlopen")] + private static extern IntPtr LoadUnixLibrary(string path, int flags); + + [DllImport("kernel32", EntryPoint = "LoadLibrary")] + private static extern IntPtr LoadWindowsLibrary(string path); + // Avoid inlining this method because otherwise mono's JITter may try // to load the library _before_ we've configured the path. [MethodImpl(MethodImplOptions.NoInlining)] - private static void LoadNativeLibrary() + private static void InitializeNativeLibrary() { - // Configure the OpenSSL locking on the true initialization - // of the library. - if (git_libgit2_init() == 1) + int initCounter; + try + { + } + finally // avoid thread aborts + { + // Initialization can be called multiple times as long as there is a corresponding shutdown to each initialization. + initCounter = git_libgit2_init(); + shutdownObject = new NativeShutdownObject(); + } + + // Configure the OpenSSL locking on the first initialization of the library in the current process. + if (initCounter == 1) { git_openssl_set_locking(); } } // Shutdown the native library in a finalizer. - private sealed class NativeShutdownObject + private sealed class NativeShutdownObject : CriticalFinalizerObject { ~NativeShutdownObject() { diff --git a/LibGit2Sharp/Core/Platform.cs b/LibGit2Sharp/Core/Platform.cs index a07d5172e..e8d536475 100644 --- a/LibGit2Sharp/Core/Platform.cs +++ b/LibGit2Sharp/Core/Platform.cs @@ -33,8 +33,43 @@ public static OperatingSystemType OperatingSystem return OperatingSystemType.MacOSX; } - throw new InvalidOperationException(); + throw new PlatformNotSupportedException(); } } + + public static string GetNativeLibraryExtension() + { + switch (OperatingSystem) + { + case OperatingSystemType.MacOSX: + return ".dylib"; + + case OperatingSystemType.Unix: + return ".so"; + + case OperatingSystemType.Windows: + return ".dll"; + } + + throw new PlatformNotSupportedException(); + } + + /// + /// Returns true if the runtime is Mono. + /// + public static bool IsRunningOnMono() + => Type.GetType("Mono.Runtime") != null; + + /// + /// Returns true if the runtime is .NET Framework. + /// + public static bool IsRunningOnNetFramework() + => typeof(object).Assembly.GetName().Name == "mscorlib" && !IsRunningOnMono(); + + /// + /// Returns true if the runtime is .NET Core. + /// + public static bool IsRunningOnNetCore() + => typeof(object).Assembly.GetName().Name != "mscorlib"; } } diff --git a/LibGit2Sharp/GlobalSettings.cs b/LibGit2Sharp/GlobalSettings.cs index c953a7b0e..f71646e76 100644 --- a/LibGit2Sharp/GlobalSettings.cs +++ b/LibGit2Sharp/GlobalSettings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using LibGit2Sharp.Core; namespace LibGit2Sharp @@ -13,44 +14,59 @@ public static class GlobalSettings { private static readonly Lazy version = new Lazy(Version.Build); private static readonly Dictionary registeredFilters; + private static readonly bool nativeLibraryPathAllowed; private static LogConfiguration logConfiguration = LogConfiguration.None; private static string nativeLibraryPath; private static bool nativeLibraryPathLocked; + private static string nativeLibraryDefaultPath; static GlobalSettings() { - if (Platform.OperatingSystem == OperatingSystemType.Windows) - { - /* Assembly.CodeBase is not actually a correctly formatted - * URI. It's merely prefixed with `file:///` and has its - * backslashes flipped. This is superior to EscapedCodeBase, - * which does not correctly escape things, and ambiguates a - * space (%20) with a literal `%20` in the path. Sigh. - */ - var managedPath = Assembly.GetExecutingAssembly().CodeBase; - if (managedPath == null) - { - managedPath = Assembly.GetExecutingAssembly().Location; - } - else if (managedPath.StartsWith("file:///")) - { - managedPath = managedPath.Substring(8).Replace('/', '\\'); - } - else if (managedPath.StartsWith("file://")) - { - managedPath = @"\\" + managedPath.Substring(7).Replace('/', '\\'); - } + bool netFX = Platform.IsRunningOnNetFramework(); + bool netCore = Platform.IsRunningOnNetCore(); - managedPath = Path.GetDirectoryName(managedPath); + nativeLibraryPathAllowed = netFX || netCore; - nativeLibraryPath = Path.Combine(managedPath, "lib", "win32"); + if (netFX) + { + // For .NET Framework apps the dependencies are deployed to lib/win32/{architecture} directory + nativeLibraryDefaultPath = Path.Combine(GetExecutingAssemblyDirectory(), "lib", "win32"); + } + else + { + nativeLibraryDefaultPath = null; } registeredFilters = new Dictionary(); } + private static string GetExecutingAssemblyDirectory() + { + // Assembly.CodeBase is not actually a correctly formatted + // URI. It's merely prefixed with `file:///` and has its + // backslashes flipped. This is superior to EscapedCodeBase, + // which does not correctly escape things, and ambiguates a + // space (%20) with a literal `%20` in the path. Sigh. + var managedPath = Assembly.GetExecutingAssembly().CodeBase; + if (managedPath == null) + { + managedPath = Assembly.GetExecutingAssembly().Location; + } + else if (managedPath.StartsWith("file:///")) + { + managedPath = managedPath.Substring(8).Replace('/', '\\'); + } + else if (managedPath.StartsWith("file://")) + { + managedPath = @"\\" + managedPath.Substring(7).Replace('/', '\\'); + } + + managedPath = Path.GetDirectoryName(managedPath); + return managedPath; + } + /// /// Returns information related to the current LibGit2Sharp /// library. @@ -148,35 +164,41 @@ public static LogConfiguration LogConfiguration } /// - /// Sets a hint path for searching for native binaries: when - /// specified, native binaries will first be searched in a - /// subdirectory of the given path corresponding to the operating - /// system and architecture (eg, "x86" or "x64") before falling - /// back to the default path ("lib\win32\x86" or "lib\win32\x64" - /// next to the application). + /// Sets a path for loading native binaries on .NET Framework or .NET Core. + /// When specified, native library will first be searched under the given path. + /// On .NET Framework a subdirectory corresponding to the architecture ("x86" or "x64") is appended, + /// otherwise the native library is expected to be found in the directory as specified. + /// + /// If the library is not found it will be searched in standard search paths: + /// , + /// and + /// . /// /// This must be set before any other calls to the library, - /// and is not available on Unix platforms: see your dynamic - /// library loader's documentation for details. + /// and is not available on other platforms than .NET Framework and .NET Core. + /// + /// + /// If not specified on .NET Framework it defaults to lib/win32 subdirectory + /// of the directory where this assembly is loaded from. /// /// public static string NativeLibraryPath { get { - if (Platform.OperatingSystem != OperatingSystemType.Windows) + if (!nativeLibraryPathAllowed) { - throw new LibGit2SharpException("Querying the native hint path is only supported on Windows platforms"); + throw new LibGit2SharpException("Querying the native hint path is only supported on .NET Framework and .NET Core platforms"); } - return nativeLibraryPath; + return nativeLibraryPath ?? nativeLibraryDefaultPath; } set { - if (Platform.OperatingSystem != OperatingSystemType.Windows) + if (!nativeLibraryPathAllowed) { - throw new LibGit2SharpException("Setting the native hint path is only supported on Windows platforms"); + throw new LibGit2SharpException("Setting the native hint path is only supported on .NET Framework and .NET Core platforms"); } if (nativeLibraryPathLocked) @@ -184,14 +206,22 @@ public static string NativeLibraryPath throw new LibGit2SharpException("You cannot set the native library path after it has been loaded"); } - nativeLibraryPath = value; + try + { + nativeLibraryPath = Path.GetFullPath(value); + } + catch (Exception e) + { + throw new LibGit2SharpException(e.Message); + } } } internal static string GetAndLockNativeLibraryPath() { nativeLibraryPathLocked = true; - return nativeLibraryPath; + string result = nativeLibraryPath ?? nativeLibraryDefaultPath; + return Platform.IsRunningOnNetFramework() ? Path.Combine(result, Platform.ProcessorArchitecture) : result; } /// diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index fc15f0e2f..9b910685e 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -38,7 +38,8 @@ - + + diff --git a/NativeLibraryLoadTestApp/Directory.Build.props b/NativeLibraryLoadTestApp/Directory.Build.props new file mode 100644 index 000000000..c55b35c72 --- /dev/null +++ b/NativeLibraryLoadTestApp/Directory.Build.props @@ -0,0 +1,7 @@ + + + + false + + + diff --git a/NativeLibraryLoadTestApp/TestApp.cs b/NativeLibraryLoadTestApp/TestApp.cs new file mode 100644 index 000000000..234169a75 --- /dev/null +++ b/NativeLibraryLoadTestApp/TestApp.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp.Tests +{ + public class TestApp + { + [DllImport("kernel32")] + private static extern IntPtr GetModuleHandle(string path); + + [DllImport("kernel32")] + private static extern int GetModuleFileName(IntPtr handle, [Out]StringBuilder path, int size); + + static int Main(string[] args) + { + if (args.Length < 1 || args.Length > 2) + { + Console.Error.WriteLine("Usage: "); + return -1; + } + + var moduleName = args[0]; + var loadFromDirectory = args[1]; + var expectedPath = Path.Combine(loadFromDirectory, (IntPtr.Size == 4) ? "x86" : "x64", moduleName + ".dll"); + + GlobalSettings.NativeLibraryPath = loadFromDirectory; + var isValid = Repository.IsValid(Path.GetTempPath()); + + var capacity = ushort.MaxValue; + var moduleHandle = GetModuleHandle(moduleName); + var buffer = new StringBuilder(capacity); + int actualLength = GetModuleFileName(moduleHandle, buffer, capacity); + var actualPath = buffer.ToString(0, actualLength); + + if (expectedPath != actualPath) + { + Console.WriteLine(actualPath); + return 1; + } + + return 0; + } + } +} diff --git a/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj new file mode 100644 index 000000000..a3c313a59 --- /dev/null +++ b/NativeLibraryLoadTestApp/x64/NativeLibraryLoadTestApp.x64.csproj @@ -0,0 +1,17 @@ + + + + Exe + net461 + x64 + + + + + + + + + + + diff --git a/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj new file mode 100644 index 000000000..daaf8f51f --- /dev/null +++ b/NativeLibraryLoadTestApp/x86/NativeLibraryLoadTestApp.x86.csproj @@ -0,0 +1,17 @@ + + + + Exe + net461 + x86 + + + + + + + + + + + diff --git a/LibGit2Sharp/CodeGenerator.targets b/Targets/CodeGenerator.targets similarity index 75% rename from LibGit2Sharp/CodeGenerator.targets rename to Targets/CodeGenerator.targets index a317d9261..ae73a4725 100644 --- a/LibGit2Sharp/CodeGenerator.targets +++ b/Targets/CodeGenerator.targets @@ -6,36 +6,12 @@ - $(IntermediateOutputPath)NativeDllName.g.cs $(IntermediateOutputPath)UniqueIdentifier.g.cs $(IntermediateOutputPath)AssemblyCommitIds.g.cs - - - - namespace LibGit2Sharp.Core - { - internal static class NativeDllName - { - public const string Name = "$(libgit2_filename)"%3b - } - } - - - - - - - - - - - - - diff --git a/Targets/GenerateNativeDllName.targets b/Targets/GenerateNativeDllName.targets new file mode 100644 index 000000000..244b707b4 --- /dev/null +++ b/Targets/GenerateNativeDllName.targets @@ -0,0 +1,33 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + $(IntermediateOutputPath)NativeDllName.g.cs + + + + + + + namespace LibGit2Sharp.Core + { + internal static class NativeDllName + { + public const string Name = "$(libgit2_filename)"%3b + } + } + + + + + + + + + + +