diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 8367f9cda..7deb00830 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -338,6 +338,12 @@ if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_15}" ]; then patch -p1 -i "${ROOT}/patch-testinternalcapi-interpreter-extern.patch" fi +# Apply https://github.com/python/cpython/pull/149516 for a 3.15.0b1 +# site startup regression. +if [ "${PYTHON_MAJMIN_VERSION}" = "3.15" ]; then + patch -p1 -i "${ROOT}/patch-site-reentrant-startup-files-3.15.patch" +fi + # Most bits look at CFLAGS. But setup.py only looks at CPPFLAGS. # So we need to set both. CFLAGS="${EXTRA_TARGET_CFLAGS} -fPIC -I${TOOLS_PATH}/deps/include -I${TOOLS_PATH}/deps/include/ncursesw" @@ -415,10 +421,13 @@ CONFIGURE_FLAGS=" # Build a libpython3.x.so, but statically link the interpreter against # libpython. -if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_12}" ]; then - patch -p1 -i "${ROOT}/patch-python-configure-add-enable-static-libpython-for-interpreter.patch" -else - patch -p1 -i "${ROOT}/patch-python-configure-add-enable-static-libpython-for-interpreter-${PYTHON_MAJMIN_VERSION}.patch" +# Merged upstream in Python 3.15, https://github.com/python/cpython/pull/133313 +if [ -n "${PYTHON_MEETS_MAXIMUM_VERSION_3_14}" ]; then + if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_12}" ]; then + patch -p1 -i "${ROOT}/patch-python-configure-add-enable-static-libpython-for-interpreter.patch" + else + patch -p1 -i "${ROOT}/patch-python-configure-add-enable-static-libpython-for-interpreter-${PYTHON_MAJMIN_VERSION}.patch" + fi fi CONFIGURE_FLAGS="${CONFIGURE_FLAGS} --enable-static-libpython-for-interpreter" diff --git a/cpython-unix/extension-modules.yml b/cpython-unix/extension-modules.yml index ab44fcc79..797e42243 100644 --- a/cpython-unix/extension-modules.yml +++ b/cpython-unix/extension-modules.yml @@ -442,6 +442,8 @@ _remote_debugging: - _remote_debugging/code_objects.c - _remote_debugging/frame_cache.c - _remote_debugging/frames.c + - _remote_debugging/gc_stats.c + - _remote_debugging/interpreters.c - _remote_debugging/module.c - _remote_debugging/object_reading.c - _remote_debugging/subprocess.c @@ -786,6 +788,8 @@ _testlimitedcapi: sources-conditional: - source: _testlimitedcapi/codec.c minimum-python-version: "3.14" + - source: _testlimitedcapi/slots.c + minimum-python-version: "3.15" - source: _testlimitedcapi/threadstate.c minimum-python-version: "3.15" - source: _testlimitedcapi/version.c diff --git a/cpython-unix/patch-site-reentrant-startup-files-3.15.patch b/cpython-unix/patch-site-reentrant-startup-files-3.15.patch new file mode 100644 index 000000000..355467182 --- /dev/null +++ b/cpython-unix/patch-site-reentrant-startup-files-3.15.patch @@ -0,0 +1,158 @@ +From 8e4ad95833c302dc3ef1edf0b58eb9ef21f56889 Mon Sep 17 00:00:00 2001 +From: Zsolt Dollenstein +Date: Thu, 7 May 2026 13:11:52 -0700 +Subject: [PATCH] gh-149504: Fix reentrant site startup processing + +Copy and clear pending startup file data before executing import lines or .start entry points so recursive site.addsitedir() calls process a separate batch. +--- + Lib/site.py | 47 ++++++++++++++----- + Lib/test/test_site.py | 23 +++++++++ + ...6-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst | 2 + + 3 files changed, 59 insertions(+), 13 deletions(-) + create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst + +diff --git a/Lib/site.py b/Lib/site.py +index 52dd9648734c3e..766cbed460eac1 100644 +--- a/Lib/site.py ++++ b/Lib/site.py +@@ -163,6 +163,13 @@ def _init_pathinfo(): + _pending_importexecs = {} + + ++def _take_pending(mapping): ++ """Return the pending data and clear it before running startup code.""" ++ pending = mapping.copy() ++ mapping.clear() ++ return pending ++ ++ + def _read_pthstart_file(sitedir, name, suffix): + """Parse a .start or .pth file and return (lines, filename). + +@@ -280,11 +287,14 @@ def _read_start_file(sitedir, name): + entrypoints.append(line) + + +-def _extend_syspath(): ++def _extend_syspath(pending_syspaths=None): + # We've already filtered out duplicates, either in the existing sys.path + # or in all the .pth files we've seen. We've also abspath/normpath'd all + # the entries, so all that's left to do is to ensure that the path exists. +- for filename, dirs in _pending_syspaths.items(): ++ if pending_syspaths is None: ++ pending_syspaths = _pending_syspaths ++ ++ for filename, dirs in pending_syspaths.items(): + for dir_ in dirs: + if os.path.exists(dir_): + _trace(f"Extending sys.path with {dir_} from {filename}") +@@ -295,16 +305,21 @@ def _extend_syspath(): + f"skipping sys.path append") + + +-def _exec_imports(): ++def _exec_imports(pending_importexecs=None, pending_entrypoints=None): + # For all the `import` lines we've seen in .pth files, exec() them in + # order. However, if they come from a file with a matching .start, then + # we ignore these import lines. For the ones we do process, print a + # warning but only when -v was given. +- for filename, imports in _pending_importexecs.items(): ++ if pending_importexecs is None: ++ pending_importexecs = _pending_importexecs ++ if pending_entrypoints is None: ++ pending_entrypoints = _pending_entrypoints ++ ++ for filename, imports in pending_importexecs.items(): + name, dot, pth = filename.rpartition(".") + assert dot == "." and pth == "pth", f"Bad startup filename: {filename}" + +- if f"{name}.start" in _pending_entrypoints: ++ if f"{name}.start" in pending_entrypoints: + # Skip import lines in favor of entry points. + continue + +@@ -322,7 +337,7 @@ def _exec_imports(): + f"Error in import line from {filename}: {line}", exc) + + +-def _execute_start_entrypoints(): ++def _execute_start_entrypoints(pending_entrypoints=None): + """Execute all accumulated .start file entry points. + + Called after all site-packages directories have been processed so that +@@ -330,7 +345,10 @@ def _execute_start_entrypoints(): + pkgutil.resolve_name(strict=True) which both validates the strict + pkg.mod:callable form and resolves the entry point in one step. + """ +- for filename, entrypoints in _pending_entrypoints.items(): ++ if pending_entrypoints is None: ++ pending_entrypoints = _pending_entrypoints ++ ++ for filename, entrypoints in pending_entrypoints.items(): + for entrypoint in entrypoints: + try: + _trace(f"Executing entry point: {entrypoint} from {filename}") +@@ -355,12 +373,15 @@ def _execute_start_entrypoints(): + + def process_startup_files(): + """Flush all pending sys.path and entry points.""" +- _extend_syspath() +- _exec_imports() +- _execute_start_entrypoints() +- _pending_syspaths.clear() +- _pending_importexecs.clear() +- _pending_entrypoints.clear() ++ # Startup code may call addsitedir(), so remove this batch from the ++ # globals before executing any import lines or entry points. ++ pending_syspaths = _take_pending(_pending_syspaths) ++ pending_importexecs = _take_pending(_pending_importexecs) ++ pending_entrypoints = _take_pending(_pending_entrypoints) ++ ++ _extend_syspath(pending_syspaths) ++ _exec_imports(pending_importexecs, pending_entrypoints) ++ _execute_start_entrypoints(pending_entrypoints) + + + def addpackage(sitedir, name, known_paths): +diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py +index ac69e2cbdbbe54..62fc8820f3b6f0 100644 +--- a/Lib/test/test_site.py ++++ b/Lib/test/test_site.py +@@ -1297,6 +1297,29 @@ def startup(): + import epmod + self.assertFalse(epmod.called) + ++ def test_exec_imports_allows_reentrant_addsitedir(self): ++ nested = os.path.join(self.sitedir, 'nested') ++ nestedlib = os.path.join(nested, 'nestedlib') ++ os.mkdir(nested) ++ os.mkdir(nestedlib) ++ with open(os.path.join(nested, 'nested.pth'), 'w', ++ encoding='utf-8') as f: ++ f.write("nestedlib\n") ++ with open(os.path.join(nestedlib, 'nestedmod.py'), 'w', ++ encoding='utf-8') as f: ++ f.write("value = 42\n") ++ self.addCleanup(sys.modules.pop, 'nestedmod', None) ++ ++ outer_pth = os.path.join(self.sitedir, 'outer.pth') ++ site._pending_importexecs[outer_pth] = [ ++ f"import site; site.addsitedir({nested!r}); import nestedmod" ++ ] ++ ++ site.process_startup_files() ++ ++ self.assertIn(nestedlib, sys.path) ++ self.assertEqual(sys.modules['nestedmod'].value, 42) ++ + # --- _extend_syspath tests --- + + def test_extend_syspath_existing_dir(self): +diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst +new file mode 100644 +index 00000000000000..77f72ea2247fe8 +--- /dev/null ++++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst +@@ -0,0 +1,2 @@ ++Fix reentrant processing of site startup files when a ``.pth`` import line ++calls :func:`site.addsitedir`. diff --git a/cpython-windows/build.py b/cpython-windows/build.py index f93a59a50..d7201f53f 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -311,6 +311,30 @@ def static_replace_in_file(p: pathlib.Path, search, replace): fh.write(data) +def apply_source_patch(cpython_source_path: pathlib.Path, patch_path: pathlib.Path): + with patch_path.open("rb") as fh: + patch = fh.read().replace(b"\r\n", b"\n") + + with tempfile.NamedTemporaryFile("wb", delete=False) as fh: + fh.write(patch) + normalized_patch = pathlib.Path(fh.name) + + try: + subprocess.run( + [ + "git.exe", + "-C", + str(cpython_source_path), + "apply", + "--whitespace=nowarn", + str(normalized_patch), + ], + check=True, + ) + finally: + normalized_patch.unlink() + + OPENSSL_PROPS_REMOVE_RULES_LEGACY = b""" <_SSLDLL Include="$(opensslOutDir)\libcrypto$(_DLLSuffix).dll" /> @@ -1032,20 +1056,23 @@ def collect_python_build_artifacts( pcbuild_path: pathlib.Path, out_dir: pathlib.Path, python_majmin: str, - arch: str, + pcbuild_directory: str, config: str, openssl_entry: str, zlib_entry: str, freethreaded: bool, ): """Collect build artifacts from Python. - Copies them into an output directory and returns a data structure describing the files. """ - outputs_path = pcbuild_path / arch + arch = pcbuild_directory + # Python 3.15 suffixes the directory with 't' for free-threading + if arch.endswith("t"): + arch = arch.removesuffix("t") + outputs_path = pcbuild_path / pcbuild_directory intermediates_path = ( - pcbuild_path / "obj" / ("%s%s_%s" % (python_majmin, arch, config)) + pcbuild_path / "obj" / ("%s%s_%s" % (python_majmin, pcbuild_directory, config)) ) if not outputs_path.exists(): @@ -1094,6 +1121,7 @@ def collect_python_build_artifacts( other_projects = {"pythoncore"} other_projects.add("python3dll") + other_projects.add("python3tdll") # Projects providing dependencies. depends_projects = set() @@ -1395,6 +1423,11 @@ def build_cpython( python_exe = "python.exe" pythonw_exe = "pythonw.exe" + # Python 3.15 uses the default name for the executable in a suffixed directory + instrumented_python_exe = python_exe + if meets_python_minimum_version(python_version, "3.15") and freethreaded: + instrumented_python_exe = "python.exe" + if arch == "amd64": build_platform = "x64" build_directory = "amd64" @@ -1407,6 +1440,12 @@ def build_cpython( else: raise Exception("unhandled architecture: %s" % arch) + pcbuild_directory = build_directory + # Starting with 3.15, free-threaded CPython outputs use a `t` suffix. + # The third-party dependency archives still use the base architecture name. + if freethreaded and meets_python_minimum_version(python_version, "3.15"): + pcbuild_directory = f"{build_directory}t" + tempdir_opts = ( {"ignore_cleanup_errors": True} if sys.version_info >= (3, 12) else {} ) @@ -1477,6 +1516,12 @@ def build_cpython( cpython_source_path = td / ("Python-%s" % python_version) pcbuild_path = cpython_source_path / "PCbuild" + if python_version.startswith("3.15."): + apply_source_patch( + cpython_source_path, + SUPPORT / "patch-site-reentrant-startup-files-3.15.patch", + ) + out_dir = td / "out" build_dir = out_dir / "python" / "build" @@ -1528,7 +1573,10 @@ def build_cpython( # test execution. We work around this by invoking the test harness # separately for each test. instrumented_python = ( - pcbuild_path / build_directory / "instrumented" / python_exe + pcbuild_path + / pcbuild_directory + / "instrumented" + / instrumented_python_exe ) tests = subprocess.run( @@ -1608,7 +1656,7 @@ def build_cpython( "--source", str(cpython_source_path), "--build", - str(pcbuild_path / build_directory), + str(pcbuild_path / pcbuild_directory), "--copy", str(install_dir), "--temp", @@ -1695,7 +1743,7 @@ def build_cpython( pcbuild_path, out_dir / "python", "".join(entry["version"].split(".")[0:2]), - build_directory, + pcbuild_directory, artifact_config, openssl_entry=openssl_entry, zlib_entry=zlib_entry, diff --git a/cpython-windows/patch-site-reentrant-startup-files-3.15.patch b/cpython-windows/patch-site-reentrant-startup-files-3.15.patch new file mode 100644 index 000000000..4c63d6247 --- /dev/null +++ b/cpython-windows/patch-site-reentrant-startup-files-3.15.patch @@ -0,0 +1,145 @@ +diff --git a/Lib/site.py b/Lib/site.py +index 52dd9648734c3e..766cbed460eac1 100644 +--- a/Lib/site.py ++++ b/Lib/site.py +@@ -163,6 +163,13 @@ def _init_pathinfo(): + _pending_importexecs = {} + + ++def _take_pending(mapping): ++ """Return the pending data and clear it before running startup code.""" ++ pending = mapping.copy() ++ mapping.clear() ++ return pending ++ ++ + def _read_pthstart_file(sitedir, name, suffix): + """Parse a .start or .pth file and return (lines, filename). + +@@ -280,11 +287,14 @@ def _read_start_file(sitedir, name): + entrypoints.append(line) + + +-def _extend_syspath(): ++def _extend_syspath(pending_syspaths=None): + # We've already filtered out duplicates, either in the existing sys.path + # or in all the .pth files we've seen. We've also abspath/normpath'd all + # the entries, so all that's left to do is to ensure that the path exists. +- for filename, dirs in _pending_syspaths.items(): ++ if pending_syspaths is None: ++ pending_syspaths = _pending_syspaths ++ ++ for filename, dirs in pending_syspaths.items(): + for dir_ in dirs: + if os.path.exists(dir_): + _trace(f"Extending sys.path with {dir_} from {filename}") +@@ -295,16 +305,21 @@ def _extend_syspath(): + f"skipping sys.path append") + + +-def _exec_imports(): ++def _exec_imports(pending_importexecs=None, pending_entrypoints=None): + # For all the `import` lines we've seen in .pth files, exec() them in + # order. However, if they come from a file with a matching .start, then + # we ignore these import lines. For the ones we do process, print a + # warning but only when -v was given. +- for filename, imports in _pending_importexecs.items(): ++ if pending_importexecs is None: ++ pending_importexecs = _pending_importexecs ++ if pending_entrypoints is None: ++ pending_entrypoints = _pending_entrypoints ++ ++ for filename, imports in pending_importexecs.items(): + name, dot, pth = filename.rpartition(".") + assert dot == "." and pth == "pth", f"Bad startup filename: {filename}" + +- if f"{name}.start" in _pending_entrypoints: ++ if f"{name}.start" in pending_entrypoints: + # Skip import lines in favor of entry points. + continue + +@@ -322,7 +337,7 @@ def _exec_imports(): + f"Error in import line from {filename}: {line}", exc) + + +-def _execute_start_entrypoints(): ++def _execute_start_entrypoints(pending_entrypoints=None): + """Execute all accumulated .start file entry points. + + Called after all site-packages directories have been processed so that +@@ -330,7 +345,10 @@ def _execute_start_entrypoints(): + pkgutil.resolve_name(strict=True) which both validates the strict + pkg.mod:callable form and resolves the entry point in one step. + """ +- for filename, entrypoints in _pending_entrypoints.items(): ++ if pending_entrypoints is None: ++ pending_entrypoints = _pending_entrypoints ++ ++ for filename, entrypoints in pending_entrypoints.items(): + for entrypoint in entrypoints: + try: + _trace(f"Executing entry point: {entrypoint} from {filename}") +@@ -355,12 +373,15 @@ def _execute_start_entrypoints(): + + def process_startup_files(): + """Flush all pending sys.path and entry points.""" +- _extend_syspath() +- _exec_imports() +- _execute_start_entrypoints() +- _pending_syspaths.clear() +- _pending_importexecs.clear() +- _pending_entrypoints.clear() ++ # Startup code may call addsitedir(), so remove this batch from the ++ # globals before executing any import lines or entry points. ++ pending_syspaths = _take_pending(_pending_syspaths) ++ pending_importexecs = _take_pending(_pending_importexecs) ++ pending_entrypoints = _take_pending(_pending_entrypoints) ++ ++ _extend_syspath(pending_syspaths) ++ _exec_imports(pending_importexecs, pending_entrypoints) ++ _execute_start_entrypoints(pending_entrypoints) + + + def addpackage(sitedir, name, known_paths): +diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py +index ac69e2cbdbbe54..62fc8820f3b6f0 100644 +--- a/Lib/test/test_site.py ++++ b/Lib/test/test_site.py +@@ -1297,6 +1297,29 @@ def startup(): + import epmod + self.assertFalse(epmod.called) + ++ def test_exec_imports_allows_reentrant_addsitedir(self): ++ nested = os.path.join(self.sitedir, 'nested') ++ nestedlib = os.path.join(nested, 'nestedlib') ++ os.mkdir(nested) ++ os.mkdir(nestedlib) ++ with open(os.path.join(nested, 'nested.pth'), 'w', ++ encoding='utf-8') as f: ++ f.write("nestedlib\n") ++ with open(os.path.join(nestedlib, 'nestedmod.py'), 'w', ++ encoding='utf-8') as f: ++ f.write("value = 42\n") ++ self.addCleanup(sys.modules.pop, 'nestedmod', None) ++ ++ outer_pth = os.path.join(self.sitedir, 'outer.pth') ++ site._pending_importexecs[outer_pth] = [ ++ f"import site; site.addsitedir({nested!r}); import nestedmod" ++ ] ++ ++ site.process_startup_files() ++ ++ self.assertIn(nestedlib, sys.path) ++ self.assertEqual(sys.modules['nestedmod'].value, 42) ++ + # --- _extend_syspath tests --- + + def test_extend_syspath_existing_dir(self): +diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst +new file mode 100644 +index 00000000000000..77f72ea2247fe8 +--- /dev/null ++++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-31-16-15-15.gh-issue-75723.BZ4Rsn.rst +@@ -0,0 +1,2 @@ ++Fix reentrant processing of site startup files when a ``.pth`` import line ++calls :func:`site.addsitedir`. diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index c53264b17..5307c07ae 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -93,10 +93,10 @@ "python_tag": "cp314", }, "cpython-3.15": { - "url": "https://www.python.org/ftp/python/3.15.0/Python-3.15.0a8.tar.xz", - "size": 35130268, - "sha256": "28f1b6358609042ebcc81488ec24569519f50804bb07dc23cc707b281b031c69", - "version": "3.15.0a8", + "url": "https://www.python.org/ftp/python/3.15.0/Python-3.15.0b1.tar.xz", + "size": 35178032, + "sha256": "d4d52ccfa1d727ef5235fbb7d70fa1dbacf10b8b3760db622875da05acbe437c", + "version": "3.15.0b1", "licenses": ["Python-2.0", "CNRI-Python"], "license_file": "LICENSE.cpython.txt", "python_tag": "cp315", diff --git a/src/validation.rs b/src/validation.rs index ec2ef2b43..ced464a07 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -122,6 +122,7 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[ "libssl-3-arm64.dll", "libssl-3-x64.dll", "python3.dll", + "python3t.dll", "python39.dll", "python310.dll", "python311.dll", @@ -252,6 +253,7 @@ static ELF_ALLOWED_LIBRARIES_BY_TRIPLE: Lazy