From a8c9463d487c71b00ef1315a3f0ae93e281f2010 Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Sun, 28 Oct 2018 16:42:45 +0000 Subject: [PATCH] remove wheeltools.py, import/require standalone wheeltools as discussed in https://github.com/pypa/auditwheel/issues/102 --- delocate/cmd/delocate_addplat.py | 2 +- delocate/delocating.py | 2 +- delocate/fuse.py | 2 +- delocate/tests/test_fuse.py | 7 +- delocate/tests/test_scripts.py | 44 +++++- delocate/tests/test_tools.py | 174 +---------------------- delocate/tests/test_wheelies.py | 2 +- delocate/tests/test_wheeltools.py | 186 ------------------------ delocate/tools.py | 228 ++---------------------------- delocate/wheeltools.py | 222 ----------------------------- setup.py | 1 + 11 files changed, 67 insertions(+), 803 deletions(-) delete mode 100644 delocate/tests/test_wheeltools.py delete mode 100644 delocate/wheeltools.py diff --git a/delocate/cmd/delocate_addplat.py b/delocate/cmd/delocate_addplat.py index 2d7742c1..db49e263 100644 --- a/delocate/cmd/delocate_addplat.py +++ b/delocate/cmd/delocate_addplat.py @@ -22,7 +22,7 @@ from optparse import OptionParser, Option from delocate import __version__ -from delocate.wheeltools import add_platforms, WheelToolsError +from wheeltools import add_platforms, WheelToolsError def main(): diff --git a/delocate/delocating.py b/delocate/delocating.py index 93b111df..99c12b7e 100644 --- a/delocate/delocating.py +++ b/delocate/delocating.py @@ -15,7 +15,7 @@ from .tools import (set_install_name, zip2dir, dir2zip, validate_signature, find_package_dirs, set_install_id, get_archs) from .tmpdirs import TemporaryDirectory -from .wheeltools import rewrite_record, InWheel +from wheeltools import rewrite_record, InWheel # Prefix for install_name_id of copied libraries DLC_PREFIX = '/DLC/' diff --git a/delocate/fuse.py b/delocate/fuse.py index a3b23007..b51c0ef0 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -20,7 +20,7 @@ from .tools import (zip2dir, dir2zip, cmp_contents, lipo_fuse, open_rw, chmod_perms) from .tmpdirs import InTemporaryDirectory -from .wheeltools import rewrite_record +from wheeltools import rewrite_record def _copyfile(in_fname, out_fname): diff --git a/delocate/tests/test_fuse.py b/delocate/tests/test_fuse.py index 1f40454d..77b262c8 100644 --- a/delocate/tests/test_fuse.py +++ b/delocate/tests/test_fuse.py @@ -9,7 +9,7 @@ open_readable) from ..fuse import fuse_trees, fuse_wheels from ..tmpdirs import InTemporaryDirectory -from ..wheeltools import rewrite_record +from wheeltools import rewrite_record from .pytest_tools import (assert_true, assert_false, assert_raises, assert_equal, assert_not_equal) @@ -17,7 +17,10 @@ from .test_tools import LIB32, LIB64, LIB64A from .test_wheelies import PURE_WHEEL -from .test_wheeltools import assert_record_equal + + +def assert_record_equal(record_orig, record_new): + assert_equal(sorted(record_orig.splitlines()), sorted(record_new.splitlines())) def assert_same_tree(tree1, tree2): diff --git a/delocate/tests/test_scripts.py b/delocate/tests/test_scripts.py index c1c54c80..b2da17f7 100644 --- a/delocate/tests/test_scripts.py +++ b/delocate/tests/test_scripts.py @@ -15,7 +15,14 @@ from ..tmpdirs import InTemporaryDirectory from ..tools import back_tick, set_install_name, zip2dir, dir2zip -from ..wheeltools import InWheel + +try: + from wheel.install import WheelFile +except ImportError: # As of Wheel 0.32.0 + from wheel.wheelfile import WheelFile + +from wheeltools import InWheel + from .scriptrunner import ScriptRunner from .pytest_tools import (assert_true, assert_false, assert_equal, assert_raises, @@ -27,7 +34,40 @@ STRAY_LIB_DEP, WHEEL_PATCH, WHEEL_PATCH_BAD, _thin_lib, _thin_mod, _rename_module) from .test_fuse import assert_same_tree -from .test_wheeltools import assert_winfo_similar + + +def get_info(wheelfile): + # Work round wheel API changes + try: + return wheelfile.parsed_wheel_info + except AttributeError: + pass + # Wheel 0.32.0 + from wheel.pkginfo import read_pkg_info_bytes + + info_name = wheelfile.dist_info_path + "/WHEEL" + return read_pkg_info_bytes(wheelfile.read(info_name)) + + +def _filter_key(items, key): + return [(k, v) for k, v in items if k != key] + + +def assert_winfo_similar(whl_fname, exp_items, drop_version=True): + wf = WheelFile(whl_fname) + wheel_parts = wf.parsed_filename.groupdict() + # Info can contain duplicate keys (e.g. Tag) + w_info = sorted(get_info(wf).items()) + if drop_version: + w_info = _filter_key(w_info, "Wheel-Version") + exp_items = _filter_key(exp_items, "Wheel-Version") + assert_equal(len(exp_items), len(w_info)) + # Extract some information from actual values + wheel_parts["pip_version"] = dict(w_info)["Generator"].split()[1] + for (key1, value1), (key2, value2) in zip(exp_items, w_info): + assert_equal(key1, key2) + value1 = value1.format(**wheel_parts) + assert_equal(value1, value2) def _proc_lines(in_str): diff --git a/delocate/tests/test_tools.py b/delocate/tests/test_tools.py index 596294ab..48def29d 100644 --- a/delocate/tests/test_tools.py +++ b/delocate/tests/test_tools.py @@ -1,19 +1,14 @@ """ Test tools module """ from __future__ import division, print_function -import os from os.path import join as pjoin, dirname -import stat import shutil -from ..tools import (back_tick, unique_by_index, ensure_writable, chmod_perms, - ensure_permissions, zip2dir, dir2zip, find_package_dirs, - cmp_contents, get_archs, lipo_fuse, replace_signature, +from ..tools import (back_tick, get_archs, lipo_fuse, replace_signature, validate_signature, add_rpath) - from ..tmpdirs import InTemporaryDirectory -from .pytest_tools import assert_true, assert_false, assert_equal, assert_raises +from .pytest_tools import assert_equal, assert_raises DATA_PATH = pjoin(dirname(__file__), 'data') LIB32 = pjoin(DATA_PATH, 'liba32.dylib') @@ -24,171 +19,6 @@ ARCH_32 = frozenset(['i386']) ARCH_BOTH = ARCH_64 | ARCH_32 -def test_back_tick(): - cmd = 'python -c "print(\'Hello\')"' - assert_equal(back_tick(cmd), "Hello") - assert_equal(back_tick(cmd, ret_err=True), ("Hello", "")) - assert_equal(back_tick(cmd, True, False), (b"Hello", b"")) - cmd = 'python -c "raise ValueError()"' - assert_raises(RuntimeError, back_tick, cmd) - - -def test_uniqe_by_index(): - assert_equal(unique_by_index([1, 2, 3, 4]), - [1, 2, 3, 4]) - assert_equal(unique_by_index([1, 2, 2, 4]), - [1, 2, 4]) - assert_equal(unique_by_index([4, 2, 2, 1]), - [4, 2, 1]) - def gen(): - yield 4 - yield 2 - yield 2 - yield 1 - assert_equal(unique_by_index(gen()), [4, 2, 1]) - - -def test_ensure_permissions(): - # Test decorator to ensure permissions - with InTemporaryDirectory(): - # Write, set zero permissions - sts = {} - for fname, contents in (('test.read', 'A line\n'), - ('test.write', 'B line')): - with open(fname, 'wt') as fobj: - fobj.write(contents) - os.chmod(fname, 0) - sts[fname] = chmod_perms(fname) - - def read_file(fname): - with open(fname, 'rt') as fobj: - contents = fobj.read() - return contents - - fixed_read_file = ensure_permissions(stat.S_IRUSR)(read_file) - non_read_file = ensure_permissions(stat.S_IWUSR)(read_file) - - def write_file(fname, contents): - with open(fname, 'wt') as fobj: - fobj.write(contents) - - fixed_write_file = ensure_permissions(stat.S_IWUSR)(write_file) - non_write_file = ensure_permissions(stat.S_IRUSR)(write_file) - - # Read fails with default, no permissions - assert_raises(IOError, read_file, 'test.read') - # Write fails with default, no permissions - assert_raises(IOError, write_file, 'test.write', 'continues') - # Read fails with wrong permissions - assert_raises(IOError, non_read_file, 'test.read') - # Write fails with wrong permissions - assert_raises(IOError, non_write_file, 'test.write', 'continues') - # Read succeeds with fixed function - assert_equal(fixed_read_file('test.read'), 'A line\n') - # Write fails, no permissions - assert_raises(IOError, non_write_file, 'test.write', 'continues') - # Write succeeds with fixed function - fixed_write_file('test.write', 'continues') - assert_equal(fixed_read_file('test.write'), 'continues') - # Permissions are as before - for fname, st in sts.items(): - assert_equal(chmod_perms(fname), st) - - -def test_ensure_writable(): - # Test ensure writable decorator - with InTemporaryDirectory(): - with open('test.bin', 'wt') as fobj: - fobj.write('A line\n') - # Set to user rw, else r - os.chmod('test.bin', 0o644) - st = os.stat('test.bin') - @ensure_writable - def foo(fname): - pass - foo('test.bin') - assert_equal(os.stat('test.bin'), st) - # No-one can write - os.chmod('test.bin', 0o444) - st = os.stat('test.bin') - foo('test.bin') - assert_equal(os.stat('test.bin'), st) - - -def _write_file(filename, contents): - with open(filename, 'wt') as fobj: - fobj.write(contents) - - -def test_zip2(): - # Test utilities to unzip and zip up - with InTemporaryDirectory(): - os.mkdir('a_dir') - os.mkdir('zips') - _write_file(pjoin('a_dir', 'file1.txt'), 'File one') - s_dir = pjoin('a_dir', 's_dir') - os.mkdir(s_dir) - _write_file(pjoin(s_dir, 'file2.txt'), 'File two') - zip_fname = pjoin('zips', 'my.zip') - dir2zip('a_dir', zip_fname) - zip2dir(zip_fname, 'another_dir') - assert_equal(os.listdir('another_dir'), ['file1.txt', 's_dir']) - assert_equal(os.listdir(pjoin('another_dir', 's_dir')), ['file2.txt']) - # Try zipping from a subdirectory, with a different extension - dir2zip(s_dir, 'another.ext') - # Remove original tree just to be sure - shutil.rmtree('a_dir') - zip2dir('another.ext', 'third_dir') - assert_equal(os.listdir('third_dir'), ['file2.txt']) - # Check permissions kept in zip unzip cycle - os.mkdir('a_dir') - permissions = stat.S_IRUSR | stat.S_IWGRP | stat.S_IXGRP - fname = pjoin('a_dir', 'permitted_file') - _write_file(fname, 'Some script or something') - os.chmod(fname, permissions) - dir2zip('a_dir', 'test.zip') - zip2dir('test.zip', 'another_dir') - out_fname = pjoin('another_dir', 'permitted_file') - assert_equal(os.stat(out_fname).st_mode & 0o777, permissions) - - -def test_find_package_dirs(): - # Test utility for finding package directories - with InTemporaryDirectory(): - os.mkdir('to_test') - a_dir = pjoin('to_test', 'a_dir') - b_dir = pjoin('to_test', 'b_dir') - c_dir = pjoin('to_test', 'c_dir') - for dir in (a_dir, b_dir, c_dir): - os.mkdir(dir) - assert_equal(find_package_dirs('to_test'), set([])) - _write_file(pjoin(a_dir, '__init__.py'), "# a package") - assert_equal(find_package_dirs('to_test'), set([a_dir])) - _write_file(pjoin(c_dir, '__init__.py'), "# another package") - assert_equal(find_package_dirs('to_test'), set([a_dir, c_dir])) - # Not recursive - assert_equal(find_package_dirs('.'), set()) - _write_file(pjoin('to_test', '__init__.py'), "# base package") - # Also - strips '.' for current directory - assert_equal(find_package_dirs('.'), set(['to_test'])) - - -def test_cmp_contents(): - # Binary compare of filenames - assert_true(cmp_contents(__file__, __file__)) - with InTemporaryDirectory(): - with open('first', 'wb') as fobj: - fobj.write(b'abc\x00\x10\x13\x10') - with open('second', 'wb') as fobj: - fobj.write(b'abc\x00\x10\x13\x11') - assert_false(cmp_contents('first', 'second')) - with open('third', 'wb') as fobj: - fobj.write(b'abc\x00\x10\x13\x10') - assert_true(cmp_contents('first', 'third')) - with open('fourth', 'wb') as fobj: - fobj.write(b'abc\x00\x10\x13\x10\x00') - assert_false(cmp_contents('first', 'fourth')) - def test_get_archs_fuse(): # Test routine to get architecture types from file diff --git a/delocate/tests/test_wheelies.py b/delocate/tests/test_wheelies.py index 8ea37cf7..f2c21029 100644 --- a/delocate/tests/test_wheelies.py +++ b/delocate/tests/test_wheelies.py @@ -11,7 +11,7 @@ DLC_PREFIX) from ..tools import (get_install_names, set_install_name, zip2dir, dir2zip, back_tick, get_install_id, get_archs) -from ..wheeltools import InWheel +from wheeltools import InWheel from ..tmpdirs import InTemporaryDirectory, InGivenDirectory diff --git a/delocate/tests/test_wheeltools.py b/delocate/tests/test_wheeltools.py deleted file mode 100644 index 4bc25c71..00000000 --- a/delocate/tests/test_wheeltools.py +++ /dev/null @@ -1,186 +0,0 @@ -""" Tests for wheeltools utilities -""" - -import os -from os.path import join as pjoin, exists, isfile, basename, realpath, splitext -import shutil - -try: - from wheel.install import WheelFile -except ImportError: # As of Wheel 0.32.0 - from wheel.wheelfile import WheelFile - -from ..wheeltools import (rewrite_record, InWheel, InWheelCtx, WheelToolsError, - add_platforms, _get_wheelinfo_name) -from ..tmpdirs import InTemporaryDirectory -from ..tools import zip2dir, open_readable - -from .pytest_tools import (assert_true, assert_false, assert_raises, assert_equal) - -from .test_wheelies import PURE_WHEEL, PLAT_WHEEL - - -def assert_record_equal(record_orig, record_new): - assert_equal(sorted(record_orig.splitlines()), - sorted(record_new.splitlines())) - - -def test_rewrite_record(): - dist_info_sdir = 'fakepkg2-1.0.dist-info' - with InTemporaryDirectory(): - zip2dir(PURE_WHEEL, 'wheel') - record_fname = pjoin('wheel', dist_info_sdir, 'RECORD') - with open_readable(record_fname, 'rt') as fobj: - record_orig = fobj.read() - # Test we get the same record by rewriting - os.unlink(record_fname) - rewrite_record('wheel') - with open_readable(record_fname, 'rt') as fobj: - record_new = fobj.read() - assert_record_equal(record_orig, record_new) - # Test that signature gets deleted - sig_fname = pjoin('wheel', dist_info_sdir, 'RECORD.jws') - with open(sig_fname, 'wt') as fobj: - fobj.write('something') - rewrite_record('wheel') - with open_readable(record_fname, 'rt') as fobj: - record_new = fobj.read() - assert_record_equal(record_orig, record_new) - assert_false(exists(sig_fname)) - # Test error for too many dist-infos - shutil.copytree(pjoin('wheel', dist_info_sdir), - pjoin('wheel', 'anotherpkg-2.0.dist-info')) - assert_raises(WheelToolsError, rewrite_record, 'wheel') - - -def test_in_wheel(): - # Test in-wheel context managers - # Stuff they share - for ctx_mgr in InWheel, InWheelCtx: - with ctx_mgr(PURE_WHEEL): # No output wheel - shutil.rmtree('fakepkg2') - res = sorted(os.listdir('.')) - assert_equal(res, ['fakepkg2-1.0.dist-info']) - # The original wheel unchanged - with ctx_mgr(PURE_WHEEL): # No output wheel - res = sorted(os.listdir('.')) - assert_equal(res, ['fakepkg2', 'fakepkg2-1.0.dist-info']) - # Make an output wheel file in a temporary directory - with InTemporaryDirectory(): - mod_path = pjoin('fakepkg2', 'module1.py') - with ctx_mgr(PURE_WHEEL, 'mungled.whl'): - assert_true(isfile(mod_path)) - os.unlink(mod_path) - with ctx_mgr('mungled.whl'): - assert_false(isfile(mod_path)) - # Different return from context manager - with InWheel(PURE_WHEEL) as wheel_path: - assert_equal(realpath(wheel_path), realpath(os.getcwd())) - with InWheelCtx(PURE_WHEEL) as ctx: - assert_equal(realpath(ctx.wheel_path), realpath(os.getcwd())) - # Set the output wheel inside the with block - with InTemporaryDirectory() as tmpdir: - mod_path = pjoin('fakepkg2', 'module1.py') - with InWheelCtx(PURE_WHEEL) as ctx: - assert_true(isfile(mod_path)) - os.unlink(mod_path) - # Set output name in context manager, so write on output - ctx.out_wheel = pjoin(tmpdir, 'mungled.whl') - with InWheel('mungled.whl'): - assert_false(isfile(mod_path)) - - -def _filter_key(items, key): - return [(k, v) for k, v in items if k != key] - - -def get_info(wheelfile): - # Work round wheel API changes - try: - return wheelfile.parsed_wheel_info - except AttributeError: - pass - # Wheel 0.32.0 - from wheel.pkginfo import read_pkg_info_bytes - info_name = _get_wheelinfo_name(wheelfile) - return read_pkg_info_bytes(wheelfile.read(info_name)) - - -def assert_winfo_similar(whl_fname, exp_items, drop_version=True): - wf = WheelFile(whl_fname) - wheel_parts = wf.parsed_filename.groupdict() - # Info can contain duplicate keys (e.g. Tag) - w_info = sorted(get_info(wf).items()) - if drop_version: - w_info = _filter_key(w_info, 'Wheel-Version') - exp_items = _filter_key(exp_items, 'Wheel-Version') - assert_equal(len(exp_items), len(w_info)) - # Extract some information from actual values - wheel_parts['pip_version'] = dict(w_info)['Generator'].split()[1] - for (key1, value1), (key2, value2) in zip(exp_items, w_info): - assert_equal(key1, key2) - value1 = value1.format(**wheel_parts) - assert_equal(value1, value2) - - -def test_add_platforms(): - # Check adding platform to wheel name and tag section - exp_items = [('Generator', 'bdist_wheel {pip_version}'), - ('Root-Is-Purelib', 'false'), - ('Tag', '{pyver}-{abi}-macosx_10_6_intel'), - ('Wheel-Version', '1.0')] - assert_winfo_similar(PLAT_WHEEL, exp_items, drop_version=False) - with InTemporaryDirectory() as tmpdir: - # First wheel needs proper wheel filename for later unpack test - out_fname = basename(PURE_WHEEL) - plats = ('macosx_10_9_intel', 'macosx_10_9_x86_64') - # Can't add platforms to a pure wheel - assert_raises(WheelToolsError, - add_platforms, PURE_WHEEL, plats, tmpdir) - assert_false(exists(out_fname)) - out_fname = (splitext(basename(PLAT_WHEEL))[0] + - '.macosx_10_9_intel.macosx_10_9_x86_64.whl') - assert_equal(realpath(add_platforms(PLAT_WHEEL, plats, tmpdir)), - realpath(out_fname)) - assert_true(isfile(out_fname)) - # Expected output minus wheel-version (that might change) - extra_exp = [('Generator', 'bdist_wheel {pip_version}'), - ('Root-Is-Purelib', 'false'), - ('Tag', '{pyver}-{abi}-macosx_10_6_intel'), - ('Tag', '{pyver}-{abi}-macosx_10_9_intel'), - ('Tag', '{pyver}-{abi}-macosx_10_9_x86_64')] - assert_winfo_similar(out_fname, extra_exp) - # If wheel exists (as it does) then raise error - assert_raises(WheelToolsError, - add_platforms, PLAT_WHEEL, plats, tmpdir) - # Unless clobber is set, no error - add_platforms(PLAT_WHEEL, plats, tmpdir, clobber=True) - # Assemble platform tags in two waves to check tags are not being - # multiplied - out_1 = splitext(basename(PLAT_WHEEL))[0] + '.macosx_10_9_intel.whl' - assert_equal(realpath(add_platforms(PLAT_WHEEL, plats[0:1], tmpdir)), - realpath(out_1)) - assert_winfo_similar(out_1, extra_exp[:-1]) - out_2 = splitext(out_1)[0] + '.macosx_10_9_x86_64.whl' - assert_equal(realpath(add_platforms(out_1, plats[1:], tmpdir, True)), - realpath(out_2)) - assert_winfo_similar(out_2, extra_exp) - # Default is to write into directory of wheel - os.mkdir('wheels') - shutil.copy2(PLAT_WHEEL, 'wheels') - local_plat = pjoin('wheels', basename(PLAT_WHEEL)) - local_out = pjoin('wheels', out_fname) - add_platforms(local_plat, plats) - assert_true(exists(local_out)) - assert_raises(WheelToolsError, add_platforms, local_plat, plats) - add_platforms(local_plat, plats, clobber=True) - # If platforms already present, don't write more - res = sorted(os.listdir('wheels')) - assert_equal(add_platforms(local_out, plats, clobber=True), None) - assert_equal(sorted(os.listdir('wheels')), res) - assert_winfo_similar(out_fname, extra_exp) - # But WHEEL tags if missing, even if file name is OK - shutil.copy2(local_plat, local_out) - add_platforms(local_out, plats, clobber=True) - assert_equal(sorted(os.listdir('wheels')), res) - assert_winfo_similar(out_fname, extra_exp) diff --git a/delocate/tools.py b/delocate/tools.py index 91df7a38..eb5192cb 100644 --- a/delocate/tools.py +++ b/delocate/tools.py @@ -1,132 +1,25 @@ """ Tools for getting and setting install names """ -from subprocess import Popen, PIPE - -import os -from os.path import join as pjoin, relpath, isdir, exists -import zipfile +from os.path import exists import re -import stat -import time + +from wheeltools.tools import ( + back_tick, + ensure_writable, + find_package_dirs, + dir2zip, + zip2dir, + cmp_contents, + open_readable, + open_rw, + chmod_perms, +) class InstallNameError(Exception): pass -def back_tick(cmd, ret_err=False, as_str=True, raise_err=None): - """ Run command `cmd`, return stdout, or stdout, stderr if `ret_err` - - Roughly equivalent to ``check_output`` in Python 2.7 - - Parameters - ---------- - cmd : sequence - command to execute - ret_err : bool, optional - If True, return stderr in addition to stdout. If False, just return - stdout - as_str : bool, optional - Whether to decode outputs to unicode string on exit. - raise_err : None or bool, optional - If True, raise RuntimeError for non-zero return code. If None, set to - True when `ret_err` is False, False if `ret_err` is True - - Returns - ------- - out : str or tuple - If `ret_err` is False, return stripped string containing stdout from - `cmd`. If `ret_err` is True, return tuple of (stdout, stderr) where - ``stdout`` is the stripped stdout, and ``stderr`` is the stripped - stderr. - - Raises - ------ - Raises RuntimeError if command returns non-zero exit code and `raise_err` - is True - """ - if raise_err is None: - raise_err = False if ret_err else True - cmd_is_seq = isinstance(cmd, (list, tuple)) - proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=not cmd_is_seq) - out, err = proc.communicate() - retcode = proc.returncode - cmd_str = ' '.join(cmd) if cmd_is_seq else cmd - if retcode is None: - proc.terminate() - raise RuntimeError(cmd_str + ' process did not terminate') - if raise_err and retcode != 0: - raise RuntimeError('{0} returned code {1} with error {2}'.format( - cmd_str, retcode, err.decode('latin-1'))) - out = out.strip() - if as_str: - out = out.decode('latin-1') - if not ret_err: - return out - err = err.strip() - if as_str: - err = err.decode('latin-1') - return out, err - - -def unique_by_index(sequence): - """ unique elements in `sequence` in the order in which they occur - - Parameters - ---------- - sequence : iterable - - Returns - ------- - uniques : list - unique elements of sequence, ordered by the order in which the element - occurs in `sequence` - """ - uniques = [] - for element in sequence: - if element not in uniques: - uniques.append(element) - return uniques - - -def chmod_perms(fname): - # Permissions relevant to chmod - return stat.S_IMODE(os.stat(fname).st_mode) - - -def ensure_permissions(mode_flags=stat.S_IWUSR): - """decorator to ensure a filename has given permissions. - - If changed, original permissions are restored after the decorated - modification. - """ - - def decorator(f): - def modify(filename, *args, **kwargs): - m = chmod_perms(filename) if exists(filename) else mode_flags - if not m & mode_flags: - os.chmod(filename, m | mode_flags) - try: - return f(filename, *args, **kwargs) - finally: - # restore original permissions - if not m & mode_flags: - os.chmod(filename, m) - return modify - - return decorator - - -# Open filename, checking for read permission -open_readable = ensure_permissions(stat.S_IRUSR)(open) - -# Open filename, checking for read / write permission -open_rw = ensure_permissions(stat.S_IRUSR | stat.S_IWUSR)(open) - -# For backward compatibility -ensure_writable = ensure_permissions() - - IN_RE = re.compile(r"(.*) \(compatibility version (\d+\.\d+\.\d+), " r"current version (\d+\.\d+\.\d+)\)") @@ -339,101 +232,6 @@ def add_rpath(filename, newpath): back_tick(['install_name_tool', '-add_rpath', newpath, filename]) -def zip2dir(zip_fname, out_dir): - """ Extract `zip_fname` into output directory `out_dir` - - Parameters - ---------- - zip_fname : str - Filename of zip archive to write - out_dir : str - Directory path containing files to go in the zip archive - """ - # Use unzip command rather than zipfile module to preserve permissions - # http://bugs.python.org/issue15795 - back_tick(['unzip', '-o', '-d', out_dir, zip_fname]) - - -def dir2zip(in_dir, zip_fname): - """ Make a zip file `zip_fname` with contents of directory `in_dir` - - The recorded filenames are relative to `in_dir`, so doing a standard zip - unpack of the resulting `zip_fname` in an empty directory will result in - the original directory contents. - - Parameters - ---------- - in_dir : str - Directory path containing files to go in the zip archive - zip_fname : str - Filename of zip archive to write - """ - z = zipfile.ZipFile(zip_fname, 'w', - compression=zipfile.ZIP_DEFLATED) - for root, dirs, files in os.walk(in_dir): - for file in files: - in_fname = pjoin(root, file) - in_stat = os.stat(in_fname) - # Preserve file permissions, but allow copy - info = zipfile.ZipInfo(in_fname) - info.filename = relpath(in_fname, in_dir) - # Set time from modification time - info.date_time = time.localtime(in_stat.st_mtime) - # See https://stackoverflow.com/questions/434641/how-do-i-set-permissions-attributes-on-a-file-in-a-zip-file-using-pythons-zip/48435482#48435482 - # Also set regular file permissions - perms = stat.S_IMODE(in_stat.st_mode) | stat.S_IFREG - info.external_attr = perms << 16 - with open_readable(in_fname, 'rb') as fobj: - contents = fobj.read() - z.writestr(info, contents, zipfile.ZIP_DEFLATED) - z.close() - - -def find_package_dirs(root_path): - """ Find python package directories in directory `root_path` - - Parameters - ---------- - root_path : str - Directory to search for package subdirectories - - Returns - ------- - package_sdirs : set - Set of strings where each is a subdirectory of `root_path`, containing - an ``__init__.py`` file. Paths prefixed by `root_path` - """ - package_sdirs = set() - for entry in os.listdir(root_path): - fname = entry if root_path == '.' else pjoin(root_path, entry) - if isdir(fname) and exists(pjoin(fname, '__init__.py')): - package_sdirs.add(fname) - return package_sdirs - - -def cmp_contents(filename1, filename2): - """ Returns True if contents of the files are the same - - Parameters - ---------- - filename1 : str - filename of first file to compare - filename2 : str - filename of second file to compare - - Returns - ------- - tf : bool - True if binary contents of `filename1` is same as binary contents of - `filename2`, False otherwise. - """ - with open_readable(filename1, 'rb') as fobj: - contents1 = fobj.read() - with open_readable(filename2, 'rb') as fobj: - contents2 = fobj.read() - return contents1 == contents2 - - def get_archs(libname): """ Return architecture types from library `libname` diff --git a/delocate/wheeltools.py b/delocate/wheeltools.py deleted file mode 100644 index ec252fcb..00000000 --- a/delocate/wheeltools.py +++ /dev/null @@ -1,222 +0,0 @@ -""" General tools for working with wheels - -Tools that aren't specific to delocation -""" - -import sys -import os -from os.path import (join as pjoin, abspath, relpath, exists, sep as psep, - splitext, basename, dirname) -import glob -import hashlib -import csv -from itertools import product - -from wheel.util import urlsafe_b64encode, native -from wheel.pkginfo import read_pkg_info, write_pkg_info -try: - from wheel.install import WheelFile -except ImportError: # As of Wheel 0.32.0 - from wheel.wheelfile import WheelFile -from .tmpdirs import InTemporaryDirectory -from .tools import unique_by_index, zip2dir, dir2zip, open_rw - -class WheelToolsError(Exception): - pass - - -def _open_for_csv(name, mode): - """ Deal with Python 2/3 open API differences """ - if sys.version_info[0] < 3: - return open_rw(name, mode + 'b') - return open_rw(name, mode, newline='', encoding='utf-8') - - -def rewrite_record(bdist_dir): - """ Rewrite RECORD file with hashes for all files in `wheel_sdir` - - Copied from :method:`wheel.bdist_wheel.bdist_wheel.write_record` - - Will also unsign wheel - - Parameters - ---------- - bdist_dir : str - Path of unpacked wheel file - """ - info_dirs = glob.glob(pjoin(bdist_dir, '*.dist-info')) - if len(info_dirs) != 1: - raise WheelToolsError("Should be exactly one `*.dist_info` directory") - record_path = pjoin(info_dirs[0], 'RECORD') - record_relpath = relpath(record_path, bdist_dir) - # Unsign wheel - because we're invalidating the record hash - sig_path = pjoin(info_dirs[0], 'RECORD.jws') - if exists(sig_path): - os.unlink(sig_path) - - def walk(): - for dir, dirs, files in os.walk(bdist_dir): - for f in files: - yield pjoin(dir, f) - - def skip(path): - """Wheel hashes every possible file.""" - return (path == record_relpath) - - with _open_for_csv(record_path, 'w+') as record_file: - writer = csv.writer(record_file) - for path in walk(): - relative_path = relpath(path, bdist_dir) - if skip(relative_path): - hash = '' - size = '' - else: - with open(path, 'rb') as f: - data = f.read() - digest = hashlib.sha256(data).digest() - hash = 'sha256=' + native(urlsafe_b64encode(digest)) - size = len(data) - path_for_record = relpath( - path, bdist_dir).replace(psep, '/') - writer.writerow((path_for_record, hash, size)) - - -class InWheel(InTemporaryDirectory): - """ Context manager for doing things inside wheels - - On entering, you'll find yourself in the root tree of the wheel. If you've - asked for an output wheel, then on exit we'll rewrite the wheel record and - pack stuff up for you. - """ - def __init__(self, in_wheel, out_wheel=None, ret_self=False): - """ Initialize in-wheel context manager - - Parameters - ---------- - in_wheel : str - filename of wheel to unpack and work inside - out_wheel : None or str: - filename of wheel to write after exiting. If None, don't write and - discard - ret_self : bool, optional - If True, return ``self`` from ``__enter__``, otherwise return the - directory path. - """ - self.in_wheel = abspath(in_wheel) - self.out_wheel = None if out_wheel is None else abspath(out_wheel) - super(InWheel, self).__init__() - - def __enter__(self): - zip2dir(self.in_wheel, self.name) - return super(InWheel, self).__enter__() - - def __exit__(self, exc, value, tb): - if not self.out_wheel is None: - rewrite_record(self.name) - dir2zip(self.name, self.out_wheel) - return super(InWheel, self).__exit__(exc, value, tb) - - -class InWheelCtx(InWheel): - """ Context manager for doing things inside wheels - - On entering, you'll find yourself in the root tree of the wheel. If you've - asked for an output wheel, then on exit we'll rewrite the wheel record and - pack stuff up for you. - - The context manager returns itself from the __enter__ method, so you can - set things like ``out_wheel``. This is useful when processing in the wheel - will dicate what the output wheel name is, or whether you want to save at - all. - - The current path of the wheel contents is set in the attribute - ``wheel_path``. - """ - def __init__(self, in_wheel, out_wheel=None): - """ Init in-wheel context manager returning self from enter - - Parameters - ---------- - in_wheel : str - filename of wheel to unpack and work inside - out_wheel : None or str: - filename of wheel to write after exiting. If None, don't write and - discard - """ - super(InWheelCtx, self).__init__(in_wheel, out_wheel) - self.wheel_path = None - - def __enter__(self): - self.wheel_path = super(InWheelCtx, self).__enter__() - return self - - -def _get_wheelinfo_name(wheelfile): - # Work round wheel API compatibility - try: - return wheelfile.wheelinfo_name - except AttributeError: - return wheelfile.dist_info_path + '/WHEEL' - - -def add_platforms(in_wheel, platforms, out_path=None, clobber=False): - """ Add platform tags `platforms` to `in_wheel` filename and WHEEL tags - - Add any platform tags in `platforms` that are missing from `in_wheel` - filename. - - Add any platform tags in `platforms` that are missing from `in_wheel` - ``WHEEL`` file. - - Parameters - ---------- - in_wheel : str - Filename of wheel to which to add platform tags - platforms : iterable - platform tags to add to wheel filename and WHEEL tags - e.g. - ``('macosx_10_9_intel', 'macosx_10_9_x86_64') - out_path : None or str, optional - Directory to which to write new wheel. Default is directory containing - `in_wheel` - clobber : bool, optional - If True, overwrite existing output filename, otherwise raise error - - Returns - ------- - out_wheel : None or str - Absolute path of wheel file written, or None if no wheel file written. - """ - in_wheel = abspath(in_wheel) - out_path = dirname(in_wheel) if out_path is None else abspath(out_path) - wf = WheelFile(in_wheel) - info_fname = _get_wheelinfo_name(wf) - # Check what tags we have - in_fname_tags = wf.parsed_filename.groupdict()['plat'].split('.') - extra_fname_tags = [tag for tag in platforms if tag not in in_fname_tags] - in_wheel_base, ext = splitext(basename(in_wheel)) - out_wheel_base = '.'.join([in_wheel_base] + list(extra_fname_tags)) - out_wheel = pjoin(out_path, out_wheel_base + ext) - if exists(out_wheel) and not clobber: - raise WheelToolsError('Not overwriting {0}; set clobber=True ' - 'to overwrite'.format(out_wheel)) - with InWheelCtx(in_wheel) as ctx: - info = read_pkg_info(info_fname) - if info['Root-Is-Purelib'] == 'true': - raise WheelToolsError('Cannot add platforms to pure wheel') - in_info_tags = [tag for name, tag in info.items() if name == 'Tag'] - # Python version, C-API version combinations - pyc_apis = ['-'.join(tag.split('-')[:2]) for tag in in_info_tags] - # unique Python version, C-API version combinations - pyc_apis = unique_by_index(pyc_apis) - # Add new platform tags for each Python version, C-API combination - required_tags = ['-'.join(tup) for tup in product(pyc_apis, platforms)] - needs_write = False - for req_tag in required_tags: - if req_tag in in_info_tags: continue - needs_write = True - info.add_header('Tag', req_tag) - if needs_write: - write_pkg_info(info_fname, info) - # Tell context manager to write wheel on exit by setting filename - ctx.out_wheel = out_wheel - return ctx.out_wheel diff --git a/setup.py b/setup.py index 3a6c3bd0..d043dea6 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "machomachomangler; sys_platform == 'win32'", "bindepend; sys_platform == 'win32'", "wheel", + "wheeltools >= 0.1.0b1", ], package_data = {'delocate.tests': [pjoin('data', '*.dylib'),