From f7b9dd79da9582a319160e4ed757dff92a078895 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Mon, 15 May 2017 12:59:39 -0400 Subject: [PATCH 01/25] Improve wheel support in pex. Wheel handling in pex files is broken. The wheel standard says that wheel files are not designed to be importable archives, but pex treats them as if they are. This causes many standard compliant wheels, including things like tensorflow and opencv, to fail to import in pexes (and thus in pants). This change modifies wheel handling, so that when a wheel is added to a pex, it's installed in an importable form. --- pex/pex_builder.py | 23 ++++++++++++++ .../pyparsing-2.1.10-py2.py3-none-any.whl | Bin 0 -> 56975 bytes tests/test_pex_builder.py | 28 ++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 tests/example_packages/pyparsing-2.1.10-py2.py3-none-any.whl diff --git a/pex/pex_builder.py b/pex/pex_builder.py index c328a993e..9eb04f321 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -7,6 +7,7 @@ import os from pkg_resources import DefaultProvider, ZipProvider, get_provider +from wheel.install import WheelFile from .common import Chroot, chmod_plus_x, open_zip, safe_mkdir, safe_mkdtemp from .compatibility import to_bytes @@ -253,7 +254,29 @@ def _add_dist_dir(self, path, dist_name): self._copy_or_link(filename, target) return CacheHelper.dir_hash(path) + def _get_installer_paths(self, dist_name): + """Set up an overrides dict for WheelFile.install that installs the contents + of a wheel into its own base in the pex dependencies cache. + """ + base = os.path.join(self.path(), self._pex_info.internal_cache, dist_name) + return { + 'purelib': os.path.join(base), + 'headers': os.path.join(base, 'headers'), + 'scripts': os.path.join(base, 'bin'), + 'platlib': base, + 'data': base + } + def _add_dist_zip(self, path, dist_name): + # We need to distinguish between wheels and other zips. Most of the time, + # when we have a zip, it contains its contents in an importable form. + # But wheels don't have to be importable, so we need to force them + # into an importable shape. We can do that by installing it into its own + # wheel dir. + if dist_name.endswith("whl"): + wf = WheelFile(path) + wf.install(overrides=self._get_installer_paths(dist_name), force=True) + with open_zip(path) as zf: for name in zf.namelist(): if name.endswith('/'): diff --git a/tests/example_packages/pyparsing-2.1.10-py2.py3-none-any.whl b/tests/example_packages/pyparsing-2.1.10-py2.py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..f4d6fc513600e21e57967b98a7e2d5ef3e1f7c51 GIT binary patch literal 56975 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 Date: Sun, 28 May 2017 13:51:35 -0400 Subject: [PATCH 02/25] Rewrite the pex wheel fix, using the wheel installer. --- pex/finders.py | 4 +--- pex/pex_builder.py | 30 ++++++++++++++++++++++++------ tests/test_pex_builder.py | 3 --- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/pex/finders.py b/pex/finders.py index b20dd7627..591c91f13 100644 --- a/pex/finders.py +++ b/pex/finders.py @@ -250,9 +250,7 @@ def safer_name(name): def get_script_from_whl(name, dist): - # This is true as of at least wheel==0.24. Might need to take into account the - # metadata version bundled with the wheel. - wheel_scripts_dir = '%s-%s.data/scripts' % (safer_name(dist.key), dist.version) + wheel_scripts_dir = 'scripts' if dist.resource_isdir(wheel_scripts_dir) and name in dist.resource_listdir(wheel_scripts_dir): script_path = os.path.join(wheel_scripts_dir, name) return ( diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 9eb04f321..d4e74b2e4 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -5,6 +5,7 @@ import logging import os +import tempfile from pkg_resources import DefaultProvider, ZipProvider, get_provider from wheel.install import WheelFile @@ -254,15 +255,14 @@ def _add_dist_dir(self, path, dist_name): self._copy_or_link(filename, target) return CacheHelper.dir_hash(path) - def _get_installer_paths(self, dist_name): + def _get_installer_paths(self, dist_name, base): """Set up an overrides dict for WheelFile.install that installs the contents of a wheel into its own base in the pex dependencies cache. """ - base = os.path.join(self.path(), self._pex_info.internal_cache, dist_name) return { - 'purelib': os.path.join(base), + 'purelib': base, 'headers': os.path.join(base, 'headers'), - 'scripts': os.path.join(base, 'bin'), + 'scripts': os.path.join(base, 'scripts'), 'platlib': base, 'data': base } @@ -274,8 +274,26 @@ def _add_dist_zip(self, path, dist_name): # into an importable shape. We can do that by installing it into its own # wheel dir. if dist_name.endswith("whl"): - wf = WheelFile(path) - wf.install(overrides=self._get_installer_paths(dist_name), force=True) + try: + tmp = tempfile.mkdtemp() + whltmp = os.path.join(tmp, dist_name) + os.mkdir(whltmp) + wf = WheelFile(path) + wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) + def add_wheel_file(self, dir, files): + pruned_dir = os.path.relpath(dir, tmp) + for file in files: + fullpath = os.path.join(dir, file) + if os.path.isdir(fullpath): + continue + target = os.path.join(self._pex_info.internal_cache, pruned_dir, file) + with open(fullpath, "r") as i: + self._chroot.write(i.read(), target) + os.path.walk(whltmp, add_wheel_file, self) + finally: + #shutil.rmtree(tmp) + print("crap") + return CacheHelper.dir_hash(whltmp) with open_zip(path) as zf: for name in zf.namelist(): diff --git a/tests/test_pex_builder.py b/tests/test_pex_builder.py index 10fd9d58f..4dc7c30cd 100644 --- a/tests/test_pex_builder.py +++ b/tests/test_pex_builder.py @@ -3,7 +3,6 @@ import os import stat -import subprocess import zipfile from contextlib import closing @@ -75,9 +74,7 @@ def test_pex_builder_wheeldep(): with nested(temporary_dir(), make_bdist('p1', zipped=True)) as (td, p1): pyparsing_path = "./tests/example_packages/pyparsing-2.1.10-py2.py3-none-any.whl" dist = DistributionHelper.distribution_from_path(pyparsing_path) - print("Adding dist %s @ %s" % (dist, pyparsing_path)) write_pex(td, wheeldeps_exe_main, dists=[p1, dist]) - subprocess.check_call(["cp", "-r", td, "/tmp/foo"]) success_txt = os.path.join(td, 'success.txt') PEX(td).run(args=[success_txt]) assert os.path.exists(success_txt) From bac461a68d264f26f1adef30655a1004edb03741 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Mon, 29 May 2017 20:10:54 -0400 Subject: [PATCH 03/25] Clean up. --- pex/pex_builder.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index d4e74b2e4..be24d2b56 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -274,25 +274,23 @@ def _add_dist_zip(self, path, dist_name): # into an importable shape. We can do that by installing it into its own # wheel dir. if dist_name.endswith("whl"): - try: - tmp = tempfile.mkdtemp() - whltmp = os.path.join(tmp, dist_name) - os.mkdir(whltmp) - wf = WheelFile(path) - wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) - def add_wheel_file(self, dir, files): - pruned_dir = os.path.relpath(dir, tmp) - for file in files: - fullpath = os.path.join(dir, file) - if os.path.isdir(fullpath): - continue - target = os.path.join(self._pex_info.internal_cache, pruned_dir, file) - with open(fullpath, "r") as i: - self._chroot.write(i.read(), target) - os.path.walk(whltmp, add_wheel_file, self) - finally: - #shutil.rmtree(tmp) - print("crap") + tmp = tempfile.mkdtemp() + whltmp = os.path.join(tmp, dist_name) + os.mkdir(whltmp) + wf = WheelFile(path) + wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) + + def add_wheel_file(self, wdir, files): + pruned_dir = os.path.relpath(wdir, tmp) + for f in files: + fullpath = os.path.join(wdir, f) + if os.path.isdir(fullpath): + continue + target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) + with open(fullpath, "r") as i: + self._chroot.write(i.read(), target) + + os.path.walk(whltmp, add_wheel_file, self) return CacheHelper.dir_hash(whltmp) with open_zip(path) as zf: From cb5da4119b15ab7b13141efdebb1ea092e9aae3f Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Tue, 30 May 2017 13:32:38 -0400 Subject: [PATCH 04/25] Fix some path confusion. (Code disagreed about whether scripts were stored in /bin or /scripts.) --- pex/finders.py | 2 +- pex/pex_builder.py | 38 ++++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pex/finders.py b/pex/finders.py index 591c91f13..33f30de6e 100644 --- a/pex/finders.py +++ b/pex/finders.py @@ -250,7 +250,7 @@ def safer_name(name): def get_script_from_whl(name, dist): - wheel_scripts_dir = 'scripts' + wheel_scripts_dir = 'bin' if dist.resource_isdir(wheel_scripts_dir) and name in dist.resource_listdir(wheel_scripts_dir): script_path = os.path.join(wheel_scripts_dir, name) return ( diff --git a/pex/pex_builder.py b/pex/pex_builder.py index be24d2b56..b93ba4dcd 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -5,6 +5,7 @@ import logging import os +import shutil import tempfile from pkg_resources import DefaultProvider, ZipProvider, get_provider @@ -262,7 +263,7 @@ def _get_installer_paths(self, dist_name, base): return { 'purelib': base, 'headers': os.path.join(base, 'headers'), - 'scripts': os.path.join(base, 'scripts'), + 'scripts': os.path.join(base, 'bin'), 'platlib': base, 'data': base } @@ -274,23 +275,24 @@ def _add_dist_zip(self, path, dist_name): # into an importable shape. We can do that by installing it into its own # wheel dir. if dist_name.endswith("whl"): - tmp = tempfile.mkdtemp() - whltmp = os.path.join(tmp, dist_name) - os.mkdir(whltmp) - wf = WheelFile(path) - wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) - - def add_wheel_file(self, wdir, files): - pruned_dir = os.path.relpath(wdir, tmp) - for f in files: - fullpath = os.path.join(wdir, f) - if os.path.isdir(fullpath): - continue - target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) - with open(fullpath, "r") as i: - self._chroot.write(i.read(), target) - - os.path.walk(whltmp, add_wheel_file, self) + try: + tmp = tempfile.mkdtemp() + whltmp = os.path.join(tmp, dist_name) + os.mkdir(whltmp) + wf = WheelFile(path) + wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) + def add_wheel_file(self, wdir, files): + pruned_dir = os.path.relpath(wdir, tmp) + for f in files: + fullpath = os.path.join(wdir, f) + if os.path.isdir(fullpath): + continue + target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) + with open(fullpath, "r") as i: + self._chroot.write(i.read(), target) + os.path.walk(whltmp, add_wheel_file, self) + finally: + shutil.rmtree(tmp) return CacheHelper.dir_hash(whltmp) with open_zip(path) as zf: From 5a9575c63ebae74fd0148d646a38698827af9df9 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Tue, 30 May 2017 14:53:24 -0400 Subject: [PATCH 05/25] Switch to use os.walk instead of os.path.walk to increase compatibility with python 3. --- pex/pex_builder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index b93ba4dcd..6cec5670f 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -281,16 +281,16 @@ def _add_dist_zip(self, path, dist_name): os.mkdir(whltmp) wf = WheelFile(path) wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) - def add_wheel_file(self, wdir, files): - pruned_dir = os.path.relpath(wdir, tmp) + for (root, _, files) in os.walk(whltmp): + #def add_wheel_file(self, wdir, files): + pruned_dir = os.path.relpath(root, tmp) for f in files: - fullpath = os.path.join(wdir, f) + fullpath = os.path.join(root, f) if os.path.isdir(fullpath): continue target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) with open(fullpath, "r") as i: self._chroot.write(i.read(), target) - os.path.walk(whltmp, add_wheel_file, self) finally: shutil.rmtree(tmp) return CacheHelper.dir_hash(whltmp) From 027635c023c6ef4b5dc1925e42964ef6d036f6c0 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Tue, 30 May 2017 15:04:43 -0400 Subject: [PATCH 06/25] Tests pass locally; trying to get them to pass in CI. --- pex/pex_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 6cec5670f..a1ba18a66 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -289,8 +289,7 @@ def _add_dist_zip(self, path, dist_name): if os.path.isdir(fullpath): continue target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) - with open(fullpath, "r") as i: - self._chroot.write(i.read(), target) + self._chroot.copy(fullpath, target) finally: shutil.rmtree(tmp) return CacheHelper.dir_hash(whltmp) From bb4088f8c906ed596e9173342d421d0fe990f1c7 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Wed, 31 May 2017 08:59:28 -0400 Subject: [PATCH 07/25] Update the wheel script finder so that it works during both pex generation and execution --- pex/finders.py | 21 +++++++++++++++------ pex/pex_builder.py | 1 - 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pex/finders.py b/pex/finders.py index 33f30de6e..481a5f0b6 100644 --- a/pex/finders.py +++ b/pex/finders.py @@ -250,12 +250,21 @@ def safer_name(name): def get_script_from_whl(name, dist): - wheel_scripts_dir = 'bin' - if dist.resource_isdir(wheel_scripts_dir) and name in dist.resource_listdir(wheel_scripts_dir): - script_path = os.path.join(wheel_scripts_dir, name) - return ( - os.path.join(dist.egg_info, script_path), - dist.get_resource_string('', script_path).replace(b'\r\n', b'\n').replace(b'\r', b'\n')) + # This can get called in different contexts; in some, it looks for files in the + # wheel archives being used to produce a pex; in others, it looks for files in the + # install wheel directory included in the pex. So we need to look at both locations. + datadir_name = "%s-%s.data" % (dist.project_name, dist.version) + wheel_scripts_dirs = ['bin', 'scripts', + os.path.join(datadir_name, "bin"), + os.path.join(datadir_name, "scripts")] + for wheel_scripts_dir in wheel_scripts_dirs: + if (dist.resource_isdir(wheel_scripts_dir) and + name in dist.resource_listdir(wheel_scripts_dir)): + # We always install wheel scripts into bin + script_path = os.path.join(wheel_scripts_dir, name) + return ( + os.path.join(dist.egg_info, script_path), + dist.get_resource_string('', script_path).replace(b'\r\n', b'\n').replace(b'\r', b'\n')) return None, None diff --git a/pex/pex_builder.py b/pex/pex_builder.py index a1ba18a66..f85f82a7c 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -282,7 +282,6 @@ def _add_dist_zip(self, path, dist_name): wf = WheelFile(path) wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) for (root, _, files) in os.walk(whltmp): - #def add_wheel_file(self, wdir, files): pruned_dir = os.path.relpath(root, tmp) for f in files: fullpath = os.path.join(root, f) From 75014cfd7b4448e2dd71eaaeff998aeac17beb6a Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 1 Jun 2017 09:34:02 -0400 Subject: [PATCH 08/25] Move "wheelfile" import so it only gets loaded during pex generation. --- pex/pex_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index f85f82a7c..a2b4e24b9 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -9,7 +9,6 @@ import tempfile from pkg_resources import DefaultProvider, ZipProvider, get_provider -from wheel.install import WheelFile from .common import Chroot, chmod_plus_x, open_zip, safe_mkdir, safe_mkdtemp from .compatibility import to_bytes @@ -274,6 +273,7 @@ def _add_dist_zip(self, path, dist_name): # But wheels don't have to be importable, so we need to force them # into an importable shape. We can do that by installing it into its own # wheel dir. + from wheel.install import WheelFile if dist_name.endswith("whl"): try: tmp = tempfile.mkdtemp() From 95c01e1afe29762e7e4fc142738b2f8f7b1e0765 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 1 Jun 2017 09:58:31 -0400 Subject: [PATCH 09/25] Fix isort issue. --- pex/http.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pex/http.py b/pex/http.py index a1e68419f..079e7e2e9 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,6 +7,7 @@ import shutil import uuid from abc import abstractmethod + from email import message_from_string from .common import safe_mkdtemp, safe_open From 86b420c035b8458801acf7c4ff10997be1cb7037 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 1 Jun 2017 10:16:39 -0400 Subject: [PATCH 10/25] Add an explicit dependency on "wheel" to setup.py's test deps. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e21d8975f..de32e00ee 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ 'twitter.common.lang>=0.3.1,<0.4.0', 'twitter.common.testing>=0.3.1,<0.4.0', 'twitter.common.dirutil>=0.3.1,<0.4.0', + 'wheel==0.29.0', 'pytest', ], entry_points = { From 0e45922c087977b3f6c7f04c26a41a2535ef2f3c Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 1 Jun 2017 11:10:09 -0400 Subject: [PATCH 11/25] Update requirements to pull wheel in for py26. --- pex/pex_builder.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index a2b4e24b9..3747664dd 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -273,8 +273,8 @@ def _add_dist_zip(self, path, dist_name): # But wheels don't have to be importable, so we need to force them # into an importable shape. We can do that by installing it into its own # wheel dir. - from wheel.install import WheelFile if dist_name.endswith("whl"): + from wheel.install import WheelFile try: tmp = tempfile.mkdtemp() whltmp = os.path.join(tmp, dist_name) diff --git a/setup.py b/setup.py index de32e00ee..ec268d77e 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ ], install_requires = [ SETUPTOOLS_REQUIREMENT, + WHEEL_REQUIREMENT, ], tests_require = [ 'mock', @@ -61,7 +62,6 @@ 'twitter.common.lang>=0.3.1,<0.4.0', 'twitter.common.testing>=0.3.1,<0.4.0', 'twitter.common.dirutil>=0.3.1,<0.4.0', - 'wheel==0.29.0', 'pytest', ], entry_points = { From e63644af9d8e76a28775371a5366c30ff5ad09fa Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 1 Jun 2017 11:26:06 -0400 Subject: [PATCH 12/25] Run isort to fix import sort issues. --- pex/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pex/http.py b/pex/http.py index 079e7e2e9..a1e68419f 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,7 +7,6 @@ import shutil import uuid from abc import abstractmethod - from email import message_from_string from .common import safe_mkdtemp, safe_open From 1cd5e1ba3b30cd03bf79bc5482001a0153a481a1 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 10:00:18 -0400 Subject: [PATCH 13/25] Add a bypass of the wheel install code for Python 2.6. --- pex/http.py | 1 + pex/interpreter.py | 4 ++++ pex/pex_builder.py | 9 +++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pex/http.py b/pex/http.py index a1e68419f..079e7e2e9 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,6 +7,7 @@ import shutil import uuid from abc import abstractmethod + from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/pex/interpreter.py b/pex/interpreter.py index d7fd3f022..ff9333f4f 100644 --- a/pex/interpreter.py +++ b/pex/interpreter.py @@ -400,6 +400,10 @@ def get_location(self, req): if req.key == dist_name and dist_version in req: return location + def supports_wheel_install(self): + """Wheel installs are broken in python 2.6""" + return self.version >= (2, 7) + def __hash__(self): return hash((self._binary, self._identity)) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 3747664dd..890188621 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -1,11 +1,12 @@ # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from __future__ import absolute_import +from __future__ import absolute_import, print_function import logging import os import shutil +import sys import tempfile from pkg_resources import DefaultProvider, ZipProvider, get_provider @@ -273,7 +274,11 @@ def _add_dist_zip(self, path, dist_name): # But wheels don't have to be importable, so we need to force them # into an importable shape. We can do that by installing it into its own # wheel dir. - if dist_name.endswith("whl"): + if not self.interpreter.supports_wheel_install(): + print("*** Wheel dependencies may not work correctly with Python 2.6.", file=sys.stderr) + print("*** Your generated pex may experience errors", file=sys.stderr) + + if self.interpreter.supports_wheel_install() and dist_name.endswith("whl"): from wheel.install import WheelFile try: tmp = tempfile.mkdtemp() From 1637e5a86042b6c237fb30223cfaaa0e86740c08 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 10:00:18 -0400 Subject: [PATCH 14/25] Add a bypass of the wheel install code for Python 2.6. --- tests/test_pex.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pex.py b/tests/test_pex.py index 22f2ef4a4..601fcf792 100644 --- a/tests/test_pex.py +++ b/tests/test_pex.py @@ -185,7 +185,8 @@ def test_site_libs_excludes_prefix(): assert site_packages in site_libs assert sys.prefix not in site_libs - +@pytest.mark.skipif(sys.version_info <= (2,6), + reason="wheel script installation is broken on python 2.6") @pytest.mark.parametrize('zip_safe', (False, True)) @pytest.mark.parametrize('project_name', ('my_project', 'my-project')) @pytest.mark.parametrize('installer_impl', (EggInstaller, WheelInstaller)) From e66ade49c0c5eb1a23f4e825ee96cd3950d65096 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 10:53:08 -0400 Subject: [PATCH 15/25] Another try at fixing the build. --- pex/http.py | 1 - tests/test_pex.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pex/http.py b/pex/http.py index 079e7e2e9..a1e68419f 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,7 +7,6 @@ import shutil import uuid from abc import abstractmethod - from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/tests/test_pex.py b/tests/test_pex.py index 601fcf792..641ed0d61 100644 --- a/tests/test_pex.py +++ b/tests/test_pex.py @@ -185,7 +185,7 @@ def test_site_libs_excludes_prefix(): assert site_packages in site_libs assert sys.prefix not in site_libs -@pytest.mark.skipif(sys.version_info <= (2,6), +@pytest.mark.skipif('sys.version_info <= (2, 6)', reason="wheel script installation is broken on python 2.6") @pytest.mark.parametrize('zip_safe', (False, True)) @pytest.mark.parametrize('project_name', ('my_project', 'my-project')) From 2ed5338575fb7cc37433deae87ead5c362f03c1e Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 10:53:08 -0400 Subject: [PATCH 16/25] Another try at fixing the build. --- tests/test_pex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pex.py b/tests/test_pex.py index 641ed0d61..9cc7b46ad 100644 --- a/tests/test_pex.py +++ b/tests/test_pex.py @@ -185,6 +185,7 @@ def test_site_libs_excludes_prefix(): assert site_packages in site_libs assert sys.prefix not in site_libs + @pytest.mark.skipif('sys.version_info <= (2, 6)', reason="wheel script installation is broken on python 2.6") @pytest.mark.parametrize('zip_safe', (False, True)) From 02a9e61d0827f073be1594722441d3d79d355633 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 11:09:32 -0400 Subject: [PATCH 17/25] Try fixing the "skipif" flag. --- tests/test_pex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pex.py b/tests/test_pex.py index 9cc7b46ad..418b53873 100644 --- a/tests/test_pex.py +++ b/tests/test_pex.py @@ -186,7 +186,7 @@ def test_site_libs_excludes_prefix(): assert sys.prefix not in site_libs -@pytest.mark.skipif('sys.version_info <= (2, 6)', +@pytest.mark.skipif(sys.version_info < (2, 7), reason="wheel script installation is broken on python 2.6") @pytest.mark.parametrize('zip_safe', (False, True)) @pytest.mark.parametrize('project_name', ('my_project', 'my-project')) From 562524025de0801a2661083763fc976352533cc8 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 11:15:11 -0400 Subject: [PATCH 18/25] Add "skipif" to the texs_pex_builder_wheeldep for Python 2.6 --- tests/test_pex_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_pex_builder.py b/tests/test_pex_builder.py index 4dc7c30cd..666340e07 100644 --- a/tests/test_pex_builder.py +++ b/tests/test_pex_builder.py @@ -67,6 +67,8 @@ def test_pex_builder(): assert fp.read() == 'success' +@pytest.mark.skipif(sys.version_info < (2, 7), + reason="wheel script installation is broken on python 2.6") def test_pex_builder_wheeldep(): """Repeat the pex_builder test, but this time include an import of something from a wheel that doesn't come in importable form. From 570c754779d1ba7c3ffc011a6be4d32be11c56d1 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 11:32:46 -0400 Subject: [PATCH 19/25] Duh. --- pex/http.py | 1 + tests/test_pex_builder.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pex/http.py b/pex/http.py index a1e68419f..079e7e2e9 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,6 +7,7 @@ import shutil import uuid from abc import abstractmethod + from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/tests/test_pex_builder.py b/tests/test_pex_builder.py index 666340e07..66f022689 100644 --- a/tests/test_pex_builder.py +++ b/tests/test_pex_builder.py @@ -3,6 +3,7 @@ import os import stat +import sys import zipfile from contextlib import closing From bb3e0660855ae6f8ef0a3369ae42ebfef9c81728 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 12:04:34 -0400 Subject: [PATCH 20/25] Update isort. --- tests/test_compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 6b55f033b..c17c20302 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import contextlib +import marshal import os -import marshal import pytest from twitter.common.contextutil import temporary_dir From fcdf3fd179f097f70344366582577101bfe93f77 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Thu, 22 Jun 2017 12:47:57 -0400 Subject: [PATCH 21/25] Another try at the import issue. --- pex/http.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pex/http.py b/pex/http.py index 079e7e2e9..a1e68419f 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,7 +7,6 @@ import shutil import uuid from abc import abstractmethod - from email import message_from_string from .common import safe_mkdtemp, safe_open From 031463cbcab05f0fb8e3768d12012de47890101d Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Fri, 7 Jul 2017 09:20:52 -0400 Subject: [PATCH 22/25] Address some review comments. --- pex/finders.py | 6 +----- pex/http.py | 1 + pex/pex_builder.py | 37 ++++++++++++++++--------------------- tests/test_compiler.py | 2 +- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/pex/finders.py b/pex/finders.py index 481a5f0b6..620e14a0f 100644 --- a/pex/finders.py +++ b/pex/finders.py @@ -245,12 +245,8 @@ def get_script_from_egg(name, dist): return None, None -def safer_name(name): - return name.replace('-', '_') - - def get_script_from_whl(name, dist): - # This can get called in different contexts; in some, it looks for files in the + # This can get called in different contexts; in some, it looks for files in the # wheel archives being used to produce a pex; in others, it looks for files in the # install wheel directory included in the pex. So we need to look at both locations. datadir_name = "%s-%s.data" % (dist.project_name, dist.version) diff --git a/pex/http.py b/pex/http.py index a1e68419f..079e7e2e9 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,6 +7,7 @@ import shutil import uuid from abc import abstractmethod + from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 890188621..d6452ba1c 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -5,9 +5,7 @@ import logging import os -import shutil import sys -import tempfile from pkg_resources import DefaultProvider, ZipProvider, get_provider @@ -256,7 +254,7 @@ def _add_dist_dir(self, path, dist_name): self._copy_or_link(filename, target) return CacheHelper.dir_hash(path) - def _get_installer_paths(self, dist_name, base): + def _get_installer_paths(self, base): """Set up an overrides dict for WheelFile.install that installs the contents of a wheel into its own base in the pex dependencies cache. """ @@ -275,27 +273,24 @@ def _add_dist_zip(self, path, dist_name): # into an importable shape. We can do that by installing it into its own # wheel dir. if not self.interpreter.supports_wheel_install(): - print("*** Wheel dependencies may not work correctly with Python 2.6.", file=sys.stderr) - print("*** Your generated pex may experience errors", file=sys.stderr) + self._logger.warn("Wheel dependency on %s may not work correctly with Python 2.6." % + dist_name, file=sys.stderr) if self.interpreter.supports_wheel_install() and dist_name.endswith("whl"): from wheel.install import WheelFile - try: - tmp = tempfile.mkdtemp() - whltmp = os.path.join(tmp, dist_name) - os.mkdir(whltmp) - wf = WheelFile(path) - wf.install(overrides=self._get_installer_paths(dist_name, whltmp), force=True) - for (root, _, files) in os.walk(whltmp): - pruned_dir = os.path.relpath(root, tmp) - for f in files: - fullpath = os.path.join(root, f) - if os.path.isdir(fullpath): - continue - target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) - self._chroot.copy(fullpath, target) - finally: - shutil.rmtree(tmp) + tmp = safe_mkdtemp() + whltmp = os.path.join(tmp, dist_name) + os.mkdir(whltmp) + wf = WheelFile(path) + wf.install(overrides=self._get_installer_paths(whltmp), force=True) + for (root, _, files) in os.walk(whltmp): + pruned_dir = os.path.relpath(root, tmp) + for f in files: + fullpath = os.path.join(root, f) + if os.path.isdir(fullpath): + continue + target = os.path.join(self._pex_info.internal_cache, pruned_dir, f) + self._chroot.copy(fullpath, target) return CacheHelper.dir_hash(whltmp) with open_zip(path) as zf: diff --git a/tests/test_compiler.py b/tests/test_compiler.py index c17c20302..6b55f033b 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import contextlib -import marshal import os +import marshal import pytest from twitter.common.contextutil import temporary_dir From 2701d9837bfbef43b73f1346a6fba2cd1c1f8555 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Fri, 7 Jul 2017 09:33:29 -0400 Subject: [PATCH 23/25] isort. --- pex/http.py | 1 - tests/test_compiler.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pex/http.py b/pex/http.py index 079e7e2e9..a1e68419f 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,7 +7,6 @@ import shutil import uuid from abc import abstractmethod - from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 6b55f033b..c17c20302 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import contextlib +import marshal import os -import marshal import pytest from twitter.common.contextutil import temporary_dir From d84f211009332a2ec7be1df44c80be4559c0778b Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Fri, 7 Jul 2017 09:53:49 -0400 Subject: [PATCH 24/25] Fix the 26 test. --- pex/http.py | 1 + pex/pex_builder.py | 3 +-- tests/test_compiler.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pex/http.py b/pex/http.py index a1e68419f..079e7e2e9 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,6 +7,7 @@ import shutil import uuid from abc import abstractmethod + from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/pex/pex_builder.py b/pex/pex_builder.py index d6452ba1c..5f78bd97a 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -5,7 +5,6 @@ import logging import os -import sys from pkg_resources import DefaultProvider, ZipProvider, get_provider @@ -274,7 +273,7 @@ def _add_dist_zip(self, path, dist_name): # wheel dir. if not self.interpreter.supports_wheel_install(): self._logger.warn("Wheel dependency on %s may not work correctly with Python 2.6." % - dist_name, file=sys.stderr) + dist_name) if self.interpreter.supports_wheel_install() and dist_name.endswith("whl"): from wheel.install import WheelFile diff --git a/tests/test_compiler.py b/tests/test_compiler.py index c17c20302..6b55f033b 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import contextlib -import marshal import os +import marshal import pytest from twitter.common.contextutil import temporary_dir From 1100e3f7f400fab1085d1eadfead879c2f8f53b9 Mon Sep 17 00:00:00 2001 From: "Mark C. Chu-Carroll" Date: Fri, 7 Jul 2017 10:02:18 -0400 Subject: [PATCH 25/25] Keep trying to fix the stupid isort. --- pex/http.py | 1 - tests/test_compiler.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pex/http.py b/pex/http.py index 079e7e2e9..a1e68419f 100644 --- a/pex/http.py +++ b/pex/http.py @@ -7,7 +7,6 @@ import shutil import uuid from abc import abstractmethod - from email import message_from_string from .common import safe_mkdtemp, safe_open diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 6b55f033b..c17c20302 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -2,9 +2,9 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). import contextlib +import marshal import os -import marshal import pytest from twitter.common.contextutil import temporary_dir