# 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. """ from __future__ import print_function import os, fnmatch, ctypes, sys, subprocess from ctypes.util import find_library from collections import deque try: from subprocess import getoutput except ImportError: from commands import getoutput 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 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 def pkg_config_check_exists(package): return (os.system('pkg-config --exists {0} 2>/dev/null'.format(package)) == 0) 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. try: FNULL = open(os.devnull, 'w') subprocess.check_call(["pkg-config", "--version"], stdout=FNULL, close_fds=True) 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 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,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 def get_ctypes_library(libname, packages, mode=None): """ 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 # Next, if we are in a virtual environment, search inside its '/lib' if "VIRTUAL_ENV" in os.environ: libdirs.append(os.path.join(os.environ["VIRTUAL_ENV"], "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)