Repository: pex-tool/pex

Issue:
Persistent `venvs/short` collision
Seen in Pants. This happens even after moving `/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs` aside:
```
22:15:36.87 [INFO] Initializing scheduler...
22:15:37.00 [INFO] Scheduler initialized.
22:15:40.00 [INFO] Completed: Composing 12 requirements to build requirements.pex from repository.pex
22:15:40.70 [INFO] Completed: Building pytest_runner.pex
22:15:40.72 [INFO] Canceled: Building pytest_runner.pex
22:15:40.72 [ERROR] Exception caught: (pants.engine.internals.scheduler.ExecutionError)
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/bin/local_pants_runner.py", line 236, in _run_inner
    return self._perform_run(goals)
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/bin/local_pants_runner.py", line 175, in _perform_run
    return self._perform_run_body(goals, poll=False)
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/bin/local_pants_runner.py", line 192, in _perform_run_body
    return self.graph_session.run_goal_rules(
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/init/engine_initializer.py", line 135, in run_goal_rules
    exit_code = self.scheduler_session.run_goal_rule(
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/engine/internals/scheduler.py", line 538, in run_goal_rule
    self._raise_on_error([t for _, t in throws])
  File "/Users/sinisaderasevic/.cache/pants/setup/bootstrap-Darwin-arm64/2.7.0_py39/lib/python3.9/site-packages/pants/engine/internals/scheduler.py", line 506, in _raise_on_error
    raise ExecutionError(

Exception message: 1 Exception encountered:

  ProcessExecutionFailure: Process 'Building pytest_runner.pex' failed with exit code 1.
stdout:

stderr:
Traceback (most recent call last):
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.deps/pex-2.1.44-py2.py3-none-any.whl/pex/pex_bootstrapper.py", line 404, in ensure_venv
    os.symlink(venv_dir, short_venv_path)
FileExistsError: [Errno 17] File exists: '/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b' -> '/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/short/0a22e09b2ac391980be39a125d20dcfc59451c76'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 483, in execute
    exit_value = self._wrap_coverage(self._wrap_profiling, self._execute)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 401, in _wrap_coverage
    return runner(*args)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 432, in _wrap_profiling
    return runner(*args)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 539, in _execute
    return self.execute_entry(self._pex_info.entry_point)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 655, in execute_entry
    return self.execute_pkg_resources(entry_point)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.bootstrap/pex/pex.py", line 687, in execute_pkg_resources
    return runner()
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.deps/pex-2.1.44-py2.py3-none-any.whl/pex/bin/pex.py", line 1165, in main
    seed_info = seed_cache(options, pex, verbose=options.seed == Seed.VERBOSE)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.deps/pex-2.1.44-py2.py3-none-any.whl/pex/bin/pex.py", line 1219, in seed_cache
    venv_pex = ensure_venv(pex)
  File "/Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/unzipped_pexes/592827b562c4aa09c1e9af6b3ed7b797eef1a4dc/.deps/pex-2.1.44-py2.py3-none-any.whl/pex/pex_bootstrapper.py", line 411, in ensure_venv
    raise RuntimeError(
RuntimeError: The venv for /private/var/folders/_p/m9ryyzks3qs7g0nqd_k4r0t80000gn/T/process-executionRETbSy/.tmp/tmprnzhbipe at /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b has hash collisions with 33 other venvs!
1.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
2.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
3.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
4.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
5.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
6.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
7.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
8.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
9.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
10.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
11.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
12.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
13.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
14.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
15.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
16.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
17.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
18.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
19.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
20.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
21.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
22.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
23.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
24.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
25.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
26.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
27.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
28.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
29.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
30.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
31.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
32.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
33.) /Users/sinisaderasevic/bull/apollo/.cache/pants/named_caches/pex_root/venvs/48ceaa27d6c5227e92829223e66a63f923e74b08/7844231537c9dbf01a552110f465f35678a1a22b
```


Discussion:
Afaict the only way this can happen is if we fail consistently at line 427 executing `populate_venv_with_pex`:
https://github.com/pantsbuild/pex/blob/100b687938293a022e5bb842118b662f5037069e/pex/pex_bootstrapper.py#L391-L435

Although the main venv_dir will not be created in that case, the `venvs/short/` symlinks _will_ since their path is not inside the directory being atomically locked.

Resolving patch:
diff --git a/pex/pex_bootstrapper.py b/pex/pex_bootstrapper.py
--- a/pex/pex_bootstrapper.py
+++ b/pex/pex_bootstrapper.py
@@ -404,8 +404,11 @@ def _bootstrap(entry_point):
     return pex_info
 
 
-def ensure_venv(pex):
-    # type: (PEX) -> str
+def ensure_venv(
+    pex,  # type: PEX
+    collisions_ok=True,  # type: bool
+):
+    # type: (...) -> str
     pex_info = pex.pex_info()
     venv_dir = pex_info.venv_dir(pex_file=pex.path(), interpreter=pex.interpreter)
     if venv_dir is None:
@@ -431,9 +434,6 @@ def ensure_venv(pex):
 
             pex_path = os.path.abspath(pex.path())
 
-            short_venv_dir = os.path.join(pex_info.pex_root, "venvs", "short")
-            safe_mkdir(short_venv_dir)
-
             # A sha1 hash is 160 bits -> 20 bytes -> 40 hex characters. We start with 8 characters
             # (32 bits) of entropy since that is short and _very_ unlikely to collide with another
             # PEX venv on this machine. If we still collide after using the whole sha1 (for a total
@@ -442,57 +442,58 @@ def ensure_venv(pex):
             collisions = []
             for chars in range(8, len(venv_hash) + 1):
                 entropy = venv_hash[:chars]
-                short_venv_path = os.path.join(short_venv_dir, entropy)
-                try:
-                    os.symlink(venv_dir, short_venv_path)
-                    break
-                except OSError as e:
-                    if e.errno != errno.EEXIST:
-                        raise e
-                    collisions.append(short_venv_path)
-                    if entropy == venv_hash:
-                        raise RuntimeError(
-                            "The venv for {pex} at {venv} has hash collisions with {count} other "
-                            "{venvs}!\n{collisions}".format(
+                short_venv_dir = os.path.join(pex_info.pex_root, "venvs", "s", entropy)
+                with atomic_directory(short_venv_dir, exclusive=True) as short_venv:
+                    if short_venv.is_finalized:
+                        collisions.append(short_venv_dir)
+                        if entropy == venv_hash:
+                            raise RuntimeError(
+                                "The venv for {pex} at {venv} has hash collisions with {count} "
+                                "other {venvs}!\n{collisions}".format(
+                                    pex=pex_path,
+                                    venv=venv_dir,
+                                    count=len(collisions),
+                                    venvs=pluralize(collisions, "venv"),
+                                    collisions="\n".join(
+                                        "{index}.) {venv_path}".format(
+                                            index=index, venv_path=os.path.realpath(path)
+                                        )
+                                        for index, path in enumerate(collisions, start=1)
+                                    ),
+                                )
+                            )
+                        continue
+
+                    os.symlink(venv_dir, os.path.join(short_venv.work_dir, "venv"))
+                    shebang = populate_venv_with_pex(
+                        virtualenv,
+                        pex,
+                        bin_path=pex_info.venv_bin_path,
+                        python=os.path.join(
+                            short_venv_dir, "venv", "bin", os.path.basename(pex.interpreter.binary)
+                        ),
+                        collisions_ok=collisions_ok,
+                    )
+
+                    # There are popular Linux distributions with shebang length limits
+                    # (BINPRM_BUF_SIZE in /usr/include/linux/binfmts.h) set at 128 characters, so
+                    # we warn in the _very_ unlikely case that our shortened shebang is longer than
+                    # this.
+                    if len(shebang) > 128:
+                        pex_warnings.warn(
+                            "The venv for {pex} at {venv} has script shebangs of {shebang!r} with "
+                            "{count} characters. On some systems this may be too long and cause "
+                            "problems running the venv scripts. You may be able adjust PEX_ROOT "
+                            "from {pex_root} to a shorter path as a work-around.".format(
                                 pex=pex_path,
                                 venv=venv_dir,
-                                count=len(collisions),
-                                venvs=pluralize(collisions, "venv"),
-                                collisions="\n".join(
-                                    "{index}.) {venv_path}".format(
-                                        index=index, venv_path=os.path.realpath(path)
-                                    )
-                                    for index, path in enumerate(collisions, start=1)
-                                ),
+                                shebang=shebang,
+                                count=len(shebang),
+                                pex_root=pex_info.pex_root,
                             )
                         )
 
-            shenbang = populate_venv_with_pex(
-                virtualenv,
-                pex,
-                bin_path=pex_info.venv_bin_path,
-                python=os.path.join(
-                    short_venv_path, "bin", os.path.basename(pex.interpreter.binary)
-                ),
-                collisions_ok=True,
-            )
-
-            # There are popular Linux distributions with shebang length limits (BINPRM_BUF_SIZE
-            # in /usr/include/linux/binfmts.h) set at 128 characters, so we warn in the _very_
-            # unlikely case that our shortened shebang is longer than this.
-            if len(shenbang) > 128:
-                pex_warnings.warn(
-                    "The venv for {pex} at {venv} has script shebangs of {shebang!r} with {count} "
-                    "characters. On some systems this may be too long and cause problems running "
-                    "the venv scripts. You may be able adjust PEX_ROOT from {pex_root} to a "
-                    "shorter path as a work-around.".format(
-                        pex=pex_path,
-                        venv=venv_dir,
-                        shebang=shenbang,
-                        count=len(shenbang),
-                        pex_root=pex_info.pex_root,
-                    )
-                )
+                    break
 
     return os.path.join(venv_dir, "pex")
 
diff --git a/pex/tools/commands/venv.py b/pex/tools/commands/venv.py
--- a/pex/tools/commands/venv.py
+++ b/pex/tools/commands/venv.py
@@ -39,7 +39,7 @@ def _copytree(
     # type: (...) -> Iterator[Tuple[str, str]]
     safe_mkdir(dst)
     link = True
-    for root, dirs, files in os.walk(src, topdown=True, followlinks=False):
+    for root, dirs, files in os.walk(src, topdown=True, followlinks=True):
         if src == root:
             dirs[:] = [d for d in dirs if d not in exclude]
             files[:] = [f for f in files if f not in exclude]


Test patch:
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -4,11 +4,12 @@
 from __future__ import absolute_import
 
 import os
+import shutil
 import subprocess
 
 import pytest
 
-from pex.common import atomic_directory
+from pex.common import atomic_directory, safe_mkdir
 from pex.testing import make_env, run_pex_command
 from pex.typing import TYPE_CHECKING
 
@@ -59,3 +60,21 @@ def pex_bdist(
     wheels = os.listdir(wheels_dir)
     assert 1 == len(wheels)
     return os.path.join(wheels_dir, wheels[0])
+
+
+@pytest.fixture(scope="session")
+def pex_src(
+    pex_project_dir,  # type: str
+    shared_integration_test_tmpdir,  # type: str
+):
+    # type: (...) -> str
+    src = os.path.join(shared_integration_test_tmpdir, "pex_src")
+    for root, dirs, files in os.walk(os.path.join(pex_project_dir, "pex")):
+        root_relpath = os.path.relpath(root, pex_project_dir)
+        dirs[:] = [d for d in dirs if d != "__pycache__"]
+        for d in dirs:
+            safe_mkdir(os.path.join(src, root_relpath, d))
+        for f in files:
+            if not f.endswith(".pyc"):
+                shutil.copy(os.path.join(root, f), os.path.join(src, root_relpath, f))
+    return src
diff --git a/tests/integration/test_pex_bootstrapper.py b/tests/integration/test_pex_bootstrapper.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/test_pex_bootstrapper.py
@@ -0,0 +1,79 @@
+# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+import os.path
+import re
+
+import pytest
+
+from pex.pex import PEX
+from pex.pex_bootstrapper import ensure_venv
+from pex.pex_info import PexInfo
+from pex.testing import run_pex_command
+from pex.tools.commands.venv import CollisionError
+from pex.typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+
+
+def test_ensure_venv(
+    pex_src,  # type: str
+    pex_bdist,  # type: str
+    tmpdir,  # type: Any
+):
+    # type: (...) -> None
+
+    pex_root = os.path.join(str(tmpdir), "pex_root")
+    collisions_pex = os.path.join(str(tmpdir), "collisions.pex")
+    run_pex_command(
+        args=[
+            pex_bdist,
+            "-D",
+            pex_src,
+            "-o",
+            collisions_pex,
+            "--runtime-pex-root",
+            pex_root,
+            "--venv",
+        ]
+    ).assert_success()
+
+    with pytest.raises(CollisionError):
+        ensure_venv(PEX(collisions_pex), collisions_ok=False)
+
+    # The directory structure for successfully executed --venv PEXes is:
+    #
+    # PEX_ROOT/
+    #   venvs/
+    #     s/  # shortcuts dir
+    #       <short hash>/
+    #         venv -> <real venv parent dir (see below)>
+    #     <full hash1>/
+    #       <full hash2>/
+    #         <real venv>
+    #
+    # AtomicDirectory locks are used to create both branches of the venvs/ tree; so if there is a
+    # failure creating a venv we expect just:
+    #
+    # PEX_ROOT/
+    #   venvs/
+    #     s/
+    #       .<short hash>.atomic_directory.lck
+    #     <full hash1>/
+    #       .<full hash2>.atomic_directory.lck
+
+    expected_venv_dir = PexInfo.from_pex(collisions_pex).venv_dir(collisions_pex)
+    assert expected_venv_dir is not None
+
+    full_hash1_dir = os.path.basename(os.path.dirname(expected_venv_dir))
+    full_hash2_dir = os.path.basename(expected_venv_dir)
+
+    venvs_dir = os.path.join(pex_root, "venvs")
+    assert {"s", full_hash1_dir} == set(os.listdir(venvs_dir))
+    short_listing = os.listdir(os.path.join(venvs_dir, "s"))
+    assert 1 == len(short_listing)
+    assert re.match(r"^\.[0-9a-f]+\.atomic_directory.lck", short_listing[0])
+    assert [".{full_hash2}.atomic_directory.lck".format(full_hash2=full_hash2_dir)] == os.listdir(
+        os.path.join(venvs_dir, full_hash1_dir)
+    )