Repository: pex-tool/pex

Issue:
"FileNotFoundError: [Errno 2] No such file or directory" in pex
```
Engine traceback:
  in select
  in pants.core.goals.typecheck.typecheck
  in pants.backend.python.typecheck.mypy.rules.mypy_typecheck
  in pants.backend.python.typecheck.mypy.rules.mypy_typecheck_partition
  in pants.backend.python.util_rules.pex.create_pex
  in pants.backend.python.util_rules.pex.find_interpreter (CPython<3.9,<4,>=3.7,>=3.8)
  in pants.engine.process.fallible_to_exec_result_or_raise
Traceback (most recent call last):
  File "/home/toolchain/.*****/pants/setup/bootstrap-Linux-x86_64/2.1.0.dev0_py38/lib/python3.8/site-packages/pants/engine/process.py", line 241, in fallible_to_exec_result_or_raise
    raise ProcessExecutionFailure(
pants.engine.process.ProcessExecutionFailure: Process 'Find interpreter for constraints: CPython<3.9,<4,>=3.7,>=3.8' failed with exit code 1.
stdout:

stderr:
Traceback (most recent call last):
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 443, in execute
    exit_code = self._wrap_coverage(self._wrap_profiling, self._execute)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 375, in _wrap_coverage
    return runner(*args)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 406, in _wrap_profiling
    return runner(*args)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 494, in _execute
    return self.execute_entry(self._pex_info.entry_point)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 596, in execute_entry
    return runner(entry_point)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.bootstrap/pex/pex.py", line 612, in execute_pkg_resources
    return runner()
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/bin/pex.py", line 994, in main
    pex_builder.freeze(bytecode_compile=options.compile)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/pex_builder.py", line 505, in freeze
    self._prepare_bootstrap()
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/pex_builder.py", line 463, in _prepare_bootstrap
    vendor.vendor_runtime(
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/vendor/__init__.py", line 166, in vendor_runtime
    chroot.copy(src, dest, label)
  File "/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/common.py", line 512, in copy
    shutil.copy(src, os.path.join(self.chroot, dst))
  File "/usr/local/lib/python3.8/shutil.py", line 415, in copy
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "/usr/local/lib/python3.8/shutil.py", line 261, in copyfile
    with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
FileNotFoundError: [Errno 2] No such file or directory: '/home/toolchain/.*****/pants/named_*****s/pex_root/unzipped_pexes/d7ed954089c1232b9c1a001845a5a384366d3669/.deps/pex-2.1.20-py2.py3-none-any.whl/pex/vendor/_vendored/setuptools/pkg_resources/_vendor/__py*****__/pyparsing.cpython-37.pyc.140210639452464'
```


Discussion:
This was initially reported by @asherf in https://github.com/pantsbuild/pants/issues/11091 but its a pure Pex issue with the same mechanism behind it as solved in #1080 except this time in another `os.walk` that observes temporary pyc files created by the python runtime compilation process:
https://github.com/pantsbuild/pex/blob/0cfe165f976093685d54b2ab8977394101098e39/pex/vendor/__init__.py#L137-L166

Resolving patch:
diff --git a/pex/common.py b/pex/common.py
--- a/pex/common.py
+++ b/pex/common.py
@@ -8,6 +8,7 @@
 import errno
 import fcntl
 import os
+import re
 import shutil
 import stat
 import sys
@@ -20,11 +21,10 @@
 from datetime import datetime
 from uuid import uuid4
 
-from pex.typing import TYPE_CHECKING, cast
+from pex.typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from typing import Any, DefaultDict, Iterator, NoReturn, Optional, Set
-
+    from typing import Any, DefaultDict, Iterable, Iterator, NoReturn, Optional, Set
 
 # We use the start of MS-DOS time, which is what zipfiles use (see section 4.4.6 of
 # https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT).
@@ -33,6 +33,27 @@
 )
 
 
+def filter_pyc_dirs(dirs):
+    # type: (Iterable[str]) -> Iterator[str]
+    """Return an iterator over the input `dirs` filtering out Python bytecode cache directories."""
+    for d in dirs:
+        if d != "__pycache__":
+            yield d
+
+
+def filter_pyc_files(files):
+    # type: (Iterable[str]) -> Iterator[str]
+    """Return an iterator over the input `files` filtering out any Python bytecode files."""
+    for f in files:
+        # For Python 2.7, `.pyc` files are compiled as siblings to `.py` files (there is no
+        # __pycache__ dir). We rely on the fact that the temporary files created by CPython
+        # have object id (integer) suffixes to avoid picking up either finished `.pyc` files
+        # or files where Python bytecode compilation is in-flight; i.e.:
+        # `.pyc.0123456789`-style files.
+        if not re.search(r"\.pyc(?:\.[0-9]+)?$", f):
+            yield f
+
+
 def die(msg, exit_code=1):
     # type: (str, int) -> NoReturn
     print(msg, file=sys.stderr)
@@ -83,7 +104,7 @@ def do_copy():
 class MktempTeardownRegistry(object):
     def __init__(self):
         # type: () -> None
-        self._registry = cast("DefaultDict[int, Set[str]]", defaultdict(set))
+        self._registry = defaultdict(set)  # type: DefaultDict[int, Set[str]]
         self._lock = threading.RLock()
         self._getpid = os.getpid
         self._exists = os.path.exists
diff --git a/pex/util.py b/pex/util.py
--- a/pex/util.py
+++ b/pex/util.py
@@ -5,14 +5,13 @@
 
 import contextlib
 import os
-import re
 import sys
 import tempfile
 from hashlib import sha1
 from site import makepath  # type: ignore[attr-defined]
 from zipfile import ZipFile
 
-from pex.common import atomic_directory, safe_mkdir, safe_mkdtemp
+from pex.common import atomic_directory, filter_pyc_dirs, filter_pyc_files, safe_mkdir, safe_mkdtemp
 from pex.compatibility import (  # type: ignore[attr-defined]  # `exec_function` is defined dynamically
     PY2,
     exec_function,
@@ -138,15 +137,9 @@ def _iter_non_pyc_files(cls, directory):
         # type: (str) -> Iterator[str]
         normpath = os.path.realpath(os.path.normpath(directory))
         for root, dirs, files in os.walk(normpath):
-            dirs[:] = [d for d in dirs if d != "__pycache__"]
-            for f in files:
-                # For Python 2.7, `.pyc` files are compiled as siblings to `.py` files (there is no
-                # __pycache__ dir. We rely on the fact that the temporary files created by CPython
-                # have object id (integer) suffixes to avoid picking up either finished `.pyc` files
-                # or files where Python bytecode compilation is in-flight; i.e.:
-                # `.pyc.0123456789`-style files.
-                if not re.search(r"\.pyc(?:\.[0-9]+)?$", f):
-                    yield os.path.relpath(os.path.join(root, f), normpath)
+            dirs[:] = list(filter_pyc_dirs(dirs))
+            for f in filter_pyc_files(files):
+                yield os.path.relpath(os.path.join(root, f), normpath)
 
     @classmethod
     def pex_hash(cls, d):
diff --git a/pex/vendor/__init__.py b/pex/vendor/__init__.py
--- a/pex/vendor/__init__.py
+++ b/pex/vendor/__init__.py
@@ -6,7 +6,7 @@
 import collections
 import os
 
-from pex.common import touch
+from pex.common import filter_pyc_dirs, filter_pyc_files, touch
 from pex.compatibility import urlparse
 from pex.tracer import TRACER
 
@@ -157,13 +157,14 @@ def vendor_runtime(chroot, dest_basedir, label, root_module_names):
                             "Vendoring {} from {} @ {}".format(name, spec, spec.target_dir), V=3
                         )
 
-            for filename in files:
-                if not filename.endswith(".pyc"):  # Sources and data only.
-                    src = os.path.join(root, filename)
-                    dest = os.path.join(
-                        dest_basedir, spec.relpath, os.path.relpath(src, spec.target_dir)
-                    )
-                    chroot.copy(src, dest, label)
+            # We copy over sources and data only; no pyc files.
+            dirs[:] = filter_pyc_dirs(dirs)
+            for filename in filter_pyc_files(files):
+                src = os.path.join(root, filename)
+                dest = os.path.join(
+                    dest_basedir, spec.relpath, os.path.relpath(src, spec.target_dir)
+                )
+                chroot.copy(src, dest, label)
 
     if not all(vendor_module_names.values()):
         raise ValueError(


Test patch: