Skip to content

Commit

Permalink
Fix loading of libgit2 native binary on .NET Framework and .NET
Browse files Browse the repository at this point in the history
Tested on Windows.

Fixes #919
  • Loading branch information
AArnott committed Apr 15, 2023
1 parent 50b7206 commit 1cedc56
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 29 deletions.
51 changes: 40 additions & 11 deletions src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text;
using LibGit2Sharp;
using Validation;
using Windows.Win32;
using Version = System.Version;

#nullable enable
Expand Down Expand Up @@ -41,6 +42,8 @@ public static class LibGit2GitExtensions
ContextLines = 0,
};

private static FreeLibrarySafeHandle? nativeLibrary;

/// <summary>
/// Gets the number of commits in the longest single path between
/// the specified commit and the most distant ancestor (inclusive).
Expand Down Expand Up @@ -111,24 +114,50 @@ public static IEnumerable<Commit> GetCommitsFromVersion(LibGit2Context context,
/// <summary>
/// Finds the directory that contains the appropriate native libgit2 module.
/// </summary>
/// <param name="basePath">The path to the directory that contains the lib folder.</param>
/// <param name="basePath">The path to the directory that contains the runtimes folder.</param>
/// <returns>Receives the directory that native binaries are expected.</returns>
public static string? FindLibGit2NativeBinaries(string basePath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(basePath, "lib", "win32", IntPtr.Size == 4 ? "x86" : "x64");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();

// TODO: learn how to detect when to use "linux-musl".
string? os =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win" :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" :
null;

if (os is null)
{
return Path.Combine(basePath, "lib", "linux", IntPtr.Size == 4 ? "x86" : "x86_64");
return null;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))

return Path.Combine(basePath, "runtimes", $"{os}-{arch}", "native");
}

/// <summary>
/// Loads the git2 native library from the expected path so that it's available later when needed.
/// </summary>
/// <param name="basePath"><inheritdoc cref="FindLibGit2NativeBinaries(string)" path="/param" /></param>
/// <remarks>
/// This method should only be called on .NET Framework, and only loads the module when on Windows.
/// </remarks>
public static void LoadNativeBinary(string basePath)
{
#if NETCOREAPP
throw new PlatformNotSupportedException();
#else
if (nativeLibrary is null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(basePath, "lib", "osx", RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm_64" : "x86_64");
if (FindLibGit2NativeBinaries(basePath) is string directoryPath)
{
if (Directory.EnumerateFiles(directoryPath).FirstOrDefault() is string filePath)
{
nativeLibrary = PInvoke.LoadLibrary(filePath);
}
}
}

return null;
#endif
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/NerdBank.GitVersioning/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
CreateFile
FILE_ACCESS_RIGHTS
INVALID_HANDLE_VALUE
LoadLibrary
21 changes: 6 additions & 15 deletions src/Nerdbank.GitVersioning.Tasks/ContextAwareTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#endif
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Nerdbank.GitVersioning.LibGit2;

namespace MSBuildExtensionTask
{
Expand All @@ -23,10 +24,11 @@ public abstract class ContextAwareTask : Microsoft.Build.Utilities.Task
/// <inheritdoc/>
public override bool Execute()
{
#if NETCOREAPP
string taskAssemblyPath = this.GetType().GetTypeInfo().Assembly.Location;

Assembly inContextAssembly = GitLoaderContext.Instance.LoadFromAssemblyPath(taskAssemblyPath);
string unmanagedBaseDirectory = Path.GetDirectoryName(Path.GetDirectoryName(taskAssemblyPath));
#if NETCOREAPP
GitLoaderContext loaderContext = new(unmanagedBaseDirectory);
Assembly inContextAssembly = loaderContext.LoadFromAssemblyPath(taskAssemblyPath);
Type innerTaskType = inContextAssembly.GetType(this.GetType().FullName);
object innerTask = Activator.CreateInstance(innerTaskType);

Expand Down Expand Up @@ -55,18 +57,7 @@ public override bool Execute()

return result;
#else
// On .NET Framework (on Windows), we find native binaries by adding them to our PATH.
if (this.UnmanagedDllDirectory is not null)
{
string pathEnvVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
string[] searchPaths = pathEnvVar.Split(Path.PathSeparator);
if (!searchPaths.Contains(this.UnmanagedDllDirectory, StringComparer.OrdinalIgnoreCase))
{
pathEnvVar += Path.PathSeparator + this.UnmanagedDllDirectory;
Environment.SetEnvironmentVariable("PATH", pathEnvVar);
}
}

LibGit2GitExtensions.LoadNativeBinary(unmanagedBaseDirectory);
return this.ExecuteInner();
#endif
}
Expand Down
50 changes: 48 additions & 2 deletions src/Nerdbank.GitVersioning.Tasks/GitLoaderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,77 @@

// This code originally copied from https://github.com/dotnet/sourcelink/tree/c092238370e0437eb95722f28c79273244dc7f1a/src/Microsoft.Build.Tasks.Git
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See license information at https://github.com/dotnet/sourcelink/blob/c092238370e0437eb95722f28c79273244dc7f1a/License.txt.
#nullable enable

#if NETCOREAPP

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using Nerdbank.GitVersioning.LibGit2;
using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;

namespace Nerdbank.GitVersioning
{
public class GitLoaderContext : AssemblyLoadContext
{
public const string RuntimePath = "./runtimes";
private readonly string nativeDependencyBasePath;

private (string?, IntPtr) lastLoadedLibrary;

public static readonly GitLoaderContext Instance = new GitLoaderContext();
/// <summary>
/// Initializes a new instance of the <see cref="GitLoaderContext"/> class.
/// </summary>
/// <param name="nativeDependencyBasePath">The path to the directory that contains the "runtimes" folder.</param>
public GitLoaderContext(string nativeDependencyBasePath)
{
this.nativeDependencyBasePath = nativeDependencyBasePath;
}

/// <inheritdoc/>
protected override Assembly Load(AssemblyName assemblyName)
{
string path = Path.Combine(Path.GetDirectoryName(typeof(GitLoaderContext).Assembly.Location), assemblyName.Name + ".dll");
string path = Path.Combine(Path.GetDirectoryName(typeof(GitLoaderContext).Assembly.Location)!, assemblyName.Name + ".dll");
return File.Exists(path)
? this.LoadFromAssemblyPath(path)
: Default.LoadFromAssemblyName(assemblyName);
}

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
IntPtr p = base.LoadUnmanagedDll(unmanagedDllName);

if (p == IntPtr.Zero)
{
if (unmanagedDllName == this.lastLoadedLibrary.Item1)
{
return this.lastLoadedLibrary.Item2;
}

string prefix =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? string.Empty :
"lib";

string? extension =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" :
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? ".so" :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" :
null;

string fileName = $"{prefix}{unmanagedDllName}{extension}";
string? directoryPath = LibGit2GitExtensions.FindLibGit2NativeBinaries(this.nativeDependencyBasePath);
if (directoryPath is not null && NativeLibrary.TryLoad(Path.Combine(directoryPath, fileName), out p))
{
// Cache this to make us a little faster next time.
this.lastLoadedLibrary = (unmanagedDllName, p);
}
}

return p;
}
}
}
#endif
3 changes: 2 additions & 1 deletion src/nbgv/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public static int Main(string[] args)
{
string thisAssemblyPath = typeof(Program).GetTypeInfo().Assembly.Location;

Assembly inContextAssembly = GitLoaderContext.Instance.LoadFromAssemblyPath(thisAssemblyPath);
GitLoaderContext loaderContext = new(Path.GetDirectoryName(thisAssemblyPath));
Assembly inContextAssembly = loaderContext.LoadFromAssemblyPath(thisAssemblyPath);
Type innerProgramType = inContextAssembly.GetType(typeof(Program).FullName);
object innerProgram = Activator.CreateInstance(innerProgramType);

Expand Down

0 comments on commit 1cedc56

Please sign in to comment.