"""Internal platform implementation of find_library() for AIX. Lib/ctype support for LoadLibrary interface to dlopen() for AIX s implemented as a separate file, similiar to Darwin support ctype.macholib.* rather than as separate sections in utils.py for improved isolation and (I hope) readability dlopen() is implemented on AIX as initAndLoad() - official documentation at: https://www.ibm.com/support/knowledgecenter/ssw_aix_53/\ com.ibm.aix.basetechref/doc/basetrf1/dlopen.htm?lang=en and https://www.ibm.com/support/knowledgecenter/ssw_aix_53/\ com.ibm.aix.basetechref/doc/basetrf1/load.htm?lang=en """ # Author: M Felt, aixtools.net, 2016 # I thank Martin Panter for his patience and comments import os import re import sys import subprocess def aixABI(): """ Internal support function: return executable size - 32-bit, or 64-bit This is vital to the search for suitable member in an archive """ if (sys.maxsize < 2**32): return 32 else: return 64 def get_dumpH(file): """Internal support function: dump -H output provides info on archive/executable contents and related paths to shareable members. """ #dumpH parseing: # 1. Line starts with /, ./, or ../ - set object name # 2. When the string "INDEX" is located -> return object # 3. get path/archive/member info (lines starting with digits [0-9]) def get_object(p, object=None): for line in p.stdout: if (line.startswith('/') or line.startswith('./') or line.startswith('../')): object = line elif "INDEX" in line: return object return None def get_objectinfo(p): # as an object was found, return known paths, archives and members # these lines start with a digit # an empty line or end-of-file terminates the for loop lines = None for line in p.stdout: if re.match("[0-9]", line, flags=0): if lines is None: lines = line else: lines += line else: break # lines should never be blank, the if: block is only for safety if lines: return lines.strip() else: return "" # The subprocess calls dump -H without shell assistence. # Unneeded lines are removed by get_object() and get_objectinfo() p = subprocess.Popen(["/usr/bin/dump", "-X%s"%aixABI(), "-H", file], universal_newlines=True, stdout=subprocess.PIPE, shell=False) lines = None while 1: object = get_object(p) # get_object() returns None when archive has # no (more) shareable members or end-of-file was reached if object is None: break if lines is None: lines = object else: lines += "\n\n%s" % object lines += get_objectinfo(p) p.stdout.close() p.wait() return lines def get_members(input): """Internal support function: return a list of all shareable objects""" # the character "[" is identifies object_lines # so that "/usr/lib/libFOO.a[member.so]:" returns as "[member.so]" list=[] if input is None: return(list) lines = input.split("\n") for line in lines: # potential member lines contain "[" # otherwise, no processing needed if "[" in line: list.append(line[line.find("["):-1]) return(list) def get_exactMatch(expr, lines): """Internal support function: Must be only one match, otherwise result is None When there is a match, strip the leading "[" and trailing "]" """ matches = [m.group(0) for line in lines for m in [ re.search(expr, line)] if m] if len(matches) == 1: return matches[0][1:-1] else: return None # additional processing to deal with AIX legacy names for 64-bit members def get_legacy(members): """Internal support function: This routine resolves various legacy naming schemes starting in AIX4 This routine catches various schemes for 64-bit legacy names """ if aixABI() == 64: # AIX 64-bit member is usually shr_64.o although # shr64.o or shr4_64.o have been seen - in "ancient" archives expr = '\[shr4?_?64.o\]' member = get_exactMatch(expr, members) if member: return member # 32-bit legacy names # shr.o and shr4.o are both available - frequently # shr.o is preferred and returned rather than shr4.o # i.e., shr4.o is returned only when shr.o does not exist else: for expr in [ r'\[shr.o\]', r'\[shr4.o\]']: member = get_exactMatch(expr, members) if member: return member return None # Get the most recent/highest numbered version - if it exists # In an new ABI (perhaps for Python3.7) could be called # as a request for the latest version installed, or a specific version # Currently, only called when when unversioned (libFOO.so) is not available def get_version(name, members): """Internal support function: Examine member list and return highest version number """ # This is called when libFFO.a(libFFO.so) has not been found. # Version numbering is expected to follow GNU LIBTOOL conventions: # d.1 find [libFoo.so.XX] # d.2 find [libFoo.so.XX.YY] # d.3 find [libFoo.so.XX.Y.ZZZ] # Before the GNU convention took hold there was a convention in AIX # to use GNU convention "asis" for 32-bit archive members but in # 64-bit members add either 64 or _64 between libFOO and .so # to generally libFOO_64.so, but occaisionally libFOO64.so # Generally, the second expression that looks for "64" # in the member name will not be needed. # "libxyz.so.CURRENT.REVISION.AGE" => [ CURRENT, REVISION, AGE ] # the original idea for _num_version comes from # an earlier ctype.util internal function def _num_version(libname): parts = libname.split(".") nums = [] try: while parts: nums.insert(0, int(parts.pop())) except ValueError: pass return nums or [ sys.maxsize ] # A "versioned" member name ends with .so.X plus # additional, but optional dot + digit pairs # while multiple, more specific expressions could be specified # to search for .so.X, .so.X.Y and .so.X.Y.Z # we chose to use a single re that finds the required part # and then accepts additional '.' and digits # # imho, anything other that .so.digit(s).digit(s).digit(s) # with only the first .so.digit(s) required is someone purposely # trying to break the parser for expr in [ r'lib%s\.so\.[0-9]+[0-9.]*' % name, r'lib%s_?64\.so\.[0-9]+[0-9.]*' % name]: versions = [m.group(0) for line in members for m in [ re.search(expr, line)] if m] if versions: versions.sort(key=_num_version) return versions[-1] return None def get_member(name, members): """Internal support function: Given a list of members find and return the most appropriate result Priority is given to generic libXXX.so, then a versioned libXXX.so.a.b.c and lastly, legacy AIX naming scheme """ # look first for a generic match - prepend lib and append .so # '\[lib%s\.so\]' % name -> most common expr = r'\[lib%s.so\]' % name member = get_exactMatch(expr, members) if member: return member # libFOO.so was not found ... member = get_version(name, members) if member: return member else: return get_legacy(members) def getExecLibPath_aix(): """Internal support function: On AIX, the searchpath is stored in the executable. The command dump -H can extract this info. Prefix searched libraries with LIBPATH, or LD_LIBRARY_PATH if defined. Paths are appended based rather than inserted. This mimics AIX dlopen() behavior. """ # os.environ.get commented out in Python2.7 - see issue#9998 # libpaths = os.environ.get("LIBPATH") # if libpaths is None: # libpaths = os.environ.get("LD_LIBRARY_PATH") # so, libpaths is manually set to None libpaths = None lines = get_dumpH(sys.executable).split("\n") for line in lines: # valid lines begin with a digit (INDEX) # the second (optional) argument is PATH if it includes a / if re.match("[0-9]", line, flags=0): path = line.split()[1] else: continue if "/" in path: if libpaths is None: libpaths = path else: libpaths = "%s:%s" % (libpaths,path) return libpaths def find_library(name): """AIX platform implementation of find_library() This routine is called by ctype.util.find_library(). This routine should not be called directly! AIX loader behavior: Find a library looking first for an archive (.a) with a suitable member For the loader the member name is not relevant. The first archive object shared or static that resolves an unknown symbol is used by "ld" To mimic AIX rtld behavior this routines looks first for an archive libFOO.a and then examines the contents of the archive for shareable members that also match either libFOO.so, libFOO.so.X.Y.Z (so-called versioned library names) and finally AIX legacy names usually shr.o (32-bit members) and shr_64.o (64-bit members) When no archive(member) is found, look for a libFOO.so file When versioning is required - it must be provided via hard or soft-link from libFOO.so to the correct version. RTLD aka dlopen() does not do versioning. """ # paths is where to look for archives "basename" # _member is the member name based on archive basename def find_shared(paths, name): """ Search "paths" for archive, and if an archive is found return the result of get_member(member, archive). If no archives are found return None """ for dir in paths.split(":"): # /lib is a symbolic link to /usr/lib, skip it if dir == "/lib": continue # "lib" is prefixed to emulate compiler name resolution, # e.g., -lFOO to libFOO base = 'lib%s.a' % name archive = os.path.join(dir, "%s" % base) if os.path.exists(archive): members = get_members(get_dumpH(archive)) member = get_member(name, members) if member != None: return(base, member) else: return(None, None) return(None, None) libpaths = getExecLibPath_aix() (base, member) = find_shared(libpaths, name) if base != None: return "%s(%s)" % (base, member) # Being here means a member in an archive has not been found # In other words, either: # a) a .a file was not found # b) a .a file did not have a suitable member # Check libpaths for .so file soname = "lib%s.so" % name for dir in libpaths.split(":"): # /lib is a symbolic link to /usr/lib, skip it if dir == "/lib": continue shlib = os.path.join(dir, "%s" % soname) if os.path.exists(shlib): return soname # Being here means nothing suitable was found return None