Skip to content

Commit

Permalink
RF+TST refactor tree_libs dict output
Browse files Browse the repository at this point in the history
Change ``tree_libs`` output to use canonical (os.path.realpath) names
for both depending and depended libraries. The set of depending
libraries now becomes a dictionary with keys being the depending library
canonical names, and the values being the install name by which the
depending library refers to the depended library.

This means that we don't pick up the same library twice when there are
symlinks with different names pointing to the same library.
  • Loading branch information
matthew-brett committed May 1, 2014
1 parent d5365b4 commit 9f042a8
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 44 deletions.
38 changes: 29 additions & 9 deletions delocate/libsana.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

import os
from os.path import join as pjoin, relpath, abspath, isdir, exists
from os.path import join as pjoin, abspath, realpath

from .tools import get_install_names, zip2dir, find_package_dirs
from .tmpdirs import InTemporaryDirectory
Expand All @@ -24,20 +24,40 @@ def tree_libs(start_path, filt_func = None):
Returns
-------
lib_dict : dict
dictionary with (key, value) pairs of (install name, set of files in
tree with install name)
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'}.
``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``.
Notes
-----
See:
* https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dyld.1.html
* http://matthew-brett.github.io/pydagogue/mac_runtime_link.html
"""
lib_dict = {}
for dirpath, dirnames, basenames in os.walk(start_path):
for base in basenames:
fname = pjoin(dirpath, base)
if not filt_func is None and not filt_func(fname):
depending_libpath = realpath(pjoin(dirpath, base))
if not filt_func is None and not filt_func(depending_libpath):
continue
for install_name in get_install_names(fname):
if install_name in lib_dict:
lib_dict[install_name].add(fname)
for install_name in get_install_names(depending_libpath):
lib_path = (install_name if install_name.startswith('@')
else realpath(install_name))
if lib_path in lib_dict:
lib_dict[lib_path][depending_libpath] = install_name
else:
lib_dict[install_name] = set([fname])
lib_dict[lib_path] = {depending_libpath: install_name}
return lib_dict


Expand Down
105 changes: 70 additions & 35 deletions delocate/tests/test_libsana.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Utilities for analyzing library dependencies in trees and wheels
"""

from os.path import join as pjoin, split as psplit, abspath, dirname
import os
from os.path import join as pjoin, split as psplit, abspath, dirname, realpath

from ..libsana import tree_libs, wheel_libs
from ..tools import set_install_name
Expand All @@ -13,60 +14,94 @@
from nose.tools import (assert_true, assert_false, assert_raises,
assert_equal, assert_not_equal)

from .test_install_names import (LIBA, LIBB, LIBC, TEST_LIB, _copy_libs)
from .test_install_names import (LIBA, LIBB, LIBC, TEST_LIB, _copy_libs,
EXT_LIBS)
from .test_wheelies import PLAT_WHEEL, PURE_WHEEL, STRAY_LIB_DEP


def get_ext_dict(local_libs):
ext_deps = {}
for ext_lib in EXT_LIBS:
lib_deps = {}
for local_lib in local_libs:
lib_deps[realpath(local_lib)] = ext_lib
ext_deps[realpath(ext_lib)] = lib_deps
return ext_deps


def test_tree_libs():
# Test ability to walk through tree, finding dynamic libary refs
# Copy specific files to avoid working tree cruft
to_copy = [LIBA, LIBB, LIBC, TEST_LIB]
with InTemporaryDirectory() as tmpdir:
liba, libb, libc, test_lib = _copy_libs(to_copy, tmpdir)
assert_equal(
tree_libs(tmpdir), # default - no filtering
{'/usr/lib/libstdc++.6.dylib': set([liba, libb, libc, test_lib]),
'/usr/lib/libSystem.B.dylib': set([liba, libb, libc, test_lib]),
'liba.dylib': set([libb, libc]),
'libb.dylib': set([libc]),
'libc.dylib': set([test_lib])})
local_libs = _copy_libs(to_copy, tmpdir)
rp_local_libs = [realpath(L) for L in local_libs]
liba, libb, libc, test_lib = local_libs
rp_liba, rp_libb, rp_libc, rp_test_lib = rp_local_libs
exp_dict = get_ext_dict(local_libs)
exp_dict.update({
rp_liba: {rp_libb: 'liba.dylib', rp_libc: 'liba.dylib'},
rp_libb: {rp_libc: 'libb.dylib'},
rp_libc: {rp_test_lib: 'libc.dylib'}})
# default - no filtering
assert_equal(tree_libs(tmpdir), exp_dict)
def filt(fname):
return fname.endswith('.dylib')
assert_equal(
tree_libs(tmpdir, filt), # filtering
{'/usr/lib/libstdc++.6.dylib': set([liba, libb, libc]),
'/usr/lib/libSystem.B.dylib': set([liba, libb, libc]),
'liba.dylib': set([libb, libc]),
'libb.dylib': set([libc])})
exp_dict = get_ext_dict([liba, libb, libc])
exp_dict.update({
rp_liba: {rp_libb: 'liba.dylib', rp_libc: 'liba.dylib'},
rp_libb: {rp_libc: 'libb.dylib'}})
# filtering
assert_equal(tree_libs(tmpdir, filt), exp_dict)
# Copy some libraries into subtree to test tree walking
subtree = pjoin(tmpdir, 'subtree')
slibc, stest_lib = _copy_libs([libc, test_lib], subtree)
assert_equal(
tree_libs(tmpdir, filt), # filtering
{'/usr/lib/libstdc++.6.dylib':
set([liba, libb, libc, slibc]),
'/usr/lib/libSystem.B.dylib':
set([liba, libb, libc, slibc]),
'liba.dylib': set([libb, libc, slibc]),
'libb.dylib': set([libc, slibc])})
st_exp_dict = get_ext_dict([liba, libb, libc, slibc])
st_exp_dict.update({
rp_liba: {rp_libb: 'liba.dylib',
rp_libc: 'liba.dylib',
realpath(slibc): 'liba.dylib'},
rp_libb: {rp_libc: 'libb.dylib',
realpath(slibc): 'libb.dylib'}})
assert_equal(tree_libs(tmpdir, filt), st_exp_dict)
# Change an install name, check this is picked up
set_install_name(slibc, 'liba.dylib', 'newlib')
assert_equal(
tree_libs(tmpdir, filt), # filtering
{'/usr/lib/libstdc++.6.dylib':
set([liba, libb, libc, slibc]),
'/usr/lib/libSystem.B.dylib':
set([liba, libb, libc, slibc]),
'liba.dylib': set([libb, libc]),
'newlib': set([slibc]),
'libb.dylib': set([libc, slibc])})
inc_exp_dict = get_ext_dict([liba, libb, libc, slibc])
inc_exp_dict.update({
rp_liba: {rp_libb: 'liba.dylib',
rp_libc: 'liba.dylib'},
realpath('newlib'): {realpath(slibc): 'newlib'},
rp_libb: {rp_libc: 'libb.dylib',
realpath(slibc): 'libb.dylib'}})
assert_equal(tree_libs(tmpdir, filt), inc_exp_dict)
# Symlink a depending canonical lib - should have no effect because of
# the canonical names
os.symlink(liba, pjoin(dirname(liba), 'funny.dylib'))
assert_equal(tree_libs(tmpdir, filt), inc_exp_dict)
# Symlink a depended lib. Now 'newlib' is a symlink to liba, and the
# dependency of slibc on newlib appears as a dependency on liba, but
# with install name 'newlib'
os.symlink(liba, 'newlib')
sl_exp_dict = get_ext_dict([liba, libb, libc, slibc])
sl_exp_dict.update({
rp_liba: {rp_libb: 'liba.dylib',
rp_libc: 'liba.dylib',
realpath(slibc): 'newlib'},
rp_libb: {rp_libc: 'libb.dylib',
realpath(slibc): 'libb.dylib'}})
assert_equal(tree_libs(tmpdir, filt), sl_exp_dict)


def test_wheel_libs():
# Test routine to list dependencies from wheels
assert_equal(wheel_libs(PURE_WHEEL), {})
mod2 = pjoin('fakepkg1', 'subpkg', 'module2.so')
rp_stray = realpath(STRAY_LIB_DEP)
rp_mod2 = realpath(mod2)
sys_b = '/usr/lib/libSystem.B.dylib'
assert_equal(wheel_libs(PLAT_WHEEL),
{STRAY_LIB_DEP: set([mod2]),
'/usr/lib/libSystem.B.dylib': set([mod2])})
{rp_stray: {rp_mod2: STRAY_LIB_DEP},
realpath(sys_b): {rp_mod2: sys_b}})
def filt(fname):
return not fname == mod2
assert_equal(wheel_libs(PLAT_WHEEL, filt), {})

0 comments on commit 9f042a8

Please sign in to comment.