""" Tests for relocating libraries """ from __future__ import division, print_function import os from os.path import (join as pjoin, dirname, basename, relpath, realpath, splitext) import shutil from ..delocating import (DelocationError, delocate_tree_libs, copy_recurse, delocate_path, check_archs, bads_report) from ..libsana import (tree_libs, search_environment_for_lib) from ..tools import (get_install_names, set_install_name, back_tick) from ..tmpdirs import InTemporaryDirectory from .pytest_tools import (assert_true, assert_raises, assert_equal) from .test_install_names import (LIBA, LIBB, LIBC, TEST_LIB, _copy_libs, EXT_LIBS) from .test_tools import (LIB32, LIB64, LIB64A, LIBBOTH, ARCH_64, ARCH_32, ARCH_BOTH) from .test_libsana import get_ext_dict from .env_tools import TempDirWithoutEnvVars def _make_libtree(out_path): liba, libb, libc, test_lib = _copy_libs( [LIBA, LIBB, LIBC, TEST_LIB], out_path) sub_path = pjoin(out_path, 'subsub') slibc, stest_lib = _copy_libs([libc, test_lib], sub_path) # Set execute permissions for exe in (test_lib, stest_lib): os.chmod(exe, 0o744) # Check test-lib doesn't work because of relative library paths assert_raises(RuntimeError, back_tick, [test_lib]) assert_raises(RuntimeError, back_tick, [stest_lib]) # Fixup the relative path library names by setting absolute paths for fname, using, path in ( (libb, 'liba.dylib', out_path), (libc, 'liba.dylib', out_path), (libc, 'libb.dylib', out_path), (test_lib, 'libc.dylib', out_path), (slibc, 'liba.dylib', out_path), (slibc, 'libb.dylib', out_path), (stest_lib, 'libc.dylib', sub_path), ): set_install_name(fname, using, pjoin(path, using)) # Check scripts now execute correctly back_tick([test_lib]) back_tick([stest_lib]) return liba, libb, libc, test_lib, slibc, stest_lib def test_delocate_tree_libs(): # Test routine to copy library dependencies into a local directory with InTemporaryDirectory() as tmpdir: # Copy libs into a temporary directory subtree = pjoin(tmpdir, 'subtree') all_local_libs = _make_libtree(subtree) liba, libb, libc, test_lib, slibc, stest_lib = all_local_libs lib_dict = tree_libs(subtree) copy_dir = 'dynlibs' os.makedirs(copy_dir) # First check that missing library causes error set_install_name(liba, '/usr/lib/libstdc++.6.dylib', '/unlikely/libname.dylib') lib_dict = tree_libs(subtree) assert_raises(DelocationError, delocate_tree_libs, lib_dict, copy_dir, subtree) # fix it set_install_name(liba, '/unlikely/libname.dylib', '/usr/lib/libstdc++.6.dylib') lib_dict = tree_libs(subtree) copied = delocate_tree_libs(lib_dict, copy_dir, subtree) # Only the out-of-tree libraries get copied exp_dict = get_ext_dict(all_local_libs) assert_equal(copied, exp_dict) assert_equal(set(os.listdir(copy_dir)), set([basename(realpath(lib)) for lib in EXT_LIBS])) # Libraries using the copied libraries now have an # install name starting with @loader_path, then # pointing to the copied library directory for lib in all_local_libs: pathto_copies = relpath(realpath(copy_dir), dirname(realpath(lib))) lib_inames = get_install_names(lib) new_links = ['@loader_path/{0}/{1}'.format(pathto_copies, basename(elib)) for elib in copied] assert_true(set(new_links) <= set(lib_inames)) # Libraries now have a relative loader_path to their corresponding # in-tree libraries for requiring, using, rel_path in ( (libb, 'liba.dylib', ''), (libc, 'liba.dylib', ''), (libc, 'libb.dylib', ''), (test_lib, 'libc.dylib', ''), (slibc, 'liba.dylib', '../'), (slibc, 'libb.dylib', '../'), (stest_lib, 'libc.dylib', '')): loader_path = '@loader_path/' + rel_path + using assert_true(loader_path in get_install_names(requiring)) # Check test libs still work back_tick([test_lib]) back_tick([stest_lib]) # Check case where all local libraries are out of tree subtree2 = pjoin(tmpdir, 'subtree2') liba, libb, libc, test_lib, slibc, stest_lib = _make_libtree(subtree2) copy_dir2 = 'dynlibs2' os.makedirs(copy_dir2) # Trying to delocate where all local libraries appear to be # out-of-tree will raise an error because of duplicate library names # (libc and slibc both named <something>/libc.dylib) lib_dict2 = tree_libs(subtree2) assert_raises(DelocationError, delocate_tree_libs, lib_dict2, copy_dir2, '/fictional') # Rename a library to make this work new_slibc = pjoin(dirname(slibc), 'libc2.dylib') os.rename(slibc, new_slibc) # Tell test-lib about this set_install_name(stest_lib, slibc, new_slibc) slibc = new_slibc # Confirm new test-lib still works back_tick([test_lib]) back_tick([stest_lib]) # Delocation now works lib_dict2 = tree_libs(subtree2) copied2 = delocate_tree_libs(lib_dict2, copy_dir2, '/fictional') local_libs = [liba, libb, libc, slibc, test_lib, stest_lib] rp_liba, rp_libb, rp_libc, rp_slibc, rp_test_lib, rp_stest_lib = \ [realpath(L) for L in local_libs] exp_dict = get_ext_dict(local_libs) exp_dict.update({ rp_libc: {rp_test_lib: libc}, rp_slibc: {rp_stest_lib: slibc}, rp_libb: {rp_slibc: libb, rp_libc: libb}, rp_liba: {rp_slibc: liba, rp_libc: liba, rp_libb: liba}}) assert_equal(copied2, exp_dict) ext_local_libs = (set(realpath(L) for L in EXT_LIBS) | {liba, libb, libc, slibc}) assert_equal(set(os.listdir(copy_dir2)), set([basename(lib) for lib in ext_local_libs])) # Libraries using the copied libraries now have an install name starting # noqa: E501 # with @loader_path, then pointing to the copied library directory all_local_libs = liba, libb, libc, test_lib, slibc, stest_lib for lib in all_local_libs: pathto_copies = relpath(realpath(copy_dir2), dirname(realpath(lib))) lib_inames = get_install_names(lib) new_links = ['@loader_path/{0}/{1}'.format(pathto_copies, basename(elib)) for elib in copied] assert_true(set(new_links) <= set(lib_inames)) def _copy_fixpath(files, directory): new_fnames = [] for fname in files: shutil.copy2(fname, directory) new_fname = pjoin(directory, basename(fname)) for name in get_install_names(fname): if name.startswith('lib'): set_install_name(new_fname, name, pjoin(directory, name)) new_fnames.append(new_fname) return new_fnames def _copy_to(fname, directory, new_base): new_name = pjoin(directory, new_base) shutil.copy2(fname, new_name) return new_name def test_copy_recurse(): # Function to find / copy needed libraries recursively with InTemporaryDirectory(): # Get some fixed up libraries to play with os.makedirs('libcopy') test_lib, liba, libb, libc = _copy_fixpath( [TEST_LIB, LIBA, LIBB, LIBC], 'libcopy') # Set execute permissions os.chmod(test_lib, 0o744) # Check system finds libraries back_tick(['./libcopy/test-lib']) # One library, depends only on system libs, system libs filtered def filt_func(libname): return not libname.startswith('/usr/lib') os.makedirs('subtree') _copy_fixpath([LIBA], 'subtree') # Nothing copied therefore assert_equal(copy_recurse('subtree', copy_filt_func=filt_func), {}) assert_equal(set(os.listdir('subtree')), {'liba.dylib'}) # shortcut _rp = realpath # An object that depends on a library that depends on two libraries # test_lib depends on libc, libc depends on liba and libb. libc gets # copied first, then liba, libb def _st(fname): return _rp(pjoin('subtree2', basename(fname))) os.makedirs('subtree2') shutil.copy2(test_lib, 'subtree2') assert_equal(copy_recurse('subtree2', filt_func), {_rp(libc): {_st(test_lib): libc}, _rp(libb): {_rp(libc): libb}, _rp(liba): {_rp(libb): liba, _rp(libc): liba}}) assert_equal(set(os.listdir('subtree2')), {'liba.dylib', 'libb.dylib', 'libc.dylib', 'test-lib'}) # A circular set of libraries os.makedirs('libcopy2') libw = _copy_to(LIBA, 'libcopy2', 'libw.dylib') libx = _copy_to(LIBA, 'libcopy2', 'libx.dylib') liby = _copy_to(LIBA, 'libcopy2', 'liby.dylib') libz = _copy_to(LIBA, 'libcopy2', 'libz.dylib') # targets and dependencies. A copy of libw starts in the directory, # first pass should install libx and liby (dependencies of libw), # second pass should install libz, libw (dependencies of liby, libx # respectively) t_dep1_dep2 = ( (libw, libx, liby), # libw depends on libx, liby (libx, libw, liby), # libx depends on libw, liby (liby, libw, libz), # liby depends on libw, libz (libz, libw, libx)) # libz depends on libw, libx for tlib, dep1, dep2 in t_dep1_dep2: set_install_name(tlib, EXT_LIBS[0], dep1) set_install_name(tlib, EXT_LIBS[1], dep2) os.makedirs('subtree3') seed_path = pjoin('subtree3', 'seed') shutil.copy2(libw, seed_path) assert_equal(copy_recurse('subtree3'), # not filtered # First pass, libx, liby get copied {_rp(libx): {_rp(seed_path): libx, _rp(libw): libx, _rp(libz): libx}, _rp(liby): {_rp(seed_path): liby, _rp(libw): liby, _rp(libx): liby}, _rp(libw): {_rp(libx): libw, _rp(liby): libw, _rp(libz): libw}, _rp(libz): {_rp(liby): libz}}) assert_equal(set(os.listdir('subtree3')), {'seed', 'libw.dylib', 'libx.dylib', 'liby.dylib', 'libz.dylib'}) for tlib, dep1, dep2 in t_dep1_dep2: out_lib = pjoin('subtree3', basename(tlib)) assert_equal(set(get_install_names(out_lib)), {'@loader_path/' + basename(dep1), '@loader_path/' + basename(dep2)}) # Check case of not-empty copied_libs os.makedirs('subtree4') shutil.copy2(libw, 'subtree4') copied_libs = {_rp(libw): {_rp(libx): libw, _rp(liby): libw, _rp(libz): libw}} copied_copied = copied_libs.copy() assert_equal(copy_recurse('subtree4', None, copied_libs), {_rp(libw): {_rp(libx): libw, _rp(liby): libw, _rp(libz): libw}, _rp(libx): {_rp(libw): libx, _rp(libz): libx}, _rp(liby): {_rp(libw): liby, _rp(libx): liby}, _rp(libz): {_rp(liby): libz}}) # Not modified in-place assert_equal(copied_libs, copied_copied) def test_copy_recurse_overwrite(): # Check that copy_recurse won't overwrite pre-existing libs with InTemporaryDirectory(): # Get some fixed up libraries to play with os.makedirs('libcopy') test_lib, liba, libb, libc = _copy_fixpath( [TEST_LIB, LIBA, LIBB, LIBC], 'libcopy') # Filter system libs def filt_func(libname): return not libname.startswith('/usr/lib') os.makedirs('subtree') # libb depends on liba shutil.copy2(libb, 'subtree') # If liba is already present, barf shutil.copy2(liba, 'subtree') assert_raises(DelocationError, copy_recurse, 'subtree', filt_func) # Works if liba not present os.unlink(pjoin('subtree', 'liba.dylib')) copy_recurse('subtree', filt_func) def test_delocate_path(): # Test high-level path delocator script with InTemporaryDirectory(): # Make a tree; use realpath for OSX /private/var - /var _, _, _, test_lib, slibc, stest_lib = _make_libtree( realpath('subtree')) # Check it fixes up correctly assert_equal(delocate_path('subtree', 'deplibs'), {}) assert_equal(len(os.listdir('deplibs')), 0) back_tick([test_lib]) back_tick([stest_lib]) # Make a fake external library to link to os.makedirs('fakelibs') fake_lib = realpath(_copy_to(LIBA, 'fakelibs', 'libfake.dylib')) _, _, _, test_lib, slibc, stest_lib = _make_libtree( realpath('subtree2')) set_install_name(slibc, EXT_LIBS[0], fake_lib) # shortcut _rp = realpath # Check fake libary gets copied and delocated slc_rel = pjoin('subtree2', 'subsub', 'libc.dylib') assert_equal(delocate_path('subtree2', 'deplibs2'), {_rp(fake_lib): {_rp(slc_rel): fake_lib}}) assert_equal(os.listdir('deplibs2'), ['libfake.dylib']) assert_true('@loader_path/../../deplibs2/libfake.dylib' in get_install_names(slibc)) # Unless we set the filter otherwise _, _, _, test_lib, slibc, stest_lib = _make_libtree( realpath('subtree3')) set_install_name(slibc, EXT_LIBS[0], fake_lib) def filt(libname): return not (libname.startswith('/usr') or 'libfake' in libname) assert_equal(delocate_path('subtree3', 'deplibs3', None, filt), {}) assert_equal(len(os.listdir('deplibs3')), 0) # Test tree names filtering works _, _, _, test_lib, slibc, stest_lib = _make_libtree( realpath('subtree4')) set_install_name(slibc, EXT_LIBS[0], fake_lib) def lib_filt(filename): return not filename.endswith('subsub/libc.dylib') assert_equal(delocate_path('subtree4', 'deplibs4', lib_filt), {}) assert_equal(len(os.listdir('deplibs4')), 0) # Check can use already existing directory os.makedirs('deplibs5') _, _, _, test_lib, slibc, stest_lib = _make_libtree( realpath('subtree5')) assert_equal(delocate_path('subtree5', 'deplibs5'), {}) assert_equal(len(os.listdir('deplibs5')), 0) def _make_bare_depends(): # Copy: # * liba.dylib to 'libs' dir, which is a dependency of libb.dylib # * libb.dylib to 'subtree' dir, as 'libb' (no extension). # # This is for testing delocation when the depending file does not have a # dynamic library file extension. libb, = _copy_libs([LIBB], 'subtree') liba, = _copy_libs([LIBA], 'libs') bare_b, _ = splitext(libb) os.rename(libb, bare_b) # use realpath for OSX /private/var - /var set_install_name(bare_b, 'liba.dylib', realpath(liba)) return liba, bare_b def test_delocate_path_dylibs(): # Test options for delocating everything, or just dynamic libraries _rp = realpath # shortcut with InTemporaryDirectory(): # With 'dylibs-only' - does not inspect non-dylib files liba, bare_b = _make_bare_depends() assert_equal(delocate_path('subtree', 'deplibs', lib_filt_func='dylibs-only'), {}) assert_equal(len(os.listdir('deplibs')), 0) # None - does inspect non-dylib files assert_equal(delocate_path('subtree', 'deplibs', None), {_rp(pjoin('libs', 'liba.dylib')): {_rp(bare_b): _rp(liba)}}) assert_equal(os.listdir('deplibs'), ['liba.dylib']) with InTemporaryDirectory(): # Callable, dylibs only, does not inspect liba, bare_b = _make_bare_depends() def func(fn): return fn.endswith('.dylib') assert_equal(delocate_path('subtree', 'deplibs', func), {}) def func(fn): return fn.endswith('libb') assert_equal(delocate_path('subtree', 'deplibs', None), {_rp(pjoin('libs', 'liba.dylib')): {_rp(bare_b): _rp(liba)}}) def test_check_archs(): # Test utility to check architectures in copied_libs dict # No libs always OK s0 = set() assert_equal(check_archs({}), s0) # One lib to itself OK lib_32_32 = {LIB32: {LIB32: 'install_name'}} lib_64_64 = {LIB64: {LIB64: 'install_name'}} assert_equal(check_archs(lib_32_32), s0) assert_equal(check_archs(lib_64_64), s0) # OK matching to another static lib of same arch assert_equal(check_archs({LIB64A: {LIB64: 'install_name'}}), s0) # Or two libs two_libs = {LIB64A: {LIB64: 'install_name'}, LIB32: {LIB32: 'install_name'}} assert_equal(check_archs(two_libs), s0) # Same as empty sequence required_args argument assert_equal(check_archs(lib_32_32, ()), s0) assert_equal(check_archs(lib_64_64, ()), s0) assert_equal(check_archs(two_libs, ()), s0) assert_equal(check_archs(two_libs, []), s0) assert_equal(check_archs(two_libs, set()), s0) # bads if we require more archs than present for in_libs, exp_arch, missing in ((lib_32_32, ARCH_64, ARCH_64), (lib_32_32, ARCH_BOTH, ARCH_64), (lib_32_32, 'x86_64', ARCH_64), (lib_32_32, 'intel', ARCH_64), (lib_64_64, ARCH_32, ARCH_32), (lib_64_64, ARCH_BOTH, ARCH_32), (lib_64_64, 'i386', ARCH_32), (lib_64_64, 'intel', ARCH_32)): ded, value = list(in_libs.items())[0] ding, _ = list(value.items())[0] assert_equal(check_archs(in_libs, exp_arch), {(ding, missing)}) # Two libs assert_equal(check_archs(two_libs, ARCH_32), {(LIB64, ARCH_32)}) assert_equal(check_archs(two_libs, ARCH_64), {(LIB32, ARCH_64)}) assert_equal(check_archs(two_libs, ARCH_BOTH), {(LIB64, ARCH_32), (LIB32, ARCH_64)}) # Libs must match architecture with second arg of None assert_equal(check_archs( {LIB64: {LIB32: 'install_name'}}), {(LIB64, LIB32, ARCH_32)}) assert_equal(check_archs( {LIB64A: {LIB64: 'install_name'}, LIB32: {LIB32: 'install_name'}, LIB64: {LIB32: 'install_name'}}), {(LIB64, LIB32, ARCH_32)}) # For single archs depending, dual archs in depended is OK assert_equal(check_archs( {LIBBOTH: {LIB64A: 'install_name'}}), s0) # For dual archs in depending, both must be present assert_equal(check_archs( {LIBBOTH: {LIBBOTH: 'install_name'}}), s0) assert_equal(check_archs( {LIB64A: {LIBBOTH: 'install_name'}}), {(LIB64A, LIBBOTH, ARCH_32)}) # More than one bad in_dict = {LIB64A: {LIBBOTH: 'install_name'}, LIB64: {LIB32: 'install_name'}} exp_res = {(LIB64A, LIBBOTH, ARCH_32), (LIB64, LIB32, ARCH_32)} assert_equal(check_archs(in_dict), exp_res) # Check stop_fast flag; can't predict return, but there should only be one stopped = check_archs(in_dict, (), True) assert_equal(len(stopped), 1) # More than one bad in dependings assert_equal(check_archs( {LIB64A: {LIBBOTH: 'install_name', LIB32: 'install_name'}, LIB64: {LIB32: 'install_name'}}), {(LIB64A, LIBBOTH, ARCH_32), (LIB64A, LIB32, ARCH_32), (LIB64, LIB32, ARCH_32)}) def test_bads_report(): # Test bads_report of architecture errors # No bads, no report assert_equal(bads_report(set()), '') fmt_str_2 = 'Required arch i386 missing from {0}' fmt_str_3 = '{0} needs arch i386 missing from {1}' # One line report assert_equal(bads_report({(LIB64, LIB32, ARCH_32)}), fmt_str_3.format(LIB32, LIB64)) # One line report applying path stripper assert_equal(bads_report({(LIB64, LIB32, ARCH_32)}, dirname(LIB64)), fmt_str_3.format(basename(LIB32), basename(LIB64))) # Multi-line report assert_equal( bads_report({(LIB64A, LIBBOTH, ARCH_32), (LIB64A, LIB32, ARCH_32), (LIB64, LIB32, ARCH_32)}), '\n'.join([fmt_str_3.format(LIB32, LIB64A), fmt_str_3.format(LIB32, LIB64), fmt_str_3.format(LIBBOTH, LIB64A)])) # Two tuples and three tuples assert_equal(bads_report( {(LIB64A, LIBBOTH, ARCH_32), (LIB64, ARCH_32), (LIB32, ARCH_32)}), '\n'.join([fmt_str_3.format(LIBBOTH, LIB64A), fmt_str_2.format(LIB64), fmt_str_2.format(LIB32)])) # Tuples must be length 2 or 3 assert_raises(ValueError, bads_report, {(LIB64A, LIBBOTH, ARCH_32), (LIB64,), (LIB32, ARCH_32)}) # Tuples must be length 2 or 3 assert_raises( ValueError, bads_report, {(LIB64A, LIBBOTH, ARCH_32), (LIB64, LIB64, ARCH_32, ARCH_64), (LIB32, ARCH_32)}) def test_dyld_library_path_lookups(): # Test that DYLD_LIBRARY_PATH can be used to find libs during # delocation with TempDirWithoutEnvVars('DYLD_LIBRARY_PATH') as tmpdir: # Copy libs into a temporary directory subtree = pjoin(tmpdir, 'subtree') all_local_libs = _make_libtree(subtree) liba, libb, libc, test_lib, slibc, stest_lib = all_local_libs # move libb and confirm that test_lib doesn't work hidden_dir = 'hidden' os.mkdir(hidden_dir) new_libb = os.path.join(hidden_dir, os.path.basename(LIBB)) shutil.move(libb, new_libb) assert_raises(RuntimeError, back_tick, [test_lib]) # Update DYLD_LIBRARY_PATH and confirm that we can now # successfully delocate test_lib os.environ['DYLD_LIBRARY_PATH'] = hidden_dir delocate_path('subtree', 'deplibs') back_tick(test_lib) def test_dyld_library_path_beats_basename(): # Test that we find libraries on DYLD_LIBRARY_PATH before basename with TempDirWithoutEnvVars('DYLD_LIBRARY_PATH') as tmpdir: # Copy libs into a temporary directory subtree = pjoin(tmpdir, 'subtree') all_local_libs = _make_libtree(subtree) liba, libb, libc, test_lib, slibc, stest_lib = all_local_libs # Copy liba into a subdirectory subdir = os.path.join(subtree, 'subdir') os.mkdir(subdir) new_libb = os.path.join(subdir, os.path.basename(LIBB)) shutil.copyfile(libb, new_libb) # Without updating the environment variable, we find the lib normally predicted_lib_location = search_environment_for_lib(libb) # tmpdir can end up in /var, and that can be symlinked to # /private/var, so we'll use realpath to resolve the two assert_equal(predicted_lib_location, os.path.realpath(libb)) # Updating shows us the new lib os.environ['DYLD_LIBRARY_PATH'] = subdir predicted_lib_location = search_environment_for_lib(libb) assert_equal(predicted_lib_location, new_libb) def test_dyld_fallback_library_path_loses_to_basename(): # Test that we find libraries on basename before DYLD_FALLBACK_LIBRARY_PATH with TempDirWithoutEnvVars('DYLD_FALLBACK_LIBRARY_PATH') as tmpdir: # Copy libs into a temporary directory subtree = pjoin(tmpdir, 'subtree') all_local_libs = _make_libtree(subtree) liba, libb, libc, test_lib, slibc, stest_lib = all_local_libs # Copy liba into a subdirectory subdir = 'subdir' os.mkdir(subdir) new_libb = os.path.join(subdir, os.path.basename(LIBB)) shutil.copyfile(libb, new_libb) os.environ['DYLD_FALLBACK_LIBRARY_PATH'] = subdir predicted_lib_location = search_environment_for_lib(libb) # tmpdir can end up in /var, and that can be symlinked to # /private/var, so we'll use realpath to resolve the two assert_equal(predicted_lib_location, os.path.realpath(libb))