1# -*- coding: utf-8 -*-
2# Copyright (c) 2015-1018, imageio contributors
3# Copyright (C) 2013, Zach Pincus, Almar Klein and others
4
5""" This module contains generic code to find and load a dynamic library.
6"""
7
8import os
9import sys
10import ctypes
11
12
13LOCALDIR = os.path.abspath(os.path.dirname(__file__))
14
15# Flag that can be patched / set to True to disable loading non-system libs
16SYSTEM_LIBS_ONLY = False
17
18
19def looks_lib(fname):
20 """Returns True if the given filename looks like a dynamic library.
21 Based on extension, but cross-platform and more flexible.
22 """
23 fname = fname.lower()
24 if sys.platform.startswith("win"):
25 return fname.endswith(".dll")
26 elif sys.platform.startswith("darwin"):
27 return fname.endswith(".dylib")
28 else:
29 return fname.endswith(".so") or ".so." in fname
30
31
32def generate_candidate_libs(lib_names, lib_dirs=None):
33 """Generate a list of candidate filenames of what might be the dynamic
34 library corresponding with the given list of names.
35 Returns (lib_dirs, lib_paths)
36 """
37 lib_dirs = lib_dirs or []
38
39 # Get system dirs to search
40 sys_lib_dirs = [
41 "/lib",
42 "/usr/lib",
43 "/usr/lib/x86_64-linux-gnu",
44 "/usr/lib/aarch64-linux-gnu",
45 "/usr/local/lib",
46 "/opt/local/lib",
47 ]
48
49 # Get Python dirs to search (shared if for Pyzo)
50 py_sub_dirs = ["bin", "lib", "DLLs", "Library/bin", "shared"]
51 py_lib_dirs = [os.path.join(sys.prefix, d) for d in py_sub_dirs]
52 if hasattr(sys, "base_prefix"):
53 py_lib_dirs += [os.path.join(sys.base_prefix, d) for d in py_sub_dirs]
54
55 # Get user dirs to search (i.e. HOME)
56 home_dir = os.path.expanduser("~")
57 user_lib_dirs = [os.path.join(home_dir, d) for d in ["lib"]]
58
59 # Select only the dirs for which a directory exists, and remove duplicates
60 potential_lib_dirs = lib_dirs + sys_lib_dirs + py_lib_dirs + user_lib_dirs
61 lib_dirs = []
62 for ld in potential_lib_dirs:
63 if os.path.isdir(ld) and ld not in lib_dirs:
64 lib_dirs.append(ld)
65
66 # Now attempt to find libraries of that name in the given directory
67 # (case-insensitive)
68 lib_paths = []
69 for lib_dir in lib_dirs:
70 # Get files, prefer short names, last version
71 files = os.listdir(lib_dir)
72 files = reversed(sorted(files))
73 files = sorted(files, key=len)
74 for lib_name in lib_names:
75 # Test all filenames for name and ext
76 for fname in files:
77 if fname.lower().startswith(lib_name) and looks_lib(fname):
78 lib_paths.append(os.path.join(lib_dir, fname))
79
80 # Return (only the items which are files)
81 lib_paths = [lp for lp in lib_paths if os.path.isfile(lp)]
82 return lib_dirs, lib_paths
83
84
85def load_lib(exact_lib_names, lib_names, lib_dirs=None):
86 """load_lib(exact_lib_names, lib_names, lib_dirs=None)
87
88 Load a dynamic library.
89
90 This function first tries to load the library from the given exact
91 names. When that fails, it tries to find the library in common
92 locations. It searches for files that start with one of the names
93 given in lib_names (case insensitive). The search is performed in
94 the given lib_dirs and a set of common library dirs.
95
96 Returns ``(ctypes_library, library_path)``
97 """
98
99 # Checks
100 assert isinstance(exact_lib_names, list)
101 assert isinstance(lib_names, list)
102 if lib_dirs is not None:
103 assert isinstance(lib_dirs, list)
104 exact_lib_names = [n for n in exact_lib_names if n]
105 lib_names = [n for n in lib_names if n]
106
107 # Get reference name (for better messages)
108 if lib_names:
109 the_lib_name = lib_names[0]
110 elif exact_lib_names:
111 the_lib_name = exact_lib_names[0]
112 else:
113 raise ValueError("No library name given.")
114
115 # Collect filenames of potential libraries
116 # First try a few bare library names that ctypes might be able to find
117 # in the default locations for each platform.
118 if SYSTEM_LIBS_ONLY:
119 lib_dirs, lib_paths = [], []
120 else:
121 lib_dirs, lib_paths = generate_candidate_libs(lib_names, lib_dirs)
122 lib_paths = exact_lib_names + lib_paths
123
124 # Select loader
125 if sys.platform.startswith("win"):
126 loader = ctypes.windll
127 else:
128 loader = ctypes.cdll
129
130 # Try to load until success
131 the_lib = None
132 errors = []
133 for fname in lib_paths:
134 try:
135 the_lib = loader.LoadLibrary(fname)
136 break
137 except Exception as err:
138 # Don't record errors when it couldn't load the library from an
139 # exact name -- this fails often, and doesn't provide any useful
140 # debugging information anyway, beyond "couldn't find library..."
141 if fname not in exact_lib_names:
142 errors.append((fname, err))
143
144 # No success ...
145 if the_lib is None:
146 if errors:
147 # No library loaded, and load-errors reported for some
148 # candidate libs
149 err_txt = ["%s:\n%s" % (lib, str(e)) for lib, e in errors]
150 msg = (
151 "One or more %s libraries were found, but "
152 + "could not be loaded due to the following errors:\n%s"
153 )
154 raise OSError(msg % (the_lib_name, "\n\n".join(err_txt)))
155 else:
156 # No errors, because no potential libraries found at all!
157 msg = "Could not find a %s library in any of:\n%s"
158 raise OSError(msg % (the_lib_name, "\n".join(lib_dirs)))
159
160 # Done
161 return the_lib, fname