diff --git a/Changelog b/Changelog index 0deb14f4..1df57cbb 100644 --- a/Changelog +++ b/Changelog @@ -21,6 +21,10 @@ Releases * [Unreleased] + * Dependency paths with ``@rpath``, ``@loader_path`` and ``@executable_path`` + will now look at ``/usr/local/lib`` and ``/usr/lib`` after the + rpaths, loader path and executable path are searched respectively. + * [0.10.3] - 2022-11-04 * Support for Python 3.6 has been dropped. diff --git a/delocate/libsana.py b/delocate/libsana.py index 0fa30be8..2b1af695 100644 --- a/delocate/libsana.py +++ b/delocate/libsana.py @@ -473,6 +473,9 @@ def tree_libs( return lib_dict +_default_paths_to_search = ("/usr/local/lib", "/usr/lib") + + def resolve_dynamic_paths(lib_path, rpaths, loader_path, executable_path=None): # type: (Text, Iterable[Text], Text, Optional[Text]) -> Text """Return `lib_path` with any special runtime linking names resolved. @@ -511,20 +514,33 @@ def resolve_dynamic_paths(lib_path, rpaths, loader_path, executable_path=None): """ if executable_path is None: executable_path = dirname(sys.executable) - 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/"): + + if not lib_path.startswith( + ("@rpath/", "@loader_path/", "@executable_path/") + ): return realpath(lib_path) - lib_rpath = lib_path.split("/", 1)[1] - for rpath in rpaths: - rpath_lib = resolve_dynamic_paths( - pjoin(rpath, lib_rpath), (), loader_path, executable_path - ) - if os.path.exists(rpath_lib): - return realpath(rpath_lib) + if lib_path.startswith("@loader_path/"): + paths_to_search = [loader_path] + elif lib_path.startswith("@executable_path/"): + paths_to_search = [executable_path] + elif lib_path.startswith("@rpath/"): + paths_to_search = list(rpaths) + + # these paths are searched by the macos loader in order if the + # library is not in the previous paths. + paths_to_search.extend(_default_paths_to_search) + + rel_path = lib_path.split("/", 1)[1] + for prefix_path in paths_to_search: + try: + abs_path = resolve_dynamic_paths( + pjoin(prefix_path, rel_path), (), loader_path, executable_path + ) + except DependencyNotFound: + continue + if os.path.exists(abs_path): + return realpath(abs_path) raise DependencyNotFound(lib_path) @@ -561,7 +577,7 @@ def resolve_rpath(lib_path, rpaths): return lib_path lib_rpath = lib_path.split("/", 1)[1] - for rpath in rpaths: + for rpath in (*rpaths, *_default_paths_to_search): rpath_lib = realpath(pjoin(rpath, lib_rpath)) if os.path.exists(rpath_lib): return rpath_lib diff --git a/delocate/tests/test_libsana.py b/delocate/tests/test_libsana.py index 0f2920bd..c4946fec 100644 --- a/delocate/tests/test_libsana.py +++ b/delocate/tests/test_libsana.py @@ -11,6 +11,7 @@ from os.path import join as pjoin from os.path import realpath, relpath, split from typing import Dict, Iterable, Text +from unittest import mock import pytest @@ -499,6 +500,21 @@ def test_resolve_rpath(): assert_equal(resolve_rpath(lib_rpath, []), lib_rpath) +@pytest.mark.xfail(sys.platform == "win32", reason="Needs Unix linkage.") +def test_resolve_dynamic_paths_fallthrough() -> None: + # A minimal test of the resolve_dynamic_paths fallthrough + path, lib = split(LIBA) + lib_rpath = pjoin("@rpath", lib) + # Should fail as rpath is not given and the library cannot be found + # in default paths to search + with pytest.raises(DependencyNotFound): + resolve_dynamic_paths(lib_rpath, [], path) + # Since the library is in the default paths to search, this should + # return the full path to the library + with mock.patch("delocate.libsana._default_paths_to_search", (path,)): + assert resolve_dynamic_paths(lib_rpath, [], path) == realpath(LIBA) + + @pytest.mark.xfail(sys.platform != "darwin", reason="otool") def test_get_dependencies(tmpdir): # type: (object) -> None