Repository: pypa/hatch

Issue:
hatch 1.6.0 fails to build if user has no home
This is a regression in 1.6.0, probably caused by #524' and followups' access to `Path.home()`.

Under Windows, can be reproduced by
````
set USERPROFILE=
set HOMEPATH=
set HOMEDRIVE=

hatch build
````
which results in:

<details>
  <summary>Expand</summary>

````
┌─────────────────────────────── Traceback (most recent call last) ───────────────────────────────┐
│ C:\somepath\hatch\src\hatch\cli\__init__.py:205 in main                                         │
│                                                                                                 │
│   202                                                                                           │
│   203 def main():  # no cov                                                                     │
│   204 │   try:                                                                                  │
│ > 205 │   │   return hatch(windows_expand_args=False)                                           │
│   206 │   except Exception:                                                                     │
│   207 │   │   from rich.console import Console                                                  │
│   208                                                                                           │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\core.py:1130 in __call__                                    │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\core.py:1055 in main                                        │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\core.py:1657 in invoke                                      │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\core.py:1404 in invoke                                      │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\core.py:760 in invoke                                       │
│                                                                                                 │
│ C:\Python38\lib\site-packages\click\decorators.py:38 in new_func                                │
│                                                                                                 │
│ C:\somepath\hatch\src\hatch\cli\build\__init__.py:83 in build                                   │
│                                                                                                 │
│    80 │   │   │   return {}                                                                     │
│    81 │                                                                                         │
│    82 │   with app.project.location.as_cwd(env_vars):                                           │
│ >  83 │   │   environment = app.get_environment()                                               │
│    84 │   │                                                                                     │
│    85 │   │   for i, target in enumerate(targets):                                              │
│    86 │   │   │   # Separate targets with a blank line                                          │
│                                                                                                 │
│ C:\somepath\hatch\src\hatch\cli\application.py:55 in get_environment                            │
│                                                                                                 │
│    52 │   │                                                                                     │
│    53 │   │   data_dir = self.get_env_directory(environment_type)                               │
│    54 │   │                                                                                     │
│ >  55 │   │   return environment_class(                                                         │
│    56 │   │   │   self.project.location,                                                        │
│    57 │   │   │   self.project.metadata,                                                        │
│    58 │   │   │   env_name,                                                                     │
│                                                                                                 │
│ C:\somepath\hatch\src\hatch\env\virtual.py:35 in __init__                                       │
│                                                                                                 │
│    32 │   │   │   │   Path(chosen_directory) if isabs(chosen_directory) else (self.root / chos  │
│    33 │   │   │   )                                                                             │
│    34 │   │   # Conditions requiring a flat structure                                           │
│ >  35 │   │   elif self.root in self.data_directory.parents or self.data_directory == Path.hom  │
│    36 │   │   │   self.storage_path = self.data_directory                                       │
│    37 │   │   │   self.virtual_env_path = self.storage_path / venv_name                         │
│    38 │   │   # Otherwise the standard app path                                                 │
│                                                                                                 │
│ C:\Python38\lib\pathlib.py:1103 in home                                                         │
│                                                                                                 │
│   1100 │   │   """Return a new path pointing to the user's home directory (as                   │
│   1101 │   │   returned by os.path.expanduser('~')).                                            │
│   1102 │   │   """                                                                              │
│ > 1103 │   │   return cls(cls()._flavour.gethomedir(None))                                      │
│   1104 │                                                                                        │
│   1105 │   def samefile(self, other_path):                                                      │
│   1106 │   │   """Return whether other_path is the same or not as this file                     │
│                                                                                                 │
│ C:\Python38\lib\pathlib.py:266 in gethomedir                                                    │
│                                                                                                 │
│    263 │   │   │   │   drv = ''                                                                 │
│    264 │   │   │   userhome = drv + os.environ['HOMEPATH']                                      │
│    265 │   │   else:                                                                            │
│ >  266 │   │   │   raise RuntimeError("Can't determine home directory")                         │
│    267 │   │                                                                                    │
│    268 │   │   if username:                                                                     │
│    269 │   │   │   # Try to guess user home directory.  By default all users                    │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
RuntimeError: Can't determine home directory
````
</details>

~~I can't reproduce this quickly on Linux (because you'd have to create a home-less profile), but this is what I'm getting from Jenkins running inside a docker container without a home:~~

<details>
  <summary><del>Expand</del></summary>

````
+ python -m hatch build
Setting up build environment
╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: 
'/.local/share/hatch/env/virtual/myproject/gkfO1hd3'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: 
'/.local/share/hatch/env/virtual/myproject'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: 
'/.local/share/hatch/env/virtual'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: 
'/.local/share/hatch/env'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: '/.local/share/hatch'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
FileNotFoundError: [Errno 2] No such file or directory: '/.local/share'

During handling of the above exception, another exception occurred:

╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/hatch/cli/__init__.py:205 in main      │
│                                                                              │
│   202                                                                        │
│   203 def main():  # no cov                                                  │
│   204 │   try:                                                               │
│ ❱ 205 │   │   return hatch(windows_expand_args=False)                        │
│   206 │   except Exception:                                                  │
│   207 │   │   from rich.console import Console                               │
│   208                                                                        │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/core.py:1130 in __call__         │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/core.py:1055 in main             │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/core.py:1657 in invoke           │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/core.py:1404 in invoke           │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/core.py:760 in invoke            │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/click/decorators.py:38 in new_func     │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/hatch/cli/build/__init__.py:105 in     │
│ build                                                                        │
│                                                                              │
│   102 │   │   │   with app.status_waiting(                                   │
│   103 │   │   │   │   'Setting up build environment', condition=not environm │
│   104 │   │   │   ) as status:                                               │
│ ❱ 105 │   │   │   │   with environment.build_environment(dependencies) as bu │
│   106 │   │   │   │   │   status.stop()                                      │
│   107 │   │   │   │   │                                                      │
│   108 │   │   │   │   │   process = environment.get_build_process(           │
│                                                                              │
│ /usr/local/lib/python3.8/contextlib.py:113 in __enter__                      │
│                                                                              │
│   110 │   │   # they are only needed for recreation, which is not possible a │
│   111 │   │   del self.args, self.kwds, self.func                            │
│   112 │   │   try:                                                           │
│ ❱ 113 │   │   │   return next(self.gen)                                      │
│   114 │   │   except StopIteration:                                          │
│   115 │   │   │   raise RuntimeError("generator didn't yield") from None     │
│   116                                                                        │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/hatch/env/virtual.py:125 in            │
│ build_environment                                                            │
│                                                                              │
│   122 │   │   from hatchling.dep.core import dependencies_in_sync            │
│   123 │   │                                                                  │
│   124 │   │   if not self.build_environment_exists():                        │
│ ❱ 125 │   │   │   self.build_virtual_env.create(self.parent_python)          │
│   126 │   │                                                                  │
│   127 │   │   with self.get_env_vars(), self.build_virtual_env:              │
│   128 │   │   │   if not dependencies_in_sync(                               │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/hatch/venv/core.py:49 in create        │
│                                                                              │
│    46 │   │   # WARNING: extremely slow import                               │
│    47 │   │   from virtualenv import cli_run                                 │
│    48 │   │                                                                  │
│ ❱  49 │   │   self.directory.ensure_parent_dir_exists()                      │
│    50 │   │                                                                  │
│    51 │   │   command = [str(self.directory), '--no-download', '--no-periodi │
│    52                                                                        │
│                                                                              │
│ /tmp/appvl8c119cmyoqf/jenkins/workspace/myproject_hatch-bump-1.6.0@tmp/pytho │
│ nuserbase/lib/python3.8/site-packages/hatch/utils/fs.py:35 in                │
│ ensure_parent_dir_exists                                                     │
│                                                                              │
│    32 │   │   self.mkdir(parents=True, exist_ok=True)                        │
│    33 │                                                                      │
│    34 │   def ensure_parent_dir_exists(self):                                │
│ ❱  35 │   │   self.parent.mkdir(parents=True, exist_ok=True)                 │
│    36 │                                                                      │
│    37 │   def resolve(self, strict=False) -> Path:                           │
│    38 │   │   # https://bugs.python.org/issue38671                           │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1292 in mkdir                            │
│                                                                              │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
│ ❱ 1292 │   │   │   self.parent.mkdir(parents=True, exist_ok=True)            │
│   1293 │   │   │   self.mkdir(mode, parents=False, exist_ok=exist_ok)        │
│   1294 │   │   except OSError:                                               │
│   1295 │   │   │   # Cannot rely on checking for EEXIST, since the operating │
│                                                                              │
│ /usr/local/lib/python3.8/pathlib.py:1288 in mkdir                            │
│                                                                              │
│   1285 │   │   if self._closed:                                              │
│   1286 │   │   │   self._raise_closed()                                      │
│   1287 │   │   try:                                                          │
│ ❱ 1288 │   │   │   self._accessor.mkdir(self, mode)                          │
│   1289 │   │   except FileNotFoundError:                                     │
│   1290 │   │   │   if not parents or self.parent == self:                    │
│   1291 │   │   │   │   raise                                                 │
╰──────────────────────────────────────────────────────────────────────────────╯
PermissionError: [Errno 13] Permission denied: '/.local'
````
</details>

<ins>I can't reproduce this on Linux, see https://github.com/pypa/hatch/issues/544#issuecomment-1273084458</ins>

Had to report a [similar error](https://github.com/PyCQA/pylint/issues/6802) for pylint in June.

Discussion:
github-actions[bot]: ![Code Coverage](https://img.shields.io/badge/coverage-98.96%25-ok?style=flat)

Package | Statements
--- | ---
hatch | 97.11% (3868 / 3983)
hatchling | 97.23% (3204 / 3295)
tests | 100% (12596 / 12596)
**Summary** | 98.96% (19668 / 19874)

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/docs/history.md b/docs/history.md
index 83ad0ebf..5716074a 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ### Unreleased
 
+***Fixed:***
+
+- Computing the path to the user's home directory now gracefully falls back to `~` when it cannot be determined
+
 ### [1.6.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.6.0) - 2022-10-08 ### {: #hatch-v1.6.0 }
 
 ***Changed:***
diff --git a/docs/plugins/utilities.md b/docs/plugins/utilities.md
index 847626d9..e9c00099 100644
--- a/docs/plugins/utilities.md
+++ b/docs/plugins/utilities.md
@@ -44,6 +44,7 @@
       - exit_with_command
       - default_shell
       - modules
+      - home
       - name
       - windows
       - macos
diff --git a/src/hatch/env/virtual.py b/src/hatch/env/virtual.py
index 2a542a38..91a3aa92 100644
--- a/src/hatch/env/virtual.py
+++ b/src/hatch/env/virtual.py
@@ -32,7 +32,7 @@ class VirtualEnvironment(EnvironmentInterface):
                 Path(chosen_directory) if isabs(chosen_directory) else (self.root / chosen_directory).resolve()
             )
         # Conditions requiring a flat structure
-        elif self.root in self.data_directory.parents or self.data_directory == Path.home() / '.virtualenvs':
+        elif self.root in self.data_directory.parents or self.data_directory == self.platform.home / '.virtualenvs':
             self.storage_path = self.data_directory
             self.virtual_env_path = self.storage_path / venv_name
         # Otherwise the standard app path
@@ -82,7 +82,7 @@ class VirtualEnvironment(EnvironmentInterface):
         self.build_virtual_env.remove()
 
         # Clean up root directory of all virtual environments belonging to the project
-        if self.storage_path != Path.home() / '.virtualenvs' and self.storage_path.is_dir():
+        if self.storage_path != self.platform.home / '.virtualenvs' and self.storage_path.is_dir():
             entries = [entry.name for entry in self.storage_path.iterdir()]
             if not entries or (entries == ['.gitignore'] and self.root in self.storage_path.parents):
                 self.storage_path.remove()
diff --git a/src/hatch/utils/platform.py b/src/hatch/utils/platform.py
index 1c31accb..5e3a27d1 100644
--- a/src/hatch/utils/platform.py
+++ b/src/hatch/utils/platform.py
@@ -27,6 +27,7 @@ class Platform:
         self.__format_file_uri = None
         self.__join_command_args = None
         self.__name = None
+        self.__home = None
 
         # Whether or not an interactive status is being displayed
         self.displaying_status = False
@@ -222,6 +223,18 @@ class Platform:
 
         return self.__name
 
+    @property
+    def home(self):
+        """
+        The user's home directory as a path-like object.
+        """
+        if self.__home is None:
+            from hatch.utils.fs import Path
+
+            self.__home = Path(os.path.expanduser('~'))
+
+        return self.__home
+
 
 class LazilyLoadedModules:
     def __getattr__(self, name):


Test patch:
diff --git a/tests/utils/test_platform.py b/tests/utils/test_platform.py
index 1b99a2de..e365ea7b 100644
--- a/tests/utils/test_platform.py
+++ b/tests/utils/test_platform.py
@@ -2,6 +2,7 @@ import os
 
 import pytest
 
+from hatch.utils.fs import Path
 from hatch.utils.platform import Platform
 
 
@@ -25,6 +26,11 @@ class TestWindows:
     def test_format_for_subprocess_string_shell(self):
         assert Platform().format_for_subprocess('foo bar', shell=True) == 'foo bar'
 
+    def test_home(self):
+        platform = Platform()
+
+        assert platform.home == platform.home == Path(os.path.expanduser('~'))
+
 
 @pytest.mark.requires_macos
 class TestMacOS:
@@ -46,6 +52,11 @@ class TestMacOS:
     def test_format_for_subprocess_string_shell(self):
         assert Platform().format_for_subprocess('foo bar', shell=True) == 'foo bar'
 
+    def test_home(self):
+        platform = Platform()
+
+        assert platform.home == platform.home == Path(os.path.expanduser('~'))
+
 
 @pytest.mark.requires_linux
 class TestLinux:
@@ -66,3 +77,8 @@ class TestLinux:
 
     def test_format_for_subprocess_string_shell(self):
         assert Platform().format_for_subprocess('foo bar', shell=True) == 'foo bar'
+
+    def test_home(self):
+        platform = Platform()
+
+        assert platform.home == platform.home == Path(os.path.expanduser('~'))