NativeDllResolveHandler.fs 6.8 KB
Newer Older
1 2
// Copyright (c) Microsoft Corporation.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

3
namespace FSharp.Compiler.DependencyManager
4 5

open System
6
open System.Collections.Concurrent
7 8 9
open System.IO
open System.Reflection
open System.Runtime.InteropServices
10
open Internal.Utilities
11
open Internal.Utilities.FSharpEnvironment
12
open FSharp.Compiler.IO
13 14 15

/// Signature for Native library resolution probe callback
/// host implements this, it's job is to return a list of package roots to probe.
16
type NativeResolutionProbe = delegate of Unit -> seq<string>
17

18
/// Type that encapsulates Native library probing for managed packages
19
type NativeDllResolveHandlerCoreClr(nativeProbingRoots: NativeResolutionProbe option) =
20

21
    let nativeLibraryTryLoad =
22 23 24 25
        let nativeLibraryType: Type =
            Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices", false)

        nativeLibraryType.GetMethod("TryLoad", [| typeof<string>; typeof<IntPtr>.MakeByRefType () |])
26

27
    let loadNativeLibrary path =
28 29
        let arguments = [| path :> obj; IntPtr.Zero :> obj |]

30
        if nativeLibraryTryLoad.Invoke(null, arguments) :?> bool then
31
            arguments[1] :?> IntPtr
32 33
        else
            IntPtr.Zero
34 35 36 37

    let probingFileNames (name: string) =
        // coreclr native library probing algorithm: https://github.com/dotnet/coreclr/blob/9773db1e7b1acb3ec75c9cc0e36bd62dcbacd6d5/src/System.Private.CoreLib/shared/System/Runtime/Loader/LibraryNameVariation.Unix.cs
        let isRooted = Path.IsPathRooted name
38 39 40 41 42 43 44 45 46 47

        let useSuffix s =
            not (name.Contains(s + ".") || name.EndsWith(s)) // linux devs often append version # to libraries I.e mydll.so.5.3.2

        let usePrefix =
            name.IndexOf(Path.DirectorySeparatorChar) = -1 // If name has directory information no add no prefix
            && name.IndexOf(Path.AltDirectorySeparatorChar) = -1
            && name.IndexOf(Path.PathSeparator) = -1
            && name.IndexOf(Path.VolumeSeparatorChar) = -1

48
        let prefix = [| "lib" |]
49 50 51

        let suffix =
            [|
52 53 54 55 56 57 58 59 60 61
                if RuntimeInformation.IsOSPlatform(OSPlatform.Windows) then
                    ".dll"
                    ".exe"
                elif RuntimeInformation.IsOSPlatform(OSPlatform.OSX) then
                    ".dylib"
                else
                    ".so"
            |]

        [|
62
            yield name // Bare name
63
            if not isRooted then
64
                for s in suffix do
65
                    if useSuffix s then // Suffix without prefix
66
                        yield (sprintf "%s%s" name s)
67

68
                        if usePrefix then
69
                            for p in prefix do // Suffix with prefix
70 71
                                yield (sprintf "%s%s%s" p name s)
                    elif usePrefix then
72
                        for p in prefix do // Prefix
73 74 75
                            yield (sprintf "%s%s" p name)
        |]

76
    let resolveUnmanagedDll (_: Assembly) (name: string) : IntPtr =
77 78 79
        // Enumerate probing roots looking for a dll that matches the probing name in the probed locations
        let probeForNativeLibrary root rid name =
            // Look for name in root
80 81
            probingFileNames name
            |> Array.tryPick (fun name ->
82
                let path = Path.Combine(root, "runtimes", rid, "native", name)
83
                if FileSystem.FileExistsShim(path) then Some path else None)
84 85

        let probe =
86
            match nativeProbingRoots with
87
            | None -> None
88
            | Some nativeProbingRoots ->
89
                nativeProbingRoots.Invoke()
90 91 92
                |> Seq.tryPick (fun root ->
                    probingFileNames name
                    |> Seq.tryPick (fun name ->
93
                        let path = Path.Combine(root, name)
94

95
                        if FileSystem.FileExistsShim(path) then
96 97
                            Some path
                        else
98 99
                            RidHelpers.probingRids
                            |> Seq.tryPick (fun rid -> probeForNativeLibrary root rid name)))
100 101

        match probe with
102
        | Some path -> loadNativeLibrary (path)
103 104 105 106
        | None -> IntPtr.Zero

    // netstandard 2.1 has this property, unfortunately we don't build with that yet
    //public event Func<Assembly, string, IntPtr> ResolvingUnmanagedDll
107 108 109
    let assemblyLoadContextType: Type =
        Type.GetType("System.Runtime.Loader.AssemblyLoadContext, System.Runtime.Loader", false)

110
    let eventInfo, handler, defaultAssemblyLoadContext =
111
        assemblyLoadContextType.GetEvent("ResolvingUnmanagedDll"),
112
        Func<Assembly, string, IntPtr> resolveUnmanagedDll,
113 114 115
        assemblyLoadContextType
            .GetProperty("Default", BindingFlags.Static ||| BindingFlags.Public)
            .GetValue(null, null)
116

117
    do eventInfo.AddEventHandler(defaultAssemblyLoadContext, handler)
118 119

    interface IDisposable with
120 121
        member _x.Dispose() =
            eventInfo.RemoveEventHandler(defaultAssemblyLoadContext, handler)
122

123
type NativeDllResolveHandler(nativeProbingRoots: NativeResolutionProbe option) =
124

125
    let handler: IDisposable option =
126
        if isRunningOnCoreClr then
127
            Some(new NativeDllResolveHandlerCoreClr(nativeProbingRoots) :> IDisposable)
128 129 130
        else
            None

131
    let appendPathSeparator (p: string) =
132
        let separator = string Path.PathSeparator
133 134

        if not (p.EndsWith(separator, StringComparison.OrdinalIgnoreCase)) then
135
            p + separator
136 137 138 139 140 141
        else
            p

    let addedPaths = ConcurrentBag<string>()

    let addProbeToProcessPath probePath =
142 143
        let probe = appendPathSeparator probePath
        let path = appendPathSeparator (Environment.GetEnvironmentVariable("PATH"))
144

145 146 147 148 149
        if not (path.Contains(probe)) then
            Environment.SetEnvironmentVariable("PATH", path + probe)
            addedPaths.Add probe

    let removeProbeFromProcessPath probePath =
150
        if not (String.IsNullOrWhiteSpace(probePath)) then
151 152
            let probe = appendPathSeparator probePath
            let path = appendPathSeparator (Environment.GetEnvironmentVariable("PATH"))
153

154 155 156 157
            if path.Contains(probe) then
                Environment.SetEnvironmentVariable("PATH", path.Replace(probe, ""))

    new(nativeProbingRoots: NativeResolutionProbe) = new NativeDllResolveHandler(Option.ofObj nativeProbingRoots)
158

159 160 161 162
    member internal _.RefreshPathsInEnvironment(roots: string seq) =
        for probePath in roots do
            addProbeToProcessPath probePath

163 164 165 166 167
    interface IDisposable with
        member _.Dispose() =
            match handler with
            | None -> ()
            | Some handler -> handler.Dispose()
168

169 170
            let mutable probe: string = Unchecked.defaultof<string>

171 172
            while (addedPaths.TryTake(&probe)) do
                removeProbeFromProcessPath probe