Repository: pypa/hatch

Issue:
OSError: Unable to locate executables directory
### Steps

1. Installed hatch via pipx / pip (tried both)
2. Ran `hatch new --init` over project [ref: https://github.com/frappe/bench]
3. `pyproject.toml` file generated _(which is pretty sick btw)_
4. Did a couple of tweaks for the license and dependencies keys
6. `hatch build` throws the following error

```bash
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/cli/__init__.py:203 in main               │
│                                                                                                  │
│   200                                                                                            │
│   201 def main():  # no cov                                                                      │
│   202 │   try:                                                                                   │
│ ❱ 203 │   │   return hatch(windows_expand_args=False)                                            │
│   204 │   except Exception:                                                                      │
│   205 │   │   from rich.console import Console                                                   │
│   206                                                                                            │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/core.py:1130 in __call__                  │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/core.py:1055 in main                      │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/core.py:1657 in invoke                    │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/core.py:1404 in invoke                    │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/core.py:760 in invoke                     │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/click/decorators.py:38 in new_func              │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/cli/build/__init__.py:101 in build        │
│                                                                                                  │
│    98 │   │   │   │   dependencies.extend(builder.config.dependencies)                           │
│    99 │   │   │                                                                                  │
│   100 │   │   │   with app.status_waiting('Setting up build environment') as status:             │
│ ❱ 101 │   │   │   │   with environment.build_environment(dependencies) as build_environment:     │
│   102 │   │   │   │   │   status.stop()                                                          │
│   103 │   │   │   │   │                                                                          │
│   104 │   │   │   │   │   build_process = environment.get_build_process(                         │
│                                                                                                  │
│ /usr/lib/python3.10/contextlib.py:135 in __enter__                                               │
│                                                                                                  │
│   132 │   │   # they are only needed for recreation, which is not possible anymore               │
│   133 │   │   del self.args, self.kwds, self.func                                                │
│   134 │   │   try:                                                                               │
│ ❱ 135 │   │   │   return next(self.gen)                                                          │
│   136 │   │   except StopIteration:                                                              │
│   137 │   │   │   raise RuntimeError("generator didn't yield") from None                         │
│   138                                                                                            │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/env/virtual.py:89 in build_environment    │
│                                                                                                  │
│    86 │                                                                                          │
│    87 │   @contextmanager                                                                        │
│    88 │   def build_environment(self, dependencies):                                             │
│ ❱  89 │   │   with self.get_env_vars(), TempVirtualEnv(self.parent_python, self.platform, self   │
│    90 │   │   │   self.platform.check_command(self.construct_pip_install_command(dependencies)   │
│    91 │   │   │                                                                                  │
│    92 │   │   │   yield                                                                          │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/venv/core.py:115 in __enter__             │
│                                                                                                  │
│   112 │                                                                                          │
│   113 │   def __enter__(self):                                                                   │
│   114 │   │   self.create(self.parent_python)                                                    │
│ ❱ 115 │   │   return super().__enter__()                                                         │
│   116 │                                                                                          │
│   117 │   def __exit__(self, exc_type, exc_value, traceback):                                    │
│   118 │   │   super().__exit__(exc_type, exc_value, traceback)                                   │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/venv/core.py:94 in __enter__              │
│                                                                                                  │
│    91 │   │   return self.python_info.sys_path                                                   │
│    92 │                                                                                          │
│    93 │   def __enter__(self):                                                                   │
│ ❱  94 │   │   self.activate()                                                                    │
│    95 │   │   return self                                                                        │
│    96 │                                                                                          │
│    97 │   def __exit__(self, exc_type, exc_value, traceback):                                    │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/venv/core.py:31 in activate               │
│                                                                                                  │
│    28 │   │   if old_path is None:                                                               │
│    29 │   │   │   os.environ['PATH'] = str(self.executables_directory)                           │
│    30 │   │   else:                                                                              │
│ ❱  31 │   │   │   os.environ['PATH'] = f'{self.executables_directory}{os.pathsep}{old_path}'     │
│    32 │   │                                                                                      │
│    33 │   │   for env_var in self.IGNORED_ENV_VARS:                                              │
│    34 │   │   │   self._env_vars_to_restore[env_var] = os.environ.pop(env_var, None)             │
│                                                                                                  │
│ /home/frappe/.local/lib/python3.10/site-packages/hatch/venv/core.py:81 in executables_directory  │
│                                                                                                  │
│    78 │   │   │   │   else:                                                                      │
│    79 │   │   │   │   │   raise OSError('Unable to locate executables directory')                │
│    80 │   │   │   else:                                                                          │
│ ❱  81 │   │   │   │   raise OSError('Unable to locate executables directory')                    │
│    82 │   │                                                                                      │
│    83 │   │   return self._executables_directory                                                 │
│    84                                                                                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
OSError: Unable to locate executables directory
```

Generated `pyproject.toml` file:

```toml
[project]
name = "frappe-bench"
description = "CLI to manage Multi-tenant deployments for Frappe apps"
readme = "README.md"
license = "GPL-3.0-only"
requires-python = ">=3.7"
authors = [
    { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io" },
]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Environment :: Console",
    "License :: OSI Approved :: GNU Affero General Public License v3",
    "Natural Language :: English",
    "Operating System :: MacOS",
    "Operating System :: OS Independent",
    "Topic :: Software Development :: Build Tools",
    "Topic :: Software Development :: User Interfaces",
    "Topic :: System :: Installation/Setup",
]
dependencies = [
    "Click>=7.0",
    "GitPython~=2.1.15",
    "honcho",
    "Jinja2~=3.0.3",
    "python-crontab~=2.4.0",
    "requests",
    "semantic-version~=2.8.2",
    "setuptools>40.9.0",
    "tomli;python_version<'3.11'",
]
dynamic = [
    "version",
]

[project.scripts]
bench = "bench.cli:cli"

[project.urls]
Changelog = "https://github.com/frappe/bench/releases"
Documentation = "https://frappeframework.com/docs/user/en/bench"
Homepage = "https://frappe.io/bench"
Source = "https://github.com/frappe/bench"

[build-system]
requires = [
    "hatchling>=1.6.0",
]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "bench/__init__.py"

[tool.hatch.build.targets.sdist]
include = [
    "/bench",
]
```

Am I missing something? Followed the docs until this point.

Discussion:

Patch:
diff --git a/docs/history.md b/docs/history.md
index 261cc07a..950fed3a 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 ***Fixed:***
 
 - Acknowledge `extra-dependencies` for the `env show` command
+- Fix locating executables within virtual environments on Debian
 
 ### [1.3.1](https://github.com/pypa/hatch/releases/tag/hatch-v1.3.1) - 2022-07-11 ### {: #hatch-v1.3.1 }
 
diff --git a/src/hatch/venv/core.py b/src/hatch/venv/core.py
index cfce9ba5..bccf179d 100644
--- a/src/hatch/venv/core.py
+++ b/src/hatch/venv/core.py
@@ -67,7 +67,7 @@ class VirtualEnv:
     @property
     def executables_directory(self):
         if self._executables_directory is None:
-            exe_dir = self.directory / 'Scripts' if self.platform.windows else self.directory / 'bin'
+            exe_dir = self.directory / ('Scripts' if self.platform.windows else 'bin')
             if exe_dir.is_dir():
                 self._executables_directory = exe_dir
             # PyPy
@@ -76,9 +76,16 @@ class VirtualEnv:
                 if exe_dir.is_dir():
                     self._executables_directory = exe_dir
                 else:
-                    raise OSError('Unable to locate executables directory')
+                    raise OSError(f'Unable to locate executables directory within: {self.directory}')
+            # Debian
+            elif (self.directory / 'local').is_dir():  # no cov
+                exe_dir = self.directory / 'local' / 'bin'
+                if exe_dir.is_dir():
+                    self._executables_directory = exe_dir
+                else:
+                    raise OSError(f'Unable to locate executables directory within: {self.directory}')
             else:
-                raise OSError('Unable to locate executables directory')
+                raise OSError(f'Unable to locate executables directory within: {self.directory}')
 
         return self._executables_directory
 


Test patch:
diff --git a/backend/tests/downstream/integrate.py b/backend/tests/downstream/integrate.py
index 1a30a904..4d7ac203 100644
--- a/backend/tests/downstream/integrate.py
+++ b/backend/tests/downstream/integrate.py
@@ -78,6 +78,28 @@ def temp_dir():
         shutil.rmtree(d, ignore_errors=False, onerror=handle_remove_readonly)
 
 
+def get_venv_exe_dir(venv_dir):
+    exe_dir = os.path.join(venv_dir, 'Scripts' if ON_WINDOWS else 'bin')
+    if os.path.isdir(exe_dir):
+        return exe_dir
+    # PyPy
+    elif ON_WINDOWS:
+        exe_dir = os.path.join(venv_dir, 'bin')
+        if os.path.isdir(exe_dir):
+            return exe_dir
+        else:
+            raise OSError(f'Unable to locate executables directory within: {venv_dir}')
+    # Debian
+    elif os.path.isdir(os.path.join(venv_dir, 'local')):
+        exe_dir = os.path.join(venv_dir, 'local', 'bin')
+        if os.path.isdir(exe_dir):
+            return exe_dir
+        else:
+            raise OSError(f'Unable to locate executables directory within: {venv_dir}')
+    else:
+        raise OSError(f'Unable to locate executables directory within: {venv_dir}')
+
+
 def main():
     original_backend_path = os.path.dirname(os.path.dirname(HERE))
     with temp_dir() as links_dir, temp_dir() as build_dir:
@@ -195,9 +217,7 @@ def main():
 
                 env_vars = dict(test_data.get('env_vars', {}))
                 env_vars['VIRTUAL_ENV'] = venv_dir
-                env_vars[
-                    'PATH'
-                ] = f'{os.path.join(venv_dir, "Scripts" if ON_WINDOWS else "bin")}{os.pathsep}{os.environ["PATH"]}'
+                env_vars['PATH'] = f'{get_venv_exe_dir(venv_dir)}{os.pathsep}{os.environ["PATH"]}'
                 env_vars['PIP_CONSTRAINT'] = constraints_file
                 with EnvVars(env_vars, ignore=('__PYVENV_LAUNCHER__', 'PYTHONHOME')):
                     print('--> Installing project')
diff --git a/tests/venv/test_core.py b/tests/venv/test_core.py
index 503e23ed..21bd80d3 100644
--- a/tests/venv/test_core.py
+++ b/tests/venv/test_core.py
@@ -1,5 +1,6 @@
 import json
 import os
+import re
 import sys
 
 import pytest
@@ -14,7 +15,7 @@ def test_initialization_does_not_create(temp_dir, platform):
 
     assert not venv.exists()
 
-    with pytest.raises(OSError, match='Unable to locate executables directory'):
+    with pytest.raises(OSError, match=f'Unable to locate executables directory within: {re.escape(str(venv_dir))}'):
         _ = venv.executables_directory