From fd45b9024f6dd4d982c6c0bae0167a1993fc26f3 Mon Sep 17 00:00:00 2001 From: Bibek Date: Sun, 29 Mar 2026 11:17:56 -0400 Subject: [PATCH] fix: use per-extension subdirectory in build_temp to prevent parallel build race When building multiple extensions in parallel with -j, all extensions shared the same build_temp directory. Object files with the same name would overwrite each other, causing random build failures. Fixed: derive a per-extension output_dir from ext.name.split('.'). e.g. pkg.ext1 -> build/temp.../pkg/ext1/ pkg.ext2 -> build/temp.../pkg/ext2/ Fixes #5196 --- setuptools/_distutils/command/build_ext.py | 5 ++- setuptools/tests/test_build_ext.py | 43 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/setuptools/_distutils/command/build_ext.py b/setuptools/_distutils/command/build_ext.py index df623d7e0e..b13cf6ca6e 100644 --- a/setuptools/_distutils/command/build_ext.py +++ b/setuptools/_distutils/command/build_ext.py @@ -560,10 +560,11 @@ def build_extension(self, ext) -> None: macros = ext.define_macros[:] for undef in ext.undef_macros: macros.append((undef,)) - + + ext_build_temp = os.path.join(self.build_temp, *ext.name.split('.')) objects = self.compiler.compile( sources, - output_dir=self.build_temp, + output_dir=ext_build_temp, macros=macros, include_dirs=ext.include_dirs, debug=self.debug, diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index c7b60ac32f..1c093224fc 100644 --- a/setuptools/tests/test_build_ext.py +++ b/setuptools/tests/test_build_ext.py @@ -18,6 +18,8 @@ import distutils.command.build_ext as orig from distutils.sysconfig import get_config_var +from unittest.mock import MagicMock + IS_PYPY = '__pypy__' in sys.builtin_module_names @@ -291,3 +293,44 @@ def test_build_ext_config_handling(tmpdir_cwd): data_stream=(0, 2), ) assert code == 0, f'\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}' + +def test_parallel_build_uses_per_extension_temp_dir(tmp_path): + """Each extension compiles into its own build_temp subdir (issue #5196).""" + from setuptools._distutils.command.build_ext import build_ext + dist = Distribution({ + 'ext_modules': [ + Extension('pkg.ext1', sources=['a.c']), + Extension('pkg.ext2', sources=['b.c']), + ] + }) + cmd = build_ext(dist) + cmd.build_temp = str(tmp_path / 'build_temp') + cmd.build_lib = str(tmp_path / 'build_lib') + cmd.inplace = False + cmd.parallel = None + cmd.force = True # skip up-to-date check + cmd.swig_opts = [] # fix NoneType error + cmd.swig_cpp = False + cmd.debug = False + + output_dirs = [] + + def fake_compile(sources, output_dir=None, **kwargs): + output_dirs.append(output_dir) + return [] + + cmd.compiler = MagicMock() + cmd.compiler.compile.side_effect = fake_compile + cmd.compiler.link_shared_object = MagicMock() + cmd.get_ext_fullpath = MagicMock(return_value=str(tmp_path / 'out.so')) + cmd.get_libraries = MagicMock(return_value=[]) + cmd.get_export_symbols = MagicMock(return_value=[]) + + for ext in dist.ext_modules: + cmd.build_extension(ext) + + assert len(output_dirs) == 2 + assert output_dirs[0] != output_dirs[1], \ + "Both extensions use the same build_temp — race condition!" + assert output_dirs[0].endswith(os.path.join('pkg', 'ext1')) + assert output_dirs[1].endswith(os.path.join('pkg', 'ext2'))