Fix native library resolution for dotnet tools on Linux#700
Fix native library resolution for dotnet tools on Linux#700Happypig375 wants to merge 6 commits into
Conversation
The TryResolveClang method on Linux tried versioned SONAME names (libclang.so.20, libclang-20) that were hardcoded for LLVM 20. ClangSharp 21.1.8.3 is built for LLVM 21, so these names never match. The hardcoded version number gets stale with every LLVM major release. Remove the version-specific names entirely and try only version-agnostic names: - libclang.so (finds co-located binary in RID packages or system symlink) - libclang.so.1 (Debian/Ubuntu runtime SONAME from libclang1 package) This fixes ClangSharpPInvokeGenerator when running as a dotnet tool from a RID-specific package (e.g. ClangSharpPInvokeGenerator.linux-x64), where libclang.so is placed alongside the executable. On Linux, dlopen does not search the executable's directory by default, but the SafeDirectories DllImportSearchPath adds it, allowing NativeLibrary.TryLoad to find the co-located libclang.so. Relates to dotnet#586.
9dbe7ae to
7f99dd2
Compare
The TryResolveClang method was a Linux-specific fallback that tried versioned SONAMEs (libclang.so.20, libclang-20, libclang.so.1) before the default resolver could run. This was problematic: 1. The version number (20) was hardcoded and didn't match ClangSharp 21.x (built for LLVM 21). This caused version mismatch errors when a system libclang-20 was found instead of v21. 2. The method was unnecessary: when it returned IntPtr.Zero, the default runtime resolver with SafeDirectories would find libclang.so anyway, the same way it finds libclang.dll on Windows (which has no special handling). 3. The versioned SONAME approach is inherently fragile - it breaks with every LLVM major version bump and different distros use different naming conventions. Removing the method entirely lets the default resolver handle all platforms uniformly. The DllImportResolver still supports user-provided resolvers via the ResolveLibrary event for advanced scenarios. Relates to dotnet#586.
The default resolver with SafeDirectories should search the assembly's directory, but this doesn't work reliably for dotnet tools (especially AOT-compiled executables run from the NuGet cache via 'dotnet tool run'). This caused both libclang and libClangSharp to not be found on Linux, even though they are co-located with the executable in the RID package. Fix: After the user-provided resolver event, explicitly try loading from the assembly's own directory using Path.GetDirectoryName(assembly.Location). This handles all native dependencies (libclang, libClangSharp) uniformly on all platforms, without needing versioned SONAME fallbacks or LD_LIBRARY_PATH workarounds.
The previous approach used Path.Combine(AppContext.BaseDirectory, libraryName) which produced a path like '.../tools/any/linux-x64/libclang' without the platform-specific extension (.so/.dylib/.dll). NativeLibrary.TryLoad with an absolute path does NOT apply platform-specific name mangling, so dlopen would fail to find the file. Added platform suffix based on RuntimeInformation: Windows → .dll macOS → .dylib Linux → .so AppContext.BaseDirectory IS the correct directory for co-located native libraries in dotnet tool NuGet packages (tools/any/<rid>/), confirmed by inspecting the actual package layout on disk.
|
Will get to looking at this (and other PRs) tomorrow. Sorry for the delay, I've been fairly heads down with everything going on. |
| return IntPtr.Zero; | ||
| } | ||
|
|
||
| private static bool TryResolveClang(Assembly assembly, DllImportSearchPath? searchPath, out IntPtr nativeLibrary) |
There was a problem hiding this comment.
We explicitly want to keep this functionality.
I'm fine with explicitly trying the application directory first on non-Windows (which then is similar to SafeDirectories for Windows which is application directory, %windir%\system32, and user directories in the dll search path)
But we do want to search the system folder still otherwise and check for the various names it can bind to there
There was a problem hiding this comment.
Since LoadLibraryHelper on unix ignores the flags, we'd rather want to effectively do something like
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (searchPath.GetValueOrDefault().HasFlag(DllImportSearchPath.ApplicationDirectory))
{
// Try the names using AppContext.BaseDirectory
}
// Try the standard variations
}
For apple target we want to try just the name + .dylib
For Linux we want to try the 3 variations already covered, in that order; first for app directory then for the normal system search
Problem
ClangSharpPInvokeGenerator fails to load
libclangandlibClangSharpon Linux when run as a dotnet tool, even though the RID-specific packages (e.g.ClangSharpPInvokeGenerator.linux-x64) bundle these libraries alongside the executable.Root causes
1. Broken
TryResolveClangmethod: The customDllImportResolvertried hardcoded versioned SONAMEs (libclang.so.20,libclang-20) that don't match the LLVM version ClangSharp is built against (21). This could find a wrong-version system library or fail entirely.2. Default resolver can't find co-located libraries: The
SafeDirectoriessearch path should include the assembly's directory, but this doesn't work reliably for dotnet tools — especially AOT-compiled executables run from the NuGet cache viadotnet tool run. Bothlibclang.soandlibClangSharp.soare co-located with the executable but never found.Fix
1. Remove
TryResolveClangentirely. The versioned SONAME approach was inherently fragile (breaks with every LLVM major version bump, different distros use different naming conventions). The method was also unnecessary — when it returnedIntPtr.Zero, the default resolver should have handled it.2. Explicitly search the assembly's own directory. After the user-provided
ResolveLibraryevent, try loading fromPath.GetDirectoryName(assembly.Location). This handles all native dependencies (libclang,libClangSharp) uniformly on all platforms, without needing versioned SONAMEs orLD_LIBRARY_PATHworkarounds.This is a follow-up to #586 which was closed as resolved by the RID-specific tool package support (20.1.2.2+). While the RID package mechanism correctly makes native binaries available alongside the executable, the custom resolver prevented the default resolution path from finding them on Linux.