Repository: pex-tool/pex

Issue:
`pex -c` does not work for multiplatform pexes when the target dist is platform specific
If the console script is housed in a dist that is platform-specific and the pex is multiplatform, then the same script is found more than once and pex creation fails with something like:
```
RuntimeError: Ambiguous script specification pants matches multiple entry points:pants = pants.bin.pants_loader:main pants = pants.bin.pants_loader:main
```
Pex should probably allow dups from a single requirement key if the found scripts are bytewise identical as they are in this example case.

See https://github.com/pantsbuild/pants/pull/6349 for an example of getting bit by this.


Discussion:

Resolving patch:
diff --git a/pex/finders.py b/pex/finders.py
--- a/pex/finders.py
+++ b/pex/finders.py
@@ -293,17 +293,22 @@ def get_script_from_distributions(name, dists):
 
 
 def get_entry_point_from_console_script(script, dists):
-  # check all distributions for the console_script "script"
-  entries = frozenset(filter(None, (
-      dist.get_entry_map().get('console_scripts', {}).get(script) for dist in dists)))
+  # Check all distributions for the console_script "script". De-dup by dist key to allow for a
+  # duplicate console script IFF the distribution is platform-specific and this is a multi-platform
+  # pex.
+  def get_entrypoint(dist):
+    script_entry = dist.get_entry_map().get('console_scripts', {}).get(script)
+    if script_entry is not None:
+      # Entry points are of the form 'foo = bar', we just want the 'bar' part.
+      return dist.key, str(script_entry).split('=')[1].strip()
+
+  entries = frozenset(filter(None, (get_entrypoint(dist) for dist in dists)))
 
-  # if multiple matches, freak out
   if len(entries) > 1:
     raise RuntimeError(
-        'Ambiguous script specification %s matches multiple entry points:%s' % (
-            script, ' '.join(map(str, entries))))
+        'Ambiguous script specification %s matches multiple entry points:\n\t%s' % (
+            script, '\n\t'.join('%s from %s' % (entry_point, key) for key, entry_point in entries)))
 
   if entries:
-    entry_point = next(iter(entries))
-    # entry points are of the form 'foo = bar', we just want the 'bar' part:
-    return str(entry_point).split('=')[1].strip()
+    _, entry_point = next(iter(entries))
+    return entry_point


Test patch:
diff --git a/pex/testing.py b/pex/testing.py
--- a/pex/testing.py
+++ b/pex/testing.py
@@ -330,8 +330,9 @@ def ensure_python_distribution(version):
     bootstrap_python_installer(pyenv_root)
 
   if not os.path.exists(interpreter_location):
-    os.environ['PYENV_ROOT'] = pyenv_root
-    subprocess.check_call([pyenv, 'install', '--keep', version])
+    env = {'PYENV_ROOT': pyenv_root,
+           'CONFIGURE_OPTS': '--enable-shared'}
+    subprocess.check_call([pyenv, 'install', '--keep', version], env=env)
     subprocess.check_call([pip, 'install', '-U', 'pip'])
     subprocess.check_call([pip, 'install', SETUPTOOLS_REQUIREMENT])
 
diff --git a/tests/test_finders.py b/tests/test_finders.py
--- a/tests/test_finders.py
+++ b/tests/test_finders.py
@@ -4,11 +4,12 @@
 import zipimport
 
 import pkg_resources
+import pytest
 
 from pex.finders import ChainedFinder
 from pex.finders import _add_finder as add_finder
 from pex.finders import _remove_finder as remove_finder
-from pex.finders import find_eggs_in_zip, get_script_from_egg
+from pex.finders import find_eggs_in_zip, get_entry_point_from_console_script, get_script_from_egg
 
 try:
   import mock
@@ -118,3 +119,32 @@ def test_get_script_from_egg():
 
   assert location is None
   assert content is None
+
+
+class FakeDist(object):
+  def __init__(self, key, console_script_entry):
+    self.key = key
+    script = console_script_entry.split('=')[0].strip()
+    self._entry_map = {'console_scripts': {script: console_script_entry}}
+
+  def get_entry_map(self):
+    return self._entry_map
+
+
+def test_get_entry_point_from_console_script():
+  dists = [FakeDist(key='fake', console_script_entry='bob= bob.main:run'),
+           FakeDist(key='fake', console_script_entry='bob =bob.main:run')]
+  assert 'bob.main:run' == get_entry_point_from_console_script('bob', dists)
+
+
+def test_get_entry_point_from_console_script_conflict():
+  dists = [FakeDist(key='bob', console_script_entry='bob= bob.main:run'),
+           FakeDist(key='fake', console_script_entry='bob =bob.main:run')]
+  with pytest.raises(RuntimeError):
+    get_entry_point_from_console_script('bob', dists)
+
+
+def test_get_entry_point_from_console_script_dne():
+  dists = [FakeDist(key='bob', console_script_entry='bob= bob.main:run'),
+           FakeDist(key='fake', console_script_entry='bob =bob.main:run')]
+  assert None is get_entry_point_from_console_script('jane', dists)
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -981,3 +981,23 @@ def test_invalid_entry_point_verification_3rdparty():
                            '-o', pex_out_path,
                            '--validate-entry-point'])
     res.assert_failure()
+
+
+@pytest.mark.skipif(IS_PYPY)
+def test_multiplatform_entrypoint():
+  with temporary_dir() as td:
+    pex_out_path = os.path.join(td, 'p537.pex')
+    interpreter = ensure_python_interpreter('3.6.3')
+    res = run_pex_command(['p537==1.0.3',
+                           '--no-build',
+                           '--python={}'.format(interpreter),
+                           '--python-shebang=#!{}'.format(interpreter),
+                           '--platform=linux-x86_64',
+                           '--platform=macosx-10.13-x86_64',
+                           '-c', 'p537',
+                           '-o', pex_out_path,
+                           '--validate-entry-point'])
+    res.assert_success()
+
+    greeting = subprocess.check_output([pex_out_path])
+    assert b'Hello World!' == greeting.strip()