Repository: pypa/hatch

Issue:
Error: AttributeError: 'NoneType' object has no attribute 'executable'
# Context
I tested on
- *Python*: 3.10.5, 3.10.7
- *OS*: Ubuntu LTS, Windows 11, WSL2

# What happened

i'm running `hatch build -t wheel`

Windows Error Log
```
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮                                                                                                       
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\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                                                                                            │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\core.py:1130 in __call__          │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\core.py:1055 in main              │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\core.py:1657 in invoke            │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\core.py:1404 in invoke            │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\core.py:760 in invoke             │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\click\decorators.py:38 in new_func      │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\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(                         │
│                                                                                                  │
│ C:\Users\lifep\AppData\Local\Programs\Python\Python310\lib\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                                                                                            │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\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                                                                          │
│                                                                                                  │
│ C:\Users\lifep\.local\pipx\venvs\hatch\lib\site-packages\hatch\env\virtual.py:133 in             │
│ parent_python                                                                                    │
│                                                                                                  │
│   130 │   │   │   else:                                                                          │
│   131 │   │   │   │   from virtualenv.discovery.builtin import get_interpreter                   │
│   132 │   │   │   │                                                                              │
│ ❱ 133 │   │   │   │   python_executable = get_interpreter(python_choice, ()).executable          │
│   134 │   │   │                                                                                  │
│   135 │   │   │   self._parent_python = python_executable                                        │
│   136                                                                                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'NoneType' object has no attribute 'executable'
```

# My Attempt
1. reinstall, upgrade hatch to latest
2. tried on a few setups(OS, python version)
3. read #120 

Discussion:

Patch:
diff --git a/docs/history.md b/docs/history.md
index db264b5f..e6e8afae 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - Environments now respect dynamically defined project dependencies
 - The `dep hash` and all `dep show` commands now respect dynamically defined project dependencies
 - The `env show`, `dep hash`, and all `dep show` commands now honor context formatting
+- Build environment compatibility is now checked before use
 - Decreasing verbosity now has no affect on output that should always be displayed
 - Handle more edge cases in the `setuptools` migration script
 - Environments now respect user defined environment variables for context formatting
diff --git a/src/hatch/cli/build/__init__.py b/src/hatch/cli/build/__init__.py
index b8953938..94b24424 100644
--- a/src/hatch/cli/build/__init__.py
+++ b/src/hatch/cli/build/__init__.py
@@ -81,6 +81,10 @@ def build(app, location, targets, hooks_only, no_hooks, ext, clean, clean_hooks_
 
     with app.project.location.as_cwd(env_vars):
         environment = app.get_environment()
+        try:
+            environment.check_compatibility()
+        except Exception as e:
+            app.abort(f'Environment `{environment.name}` is incompatible: {e}')
 
         for i, target in enumerate(targets):
             # Separate targets with a blank line
diff --git a/src/hatch/cli/project/metadata.py b/src/hatch/cli/project/metadata.py
index 28d19d94..6ffc48ed 100644
--- a/src/hatch/cli/project/metadata.py
+++ b/src/hatch/cli/project/metadata.py
@@ -18,6 +18,10 @@ def metadata(app, field):
     else:
         with app.project.location.as_cwd():
             environment = app.get_environment()
+            try:
+                environment.check_compatibility()
+            except Exception as e:
+                app.abort(f'Environment `{environment.name}` is incompatible: {e}')
 
             with app.status_waiting(
                 'Setting up build environment for missing dependencies',
diff --git a/src/hatch/cli/version/__init__.py b/src/hatch/cli/version/__init__.py
index ee1086ae..ee06d184 100644
--- a/src/hatch/cli/version/__init__.py
+++ b/src/hatch/cli/version/__init__.py
@@ -36,6 +36,10 @@ def version(app, desired_version):
             app.display_info(f'New: {updated_version}')
         else:
             environment = app.get_environment()
+            try:
+                environment.check_compatibility()
+            except Exception as e:
+                app.abort(f'Environment `{environment.name}` is incompatible: {e}')
 
             with app.status_waiting(
                 'Setting up build environment for missing build dependencies',
diff --git a/src/hatch/utils/dep.py b/src/hatch/utils/dep.py
index b382c84c..195ab7e0 100644
--- a/src/hatch/utils/dep.py
+++ b/src/hatch/utils/dep.py
@@ -25,6 +25,11 @@ def get_project_dependencies_complex(environment):
         dependencies_complex.update(environment.metadata.core.dependencies_complex)
         optional_dependencies_complex.update(environment.metadata.core.optional_dependencies_complex)
     else:
+        try:
+            environment.check_compatibility()
+        except Exception as e:
+            environment.app.abort(f'Environment `{environment.name}` is incompatible: {e}')
+
         import json
 
         from packaging.requirements import Requirement


Test patch:
diff --git a/tests/cli/build/test_build.py b/tests/cli/build/test_build.py
index 15333796..f8685eae 100644
--- a/tests/cli/build/test_build.py
+++ b/tests/cli/build/test_build.py
@@ -64,6 +64,32 @@ def test_backend_not_build_dependency(hatch, temp_dir, helpers):
     )
 
 
+@pytest.mark.allow_backend_process
+def test_incompatible_environment(hatch, temp_dir, helpers):
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+        assert result.exit_code == 0, result.output
+
+    path = temp_dir / 'my-app'
+
+    project = Project(path)
+    helpers.update_project_environment(
+        project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+    )
+
+    with path.as_cwd():
+        result = hatch('build')
+
+    assert result.exit_code == 1, result.output
+    assert result.output == helpers.dedent(
+        """
+        Environment `default` is incompatible: cannot locate Python: 9000
+        """
+    )
+
+
 def test_unknown_targets(hatch, temp_dir, helpers):
     project_name = 'My.App'
 
diff --git a/tests/cli/dep/show/test_requirements.py b/tests/cli/dep/show/test_requirements.py
index 7cacee29..dc98d564 100644
--- a/tests/cli/dep/show/test_requirements.py
+++ b/tests/cli/dep/show/test_requirements.py
@@ -1,6 +1,35 @@
 from hatch.project.core import Project
 
 
+def test_incompatible_environment(hatch, temp_dir, helpers):
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+        assert result.exit_code == 0, result.output
+
+    path = temp_dir / 'my-app'
+
+    project = Project(path)
+    config = dict(project.raw_config)
+    config['build-system']['requires'].append('foo')
+    config['project']['dynamic'].append('dependencies')
+    project.save_config(config)
+    helpers.update_project_environment(
+        project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+    )
+
+    with path.as_cwd():
+        result = hatch('dep', 'show', 'requirements')
+
+    assert result.exit_code == 1, result.output
+    assert result.output == helpers.dedent(
+        """
+        Environment `default` is incompatible: cannot locate Python: 9000
+        """
+    )
+
+
 def test_project_only(hatch, helpers, temp_dir, config_file):
     config_file.model.template.plugins['default']['tests'] = False
     config_file.save()
diff --git a/tests/cli/dep/show/test_table.py b/tests/cli/dep/show/test_table.py
index bd1993dd..acb9ef49 100644
--- a/tests/cli/dep/show/test_table.py
+++ b/tests/cli/dep/show/test_table.py
@@ -10,6 +10,35 @@ def terminal_width():
         yield
 
 
+def test_incompatible_environment(hatch, temp_dir, helpers):
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+        assert result.exit_code == 0, result.output
+
+    path = temp_dir / 'my-app'
+
+    project = Project(path)
+    config = dict(project.raw_config)
+    config['build-system']['requires'].append('foo')
+    config['project']['dynamic'].append('dependencies')
+    project.save_config(config)
+    helpers.update_project_environment(
+        project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+    )
+
+    with path.as_cwd():
+        result = hatch('dep', 'show', 'table')
+
+    assert result.exit_code == 1, result.output
+    assert result.output == helpers.dedent(
+        """
+        Environment `default` is incompatible: cannot locate Python: 9000
+        """
+    )
+
+
 def test_project_only(hatch, helpers, temp_dir, config_file):
     config_file.model.template.plugins['default']['tests'] = False
     config_file.save()
diff --git a/tests/cli/dep/test_hash.py b/tests/cli/dep/test_hash.py
index f25aafa8..cba133d2 100644
--- a/tests/cli/dep/test_hash.py
+++ b/tests/cli/dep/test_hash.py
@@ -3,6 +3,35 @@ from hashlib import sha256
 from hatch.project.core import Project
 
 
+def test_incompatible_environment(hatch, temp_dir, helpers):
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+        assert result.exit_code == 0, result.output
+
+    path = temp_dir / 'my-app'
+
+    project = Project(path)
+    config = dict(project.raw_config)
+    config['build-system']['requires'].append('foo')
+    config['project']['dynamic'].append('dependencies')
+    project.save_config(config)
+    helpers.update_project_environment(
+        project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+    )
+
+    with path.as_cwd():
+        result = hatch('dep', 'hash')
+
+    assert result.exit_code == 1, result.output
+    assert result.output == helpers.dedent(
+        """
+        Environment `default` is incompatible: cannot locate Python: 9000
+        """
+    )
+
+
 def test_all(hatch, helpers, temp_dir):
     project_name = 'My.App'
 
diff --git a/tests/cli/project/test_metadata.py b/tests/cli/project/test_metadata.py
index 9cf3a2b9..f3910378 100644
--- a/tests/cli/project/test_metadata.py
+++ b/tests/cli/project/test_metadata.py
@@ -143,6 +143,34 @@ class TestBuildDependenciesInstalled:
 
 
 class TestBuildDependenciesMissing:
+    @pytest.mark.allow_backend_process
+    def test_incompatible_environment(self, hatch, temp_dir, helpers):
+        project_name = 'My.App'
+
+        with temp_dir.as_cwd():
+            result = hatch('new', project_name)
+            assert result.exit_code == 0, result.output
+
+        path = temp_dir / 'my-app'
+
+        project = Project(path)
+        config = dict(project.raw_config)
+        config['build-system']['requires'].append('foo')
+        project.save_config(config)
+        helpers.update_project_environment(
+            project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+        )
+
+        with path.as_cwd():
+            result = hatch('project', 'metadata')
+
+        assert result.exit_code == 1, result.output
+        assert result.output == helpers.dedent(
+            """
+            Environment `default` is incompatible: cannot locate Python: 9000
+            """
+        )
+
     def test_default_all(self, hatch, temp_dir, helpers):
         project_name = 'My.App'
 
diff --git a/tests/cli/version/test_version.py b/tests/cli/version/test_version.py
index 5cb34a00..a3cb0ed3 100644
--- a/tests/cli/version/test_version.py
+++ b/tests/cli/version/test_version.py
@@ -2,6 +2,34 @@ from hatch.project.core import Project
 from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT
 
 
+def test_incompatible_environment(hatch, temp_dir, helpers):
+    project_name = 'My.App'
+
+    with temp_dir.as_cwd():
+        result = hatch('new', project_name)
+        assert result.exit_code == 0, result.output
+
+    path = temp_dir / 'my-app'
+
+    project = Project(path)
+    config = dict(project.raw_config)
+    config['build-system']['requires'].append('foo')
+    project.save_config(config)
+    helpers.update_project_environment(
+        project, 'default', {'skip-install': True, 'python': '9000', **project.config.envs['default']}
+    )
+
+    with path.as_cwd():
+        result = hatch('version')
+
+    assert result.exit_code == 1, result.output
+    assert result.output == helpers.dedent(
+        """
+        Environment `default` is incompatible: cannot locate Python: 9000
+        """
+    )
+
+
 def test_show_dynamic(hatch, temp_dir):
     project_name = 'My.App'