Source code for pycbc.libutils

# Copyright (C) 2014 Josh Willis
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
This module provides a simple interface for loading a shared library via ctypes,
allowing it to be specified in an OS-independent way and searched for preferentially
according to the paths that pkg-config specifies.
"""

import importlib, inspect
import os, fnmatch, ctypes, sys, subprocess
from ctypes.util import find_library
from collections import deque
from subprocess import getoutput


# Be careful setting the mode for opening libraries! Some libraries (e.g.
# libgomp) seem to require the DEFAULT_MODE is used. Others (e.g. FFTW when
# MKL is also present) require that os.RTLD_DEEPBIND is used. If seeing
# segfaults around this code, play about this this!
DEFAULT_RTLD_MODE = ctypes.DEFAULT_MODE


[docs]def pkg_config(pkg_libraries): """Use pkg-config to query for the location of libraries, library directories, and header directories Arguments: pkg_libries(list): A list of packages as strings Returns: libraries(list), library_dirs(list), include_dirs(list) """ libraries=[] library_dirs=[] include_dirs=[] # Check that we have the packages for pkg in pkg_libraries: if os.system('pkg-config --exists %s 2>/dev/null' % pkg) == 0: pass else: print("Could not find library {0}".format(pkg)) sys.exit(1) # Get the pck-config flags if len(pkg_libraries)>0 : # PKG_CONFIG_ALLOW_SYSTEM_CFLAGS explicitly lists system paths. # On system-wide LAL installs, this is needed for swig to find lalswig.i for token in getoutput("PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags %s" % ' '.join(pkg_libraries)).split(): if token.startswith("-l"): libraries.append(token[2:]) elif token.startswith("-L"): library_dirs.append(token[2:]) elif token.startswith("-I"): include_dirs.append(token[2:]) return libraries, library_dirs, include_dirs
[docs]def pkg_config_header_strings(pkg_libraries): """ Returns a list of header strings that could be passed to a compiler """ _, _, header_dirs = pkg_config(pkg_libraries) header_strings = [] for header_dir in header_dirs: header_strings.append("-I" + header_dir) return header_strings
[docs]def pkg_config_check_exists(package): return (os.system('pkg-config --exists {0} 2>/dev/null'.format(package)) == 0)
[docs]def pkg_config_libdirs(packages): """ Returns a list of all library paths that pkg-config says should be included when linking against the list of packages given as 'packages'. An empty return list means that the package may be found in the standard system locations, irrespective of pkg-config. """ # don't try calling pkg-config if NO_PKGCONFIG is set in environment if os.environ.get("NO_PKGCONFIG", None): return [] # if calling pkg-config failes, don't continue and don't try again. with open(os.devnull, "w") as FNULL: try: subprocess.check_call(["pkg-config", "--version"], stdout=FNULL) except: print( "PyCBC.libutils: pkg-config call failed, " "setting NO_PKGCONFIG=1", file=sys.stderr, ) os.environ['NO_PKGCONFIG'] = "1" return [] # First, check that we can call pkg-config on each package in the list for pkg in packages: if not pkg_config_check_exists(pkg): raise ValueError("Package {0} cannot be found on the pkg-config search path".format(pkg)) libdirs = [] for token in getoutput("PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs-only-L {0}".format(' '.join(packages))).split(): if token.startswith("-L"): libdirs.append(token[2:]) return libdirs
[docs]def get_libpath_from_dirlist(libname, dirs): """ This function tries to find the architecture-independent library given by libname in the first available directory in the list dirs. 'Architecture-independent' means omitting any prefix such as 'lib' or suffix such as 'so' or 'dylib' or version number. Within the first directory in which a matching pattern can be found, the lexicographically first such file is returned, as a string giving the full path name. The only supported OSes at the moment are posix and mac, and this function does not attempt to determine which is being run. So if for some reason your directory has both '.so' and '.dylib' libraries, who knows what will happen. If the library cannot be found, None is returned. """ dirqueue = deque(dirs) while (len(dirqueue) > 0): nextdir = dirqueue.popleft() possible = [] # Our directory might be no good, so try/except try: for libfile in os.listdir(nextdir): if fnmatch.fnmatch(libfile,'lib'+libname+'.so*') or \ fnmatch.fnmatch(libfile,'lib'+libname+'.dylib*') or \ fnmatch.fnmatch(libfile,'lib'+libname+'.*.dylib*') or \ fnmatch.fnmatch(libfile,libname+'.dll') or \ fnmatch.fnmatch(libfile,'cyg'+libname+'-*.dll'): possible.append(libfile) except OSError: pass # There might be more than one library found, we want the highest-numbered if (len(possible) > 0): possible.sort() return os.path.join(nextdir,possible[-1]) # If we get here, we didn't find it... return None
[docs]def get_ctypes_library(libname, packages, mode=DEFAULT_RTLD_MODE): """ This function takes a library name, specified in architecture-independent fashion (i.e. omitting any prefix such as 'lib' or suffix such as 'so' or 'dylib' or version number) and a list of packages that may provide that library, and according first to LD_LIBRARY_PATH, then the results of pkg-config, and falling back to the system search path, will try to return a CDLL ctypes object. If 'mode' is given it will be used when loading the library. """ libdirs = [] # First try to get from LD_LIBRARY_PATH if "LD_LIBRARY_PATH" in os.environ: libdirs += os.environ["LD_LIBRARY_PATH"].split(":") # Next try to append via pkg_config try: libdirs += pkg_config_libdirs(packages) except ValueError: pass # We might be using conda/pip/virtualenv or some combination. This can # leave lib files in a directory that LD_LIBRARY_PATH or pkg_config # can miss. libdirs.append(os.path.join(sys.prefix, "lib")) # Note that the function below can accept an empty list for libdirs, in # which case it will return None fullpath = get_libpath_from_dirlist(libname, libdirs) if fullpath is None: # This won't actually return a full-path, but it should be something # that can be found by CDLL fullpath = find_library(libname) if fullpath is None: # We got nothin' return None else: if mode is None: return ctypes.CDLL(fullpath) else: return ctypes.CDLL(fullpath, mode=mode)
[docs]def import_optional(library_name): """ Try to import library but and return stub if not found Parameters ---------- library_name: str The name of the python library to import Returns ------- library: library or stub Either returns the library if importing is sucessful or it returns a stub which raises an import error and message when accessed. """ try: return importlib.import_module(library_name) except ImportError: # module wasn't found so let's return a stub instead to inform # the user what has happened when they try to use related functions class no_module(object): def __init__(self, library): self.library = library def __getattribute__(self, attr): if attr == 'library': return super().__getattribute__(attr) lib = self.library curframe = inspect.currentframe() calframe = inspect.getouterframes(curframe, 2) fun = calframe[1][3] msg =""" The function {} tried to access '{}' of library '{}', however, '{}' is not currently installed. To enable this functionality install '{}' (e.g. through pip / conda / system packages / source). """.format(fun, attr, lib, lib, lib) raise ImportError(inspect.cleandoc(msg)) return no_module(library_name)