# Author: M Felt, aixtools.net, 2016 # ctype support for cdll interface to dlopen() for AIX # find .so aka shared library members in .a files (i.e., archives) # # by default, on AIX dlopen follows the "blibpath" that can be seen # by the command "/usr/bin/dump -XNN -H exe_so_member" # As python executable probably does not include the $prefix/lib # implied in the ./configure --prefix=/foo/dir # then LIBPATH, when not already defined, is set to _sys.prefix/lib # # With this 'assist' AIX dlopen() will also search ${prefix}/lib for libraries # # NOTE: RTLD_NOW is AIX default, regardless (as of March 2016) (set above!) # NOTE: RTLD_MEMBER is needed by dlopen to open a shared object from within an archive # NITE: RTLD_NOW and RTLD_MEMBER are defined and assigned (for now) in ctypes/__init__.py # # find_library() returns the archive(member) string needed by dlopen() to open a member # as part of an archive # when versioned .so files are requested, e.g. libintl.so.8, libc.so.6, # but it is not available -> None is returned # # when a 'generic' .so file is requested, e.g., libintl.so, of libc.so # - if something is available, that is returned # # "stem" names, such as "c" or "intl" find the latest available version, # or nothing, if a shared library does not exist (e.g., "m", and "libm" will probably fail) import re as _re, os as _os, subprocess as _subprocess, sys as _sys # need machine size to find the correct member in an archive def _aixABI(): if (_sys.maxsize < 2**32): return 32 else: return 64 # return a file (stdout) of the dump command of an object # stderr still goes to 'tty', even with stderr=None specified # stderr=_os.devnull, def _get_dumpH(_exe_so_or_a, p=None): p = _subprocess.Popen(["/usr/bin/dump", "-X%s"%_aixABI(), "-H", _exe_so_or_a], universal_newlines=False, stdout=_subprocess.PIPE) p.wait() return p.stdout # 64-bit archive members have different legacy names than 32-bit ones def _get_member64(stem, lines): # Recall that member names are in square brackets [] # an old convention - insert _64 between libFOO and .so expr = r'\[lib%s_*64\.so\]' % stem res = _re.findall(expr, lines) # two AIX conventions for 64-bit archive members: shr4_64.o and shr_64.o # From memory, shr4_64.o is the later convention if not res: expr = r'\[shr4_64.o\]' res = _re.findall(expr, lines) if not res: expr = r'\[shr_64.o\]' res = _re.findall(expr, lines) if res: return res[0][1:-1] else: return None # Looking for a versioned .so filename # Get the most recent/highest numbered version def _get_version(stem, lines): # sort function, borrowed from below def _num_version(libname): # "libxyz.so.MAJOR.MINOR" => [ MAJOR, MINOR ] parts = libname.split(".") nums = [] try: while parts: nums.insert(0, int(parts.pop())) except ValueError: pass return nums or [ _sys.maxint ] expr = r'\[lib%s\.so\.[0-9]+.*\]' % stem result = _re.findall(expr, lines) if result: result.sort(key=_num_version) return result[-1] return None # return an archive member matching name # if a (versioned) .so member exists, it is returned rather than a legacy AIX member name # the assumption is that a .so mamber was added to the library - and the old members are # there to support legacy applications # # member names in in the dumpH output are in square brackets # These get stripped and regular parenthesis are added by the caller def _get_member(stem, name, lines): # look first for an exact match expr = r'\[%s\]' % name res = _re.findall(expr, lines) if res: return res[0][1:-1] elif _re.findall("\d$", name): # if name ends in a digit, a specific version is wanted, this or nothing return None # look a generic match expr = r'\[lib%s\.so\]' % stem res = _re.findall(expr, lines) if res: return res[0][1:-1] else: # _get_version returns a single entry, not an array like re.findall res = _get_version(stem, lines) if res: return res[1:-1] # to be here res(ult) is still False # now look for legacy AIX member names # 64-bit legacy names - shr4.o and shr.o if _aixABI() == 64: return(_get_member64(stem, lines)) # 32-bit searches # legacy names - shr4.o and shr.o # Not using elif because first exact match counts # must try again after each mismatch expr = r'\[shr4.o\]' res = _re.findall(expr, lines) if res: return res[0][1:-1] else: expr = r'\[shr.o\]' res = _re.findall(expr, lines) # No Match, return None; else return member name without square brackets if res: return res[0][1:-1] else: return None # make an LIBPATH like string that will be searched directory by directory # where the first match is THE match def _getLibPath_aix(libpaths=None, f=None, x=None): libpaths = _os.environ.get('LIBPATH') f = _get_dumpH(_sys.executable) try: skipping = True; for line in f: if skipping == False: x = line.split()[1] if (x.startswith('/') or x.startswith('./') or x.startswith('../')): if libpaths is not None: libpaths = "%s:%s" % (libpaths, x) else: libpaths = x elif line.startswith('INDEX PATH'): skipping = False; finally: return libpaths # search all LIBPATH paths for an archive with member # return a AIX rtl/dlopen(able) target def find_library(name, _name=None, shlib=None,envpath=None,lines=None): # find the shared library for AIX # name needs to in it's shortest form: libFOO.a, libFOO.so.1.0.1, etc. # are all FOO, as in link arguments -lFOO # AIX examines .a files first. If a suitable member is not found # in libFOO.a then look for libFFO.so # if .so file is found - return the fullpathname # so that it is (more) likely dlopen() will use the .so file # and ignore a potential libFOO.a, but without a shared member # add a . to be certain of a '.' in the 'name' # so that we can extract that value # so that it has the same value as when passing -lFOO to the linker _stem = "%s." % name if _stem.find("lib") < 0: _stem = _stem[0:_stem.find(".")] else: _stem = _stem[3:_stem.find(".")] # FIXME: make three calls - find_version, find_latest, find_file # FIXME: The current 'find_library' finds the 'latest' envpath = _os.environ.get('LIBPATH') if envpath is None: _os.environ['LIBPATH'] = "%s/lib" % _sys.prefix paths = _getLibPath_aix() for dir in paths.split(":"): if dir == "/lib": continue # AIX rtl searches .a, by default, if it exists _arfile = _os.path.join(dir, "lib%s.a" % _stem) if _os.path.exists(_arfile): lines = _get_dumpH(_arfile).read() member = _get_member(_stem, name, lines) if member != None: shlib = "lib%s.a(%s)" % (_stem, member) return shlib # look for a .so FILE last, no version checking done yet # FIXME: here we could scan all directories for a file with an exact match # except, this is suppossed to be a member name. Perhaps, wrap, not wrapped # could distinquish whether member or full path # Try matching original argument as a file if envpath is None: if _aixABI() == 32: _os.environ['LIBPATH'] = "%s/lib" % _sys.prefix else: _os.environ['LIBPATH'] = "%s/lib64" % _sys.prefix paths = _getLibPath_aix() for dir in paths.split(":"): if dir == "/lib": continue shlib = _os.path.join(dir, "%s" % name) if _os.path.exists(shlib): return shlib # Try for a generic match shlib = _os.path.join(dir, "lib%s.so" % name) if _os.path.exists(shlib): return shlib # This is for people who are trying to match a file as "./libFOO.so" # Not good practice, IMHO, but I have seen many examples of 'failures' # when trying to load a shared directory in ./ # FYI: better would be to add LIBPATH="." and/or add $prefix/lib as well # e.g., export LIBPATH=".:/opt/lib" - or it may not load anyway! if _os.path.exists(name): return name # if we are here, we have not found anything plausible return None