Repository: pex-tool/pex

Issue:
PexInfo.pex_hash does not include metadata
Generally a PEX file is uniqued by a combination of its user code and dependencies. Currently, this is what the `pex_hash` is comprised of:
https://github.com/pantsbuild/pex/blob/d205e2db713ce30d70cd2df9704468f8a3b3f9e0/pex/pex_builder.py#L508-L512

This scheme is foiled by PEX files with the same code and deps but different entry points (or any other metadata for that matter). This sort of thing comes up frequently in Pants with the BUILD file pattern:
```
pex_library()
pex_binary(...)
pex_binary(...)
```
Due to the vagaries of how `./pants run` happens to work today, all `pex_binary` targets here share the same code (none) and deps but each has a different entrypoint.



Discussion:
Hm, they should not have the same code in build-support/bin. You're seeing differently? (I haven't tested now, afk)
Clarified its `./pants run ` that's the problem. In that case no code is added to the PEX file. Its adjoined loose.

Resolving patch:
diff --git a/pex/pex_builder.py b/pex/pex_builder.py
--- a/pex/pex_builder.py
+++ b/pex/pex_builder.py
@@ -504,13 +504,7 @@ def _precompile_source(self):
 
     def _prepare_code(self):
         self._pex_info.code_hash = CacheHelper.pex_code_hash(self._chroot.path())
-
-        hasher = hashlib.sha1()
-        hasher.update("code:{}".format(self._pex_info.code_hash).encode("utf-8"))
-        for location, sha in sorted(self._pex_info.distributions.items()):
-            hasher.update("{}:{}".format(location, sha).encode("utf-8"))
-        self._pex_info.pex_hash = hasher.hexdigest()
-
+        self._pex_info.pex_hash = hashlib.sha1(self._pex_info.dump().encode("utf-8")).hexdigest()
         self._chroot.write(self._pex_info.dump().encode("utf-8"), PexInfo.PATH, label="manifest")
 
         bootstrap = BOOTSTRAP_ENVIRONMENT.format(


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2697,6 +2697,59 @@ def run_isort_pex(**env):
     assert isort_pex_interpreter2 == run_isort_pex(PEX_PYTHON=other_interpreter)
 
 
+@pytest.mark.xfail(IS_PYPY3, reason="https://github.com/pantsbuild/pex/issues/1210")
+def test_venv_mode_issues_1218(tmpdir):
+    # type: (Any) -> None
+
+    def get_fabric_versions(pex):
+        # type: (str) -> Dict[str, str]
+        output, returncode = run_simple_pex(pex, args=["--version"])
+        assert 0 == returncode
+        return dict(
+            cast("Tuple[str, str]", line.split(" ", 1))
+            for line in output.decode("utf-8").splitlines()
+        )
+
+    # The only difference in these two PEX files is their entrypoint. Ensure venv execution takes
+    # that into account and disambiguates the otherwise identical PEX files.
+
+    invoke_pex = os.path.join(str(tmpdir), "invoke.pex")
+    results = run_pex_command(
+        args=["fabric==2.6.0", "--venv", "-e", "invoke", "-o", invoke_pex], quiet=True
+    )
+    results.assert_success()
+    invoke_versions = get_fabric_versions(invoke_pex)
+    assert len(invoke_versions) == 1
+    invoke_version = invoke_versions["Invoke"]
+    assert invoke_version == "1.5.0"
+
+    fabric_pex = os.path.join(str(tmpdir), "fabric.pex")
+    results = run_pex_command(
+        args=[
+            "fabric==2.6.0",
+            "--venv",
+            "-e",
+            "fabric",
+            "-o",
+            fabric_pex,
+            "--pex-repository",
+            invoke_pex,
+        ],
+        quiet=True,
+    )
+    results.assert_success()
+    fabric_versions = get_fabric_versions(fabric_pex)
+    assert len(fabric_versions) >= 2
+    assert invoke_version == fabric_versions["Invoke"]
+    assert "2.6.0" == fabric_versions["Fabric"]
+
+    invoke_pex_info = PexInfo.from_pex(invoke_pex)
+    fabric_pex_info = PexInfo.from_pex(fabric_pex)
+    assert invoke_pex_info.code_hash == fabric_pex_info.code_hash
+    assert invoke_pex_info.distributions == fabric_pex_info.distributions
+    assert invoke_pex_info.pex_hash != fabric_pex_info.pex_hash
+
+
 @pytest.mark.parametrize(
     "mode_args",
     [