diff --git a/distutils/command/build.py b/distutils/command/build.py index efa41359..49a09ce2 100644 --- a/distutils/command/build.py +++ b/distutils/command/build.py @@ -5,6 +5,7 @@ from __future__ import annotations import os +import re import sys import sysconfig from collections.abc import Callable @@ -77,16 +78,18 @@ def finalize_options(self) -> None: # noqa: C901 "using './configure --help' on your platform)" ) - plat_specifier = f".{self.plat_name}-{sys.implementation.cache_tag}" + soabi = sysconfig.get_config_var('SOABI') + plat_specifier = f".{self.plat_name}-{soabi or sys.implementation.cache_tag}" # Python 3.13+ with --disable-gil shouldn't share build directories - if sysconfig.get_config_var('Py_GIL_DISABLED'): + if not soabi and sysconfig.get_config_var('Py_GIL_DISABLED'): plat_specifier += 't' - # Make it so Python 2.x and Python 2.x with --with-pydebug don't - # share the same build directories. Doing so confuses the build - # process for C modules - if hasattr(sys, 'gettotalrefcount'): + # SOABI does not distinguish debug builds on Windows. + py_debug = sysconfig.get_config_var('Py_DEBUG') + if py_debug is None: + py_debug = hasattr(sys, 'gettotalrefcount') + if py_debug: plat_specifier += '-pydebug' # 'build_purelib' and 'build_platlib' just default to 'lib' and @@ -111,9 +114,17 @@ def finalize_options(self) -> None: # noqa: C901 if self.build_temp is None: self.build_temp = os.path.join(self.build_base, 'temp' + plat_specifier) if self.build_scripts is None: + version = re.fullmatch( + r'(\d)\.?([0-9]+)', str(sysconfig.get_config_var('VERSION')) + ) + python_version = ( + f'{version[1]}.{version[2]}' + if version + else f'{sys.version_info.major}.{sys.version_info.minor}' + ) self.build_scripts = os.path.join( self.build_base, - f'scripts-{sys.version_info.major}.{sys.version_info.minor}', + f'scripts-{python_version}', ) if self.executable is None and sys.executable: diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index ae2ef8e4..32052406 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -3,8 +3,11 @@ import os import sys from distutils.command.build import build +from distutils.extension import Extension from distutils.tests import support -from sysconfig import get_config_var, get_platform +from sysconfig import get_config_var, get_config_vars, get_platform + +import pytest class TestBuild(support.TempdirManager): @@ -20,11 +23,10 @@ def test_finalize_options(self): wanted = os.path.join(cmd.build_base, 'lib') assert cmd.build_purelib == wanted - # build_platlib is 'build/lib.platform-cache_tag[-pydebug]' - # examples: - # build/lib.macosx-10.3-i386-cpython39 - plat_spec = f'.{cmd.plat_name}-{sys.implementation.cache_tag}' - if get_config_var('Py_GIL_DISABLED'): + # build_platlib is 'build/lib.platform-soabi[-pydebug]' + soabi = get_config_var('SOABI') + plat_spec = f'.{cmd.plat_name}-{soabi or sys.implementation.cache_tag}' + if not soabi and get_config_var('Py_GIL_DISABLED'): plat_spec += 't' if hasattr(sys, 'gettotalrefcount'): assert cmd.build_platlib.endswith('-pydebug') @@ -47,3 +49,111 @@ def test_finalize_options(self): # executable is os.path.normpath(sys.executable) assert cmd.executable == os.path.normpath(sys.executable) + + @pytest.mark.parametrize( + ('soabi', 'gil_disabled', 'debug'), + [ + ('cpython-313-aarch64-linux-gnu', 0, 0), + ('cpython-314-aarch64-linux-gnu', 0, 0), + ('cpython-314d-aarch64-linux-gnu', 0, 1), + ('cpython-314t-aarch64-linux-gnu', 1, 0), + ('cpython-314td-aarch64-linux-gnu', 1, 1), + ('cp314-win_arm64', 0, 0), + ('pypy311-pp80-aarch64-linux-gnu', 0, 0), + ('graalpy231-310-native-aarch64-linux', 0, 0), + ], + ) + def test_target_abi(self, monkeypatch, soabi, gil_disabled, debug): + monkeypatch.setitem(get_config_vars(), 'SOABI', soabi) + monkeypatch.setitem(get_config_vars(), 'Py_GIL_DISABLED', gil_disabled) + monkeypatch.setitem(get_config_vars(), 'Py_DEBUG', debug) + _pkg_dir, dist = self.create_dist(ext_modules=[Extension('foo', ['foo.c'])]) + cmd = build(dist) + cmd.finalize_options() + + tag = soabi + ('-pydebug' if debug else '') + assert cmd.build_platlib == os.path.join('build', f'lib.{cmd.plat_name}-{tag}') + assert cmd.build_temp == os.path.join('build', f'temp.{cmd.plat_name}-{tag}') + assert cmd.build_lib == cmd.build_platlib + + @pytest.mark.parametrize('debug', [0, 1]) + @pytest.mark.parametrize('native_debug', [False, True]) + def test_target_debug(self, monkeypatch, debug, native_debug): + monkeypatch.setitem(get_config_vars(), 'SOABI', 'cp314-win_arm64') + monkeypatch.setitem(get_config_vars(), 'Py_DEBUG', debug) + if native_debug: + monkeypatch.setattr(sys, 'gettotalrefcount', lambda: 0, raising=False) + else: + monkeypatch.delattr(sys, 'gettotalrefcount', raising=False) + _pkg_dir, dist = self.create_dist() + cmd = build(dist) + cmd.finalize_options() + + tag = 'cp314-win_arm64' + ('-pydebug' if debug else '') + assert cmd.build_platlib == os.path.join('build', f'lib.{cmd.plat_name}-{tag}') + assert cmd.build_temp == os.path.join('build', f'temp.{cmd.plat_name}-{tag}') + + @pytest.mark.parametrize( + ('version', 'expected'), + [('3.13', '3.13'), ('3.15', '3.15'), ('315', '3.15'), (315, '3.15')], + ) + def test_target_version(self, monkeypatch, version, expected): + monkeypatch.setitem(get_config_vars(), 'VERSION', version) + _pkg_dir, dist = self.create_dist() + cmd = build(dist) + cmd.finalize_options() + + assert cmd.build_scripts == os.path.join('build', f'scripts-{expected}') + + @pytest.mark.parametrize('version', [None, '', 'invalid']) + def test_missing_version(self, monkeypatch, version): + monkeypatch.setitem(get_config_vars(), 'VERSION', version) + _pkg_dir, dist = self.create_dist() + cmd = build(dist) + cmd.finalize_options() + + expected = f'{sys.version_info.major}.{sys.version_info.minor}' + assert cmd.build_scripts == os.path.join('build', f'scripts-{expected}') + + @pytest.mark.parametrize('debug', [None, 0, 1]) + @pytest.mark.parametrize('native_debug', [False, True]) + @pytest.mark.parametrize('gil_disabled', [0, 1]) + def test_missing_soabi(self, monkeypatch, debug, native_debug, gil_disabled): + monkeypatch.setitem(get_config_vars(), 'SOABI', None) + monkeypatch.setitem(get_config_vars(), 'Py_DEBUG', debug) + monkeypatch.setitem(get_config_vars(), 'Py_GIL_DISABLED', gil_disabled) + if native_debug: + monkeypatch.setattr(sys, 'gettotalrefcount', lambda: 0, raising=False) + else: + monkeypatch.delattr(sys, 'gettotalrefcount', raising=False) + _pkg_dir, dist = self.create_dist() + cmd = build(dist) + cmd.finalize_options() + + tag = str(sys.implementation.cache_tag) + if gil_disabled: + tag += 't' + expected_debug = debug if debug is not None else native_debug + if expected_debug: + tag += '-pydebug' + assert cmd.build_platlib == os.path.join('build', f'lib.{cmd.plat_name}-{tag}') + assert cmd.build_temp == os.path.join('build', f'temp.{cmd.plat_name}-{tag}') + + def test_directory_overrides(self, monkeypatch): + monkeypatch.setitem(get_config_vars(), 'SOABI', 'cpython-315-aarch64-linux-gnu') + monkeypatch.setitem(get_config_vars(), 'VERSION', '3.15') + _pkg_dir, dist = self.create_dist() + cmd = build(dist) + directories = { + 'build_purelib': 'custom-purelib', + 'build_platlib': 'custom-platlib', + 'build_lib': 'custom-lib', + 'build_temp': 'custom-temp', + 'build_scripts': 'custom-scripts', + } + for name, value in directories.items(): + setattr(cmd, name, value) + cmd.finalize_options() + + for name, value in directories.items(): + assert getattr(cmd, name) == value diff --git a/newsfragments/+target-build-directories.bugfix.rst b/newsfragments/+target-build-directories.bugfix.rst new file mode 100644 index 00000000..172365fa --- /dev/null +++ b/newsfragments/+target-build-directories.bugfix.rst @@ -0,0 +1 @@ +Use the configured Python's SOABI and debug setting for platform-specific build directories, and its version for the script build directory. Default platform-specific directory names now include SOABI when available instead of the running interpreter's cache tag. (pypa/distutils#435)