| """ |
| Lib/ctypes.util.find_library() support for AIX |
| Similar approach as done for Darwin support by using separate files |
| but unlike Darwin - no extension such as ctypes.macholib.* |
| |
| dlopen() is an interface to AIX initAndLoad() - primary documentation at: |
| https://www.ibm.com/support/knowledgecenter/en/ssw_aix_61/com.ibm.aix.basetrf1/dlopen.htm |
| https://www.ibm.com/support/knowledgecenter/en/ssw_aix_61/com.ibm.aix.basetrf1/load.htm |
| |
| AIX supports two styles for dlopen(): svr4 (System V Release 4) which is common on posix |
| platforms, but also a BSD style - aka SVR3. |
| |
| From AIX 5.3 Difference Addendum (December 2004) |
| 2.9 SVR4 linking affinity |
| Nowadays, there are two major object file formats used by the operating systems: |
| XCOFF: The COFF enhanced by IBM and others. The original COFF (Common |
| Object File Format) was the base of SVR3 and BSD 4.2 systems. |
| ELF: Executable and Linking Format that was developed by AT&T and is a |
| base for SVR4 UNIX. |
| |
| While the shared library content is identical on AIX - one is located as a filepath name |
| (svr4 style) and the other is located as a member of an archive (and the archive |
| is located as a filepath name). |
| |
| The key difference arises when supporting multiple abi formats (i.e., 32 and 64 bit). |
| For svr4 either only one ABI is supported, or there are two directories, or there |
| are different file names. The most common solution for multiple ABI is multiple |
| directories. |
| |
| For the XCOFF (aka AIX) style - one directory (one archive file) is sufficient |
| as multiple shared libraries can be in the archive - even sharing the same name. |
| In documentation the archive is also referred to as the "base" and the shared |
| library object is referred to as the "member". |
| |
| For dlopen() on AIX (read initAndLoad()) the calls are similar. |
| Default activity occurs when no path information is provided. When path |
| information is provided dlopen() does not search any other directories. |
| |
| For SVR4 - the shared library name is the name of the file expected: libFOO.so |
| For AIX - the shared library is expressed as base(member). The search is for the |
| base (e.g., libFOO.a) and once the base is found the shared library - identified by |
| member (e.g., libFOO.so, or shr.o) is located and loaded. |
| |
| The mode bit RTLD_MEMBER tells initAndLoad() that it needs to use the AIX (SVR3) |
| naming style. |
| """ |
| __author__ = "Michael Felt <aixtools@felt.demon.nl>" |
| |
| import re |
| from os import environ, path |
| from sys import executable |
| from ctypes import c_void_p, sizeof |
| from subprocess import Popen, PIPE, DEVNULL |
| |
| # Executable bit size - 32 or 64 |
| # Used to filter the search in an archive by size, e.g., -X64 |
| AIX_ABI = sizeof(c_void_p) * 8 |
| |
| |
| from sys import maxsize |
| def _last_version(libnames, sep): |
| def _num_version(libname): |
| # "libxyz.so.MAJOR.MINOR" => [MAJOR, MINOR] |
| parts = libname.split(sep) |
| nums = [] |
| try: |
| while parts: |
| nums.insert(0, int(parts.pop())) |
| except ValueError: |
| pass |
| return nums or [maxsize] |
| return max(reversed(libnames), key=_num_version) |
| |
| def get_ld_header(p): |
| # "nested-function, but placed at module level |
| ld_header = None |
| for line in p.stdout: |
| if line.startswith(('/', './', '../')): |
| ld_header = line |
| elif "INDEX" in line: |
| return ld_header.rstrip('\n') |
| return None |
| |
| def get_ld_header_info(p): |
| # "nested-function, but placed at module level |
| # as an ld_header was found, return known paths, archives and members |
| # these lines start with a digit |
| info = [] |
| for line in p.stdout: |
| if re.match("[0-9]", line): |
| info.append(line) |
| else: |
| # blank line (separator), consume line and end for loop |
| break |
| return info |
| |
| def get_ld_headers(file): |
| """ |
| Parse the header of the loader section of executable and archives |
| This function calls /usr/bin/dump -H as a subprocess |
| and returns a list of (ld_header, ld_header_info) tuples. |
| """ |
| # get_ld_headers parsing: |
| # 1. Find a line that starts with /, ./, or ../ - set as ld_header |
| # 2. If "INDEX" in occurs in a following line - return ld_header |
| # 3. get info (lines starting with [0-9]) |
| ldr_headers = [] |
| p = Popen(["/usr/bin/dump", f"-X{AIX_ABI}", "-H", file], |
| universal_newlines=True, stdout=PIPE, stderr=DEVNULL) |
| # be sure to read to the end-of-file - getting all entries |
| while True: |
| ld_header = get_ld_header(p) |
| if ld_header: |
| ldr_headers.append((ld_header, get_ld_header_info(p))) |
| else: |
| break |
| p.stdout.close() |
| p.wait() |
| return ldr_headers |
| |
| def get_shared(ld_headers): |
| """ |
| extract the shareable objects from ld_headers |
| character "[" is used to strip off the path information. |
| Note: the "[" and "]" characters that are part of dump -H output |
| are not removed here. |
| """ |
| shared = [] |
| for (line, _) in ld_headers: |
| # potential member lines contain "[" |
| # otherwise, no processing needed |
| if "[" in line: |
| # Strip off trailing colon (:) |
| shared.append(line[line.index("["):-1]) |
| return shared |
| |
| def get_one_match(expr, lines): |
| """ |
| Must be only one match, otherwise result is None. |
| When there is a match, strip leading "[" and trailing "]" |
| """ |
| # member names in the ld_headers output are between square brackets |
| expr = rf'\[({expr})\]' |
| matches = list(filter(None, (re.search(expr, line) for line in lines))) |
| if len(matches) == 1: |
| return matches[0].group(1) |
| else: |
| return None |
| |
| # additional processing to deal with AIX legacy names for 64-bit members |
| def get_legacy(members): |
| """ |
| This routine provides historical aka legacy naming schemes started |
| in AIX4 shared library support for library members names. |
| e.g., in /usr/lib/libc.a the member name shr.o for 32-bit binary and |
| shr_64.o for 64-bit binary. |
| """ |
| if AIX_ABI == 64: |
| # AIX 64-bit member is one of shr64.o, shr_64.o, or shr4_64.o |
| expr = r'shr4?_?64\.o' |
| member = get_one_match(expr, members) |
| if member: |
| return member |
| else: |
| # 32-bit legacy names - both shr.o and shr4.o exist. |
| # shr.o is the preffered name so we look for shr.o first |
| # i.e., shr4.o is returned only when shr.o does not exist |
| for name in ['shr.o', 'shr4.o']: |
| member = get_one_match(re.escape(name), members) |
| if member: |
| return member |
| return None |
| |
| def get_version(name, members): |
| """ |
| Sort list of members and return highest numbered version - if it exists. |
| This function is called when an unversioned libFOO.a(libFOO.so) has |
| not been found. |
| |
| Versioning for the member name is expected to follow |
| GNU LIBTOOL conventions: the highest version (x, then X.y, then X.Y.z) |
| * find [libFoo.so.X] |
| * find [libFoo.so.X.Y] |
| * find [libFoo.so.X.Y.Z] |
| |
| Before the GNU convention became the standard scheme regardless of |
| binary size AIX packagers used GNU convention "as-is" for 32-bit |
| archive members but used an "distinguishing" name for 64-bit members. |
| This scheme inserted either 64 or _64 between libFOO and .so |
| - generally libFOO_64.so, but occasionally libFOO64.so |
| """ |
| # the expression ending for versions must start as |
| # '.so.[0-9]', i.e., *.so.[at least one digit] |
| # while multiple, more specific expressions could be specified |
| # to search for .so.X, .so.X.Y and .so.X.Y.Z |
| # after the first required 'dot' digit |
| # any combination of additional 'dot' digits pairs are accepted |
| # anything more than libFOO.so.digits.digits.digits |
| # should be seen as a member name outside normal expectations |
| exprs = [rf'lib{name}\.so\.[0-9]+[0-9.]*', |
| rf'lib{name}_?64\.so\.[0-9]+[0-9.]*'] |
| for expr in exprs: |
| versions = [] |
| for line in members: |
| m = re.search(expr, line) |
| if m: |
| versions.append(m.group(0)) |
| if versions: |
| return _last_version(versions, '.') |
| return None |
| |
| def get_member(name, members): |
| """ |
| Return an archive member matching the request in name. |
| Name is the library name without any prefix like lib, suffix like .so, |
| or version number. |
| 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 finally, legacy AIX naming scheme. |
| """ |
| # look first for a generic match - prepend lib and append .so |
| expr = rf'lib{name}\.so' |
| member = get_one_match(expr, members) |
| if member: |
| return member |
| elif AIX_ABI == 64: |
| expr = rf'lib{name}64\.so' |
| member = get_one_match(expr, members) |
| if member: |
| return member |
| # since an exact match with .so as suffix was not found |
| # look for a versioned name |
| # If a versioned name is not found, look for AIX legacy member name |
| member = get_version(name, members) |
| if member: |
| return member |
| else: |
| return get_legacy(members) |
| |
| def get_libpaths(): |
| """ |
| On AIX, the buildtime searchpath is stored in the executable. |
| as "loader header information". |
| The command /usr/bin/dump -H extracts this info. |
| Prefix searched libraries with LD_LIBRARY_PATH (preferred), |
| or LIBPATH if defined. These paths are appended to the paths |
| to libraries the python executable is linked with. |
| This mimics AIX dlopen() behavior. |
| """ |
| libpaths = environ.get("LD_LIBRARY_PATH") |
| if libpaths is None: |
| libpaths = environ.get("LIBPATH") |
| if libpaths is None: |
| libpaths = [] |
| else: |
| libpaths = libpaths.split(":") |
| objects = get_ld_headers(executable) |
| for (_, lines) in objects: |
| for line in lines: |
| # the second (optional) argument is PATH if it includes a / |
| path = line.split()[1] |
| if "/" in path: |
| libpaths.extend(path.split(":")) |
| return libpaths |
| |
| def find_shared(paths, name): |
| """ |
| paths is a list of directories to search for an archive. |
| name is the abbreviated name given to find_library(). |
| Process: search "paths" for archive, and if an archive is found |
| return the result of get_member(). |
| If an archive is not found then return None |
| """ |
| for dir in paths: |
| # /lib is a symbolic link to /usr/lib, skip it |
| if dir == "/lib": |
| continue |
| # "lib" is prefixed to emulate compiler name resolution, |
| # e.g., -lc to libc |
| base = f'lib{name}.a' |
| archive = path.join(dir, base) |
| if path.exists(archive): |
| members = get_shared(get_ld_headers(archive)) |
| member = get_member(re.escape(name), members) |
| if member != None: |
| return (base, member) |
| else: |
| return (None, None) |
| return (None, None) |
| |
| def find_library(name): |
| """AIX implementation of ctypes.util.find_library() |
| Find an archive member that will dlopen(). If not available, |
| also search for a file (or link) with a .so suffix. |
| |
| AIX supports two types of schemes that can be used with dlopen(). |
| The so-called SystemV Release4 (svr4) format is commonly suffixed |
| with .so while the (default) AIX scheme has the library (archive) |
| ending with the suffix .a |
| As an archive has multiple members (e.g., 32-bit and 64-bit) in one file |
| the argument passed to dlopen must include both the library and |
| the member names in a single string. |
| |
| find_library() looks first for an archive (.a) with a suitable member. |
| If no archive+member pair is found, look for a .so file. |
| """ |
| |
| libpaths = get_libpaths() |
| (base, member) = find_shared(libpaths, name) |
| if base != None: |
| return f"{base}({member})" |
| |
| # To get here, 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 |
| # So, look for a .so file |
| # Check libpaths for .so file |
| # Note, the installation must prepare a link from a .so |
| # to a versioned file |
| # This is common practice by GNU libtool on other platforms |
| soname = f"lib{name}.so" |
| for dir in libpaths: |
| # /lib is a symbolic link to /usr/lib, skip it |
| if dir == "/lib": |
| continue |
| shlib = path.join(dir, soname) |
| if path.exists(shlib): |
| return soname |
| # if we are here, we have not found anything plausible |
| return None |