Skip to content

Commit

Permalink
Install one copy of libs used by multiple extension modules
Browse files Browse the repository at this point in the history
If you have a package that contains multiple C extensions that
depend on the same shared libraries, then `delocate-wheel` saves
duplicate copies of all of the shared libraries.

This makes for packages that are several times larger than
necessary. This patch causes `delocate-wheel` to sweep up all
of the dependencies and put them in a single directory, rather
than creating duplicates.

The convention is based on the proposal in pypa/auditwheel#89
and pypa/auditwheel#90.
  • Loading branch information
lpsinger committed Feb 7, 2019
1 parent 138203f commit 5193592
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 35 deletions.
15 changes: 9 additions & 6 deletions delocate/delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
import warnings
from subprocess import Popen, PIPE

try:
from wheel.install import WHEEL_INFO_RE # type: ignore
except ImportError: # As of Wheel 0.32.0
from wheel.wheelfile import WHEEL_INFO_RE # type: ignore
WHEEL_INFO_RE = WHEEL_INFO_RE.match

from .pycompat import string_types
from .libsana import tree_libs, stripped_lib_dict, get_rp_stripper
from .tools import (set_install_name, zip2dir, dir2zip, validate_signature,
Expand Down Expand Up @@ -227,7 +233,7 @@ def _copy_required(lib_path, copy_filt_func, copied_libs):
# Haven't see this one before, add entry to copied_libs
out_path = pjoin(lib_path, basename(required))
if exists(out_path):
raise DelocationError(out_path + ' already exists')
continue
shutil.copy(required, lib_path)
copied2orig[out_path] = required
copied_libs[required] = procd_requirings
Expand Down Expand Up @@ -370,15 +376,12 @@ def delocate_wheel(in_wheel,
all_copied = {}
wheel_dir = realpath(pjoin(tmpdir, 'wheel'))
zip2dir(in_wheel, wheel_dir)
wheel_fname = basename(out_wheel)
lib_path = pjoin(wheel_dir, WHEEL_INFO_RE(wheel_fname).group('name') + lib_sdir)
for package_path in find_package_dirs(wheel_dir):
lib_path = pjoin(package_path, lib_sdir)
lib_path_exists = exists(lib_path)
copied_libs = delocate_path(package_path, lib_path,
lib_filt_func, copy_filt_func)
if copied_libs and lib_path_exists:
raise DelocationError(
'{0} already exists in wheel but need to copy '
'{1}'.format(lib_path, '; '.join(copied_libs)))
if len(os.listdir(lib_path)) == 0:
shutil.rmtree(lib_path)
# Check architectures
Expand Down
11 changes: 5 additions & 6 deletions delocate/tests/test_delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from __future__ import division, print_function

import os
from os.path import (join as pjoin, dirname, basename, relpath, realpath,
splitext)
from os.path import (join as pjoin, dirname, basename, getmtime, relpath,
realpath, splitext)
import shutil

from ..delocating import (DelocationError, delocate_tree_libs, copy_recurse,
Expand Down Expand Up @@ -282,18 +282,17 @@ def test_copy_recurse_overwrite():
os.makedirs('libcopy')
test_lib, liba, libb, libc = _copy_fixpath(
[TEST_LIB, LIBA, LIBB, LIBC], 'libcopy')
liba_mtime = getmtime(LIBA)
# 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
# If liba is already present, we shouldn't try to copy it
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)
assert_equal(liba_mtime, getmtime(pjoin('subtree', 'liba.dylib')))


def test_delocate_path():
Expand Down
33 changes: 17 additions & 16 deletions delocate/tests/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def _check_wheel(wheel_fname, lib_sdir):
wheel_fname = abspath(wheel_fname)
with InTemporaryDirectory():
zip2dir(wheel_fname, 'plat_pkg')
dylibs = pjoin('plat_pkg', 'fakepkg1', lib_sdir)
dylibs = pjoin('plat_pkg', lib_sdir)
assert_true(exists(dylibs))
assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])

Expand All @@ -171,30 +171,30 @@ def test_wheel():
fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
code, stdout, stderr = run_command(
['delocate-wheel', fixed_wheel])
_check_wheel(fixed_wheel, '.dylibs')
_check_wheel(fixed_wheel, 'fakepkg1.dylibs')
# Make another copy to test another output directory
fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
code, stdout, stderr = run_command(
['delocate-wheel', '-L', 'dynlibs_dir', fixed_wheel])
_check_wheel(fixed_wheel, 'dynlibs_dir')
_check_wheel(fixed_wheel, 'fakepkg1.dynlibs_dir')
# Another output directory
fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
code, stdout, stderr = run_command(
['delocate-wheel', '-w', 'fixed', fixed_wheel])
_check_wheel(pjoin('fixed', basename(fixed_wheel)), '.dylibs')
_check_wheel(pjoin('fixed', basename(fixed_wheel)), 'fakepkg1.dylibs')
# More than one wheel
shutil.copy2(fixed_wheel, 'wheel_copy.ext')
code, stdout, stderr = run_command(
['delocate-wheel', '-w', 'fixed2', fixed_wheel, 'wheel_copy.ext'])
assert_equal(stdout,
['Fixing: ' + name
for name in (fixed_wheel, 'wheel_copy.ext')])
_check_wheel(pjoin('fixed2', basename(fixed_wheel)), '.dylibs')
_check_wheel(pjoin('fixed2', 'wheel_copy.ext'), '.dylibs')
_check_wheel(pjoin('fixed2', basename(fixed_wheel)), 'fakepkg1.dylibs')
_check_wheel(pjoin('fixed2', 'wheel_copy.ext'), 'fakepkg1.dylibs')
# Verbose - single wheel
code, stdout, stderr = run_command(
['delocate-wheel', '-w', 'fixed3', fixed_wheel, '-v'])
_check_wheel(pjoin('fixed3', basename(fixed_wheel)), '.dylibs')
_check_wheel(pjoin('fixed3', basename(fixed_wheel)), 'fakepkg1.dylibs')
wheel_lines1 = ['Fixing: ' + fixed_wheel,
'Copied to package .dylibs directory:',
stray_lib]
Expand All @@ -213,17 +213,18 @@ def test_fix_wheel_dylibs():
with InTemporaryDirectory() as tmpdir:
# Default in-place fix
fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
_rename_module(fixed_wheel, 'module.other', 'test.whl')
shutil.copyfile('test.whl', 'test2.whl')
_rename_module(fixed_wheel, 'module.other', None)
# Default is to look in all files and therefore fix
code, stdout, stderr = run_command(
['delocate-wheel', 'test.whl'])
_check_wheel('test.whl', '.dylibs')
['delocate-wheel', fixed_wheel])
_check_wheel(fixed_wheel, 'fakepkg1.dylibs')
# Can turn this off to only look in dynamic lib exts
fixed_wheel, stray_lib = _fixed_wheel(tmpdir)
_rename_module(fixed_wheel, 'module.other', None)
code, stdout, stderr = run_command(
['delocate-wheel', 'test2.whl', '-d'])
with InWheel('test2.whl'): # No fix
assert_false(exists(pjoin('fakepkg1', '.dylibs')))
['delocate-wheel', fixed_wheel, '-d'])
with InWheel(fixed_wheel): # No fix
assert_false(exists(pjoin('fakepkg1.dylibs')))


def test_fix_wheel_archs():
Expand All @@ -234,7 +235,7 @@ def test_fix_wheel_archs():
# Fixed wheel, architectures are OK
code, stdout, stderr = run_command(
['delocate-wheel', fixed_wheel, '-k'])
_check_wheel(fixed_wheel, '.dylibs')
_check_wheel(fixed_wheel, 'fakepkg1.dylibs')
# Broken with one architecture removed still OK without checking
# But if we check, raise error
fmt_str = 'Fixing: {0}\n{1} needs arch {2} missing from {3}'
Expand All @@ -251,7 +252,7 @@ def _fix_break_fix(arch):
_fix_break(arch)
code, stdout, stderr = run_command(
['delocate-wheel', fixed_wheel])
_check_wheel(fixed_wheel, '.dylibs')
_check_wheel(fixed_wheel, 'fakepkg1.dylibs')
# Checked
_fix_break(arch)
code, stdout, stderr = bytes_runner.run_command(
Expand Down
15 changes: 8 additions & 7 deletions delocate/tests/test_wheelies.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_fix_plat():
{_rp(stray_lib): {dep_mod: stray_lib}})
zip2dir(fixed_wheel, 'plat_pkg')
assert_true(exists(pjoin('plat_pkg', 'fakepkg1')))
dylibs = pjoin('plat_pkg', 'fakepkg1', '.dylibs')
dylibs = pjoin('plat_pkg', 'fakepkg1.dylibs')
assert_true(exists(dylibs))
assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])
# New output name
Expand All @@ -103,7 +103,7 @@ def test_fix_plat():
{_rp(stray_lib): {dep_mod: stray_lib}})
zip2dir('fixed_wheel.ext', 'plat_pkg1')
assert_true(exists(pjoin('plat_pkg1', 'fakepkg1')))
dylibs = pjoin('plat_pkg1', 'fakepkg1', '.dylibs')
dylibs = pjoin('plat_pkg1', 'fakepkg1.dylibs')
assert_true(exists(dylibs))
assert_equal(os.listdir(dylibs), ['libextfunc.dylib'])
# Test another lib output directory
Expand All @@ -130,7 +130,7 @@ def test_fix_plat():
# Check that copied libraries have modified install_name_ids
zip2dir(fixed_wheel, 'plat_pkg3')
base_stray = basename(stray_lib)
the_lib = pjoin('plat_pkg3', 'fakepkg1', '.dylibs', base_stray)
the_lib = pjoin('plat_pkg3', 'fakepkg1.dylibs', base_stray)
inst_id = DLC_PREFIX + 'fakepkg1/' + base_stray
assert_equal(get_install_id(the_lib), inst_id)

Expand Down Expand Up @@ -285,7 +285,7 @@ def test_patch_wheel():
def test_fix_rpath():
# Test wheels which have an @rpath dependency
# Also verifies the delocated libraries signature
with InTemporaryDirectory():
with InTemporaryDirectory() as tmpdir:
# The module was set to expect its dependency in the libs/ directory
os.symlink(DATA_PATH, 'libs')

Expand All @@ -296,10 +296,11 @@ def test_fix_rpath():
dep_mod = 'fakepkg/subpkg/module2.so'
dep_path = '@rpath/libextfunc_rpath.dylib'

out_wheel = pjoin(tmpdir, basename(RPATH_WHEEL))
assert_equal(
delocate_wheel(RPATH_WHEEL, 'tmp.whl'),
delocate_wheel(RPATH_WHEEL, out_wheel),
{stray_lib: {dep_mod: dep_path}},
)
with InWheel('tmp.whl'):
with InWheel(out_wheel):
check_call(['codesign', '--verify',
'fakepkg/.dylibs/libextfunc_rpath.dylib'])
'fakepkg/fakepkg.dylibs/libextfunc_rpath.dylib'])

0 comments on commit 5193592

Please sign in to comment.