Skip to content

Commit

Permalink
Merge pull request #118 from HexDecimal/listdeps-recursive
Browse files Browse the repository at this point in the history
MRG: Show recursive dependencies in listdeps command.
  • Loading branch information
matthew-brett authored Sep 18, 2021
2 parents 26074b1 + c2b9a61 commit 34f1d78
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 121 deletions.
3 changes: 3 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Releases

First release that requires Python 3.

* ``wheel_libs`` now supports recursive dependencies.
* ``listdeps`` command now supports recursive dependencies.

* 0.9.1 (Friday September 17th 2021)

Bugfix release.
Expand Down
8 changes: 4 additions & 4 deletions delocate/cmd/delocate_listdeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from os.path import isdir, realpath, sep as psep
from optparse import OptionParser, Option

from delocate import tree_libs, wheel_libs, __version__
from delocate import wheel_libs, __version__
from delocate.delocating import filter_system_libs
from delocate.libsana import stripped_lib_dict
from delocate.libsana import stripped_lib_dict, tree_libs_from_directory


def main():
Expand All @@ -38,10 +38,10 @@ def main():
else:
indent = ''
if isdir(path):
lib_dict = tree_libs(path)
lib_dict = tree_libs_from_directory(path, ignore_missing=True)
lib_dict = stripped_lib_dict(lib_dict, realpath(getcwd()) + psep)
else:
lib_dict = wheel_libs(path)
lib_dict = wheel_libs(path, ignore_missing=True)
keys = sorted(lib_dict)
if not opts.all:
keys = [key for key in keys if filter_system_libs(key)]
Expand Down
31 changes: 8 additions & 23 deletions delocate/delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Optional, Set, Text, Tuple, Union)

from .libsana import (tree_libs, stripped_lib_dict, get_rp_stripper,
walk_directory, get_dependencies)
tree_libs_from_directory)
from .tools import (set_install_name, zip2dir, dir2zip, validate_signature,
find_package_dirs, set_install_id, get_archs)
from .tmpdirs import TemporaryDirectory
Expand Down Expand Up @@ -418,28 +418,13 @@ def delocate_path(
# Do not inspect dependencies of libraries that will not be copied.
filt_func = (lambda path: lib_filt_func(path) and copy_filt_func(path))

lib_dict = {} # type: Dict[Text, Dict[Text, Text]]
missing_libs = False
for library_path in walk_directory(
tree_path, filt_func, executable_path=executable_path
):
for depending_path, install_name in get_dependencies(
library_path,
executable_path=executable_path,
filt_func=filt_func,
):
if depending_path is None:
missing_libs = True
continue
if not filt_func(depending_path):
continue
lib_dict.setdefault(depending_path, {})
lib_dict[depending_path][library_path] = install_name

if missing_libs and not ignore_missing:
# Details of missing libraries would have already reported by
# get_dependencies.
raise DelocationError("Could not find all dependencies.")
lib_dict = tree_libs_from_directory(
tree_path,
lib_filt_func=filt_func,
copy_filt_func=filt_func,
executable_path=executable_path,
ignore_missing=ignore_missing,
)

return delocate_tree_libs(lib_dict, lib_path, tree_path)

Expand Down
169 changes: 161 additions & 8 deletions delocate/libsana.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
import warnings

import delocate.delocating
from .tools import (get_install_names, zip2dir, get_rpaths,
get_environment_variable_paths)
from .tmpdirs import TemporaryDirectory
Expand Down Expand Up @@ -243,6 +244,144 @@ def walk_directory(
yield library_path


def _tree_libs_from_libraries(
libraries: Iterable[str],
*,
lib_filt_func: Callable[[str], bool],
copy_filt_func: Callable[[str], bool],
executable_path: Optional[str] = None,
ignore_missing: bool = False,
) -> Dict[str, Dict[str, str]]:
""" Return an analysis of the dependencies of `libraries`.
Parameters
----------
libraries : iterable of str
The paths to the libraries to find dependencies of.
lib_filt_func : callable, keyword-only
A callable which accepts filename as argument and returns True if we
should inspect the file or False otherwise.
If `filt_func` filters a library it will will not further analyze any
of that library's dependencies.
copy_filt_func : callable, keyword-only
Called on each library name detected as a dependency; copy
where ``copy_filt_func(libname)`` is True, don't copy otherwise.
executable_path : None or str, optional, keyword-only
If not None, an alternative path to use for resolving
`@executable_path`.
ignore_missing : bool, default=False, optional, keyword-only
Continue even if missing dependencies are detected.
Returns
-------
lib_dict : dict
dictionary with (key, value) pairs of (``libpath``,
``dependings_dict``).
``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``
is the canonical (``os.path.realpath``) filename of the library
depending on ``libpath``, and ``install_name`` is the "install_name" by
which ``depending_libpath`` refers to ``libpath``.
Raises
------
DelocationError
When any dependencies can not be located and ``ignore_missing`` is
False.
"""
lib_dict: Dict[str, Dict[str, str]] = {}
missing_libs = False
for library_path in libraries:
for depending_path, install_name in get_dependencies(
library_path,
executable_path=executable_path,
filt_func=lib_filt_func,
):
if depending_path is None:
missing_libs = True
continue
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

if missing_libs and not ignore_missing:
# get_dependencies will already have logged details of missing
# libraries.
raise delocate.delocating.DelocationError(
"Could not find all dependencies."
)

return lib_dict


def tree_libs_from_directory(
start_path: str,
*,
lib_filt_func: Callable[[str], bool] = _filter_system_libs,
copy_filt_func: Callable[[str], bool] = lambda path: True,
executable_path: Optional[str] = None,
ignore_missing: bool = False,
) -> Dict[Text, Dict[Text, Text]]:
""" Return an analysis of the libraries in the directory of `start_path`.
Parameters
----------
start_path : iterable of str
Root path of tree to search for libraries depending on other libraries.
lib_filt_func : callable, optional, keyword-only
A callable which accepts filename as argument and returns True if we
should inspect the file or False otherwise.
If `filt_func` filters a library it will will not further analyze any
of that library's dependencies.
Defaults to inspecting all files except for system libraries.
copy_filt_func : callable, optional, keyword-only
Called on each library name detected as a dependency; copy
where ``copy_filt_func(libname)`` is True, don't copy otherwise.
Defaults to copying all detected dependencies.
executable_path : None or str, optional, keyword-only
If not None, an alternative path to use for resolving
`@executable_path`.
ignore_missing : bool, default=False, optional, keyword-only
Continue even if missing dependencies are detected.
Returns
-------
lib_dict : dict
dictionary with (key, value) pairs of (``libpath``,
``dependings_dict``).
``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``
is the canonical (``os.path.realpath``) filename of the library
depending on ``libpath``, and ``install_name`` is the "install_name" by
which ``depending_libpath`` refers to ``libpath``.
Raises
------
DelocationError
When any dependencies can not be located and ``ignore_missing`` is
False.
"""
return _tree_libs_from_libraries(
walk_directory(
start_path, lib_filt_func, executable_path=executable_path
),
lib_filt_func=lib_filt_func,
copy_filt_func=copy_filt_func,
ignore_missing=ignore_missing,
)


def tree_libs(
start_path, # type: Text
filt_func=None, # type: Optional[Callable[[Text], bool]]
Expand Down Expand Up @@ -285,6 +424,8 @@ def tree_libs(
.. deprecated:: 0.9
This function does not support `@loader_path` and only returns the
direct dependencies of the libraries in `start_path`.
:func:`tree_libs_from_directory` should be used instead.
"""
warnings.warn(
"tree_libs doesn't support @loader_path and has been deprecated.",
Expand Down Expand Up @@ -548,10 +689,11 @@ def stripped_lib_dict(lib_dict, strip_prefix):


def wheel_libs(
wheel_fname, # type: Text
filt_func=None # type: Optional[Callable[[Text], bool]]
):
# type: (...) -> Dict[Text, Dict[Text, Text]]
wheel_fname: str,
filt_func: Optional[Callable[[Text], bool]] = None,
*,
ignore_missing: bool = False,
) -> Dict[Text, Dict[Text, Text]]:
""" Return analysis of library dependencies with a Python wheel
Use this routine for a dump of the dependency tree.
Expand All @@ -561,9 +703,11 @@ def wheel_libs(
wheel_fname : str
Filename of wheel
filt_func : None or callable, optional
If None, inspect all files for library dependencies. If callable,
accepts filename as argument, returns True if we should inspect the
file, False otherwise.
If None, inspect all non-system files for library dependencies.
If callable, accepts filename as argument, returns True if we should
inspect the file, False otherwise.
ignore_missing : bool, default=False, optional, keyword-only
Continue even if missing dependencies are detected.
Returns
-------
Expand All @@ -574,10 +718,19 @@ def wheel_libs(
is (key, value) of (``depending_lib_path``, ``install_name``). Again,
``depending_lib_path`` is library relative to wheel root path, if
within wheel tree.
Raises
------
DelocationError
When dependencies can not be located and `ignore_missing` is False.
"""
if filt_func is None:
filt_func = _filter_system_libs
with TemporaryDirectory() as tmpdir:
zip2dir(wheel_fname, tmpdir)
lib_dict = tree_libs(tmpdir, filt_func)
lib_dict = tree_libs_from_directory(
tmpdir, lib_filt_func=filt_func, ignore_missing=ignore_missing
)
return stripped_lib_dict(lib_dict, realpath(tmpdir) + os.path.sep)


Expand Down
25 changes: 25 additions & 0 deletions delocate/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
from pathlib import Path
from typing import Iterator

import pytest

from delocate.tools import set_install_name
from delocate.wheeltools import InWheelCtx
from .test_wheelies import PlatWheel, STRAY_LIB_DEP, PLAT_WHEEL


@pytest.fixture
def plat_wheel(tmp_path: Path) -> Iterator[PlatWheel]:
""" Return a modified platform wheel for testing. """
plat_wheel_tmp = str(tmp_path / 'plat-wheel.whl')
stray_lib: str = STRAY_LIB_DEP

with InWheelCtx(PLAT_WHEEL, plat_wheel_tmp):
set_install_name(
'fakepkg1/subpkg/module2.abi3.so',
'libextfunc.dylib',
stray_lib,
)

yield PlatWheel(plat_wheel_tmp, os.path.realpath(stray_lib))
Binary file not shown.
Binary file modified delocate/tests/data/libextfunc.dylib
Binary file not shown.
1 change: 0 additions & 1 deletion delocate/tests/data/wheel_build_path.txt

This file was deleted.

1 change: 1 addition & 0 deletions delocate/tests/pytest_tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os

import pytest


Expand Down
Loading

0 comments on commit 34f1d78

Please sign in to comment.