Repository: pypa/build
Pull request #314 (2021-06-18 15:51:43)

env: override default scheme for Apple Python venv

layday: Squashed.
FFY00: @gaborbernat could you have a look before merging?

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index abfc017..8ba76d4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -8,6 +8,7 @@ Unreleased
 
 - Add ``ProjectBuilder.metadata_path`` helper (`PR #303`_, Fixes `#301`_)
 - Added a ``build.__main__.build_package_via_sdist`` method (`PR #304`_)
+- Use appropriate installation scheme for Apple Python venvs (`PR #314`_, Fixes `#310`_)
 
 Breaking Changes
 ----------------
@@ -20,8 +21,10 @@ Breaking Changes
 .. _PR #303: https://github.com/pypa/build/pull/303
 .. _PR #304: https://github.com/pypa/build/pull/304
 .. _PR #312: https://github.com/pypa/build/pull/312
+.. _PR #314: https://github.com/pypa/build/pull/314
 .. _#257: https://github.com/pypa/build/issues/257
 .. _#301: https://github.com/pypa/build/issues/301
+.. _#310: https://github.com/pypa/build/issues/310
 
 
 
diff --git a/src/build/env.py b/src/build/env.py
index 5bb0bc7..e9beeeb 100644
--- a/src/build/env.py
+++ b/src/build/env.py
@@ -246,20 +246,22 @@ if sys.version_info >= (3,):  # noqa: C901
         """
         config_vars = sysconfig.get_config_vars().copy()  # globally cached, copy before altering it
         config_vars['base'] = path
-        env_scripts = sysconfig.get_path('scripts', vars=config_vars)
-        if not env_scripts:
-            raise RuntimeError("Couldn't get environment scripts path")
-        exe = 'pypy3' if platform.python_implementation() == 'PyPy' else 'python'
-        if os.name == 'nt':
-            exe = '{}.exe'.format(exe)
-        executable = os.path.join(env_scripts, exe)
+        # The Python that ships with the macOS developer tools varies the
+        # default scheme depending on whether the ``sys.prefix`` is part of a framework.
+        # The framework "osx_framework_library" scheme
+        # can't be used to expand the paths in a venv, which
+        # can happen if build itself is not installed in a venv.
+        # If the Apple-custom "osx_framework_library" scheme is available
+        # we enforce "posix_prefix", the venv scheme, for isolated envs.
+        if 'osx_framework_library' in sysconfig.get_scheme_names():
+            paths = sysconfig.get_paths(scheme='posix_prefix', vars=config_vars)
+        else:
+            paths = sysconfig.get_paths(vars=config_vars)
+        executable = os.path.join(paths['scripts'], 'python.exe' if os.name == 'nt' else 'python')
         if not os.path.exists(executable):
             raise RuntimeError('Virtual environment creation failed, executable {} missing'.format(executable))
 
-        purelib = sysconfig.get_path('purelib', vars=config_vars)
-        if not purelib:
-            raise RuntimeError("Couldn't get environment purelib folder")
-        return executable, env_scripts, purelib
+        return executable, paths['scripts'], paths['purelib']
 
 
 __all__ = (


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
index 8545645..1eb3cba 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -53,47 +53,30 @@ def test_isolated_environment_install(mocker):
 
 @pytest.mark.skipif(IS_PY2, reason='venv module used on Python 3 only')
 @pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv')
-def test_fail_to_get_script_path(mocker):
+@pytest.mark.skipif(sys.platform != 'darwin', reason='workaround for Apple Python')
+def test_can_get_venv_paths_with_conflicting_default_scheme(mocker):
     mocker.patch.object(build.env, 'virtualenv', None)
-    get_path = mocker.patch('sysconfig.get_path', return_value=None)
-    with pytest.raises(RuntimeError, match="Couldn't get environment scripts path"):
-        env = build.env.IsolatedEnvBuilder()
-        with env:
-            pass
-    assert not os.path.exists(env._path)
-    assert get_path.call_count == 1
-
-
-@pytest.mark.skipif(IS_PY2, reason='venv module used on Python 3 only')
-@pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv')
-def test_fail_to_get_purepath(mocker):
-    mocker.patch.object(build.env, 'virtualenv', None)
-    sysconfig_get_path = sysconfig.get_path
-    mocker.patch(
-        'sysconfig.get_path',
-        side_effect=lambda path, *args, **kwargs: '' if path == 'purelib' else sysconfig_get_path(path, *args, **kwargs),
-    )
-
-    with pytest.raises(RuntimeError, match="Couldn't get environment purelib folder"):
-        with build.env.IsolatedEnvBuilder():
-            pass
+    get_scheme_names = mocker.patch('sysconfig.get_scheme_names', return_value=('osx_framework_library',))
+    with build.env.IsolatedEnvBuilder():
+        pass
+    assert get_scheme_names.call_count == 1
 
 
 @pytest.mark.skipif(IS_PY2, reason='venv module used on Python 3 only')
 @pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv')
 def test_executable_missing_post_creation(mocker):
     mocker.patch.object(build.env, 'virtualenv', None)
-    original_get_path = sysconfig.get_path
+    original_get_paths = sysconfig.get_paths
 
-    def _get_path(name, vars):  # noqa
+    def _get_paths(vars):  # noqa
         shutil.rmtree(vars['base'])
-        return original_get_path(name, vars=vars)
+        return original_get_paths(vars=vars)
 
-    get_path = mocker.patch('sysconfig.get_path', side_effect=_get_path)
+    get_paths = mocker.patch('sysconfig.get_paths', side_effect=_get_paths)
     with pytest.raises(RuntimeError, match='Virtual environment creation failed, executable .* missing'):
         with build.env.IsolatedEnvBuilder():
             pass
-    assert get_path.call_count == 1
+    assert get_paths.call_count == 1
 
 
 def test_isolated_env_abstract():