From 3ad78703807e301d414b56738b7eb90a9e54c83c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 2 Jul 2021 08:01:50 -0700 Subject: [PATCH] Add @loader_path support. Adds type hinting to the delocating and libsana modules. --- delocate/cmd/delocate_wheel.py | 13 +- delocate/delocating.py | 156 ++++++++++++----- delocate/libsana.py | 300 ++++++++++++++++++++++++++++----- setup.py | 3 + 4 files changed, 382 insertions(+), 90 deletions(-) diff --git a/delocate/cmd/delocate_wheel.py b/delocate/cmd/delocate_wheel.py index caa38792..77934acd 100644 --- a/delocate/cmd/delocate_wheel.py +++ b/delocate/cmd/delocate_wheel.py @@ -9,6 +9,8 @@ import os from os.path import join as pjoin, basename, exists, expanduser import sys +import logging +from typing import List, Optional, Text from optparse import OptionParser, Option @@ -16,6 +18,7 @@ def main(): + # type: () -> None parser = OptionParser( usage="%s WHEEL_FILENAME\n\n" % sys.argv[0] + __doc__, version="%prog " + __version__) @@ -28,8 +31,10 @@ def main(): help="Directory to store delocated wheels (default is to " "overwrite input)"), Option("-v", "--verbose", - action="store_true", - help="Show more verbose report of progress and failure"), + action="count", + help="Show a more verbose report of progress and failure." + " Additional flags show even more info, up to -vv.", + default=0), Option("-k", "--check-archs", action="store_true", help="Check architectures of depended libraries"), @@ -45,6 +50,9 @@ def main(): if len(wheels) < 1: parser.print_help() sys.exit(1) + logging.basicConfig( + level=max(logging.DEBUG, logging.WARNING - 10 * opts.verbose), + ) multi = len(wheels) > 1 if opts.wheel_dir: wheel_dir = expanduser(opts.wheel_dir) @@ -52,6 +60,7 @@ def main(): os.makedirs(wheel_dir) else: wheel_dir = None + require_archs = None # type: Optional[List[Text]] if opts.require_archs is None: require_archs = [] if opts.check_archs else None elif ',' in opts.require_archs: diff --git a/delocate/delocating.py b/delocate/delocating.py index 8bc98012..1df7a634 100644 --- a/delocate/delocating.py +++ b/delocate/delocating.py @@ -6,17 +6,24 @@ import os from os.path import (join as pjoin, dirname, basename, exists, abspath, relpath, realpath) +import logging import shutil import warnings from subprocess import Popen, PIPE +from typing import (Callable, Dict, FrozenSet, Iterable, List, Optional, + Set, Text, Tuple, Union) from .pycompat import string_types -from .libsana import tree_libs, stripped_lib_dict, get_rp_stripper +from .libsana import (tree_libs, stripped_lib_dict, get_rp_stripper, + walk_directory, get_dependencies) from .tools import (set_install_name, zip2dir, dir2zip, validate_signature, find_package_dirs, set_install_id, get_archs) from .tmpdirs import TemporaryDirectory from .wheeltools import rewrite_record, InWheel + +logger = logging.getLogger(__name__) + # Prefix for install_name_id of copied libraries DLC_PREFIX = '/DLC/' @@ -25,7 +32,12 @@ class DelocationError(Exception): pass -def delocate_tree_libs(lib_dict, lib_path, root_path): +def delocate_tree_libs( + lib_dict, # type: Dict[Text, Dict[Text, Text]] + lib_path, # type: Text + root_path # type: Text +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Move needed libraries in `lib_dict` into `lib_path` `lib_dict` has keys naming libraries required by the files in the @@ -63,7 +75,6 @@ def delocate_tree_libs(lib_dict, lib_path, root_path): delocated_libs = set() copied_basenames = set() rp_root_path = realpath(root_path) - rp_lib_path = realpath(lib_path) # Test for errors first to avoid getting half-way through changing the tree for required, requirings in lib_dict.items(): if required.startswith('@'): # assume @rpath etc are correct @@ -80,29 +91,51 @@ def delocate_tree_libs(lib_dict, lib_path, root_path): if not exists(required): raise DelocationError('library "{0}" does not exist'.format( required)) - copied_libs[required] = requirings + # Copy requirings to preserve it since it will be modified later. + copied_libs[required] = requirings.copy() copied_basenames.add(r_ed_base) else: # Is local, plan to set relative loader_path delocated_libs.add(required) - # Modify in place now that we've checked for errors - for required in copied_libs: - shutil.copy(required, lib_path) - # Set rpath and install names for this copied library - for requiring, orig_install_name in lib_dict[required].items(): - req_rel = relpath(rp_lib_path, dirname(requiring)) - set_install_name(requiring, orig_install_name, - '@loader_path/{0}/{1}'.format( - req_rel, basename(required))) + # Modify in place now that we've checked for errors. + # Copy libraries outside of root_path to lib_path. + for old_path in copied_libs: + new_path = realpath(pjoin(lib_path, basename(old_path))) + logger.info( + "Copying library %s to %s", old_path, relpath(new_path, root_path) + ) + shutil.copy(old_path, new_path) + # Delocate this file now that it is stored locally. + delocated_libs.add(new_path) + # Update lib_dict with the new file paths. + lib_dict[new_path] = lib_dict[old_path] + del lib_dict[old_path] + for required in list(lib_dict): + if old_path not in lib_dict[required]: + continue + lib_dict[required][new_path] = lib_dict[required][old_path] + del lib_dict[required][old_path] + # Update install names of libraries using lib_dict. for required in delocated_libs: # Set relative path for local library for requiring, orig_install_name in lib_dict[required].items(): req_rel = relpath(required, dirname(requiring)) - set_install_name(requiring, orig_install_name, - '@loader_path/' + req_rel) + new_install_name = '@loader_path/' + req_rel + logger.info( + "Modifying install name in %s from %s to %s", + relpath(requiring, root_path), + orig_install_name, + new_install_name, + ) + set_install_name(requiring, orig_install_name, new_install_name) return copied_libs -def copy_recurse(lib_path, copy_filt_func=None, copied_libs=None): +def copy_recurse( + lib_path, # type: Text + copy_filt_func=None, # type: Optional[Callable[[Text], bool]] + copied_libs=None # type: Optional[Dict[Text, Dict[Text, Text]]] +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Analyze `lib_path` for library dependencies and copy libraries `lib_path` is a directory containing libraries. The libraries might @@ -135,7 +168,16 @@ def copy_recurse(lib_path, copy_filt_func=None, copied_libs=None): copied_libs : dict Input `copied_libs` dict with any extra libraries and / or dependencies added. + + .. deprecated:: 0.8 + This function is obsolete. :func:`delocate_path` handles recursive + dependencies while also supporting `@loader_path`. """ + warnings.warn( + "copy_recurse is obsolete and should no longer be called.", + DeprecationWarning, + stacklevel=2, + ) if copied_libs is None: copied_libs = {} else: @@ -148,7 +190,12 @@ def copy_recurse(lib_path, copy_filt_func=None, copied_libs=None): return copied_libs -def _copy_required(lib_path, copy_filt_func, copied_libs): +def _copy_required( + lib_path, # type: Text + copy_filt_func, # type: Optional[Callable[[Text], bool]] + copied_libs # type: Dict[Text, Dict[Text, Text]] +): + # type: (...) -> None """ Copy libraries required for files in `lib_path` to `lib_path` Augment `copied_libs` dictionary with any newly copied libraries, modifying @@ -235,18 +282,24 @@ def _copy_required(lib_path, copy_filt_func, copied_libs): def _dylibs_only(filename): + # type: (Text) -> bool return (filename.endswith('.so') or filename.endswith('.dylib')) def filter_system_libs(libname): + # type: (Text) -> bool return not (libname.startswith('/usr/lib') or libname.startswith('/System')) -def delocate_path(tree_path, lib_path, - lib_filt_func=None, - copy_filt_func=filter_system_libs): +def delocate_path( + tree_path, # type: Text + lib_path, # type: Text + lib_filt_func=None, # type: Union[None, str, Callable[[Text], bool]] + copy_filt_func=filter_system_libs # type: Optional[Callable[[Text], bool]] +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Copy required libraries for files in `tree_path` into `lib_path` Parameters @@ -277,20 +330,33 @@ def delocate_path(tree_path, lib_path, is a file in the path depending on ``copied_lib_path``, and the value is the ``install_name`` of ``copied_lib_path`` in the depending library. + + Raises + ------ + DependencyNotFound + When any dependencies can not be located. """ if lib_filt_func == "dylibs-only": lib_filt_func = _dylibs_only + elif isinstance(lib_filt_func, str): + raise TypeError('lib_filt_func string can only be "dylibs-only"') + lib_filt_func = lib_filt_func or (lambda _: True) if not exists(lib_path): os.makedirs(lib_path) - lib_dict = tree_libs(tree_path, lib_filt_func) - if copy_filt_func is not None: - lib_dict = dict((key, value) for key, value in lib_dict.items() - if copy_filt_func(key)) - copied = delocate_tree_libs(lib_dict, lib_path, tree_path) - return copy_recurse(lib_path, copy_filt_func, copied) + + lib_dict = {} # type: Dict[Text, Dict[Text, Text]] + for library_path in walk_directory(tree_path, lib_filt_func): + for depending_path, install_name in get_dependencies(library_path): + if copy_filt_func and not copy_filt_func(depending_path): + continue + lib_dict.setdefault(depending_path, {}) + lib_dict[depending_path][library_path] = install_name + + return delocate_tree_libs(lib_dict, lib_path, tree_path) def _merge_lib_dict(d1, d2): + # type: (Dict[Text, Dict[Text, Text]], Dict[Text, Dict[Text, Text]]) -> None """ Merges lib_dict `d2` into lib_dict `d1` """ for required, requirings in d2.items(): @@ -301,14 +367,16 @@ def _merge_lib_dict(d1, d2): return None -def delocate_wheel(in_wheel, - out_wheel=None, - lib_sdir='.dylibs', - lib_filt_func=None, - copy_filt_func=filter_system_libs, - require_archs=None, - check_verbose=False, - ): +def delocate_wheel( + in_wheel, # type: Text + out_wheel=None, # type: Optional[Text] + lib_sdir='.dylibs', # type: Text + lib_filt_func=None, # type: Union[None, str, Callable[[Text], bool]] + copy_filt_func=filter_system_libs, # type: Optional[Callable[[Text], bool]] + require_archs=None, # type: Union[None, Text, Iterable[Text]] + check_verbose=False, # type: bool +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Update wheel by copying required libraries to `lib_sdir` in wheel Create `lib_sdir` in wheel tree only if we are copying one or more @@ -359,8 +427,6 @@ def delocate_wheel(in_wheel, is the ``install_name`` of ``copied_lib_path`` in the depending library. The filenames in the keys are relative to the wheel root path. """ - if lib_filt_func == "dylibs-only": - lib_filt_func = _dylibs_only in_wheel = abspath(in_wheel) if out_wheel is None: out_wheel = in_wheel @@ -368,7 +434,7 @@ def delocate_wheel(in_wheel, out_wheel = abspath(out_wheel) in_place = in_wheel == out_wheel with TemporaryDirectory() as tmpdir: - all_copied = {} + all_copied = {} # type: Dict[Text, Dict[Text, Text]] wheel_dir = realpath(pjoin(tmpdir, 'wheel')) zip2dir(in_wheel, wheel_dir) for package_path in find_package_dirs(wheel_dir): @@ -409,6 +475,7 @@ def delocate_wheel(in_wheel, def patch_wheel(in_wheel, patch_fname, out_wheel=None): + # type: (Text, Text, Optional[Text]) -> None """ Apply ``-p1`` style patch in `patch_fname` to contents of `in_wheel` If `out_wheel` is None (the default), overwrite the wheel `in_wheel` @@ -444,7 +511,12 @@ def patch_wheel(in_wheel, patch_fname, out_wheel=None): stdout.decode('latin1')) -def check_archs(copied_libs, require_archs=(), stop_fast=False): +def check_archs( + copied_libs, # type: Dict[Text, Dict[Text, Text]] + require_archs=(), # type: Union[Text, Iterable[Text]] + stop_fast=False # type: bool +): + # type: (...) -> Set[Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]] # noqa: E501 """ Check compatibility of archs in `copied_libs` dict Parameters @@ -486,17 +558,17 @@ def check_archs(copied_libs, require_archs=(), stop_fast=False): if isinstance(require_archs, string_types): require_archs = (['i386', 'x86_64'] if require_archs == 'intel' else [require_archs]) - require_archs = frozenset(require_archs) - bads = [] + require_archs_set = frozenset(require_archs) + bads = [] # type: List[Union[Tuple[Text, FrozenSet[Text]], Tuple[Text, Text, FrozenSet[Text]]]] # noqa: E501 for depended_lib, dep_dict in copied_libs.items(): depended_archs = get_archs(depended_lib) for depending_lib, install_name in dep_dict.items(): depending_archs = get_archs(depending_lib) - all_required = depending_archs | require_archs + all_required = depending_archs | require_archs_set all_missing = all_required.difference(depended_archs) if len(all_missing) == 0: continue - required_missing = require_archs.difference(depended_archs) + required_missing = require_archs_set.difference(depended_archs) if len(required_missing): bads.append((depending_lib, required_missing)) else: diff --git a/delocate/libsana.py b/delocate/libsana.py index 5fdba8bc..529c89da 100644 --- a/delocate/libsana.py +++ b/delocate/libsana.py @@ -4,16 +4,195 @@ """ import os -from os.path import basename, join as pjoin, realpath - +from os.path import basename, dirname, join as pjoin, realpath + +import logging +from typing import ( + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Text, + Tuple, +) import warnings +import six + from .tools import (get_install_names, zip2dir, get_rpaths, get_environment_variable_paths) from .tmpdirs import TemporaryDirectory -def tree_libs(start_path, filt_func=None): +logger = logging.getLogger(__name__) + + +class DependencyNotFound(Exception): + """Raised by tree_libs or resolve_rpath if an expected dependency is + missing. + """ + + +def get_dependencies( + lib_path, # type: Text +): + # type: (...) -> Iterator[Tuple[Text, Text]] + """Iterate over this libraries dependences. + + Parameters + ---------- + lib_path : str + The library to fetch dependencies from. Must be an existing file. + + Yields + ------ + dependency_path : str + The direct dependencies of this library which can have dependencies + themselves. + If the libaray at `install_name` can not be found then this value will + not be a valid file path. + :func:`os.path.isfile` can be used to test if this file exists. + install_name : str + The install name of `dependency_path` as if :func:`get_install_names` + was called. + + Raises + ------ + DependencyNotFound + When `lib_path` does not exist. + """ + if not os.path.isfile(lib_path): + raise DependencyNotFound(lib_path) + rpaths = get_rpaths(lib_path) + get_environment_variable_paths() + for install_name in get_install_names(lib_path): + try: + if install_name.startswith("@"): + dependency_path = resolve_rpath( + install_name, + rpaths, + loader_path=dirname(lib_path), + ) + else: + dependency_path = search_environment_for_lib(install_name) + yield dependency_path, install_name + if dependency_path != install_name: + logger.debug( + "%s resolved to: %s", install_name, dependency_path + ) + except DependencyNotFound: + logger.error( + "\n{0} not found:" + "\n Needed by: {1}" + "\n Search path:\n {2}".format( + install_name, lib_path, "\n ".join(rpaths) + ) + ) + # At this point install_name is known to be a bad path. + # Expanding install_name with realpath may be undesirable. + yield realpath(install_name), install_name + + +def walk_library( + lib_path, # type: Text + filt_func=lambda filepath: True, # type: Callable[[Text], bool] + visited=None, # type: Optional[Set[Text]] +): + # type: (...) -> Iterator[Text] + """Iterate over all of this trees dependencies inclusively. + + Dependencies which can not be resolved will be logged and ignored. + + Parameters + ---------- + lib_path : str + The library to start with. + filt_func : callable, optional + A callable which accepts filename as argument and returns True if we + should inspect the file or False otherwise. + Defaults to inspecting all files for library dependencies. If callable, + If `filt_func` filters a library it will also exclude all of that + libraries dependencies as well. + visited : set of str + This set is updated with new library_path's as they are visited. + This is used to prevent infinite recursion and duplicates. + + Yields + ------ + library_path : str + The pats of each library including `lib_path` without duplicates. + """ + if visited is None: + visited = {lib_path, } + elif lib_path in visited: + return + else: + visited.add(lib_path) + if not filt_func(lib_path): + logger.debug("Ignoring %s and its dependencies.", lib_path) + return + yield lib_path + for dependency_path, _ in get_dependencies(lib_path): + if not os.path.isfile(dependency_path): + logger.error( + "%s not found, requested by %s", dependency_path, lib_path, + ) + continue + for sub_dependency in walk_library( + dependency_path, + filt_func=filt_func, + visited=visited, + ): + yield sub_dependency + + +def walk_directory( + root_path, # type: Text + filt_func=lambda filepath: True, # type: Callable[[Text], bool] +): + # type: (...) -> Iterator[Text] + """Walk along dependences starting with the libraries within `root_path`. + + Dependencies which can not be resolved will be logged and ignored. + + Parameters + ---------- + root_path : str + The root directory to search for libraries depending on other libraries. + filt_func : None or callable, optional + A callable which accepts filename as argument and returns True if we + should inspect the file or False otherwise. + Defaults to inspecting all files for library dependencies. If callable, + If `filt_func` filters a library it will also exclude all of that + libraries dependencies as well. + + Yields + ------ + library_path : str + Iterates over the libraries in `root_path` and each of their + dependencies without any duplicates. + """ + visited_paths = set() # type: Set[Text] + for dirpath, dirnames, basenames in os.walk(root_path): + for base in basenames: + depending_path = realpath(pjoin(dirpath, base)) + if depending_path in visited_paths: + continue # A library in root_path was a dependency of another. + if not filt_func(depending_path): + continue + for library_path in walk_library( + depending_path, filt_func=filt_func, visited=visited_paths + ): + yield library_path + + +def tree_libs( + start_path, # type: Text + filt_func=None, # type: Optional[Callable[[Text], bool]] +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Return analysis of library dependencies within `start_path` Parameters @@ -24,16 +203,15 @@ def tree_libs(start_path, filt_func=None): If None, inspect all files for library dependencies. If callable, accepts filename as argument, returns True if we should inspect the file, False otherwise. - Returns ------- lib_dict : dict dictionary with (key, value) pairs of (``libpath``, ``dependings_dict``). - ``libpath`` is canonical (``os.path.realpath``) filename of library, or - library name starting with {'@rpath', '@loader_path', - '@executable_path'}. + ``libpath`` is a canonical (``os.path.realpath``) filename of library, + or library name starting with {'@loader_path'}. + ``dependings_dict`` is a dict with (key, value) pairs of (``depending_libpath``, ``install_name``), where ``dependings_libpath`` @@ -48,73 +226,94 @@ def tree_libs(start_path, filt_func=None): * https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html # noqa: E501 * http://matthew-brett.github.io/pydagogue/mac_runtime_link.html + + .. deprecated:: 0.8 + This function does not support `@loader_path` and only returns the + direct dependencies of the libraries in `start_path`. """ - lib_dict = {} - env_var_paths = get_environment_variable_paths() + warnings.warn( + "tree_libs doesn't support @loader_path and has been deprecated.", + DeprecationWarning, + stacklevel=2, + ) + lib_dict = {} # type: Dict[Text, Dict[Text, Text]] for dirpath, dirnames, basenames in os.walk(start_path): for base in basenames: - depending_libpath = realpath(pjoin(dirpath, base)) - if filt_func is not None and not filt_func(depending_libpath): + depending_path = realpath(pjoin(dirpath, base)) + if filt_func and not filt_func(depending_path): + logger.debug("Ignoring dependencies of: %s", depending_path) continue - rpaths = get_rpaths(depending_libpath) - search_paths = rpaths + env_var_paths - for install_name in get_install_names(depending_libpath): - # If the library starts with '@rpath' we'll try and resolve it - # We'll do nothing to other '@'-paths - # Otherwise we'll search for the library using env variables - if install_name.startswith('@rpath'): - lib_path = resolve_rpath(install_name, search_paths) - elif install_name.startswith('@'): - lib_path = install_name - else: - lib_path = search_environment_for_lib(install_name) - if lib_path in lib_dict: - lib_dict[lib_path][depending_libpath] = install_name - else: - lib_dict[lib_path] = {depending_libpath: install_name} + for dependancy_path, install_name in get_dependencies( + depending_path + ): + if install_name.startswith("@loader_path/"): + # Support for `@loader_path` would break existing callers. + logger.debug( + "Excluding %s because it has '@loader_path'.", + install_name + ) + continue + lib_dict.setdefault(dependancy_path, {}) + lib_dict[dependancy_path][depending_path] = install_name return lib_dict -def resolve_rpath(lib_path, rpaths): - """ Return `lib_path` with its `@rpath` resolved - - If the `lib_path` doesn't have `@rpath` then it's returned as is. +def resolve_rpath(lib_path, rpaths, loader_path, executable_path="."): + # type: (Text, Iterable[Text], Text, Text) -> Text + """ Return `lib_path` with any special runtime linking names resolved. If `lib_path` has `@rpath` then returns the first `rpaths`/`lib_path` - combination found. If the library can't be found in `rpaths` then a - detailed warning is printed and `lib_path` is returned as is. + combination found. If the library can't be found in `rpaths` then + DependencyNotFound is raised. + + `@loader_path` and `@executable_path` are resolved with their respective + parameters. Parameters ---------- lib_path : str - The path to a library file, which may or may not start with `@rpath`. + The path to a library file, which may or may not be a relative path + starting with `@rpath`, `@loader_path`, or `@executable_path`. rpaths : sequence of str A sequence of search paths, usually gotten from a call to `get_rpaths`. + loader_path : str + The path to be used for `@loader_path`. + This must be the directory of the library which is loading `lib_path`. + executable_path : str, optional + The path to be used for `@executable_path`. + Assumed to be the working directory by default. Returns ------- lib_path : str A str with the resolved libraries realpath. + + Raises + ------ + DependencyNotFound + When `lib_path` has `@rpath` in it but can not be found on any of the + provided `rpaths`. """ + if lib_path.startswith('@loader_path/'): + return realpath(pjoin(loader_path, lib_path.split('/', 1)[1])) + if lib_path.startswith('@executable_path/'): + return realpath(pjoin(executable_path, lib_path.split('/', 1)[1])) if not lib_path.startswith('@rpath/'): - return lib_path + return realpath(lib_path) lib_rpath = lib_path.split('/', 1)[1] for rpath in rpaths: - rpath_lib = realpath(pjoin(rpath, lib_rpath)) + rpath_lib = resolve_rpath( + pjoin(rpath, lib_rpath), (), loader_path, executable_path + ) if os.path.exists(rpath_lib): - return rpath_lib + return realpath(rpath_lib) - warnings.warn( - "Couldn't find {0} on paths:\n\t{1}".format( - lib_path, - '\n\t'.join(realpath(path) for path in rpaths), - ) - ) - return lib_path + raise DependencyNotFound(lib_path) def search_environment_for_lib(lib_path): + # type: (Text) -> Text """ Search common environment variables for `lib_path` We'll use a single approach here: @@ -166,6 +365,7 @@ def search_environment_for_lib(lib_path): def get_prefix_stripper(strip_prefix): + # type: (Text) -> Callable[[Text], Text] """ Return function to strip `strip_prefix` prefix from string if present Parameters @@ -182,11 +382,13 @@ def get_prefix_stripper(strip_prefix): n = len(strip_prefix) def stripper(path): + # type: (Text) -> Text return path if not path.startswith(strip_prefix) else path[n:] return stripper def get_rp_stripper(strip_path): + # type: (Text) -> Callable[[Text], Text] """ Return function to strip ``realpath`` of `strip_path` from string Parameters @@ -205,6 +407,7 @@ def get_rp_stripper(strip_path): def stripped_lib_dict(lib_dict, strip_prefix): + # type: (Dict[Text, Dict[Text, Text]], Text) -> Dict[Text, Dict[Text, Text]] """ Return `lib_dict` with `strip_prefix` removed from start of paths Use to give form of `lib_dict` that appears relative to some base path @@ -237,7 +440,11 @@ def stripped_lib_dict(lib_dict, strip_prefix): return relative_dict -def wheel_libs(wheel_fname, filt_func=None): +def wheel_libs( + wheel_fname, # type: Text + filt_func=None # type: Optional[Callable[[Text], bool]] +): + # type: (...) -> Dict[Text, Dict[Text, Text]] """ Return analysis of library dependencies with a Python wheel Use this routine for a dump of the dependency tree. @@ -268,7 +475,8 @@ def wheel_libs(wheel_fname, filt_func=None): def _paths_from_var(varname, lib_basename): - var = os.environ.get(varname) + # type: (Text, Text) -> List[Text] + var = os.environ.get(six.ensure_str(varname)) if var is None: return [] return [pjoin(path, lib_basename) for path in var.split(':')] diff --git a/setup.py b/setup.py index eba22975..7a18bf83 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,9 @@ "machomachomangler; sys_platform == 'win32'", "bindepend; sys_platform == 'win32'", "wheel", + "six", + "typing; python_version < '3.5'", + "typing_extensions", ], package_data={'delocate.tests': [pjoin('data', '*.dylib'),