Repository: pex-tool/pex

Issue:
Nondeterministic "invalid command 'bdist_wheel'" resulting in "Untranslateable" exception
I just started using pex. About half of the time I run it, it works great. The other half of the time, it fails like this.

```
> pex --python python2.7 abc.zip -v
**** Failed to install enum34-1.0.4. stdout: :: Packaging enum34

**** Failed to install enum34-1.0.4. stderr:
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

error: invalid command 'bdist_wheel'

pex: Failed to install package at /tmp/tmppcotrf/enum34-1.0.4: Failed to install /tmp/tmppcotrf/enum34-1.0.4
Traceback (most recent call last):
  File "/bin/pex", line 9, in <module>
    load_entry_point('pex==1.0.3', 'console_scripts', 'pex')()
  File "/usr/lib/python3.3/site-packages/pex/bin/pex.py", line 509, in main
    pex_builder = build_pex(reqs, options, resolver_options_builder)
  File "/usr/lib/python3.3/site-packages/pex/bin/pex.py", line 471, in build_pex
    resolveds = resolver.resolve(resolvables)
  File "/usr/lib/python3.3/site-packages/pex/resolver.py", line 191, in resolve
    dist = self.build(package, resolvable.options)
  File "/usr/lib/python3.3/site-packages/pex/resolver.py", line 248, in build
    dist = super(CachingResolver, self).build(package, options)
  File "/usr/lib/python3.3/site-packages/pex/resolver.py", line 160, in build
    raise Untranslateable('Package %s is not translateable by %s' % (package, translator))
pex.resolver.Untranslateable: Package SourcePackage('file:///home/chris/.pex/build/enum34-1.0.4.zip') is not translateable by ChainedTranslator(WheelTranslator, EggTranslator, SourceTranslator)
```

In the above example it failed on `enum34`, but which dependency it fails on appears random as well.



Discussion:
Passing `--no-wheel` seems to serve as a workaround.

I don't understand what the ramifications are, though. Am I missing out on something by doing that?

Derp, disregard my earlier comment.

Which version of pex are you using?  Is it before 47163488 ?

I'm using the latest pypi release, 1.0.3.

I'm seeing this issue with 1.0.3 as well

--no-wheel works for me (pex 1.1.2 here on Python 3.4).

it seems to happen 100% of the time for me 


```
[2017-01-31 11:23:43] markgrandi@Gypaetus:~/Code/sigfig/rabbitmq_monitor$ pex -r requirements.txt -o ~/rabbitmq_monitor.pex
**** Failed to install arrow-0.8.0 (caused by: NonZeroExit("received exit code 1 during execution of `['/Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6', '-', 'bdist_wheel', '--dist-dir=/var/folders/7p/65pblygs2lq0mk242wl3cg240000gn/T/tmpmjul6i29']` while trying to execute `['/Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6', '-', 'bdist_wheel', '--dist-dir=/var/folders/7p/65pblygs2lq0mk242wl3cg240000gn/T/tmpmjul6i29']`",)
):
stdout:

stderr:
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

error: invalid command 'bdist_wheel'


Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/bin/pex", line 11, in <module>
    sys.exit(main())
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/pex/bin/pex.py", line 578, in main
    pex_builder = build_pex(reqs, options, resolver_options_builder)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/pex/bin/pex.py", line 527, in build_pex
    resolveds = resolver.resolve(resolvables)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/pex/resolver.py", line 207, in resolve
    dist = self.build(package, resolvable.options)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/pex/resolver.py", line 264, in build
    dist = super(CachingResolver, self).build(package, options)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/pex/resolver.py", line 173, in build
    raise Untranslateable('Package %s is not translateable by %s' % (package, translator))
pex.resolver.Untranslateable: Package SourcePackage('file:///Users/markgrandi/.pex/build/arrow-0.8.0.tar.gz') is not translateable by ChainedTranslator(WheelTranslator, EggTranslator, SourceTranslator)
```
I've got this happening too. My particular problem appeared to be that the Python interpreter I was using didn't have setuptools available to it.

I'm suspicious of that command it's running though, why is there a hyphen there instead of setup.py?
any news on this? im pretty sure that the interpreter im using has setuptools installed.
does using '--no-wheel' not help? seems to be a bug specific to handling wheel files
this isn't pex specific - but in order to have the `bdist_wheel` command for pex to target when translating deps to wheels, you'd need both `setuptools` and `wheel` installed in the interpreter's environment. `--no-wheel` works around this by avoiding targeting of that command, but the proper fix should be as simple as `pip install wheel`. let me know if that doesn't work.
also noting that `pip install pex` should transitively install both `setuptools` and `wheel` for you, but it's possible that `pip` is not how most folks reporting here are getting `pex` (or perhaps their `pex` versions are really old)? or perhaps you guys are targeting alternate interpreters at runtime?

would be interested to understand how this is happening from someone who has it repro'd. beyond that, would propose that we add some eager validation that `setuptools` and `wheel` exist in targeted interpreter environments - then we can throw a clearer error message before we get to the lower level `setup.py` failure.
Wow this was old!

I hit this writing a pex unit test last night. The offending code is in [`InstallerBase`](https://github.com/pantsbuild/pex/blob/d1f946c8b46f8c5123b01bcd376c87bb67c8a884/pex/installer.py#L36). Right now , for the `WheelInstaller` we write out:
```
import sys
sys.path.insert(0, u'/home/jsirois/dev/pantsbuild/jsirois-pex/.pyenv_test/versions/3.6.3/lib/python3.6/site-packages'); import setuptools
sys.path.insert(0, '/tmp/tmpdzU0rv/interpreters/CPython-3.6.3/wheel-0.27.0-py3.6.egg'); import wheel

__file__ = 'setup.py'
sys.argv[0] = 'setup.py'
exec(compile(open(__file__, 'rb').read(), __file__, 'exec'))
```

Here - the `sys.path.insert(...)` lines are randomly ordered by dict item iteration order, and the order show above is problematic, since `setuptools` is imported before `wheel` is on `sys.path`, foiling discovery of the `wheel` `distutils.commands` `bdist_wheel` entry point.

The fix is to just do all imports after we fully modify `sys.path`.

Resolving patch:
diff --git a/pex/installer.py b/pex/installer.py
--- a/pex/installer.py
+++ b/pex/installer.py
@@ -33,7 +33,8 @@ def function_wrapper(self, *args, **kw):
 
 class InstallerBase(object):
   SETUP_BOOTSTRAP_HEADER = "import sys"
-  SETUP_BOOTSTRAP_MODULE = "sys.path.insert(0, %(path)r); import %(module)s"
+  SETUP_BOOTSTRAP_PYPATH = "sys.path.insert(0, %(path)r)"
+  SETUP_BOOTSTRAP_MODULE = "import %(module)s"
   SETUP_BOOTSTRAP_FOOTER = """
 __file__ = 'setup.py'
 sys.argv[0] = 'setup.py'
@@ -85,15 +86,21 @@ def capability(self):
 
   @property
   def bootstrap_script(self):
+    bootstrap_sys_paths = []
     bootstrap_modules = []
     for module, requirement in self.mixins().items():
       path = self._interpreter.get_location(requirement)
       if not path:
         assert not self._strict  # This should be caught by validation
         continue
-      bootstrap_modules.append(self.SETUP_BOOTSTRAP_MODULE % {'path': path, 'module': module})
+      bootstrap_sys_paths.append(self.SETUP_BOOTSTRAP_PYPATH % {'path': path})
+      bootstrap_modules.append(self.SETUP_BOOTSTRAP_MODULE % {'module': module})
     return '\n'.join(
-        [self.SETUP_BOOTSTRAP_HEADER] + bootstrap_modules + [self.SETUP_BOOTSTRAP_FOOTER])
+      [self.SETUP_BOOTSTRAP_HEADER] +
+      bootstrap_sys_paths +
+      bootstrap_modules +
+      [self.SETUP_BOOTSTRAP_FOOTER]
+    )
 
   def run(self):
     if self._installed is not None:


Test patch:
diff --git a/pex/testing.py b/pex/testing.py
--- a/pex/testing.py
+++ b/pex/testing.py
@@ -126,13 +126,13 @@ def write_zipfile(directory, dest, reverse=False):
 
 @contextlib.contextmanager
 def make_installer(name='my_project', version='0.0.0', installer_impl=EggInstaller, zip_safe=True,
-                   install_reqs=None):
+                   install_reqs=None, **kwargs):
   interp = {'project_name': name,
             'version': version,
             'zip_safe': zip_safe,
             'install_requires': install_reqs or []}
   with temporary_content(PROJECT_CONTENT, interp=interp) as td:
-    yield installer_impl(td)
+    yield installer_impl(td, **kwargs)
 
 
 @contextlib.contextmanager
diff --git a/tests/test_installer.py b/tests/test_installer.py
new file mode 100644
--- /dev/null
+++ b/tests/test_installer.py
@@ -0,0 +1,60 @@
+# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+import contextlib
+from collections import OrderedDict
+
+import pytest
+
+from pex.bin.pex import get_interpreter
+from pex.installer import WheelInstaller
+from pex.testing import ensure_python_interpreter, make_installer, temporary_dir
+from pex.version import SETUPTOOLS_REQUIREMENT, WHEEL_REQUIREMENT
+
+
+class OrderableInstaller(WheelInstaller):
+  def __init__(self, source_dir, strict=True, interpreter=None, install_dir=None, mixins=None):
+    self._mixins = mixins
+    super(OrderableInstaller, self).__init__(source_dir, strict, interpreter, install_dir)
+
+  def mixins(self):
+    return self._mixins
+
+
+@contextlib.contextmanager
+def bare_interpreter():
+  with temporary_dir() as interpreter_cache:
+    yield get_interpreter(
+      python_interpreter=ensure_python_interpreter('3.6.3'),
+      interpreter_cache_dir=interpreter_cache,
+      repos=None,
+      use_wheel=True
+    )
+
+
+@contextlib.contextmanager
+def wheel_installer(*mixins):
+  with bare_interpreter() as interpreter:
+    with make_installer(installer_impl=OrderableInstaller,
+                        interpreter=interpreter,
+                        mixins=OrderedDict(mixins)) as installer:
+      yield installer
+
+
+WHEEL_EXTRA = ('wheel', WHEEL_REQUIREMENT)
+SETUPTOOLS_EXTRA = ('setuptools', SETUPTOOLS_REQUIREMENT)
+
+
+def test_wheel_before_setuptools():
+  with wheel_installer(WHEEL_EXTRA, SETUPTOOLS_EXTRA) as installer:
+    installer.bdist()
+
+
+def test_setuptools_before_wheel():
+  with wheel_installer(SETUPTOOLS_EXTRA, WHEEL_EXTRA) as installer:
+    installer.bdist()
+
+
+def test_no_wheel():
+  with wheel_installer(SETUPTOOLS_EXTRA) as installer:
+    with pytest.raises(installer.InstallFailure):
+      installer.bdist()