From f216de07c9efbfb9f5e25043870465600afa83c1 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Tue, 2 Jun 2026 13:35:17 -0500 Subject: [PATCH 1/4] 3.15.0b1 -> 3.15.0b2 --- pythonbuild/downloads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 6a3dbd38f..4d3876ff2 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.0b1.tar.xz", - "size": 35178032, - "sha256": "d4d52ccfa1d727ef5235fbb7d70fa1dbacf10b8b3760db622875da05acbe437c", - "version": "3.15.0b1", + "url": "https://www.python.org/ftp/python/3.15.0/Python-3.15.0b2.tar.xz", + "size": 35381676, + "sha256": "d14f474ab679e90bc734b02ff58447b6ec99a821af61d6ff0c1da0f86e341a71", + "version": "3.15.0b2", "licenses": ["Python-2.0", "CNRI-Python"], "license_file": "LICENSE.cpython.txt", "python_tag": "cp315", From d5b46a8d9524621a0ce13a6f92ce852915780ccb Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Tue, 2 Jun 2026 13:40:51 -0500 Subject: [PATCH 2/4] remove site.py patch, fixed upstream --- cpython-unix/build-cpython.sh | 6 - ...ch-site-reentrant-startup-files-3.15.patch | 158 ------------------ cpython-windows/build.py | 30 ---- ...ch-site-reentrant-startup-files-3.15.patch | 145 ---------------- 4 files changed, 339 deletions(-) delete mode 100644 cpython-unix/patch-site-reentrant-startup-files-3.15.patch delete mode 100644 cpython-windows/patch-site-reentrant-startup-files-3.15.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 7deb00830..422d4847b 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -338,12 +338,6 @@ 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" diff --git a/cpython-unix/patch-site-reentrant-startup-files-3.15.patch b/cpython-unix/patch-site-reentrant-startup-files-3.15.patch deleted file mode 100644 index 355467182..000000000 --- a/cpython-unix/patch-site-reentrant-startup-files-3.15.patch +++ /dev/null @@ -1,158 +0,0 @@ -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 fed9b06ff..37dcd23f3 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -311,30 +311,6 @@ 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" /> @@ -1523,12 +1499,6 @@ 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" diff --git a/cpython-windows/patch-site-reentrant-startup-files-3.15.patch b/cpython-windows/patch-site-reentrant-startup-files-3.15.patch deleted file mode 100644 index 4c63d6247..000000000 --- a/cpython-windows/patch-site-reentrant-startup-files-3.15.patch +++ /dev/null @@ -1,145 +0,0 @@ -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`. From 2f400e33732d2db56d0545b7142eb9710598b507 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Tue, 2 Jun 2026 15:45:56 -0500 Subject: [PATCH 3/4] use Tcl/Tk 9.0.3 on Python 3.15 for Windows --- cpython-windows/build.py | 27 +++++++++++++++------------ pythonbuild/downloads.py | 9 ++++++++- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 37dcd23f3..65fd9888b 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -372,8 +372,10 @@ def hack_props( mpdecimal_version = DOWNLOADS["mpdecimal"]["version"] - if meets_python_minimum_version(python_version, "3.14") or arch == "arm64": - tcltk_commit = DOWNLOADS["tk-windows-bin"]["git_commit"] + if meets_python_minimum_version(python_version, "3.15"): + tcltk_commit = DOWNLOADS["tk-windows-bin-903"]["git_commit"] + elif meets_python_minimum_version(python_version, "3.14") or arch == "arm64": + tcltk_commit = DOWNLOADS["tk-windows-bin-8614"]["git_commit"] else: tcltk_commit = DOWNLOADS["tk-windows-bin-8612"]["git_commit"] @@ -1371,15 +1373,16 @@ def build_cpython( setuptools_wheel = download_entry("setuptools", BUILD) pip_wheel = download_entry("pip", BUILD) - # On CPython 3.14+, we use the latest tcl/tk version which has additional - # runtime dependencies, so we are conservative and use the old version - # elsewhere. The old version isn't built for arm64, so we use the new - # version there too - tk_bin_entry = ( - "tk-windows-bin" - if meets_python_minimum_version(python_version, "3.14") or arch == "arm64" - else "tk-windows-bin-8612" - ) + # We match the upstream tcl/tk version when possible + # 3.15 : 9.0.3 + # 3.14 and arm64 : 8.6.14 + # Others : 8.6.12 + if meets_python_minimum_version(python_version, "3.15"): + tk_bin_entry = "tk-windows-bin-903" + elif meets_python_minimum_version(python_version, "3.14") or arch == "arm64": + tk_bin_entry = "tk-windows-bin-8614" + else: + tk_bin_entry = "tk-windows-bin-8612" tk_bin_archive = download_entry( tk_bin_entry, BUILD, local_name="tk-windows-bin.tar.gz" ) @@ -1485,7 +1488,7 @@ def build_cpython( shutil.copyfile(source, dest) # Delete the tk nmake helper, it's not needed and links msvc - if tk_bin_entry == "tk-windows-bin": + if tk_bin_entry in ("tk-windows-bin-8614", "tk-windows-bin-903"): tcltk_commit: str = DOWNLOADS[tk_bin_entry]["git_commit"] tcltk_path = td / ("cpython-bin-deps-%s" % tcltk_commit) ( diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 4d3876ff2..521bc9b94 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -344,7 +344,14 @@ "licenses": ["TCL"], "license_file": "LICENSE.tcl.txt", }, - "tk-windows-bin": { + "tk-windows-bin-903": { + "url": "https://github.com/python/cpython-bin-deps/archive/2f788ebecac8d4bc3c7fa982b55a6c6923aa55fb.tar.gz", + "size": 18527780, + "sha256": "ac7e489d1fdabb0dbb69896aa8d191b5a87d053ce306fdffa51bbd77b94dbafc", + "version": "9.0.3", + "git_commit": "2f788ebecac8d4bc3c7fa982b55a6c6923aa55fb", + }, + "tk-windows-bin-8614": { "url": "https://github.com/python/cpython-bin-deps/archive/c624cc881bd0e5071dec9de4b120cbe9985d8c14.tar.gz", "size": 9497943, "sha256": "9b8e77d55f40ceaedd140ccca0daa804f0d43346d5abfcead9b547b5590f82f8", From a34357eb46425e4af2150f2f2d8e71a4c1be2352 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Tue, 2 Jun 2026 16:04:50 -0500 Subject: [PATCH 4/4] fixup comment about tcl/tk version on windows --- cpython-windows/build.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 65fd9888b..896c8d433 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -1373,10 +1373,11 @@ def build_cpython( setuptools_wheel = download_entry("setuptools", BUILD) pip_wheel = download_entry("pip", BUILD) - # We match the upstream tcl/tk version when possible - # 3.15 : 9.0.3 - # 3.14 and arm64 : 8.6.14 - # Others : 8.6.12 + # We use a prebuild tcl/tk from the upstream CPython project. + # Tcl/tk 8.6.14+ has an additional runtime dependency. We are conservative and + # use an old version prior to CPython 3.14. The older tck/tk release + # is not available for arm64 so we use a newer release there as well. + # On CPython 3.14+ we match the version included in the Python.org release. if meets_python_minimum_version(python_version, "3.15"): tk_bin_entry = "tk-windows-bin-903" elif meets_python_minimum_version(python_version, "3.14") or arch == "arm64":