From a186551687c3bc4e82684336fb967b94d189081b Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Thu, 16 Apr 2026 17:09:16 -0400 Subject: [PATCH 01/11] Add regression tests for MemoryDenyWriteExecute=true (#1071) As reported in #956, executable stacks cause thread creation to fail under MemoryDenyWriteExecute=true. In kernel 6.3+ (which the GitHub Actions runners should have) this is implemented using prctl(PR_SET_MDWE). In older versions systemd uses a more complicated seccomp filter. --- pythonbuild/disttests/__init__.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pythonbuild/disttests/__init__.py b/pythonbuild/disttests/__init__.py index af812d4df..f7186e13d 100644 --- a/pythonbuild/disttests/__init__.py +++ b/pythonbuild/disttests/__init__.py @@ -320,6 +320,42 @@ def assertPythonWorks(path: Path, argv0: Optional[str] = None): with self.subTest(msg="weird argv[0]"): assertPythonWorks(sys.executable, argv0="/dev/null") + @unittest.skipUnless(sys.platform == "linux", "Linux-specific prctl") + @unittest.skipIf( + "static" in os.environ["BUILD_OPTIONS"], + "cannot import libc on static builds", + ) + def test_nx_thread_creation(self): + "Test that thread creation works under e.g. systemd's MemoryDenyWriteExecute." + # Note that NX cannot be unset so this pollutes the current process, + # but if something else breaks under NX we probably want to know! + import ctypes + import threading + + libc = ctypes.CDLL(None, use_errno=True) + # + PR_SET_MDWE = 65 + PR_GET_MDWE = 66 + PR_MDWE_REFUSE_EXEC_GAIN = 1 << 0 + PR_MDWE_NO_INHERIT = 1 << 1 + mdwe = libc.prctl(PR_GET_MDWE, 0, 0, 0, 0) + if mdwe < 0: + self.skipTest("prctl(PR_SET_MDWE) unsupported") + elif not (mdwe & PR_MDWE_REFUSE_EXEC_GAIN): + if ( + libc.prctl( + PR_SET_MDWE, PR_MDWE_REFUSE_EXEC_GAIN | PR_MDWE_NO_INHERIT, 0, 0, 0 + ) + != 0 + ): + self.fail("prctl(PR_SET_MDWE): " + os.strerror(ctypes.get_errno())) + + a = [] + t = threading.Thread(target=a.append, args=("Thread was here",)) + t.start() + t.join() + self.assertEqual(a, ["Thread was here"]) + if __name__ == "__main__": unittest.main() From 7c9f6cfa5b5d623ef0ff9cae29aeb7228fb631f4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 23 Apr 2026 14:38:30 -0400 Subject: [PATCH 02/11] Run a smaller subset of targets on pull requests by default (#1075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, pull requests run all targets by default and labels can be used to select a subset of the matrix. However, CI is quite expensive and it's very rare to need to test the whole matrix, so the default is a bit backwards. In this change, we update pull requests to run on a subset of targets by default: Python 3.14 on macOS, Linux, and Windows with the most popular architecture. We include both glibc and musl variants of Linux. We include armv7 as an arbitrary cross-compile case — I'd be happy to take suggestions on an alternative there. This breaks our labeling concept a bit, as our labels currently do pure subsetting of the matrix. The labels will continue to subset, but with some nuances: - Applying a label will generally subset the default selection, e.g., `platform:linux` will only run the Linux subset of the defaults described above. - Labels that would null the default subset, e.g., `python:3.12` or `build:debug`, will instead change the default set to target that variant. - There are new `platform:all`, `python:all`, `arch:all`, `libc:all`, and `build:all` labels that can be used to expand the targets. --- .github/workflows/linux.yml | 1 + .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- CONTRIBUTING.rst | 47 +++--- ci-defaults.yaml | 28 ++++ ci-matrix.py | 264 +++++++++++++++++++++++++++++----- 6 files changed, 290 insertions(+), 54 deletions(-) create mode 100644 ci-defaults.yaml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2e934c5fd..884a94559 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -178,6 +178,7 @@ jobs: --platform linux \ --labels "${STEPS_GET_LABELS_OUTPUTS_LABELS}" \ --max-shards 2 \ + --event ${{ github.event_name }} \ ${{ (steps.check-pythonbuild.outputs.changed == 'true' || github.ref == 'refs/heads/main') && '--force-crate-build' || '' }} \ > matrix.json diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3a992a9a4..c0086296f 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -92,7 +92,7 @@ jobs: - name: Generate build matrix id: set-matrix run: | - uv run ci-matrix.py --platform darwin --labels "${STEPS_GET_LABELS_OUTPUTS_LABELS}" ${{ (steps.check-pythonbuild.outputs.changed == 'true' || github.ref == 'refs/heads/main') && '--force-crate-build' || '' }} > matrix.json + uv run ci-matrix.py --platform darwin --labels "${STEPS_GET_LABELS_OUTPUTS_LABELS}" --event ${{ github.event_name }} ${{ (steps.check-pythonbuild.outputs.changed == 'true' || github.ref == 'refs/heads/main') && '--force-crate-build' || '' }} > matrix.json # Extract python-build matrix echo "matrix=$(jq -c '."python-build"' matrix.json)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cc8b866f0..d213c8844 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -92,7 +92,7 @@ jobs: - name: Generate build matrix id: set-matrix run: | - uv run ci-matrix.py --platform windows --labels "${STEPS_GET_LABELS_OUTPUTS_LABELS}" ${{ (steps.check-pythonbuild.outputs.changed == 'true' || github.ref == 'refs/heads/main') && '--force-crate-build' || '' }} > matrix.json + uv run ci-matrix.py --platform windows --labels "${STEPS_GET_LABELS_OUTPUTS_LABELS}" --event ${{ github.event_name }} ${{ (steps.check-pythonbuild.outputs.changed == 'true' || github.ref == 'refs/heads/main') && '--force-crate-build' || '' }} > matrix.json # Extract python-build matrix echo "matrix=$(jq -c '."python-build"' matrix.json)" >> $GITHUB_OUTPUT diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c7e0193b5..4a9716cb3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,28 +8,41 @@ Building distributions See the [documentation](https://gregoryszorc.com/docs/python-build-standalone/main/building.html) for instructions on building distributions locally. -CI labels -========= -By default, submitting a pull request triggers a complete build of all -distributions in CI, which can be time-consuming. +Pull request labels +=================== +By default, pull requests build a small subset of targets defined in +``ci-defaults.yaml`` under ``pull_request``. Pushes to ``main`` build the full +matrix from ``ci-targets.yaml``. -To conserve CI resources and reduce build times, you can limit the matrix of -distributions built by applying specific labels to your pull request. Only -distributions matching the specified labels will be built. +Pull request labels can be used to change what CI builds: -The following label prefixes can be used to customize the build matrix: +* ``platform:`` filters the selected targets by platform. +* ``arch:`` filters the selected targets by architecture. +* ``libc:`` filters the selected targets by libc. +* ``python:`` filters the selected Python versions. +* ``build:`` filters the selected build options by component. -* `platform` -* `python` -* `build` -* `arch` -* `libc` +The ``:all`` labels expand only their own dimension: -To bypass CI entirely for changes that do not affect the build (such as -documentation updates), use the `ci:skip` label. +* ``platform:all`` expands the selected platforms. +* ``arch:all`` expands the selected architectures. +* ``libc:all`` expands the selected libc variants. +* ``python:all`` expands the selected Python versions. +* ``build:all`` expands the selected build options. -Please utilize these tags when appropriate for your changes to minimize CI -resource consumption. +Use ``ci:all-targets`` to build the full matrix from ``ci-targets.yaml``. + +Examples: + +* ``platform:linux`` builds only the Linux targets from ``ci-defaults.yaml``. +* ``python:3.13`` builds the default targets with Python 3.13. +* ``build:pgo`` builds the selected targets whose build options include ``pgo``. +* ``platform:linux,arch:all,libc:all,python:all,build:all`` builds the full + Linux matrix. + +To bypass CI entirely for changes that do not affect the build, use the +``ci:skip`` label. The ``documentation`` label is treated the same way. To run +a dry-run build matrix, use ``ci:dry-run``. Releases ======== diff --git a/ci-defaults.yaml b/ci-defaults.yaml new file mode 100644 index 000000000..56f612675 --- /dev/null +++ b/ci-defaults.yaml @@ -0,0 +1,28 @@ +# Describes the default targets that CI will build for different events. + +pull_request: + python_version: "3.14" + + targets: + x86_64-pc-windows-msvc: + build_options: + - pgo + + aarch64-apple-darwin: + build_options: + - pgo+lto + + x86_64-unknown-linux-gnu: + build_options: + - pgo+lto + - freethreaded+pgo+lto + + x86_64-unknown-linux-musl: + build_options: + - lto + - lto+static + - freethreaded+lto + + armv7-unknown-linux-gnueabihf: + build_options: + - lto diff --git a/ci-matrix.py b/ci-matrix.py index c88f09b2e..2dabb8586 100644 --- a/ci-matrix.py +++ b/ci-matrix.py @@ -16,6 +16,7 @@ CI_TARGETS_YAML = "ci-targets.yaml" CI_RUNNERS_YAML = "ci-runners.yaml" +CI_DEFAULTS_YAML = "ci-defaults.yaml" CI_EXTRA_SKIP_LABELS = ["documentation"] CI_MATRIX_SIZE_LIMIT = 256 # The maximum size of a matrix in GitHub Actions @@ -40,7 +41,7 @@ def meets_conditional_version(version: str, min_version: str) -> bool: def parse_labels(labels: str | None) -> dict[str, set[str]]: - """Parse labels into a dict of category filters.""" + """Parse labels into a dict of category -> set of values.""" if not labels: return {} @@ -75,30 +76,136 @@ def parse_labels(labels: str | None) -> dict[str, set[str]]: return result -def should_include_entry(entry: dict[str, str], filters: dict[str, set[str]]) -> bool: - """Check if an entry satisfies the label filters.""" - if filters.get("directives") and "skip" in filters["directives"]: - return False +def get_all_build_options(ci_config: dict[str, Any], target_triple: str) -> list[str]: + """Get all build options (including conditional) for a target from ci-targets.yaml.""" + for platform_config in ci_config.values(): + if target_triple in platform_config: + config = platform_config[target_triple] + options = list(config["build_options"]) + for conditional in config.get("build_options_conditional", []): + options.extend(conditional["options"]) + return options + raise KeyError(f"Target triple {target_triple!r} not found in ci-targets.yaml") + + +def find_target_platform(ci_config: dict[str, Any], target_triple: str) -> str: + """Find which platform a target triple belongs to in ci-targets.yaml.""" + for platform, platform_config in ci_config.items(): + if target_triple in platform_config: + return platform + raise KeyError(f"Target triple {target_triple!r} not found in ci-targets.yaml") + + +def expand_default_triples( + ci_config: dict[str, Any], + pull_request_defaults: dict[str, Any], + labels: dict[str, set[str]], +) -> set[str]: + """Compute the set of allowed target triples for a pull request. + + Starts from the explicit defaults in ci-defaults.yaml. When a target + dimension has an :all label, that dimension is relaxed and additional + triples from ci-targets.yaml that match on the non-expanded dimensions + are included. + """ + default_triples = set(pull_request_defaults["targets"]) + + expand_platform = "all" in labels.get("platform", set()) + expand_arch = "all" in labels.get("arch", set()) + expand_libc = "all" in labels.get("libc", set()) + + if not (expand_platform or expand_arch or expand_libc): + return default_triples + + # Build reference tuples from the default triples. + default_attrs = [] + for triple in default_triples: + platform = find_target_platform(ci_config, triple) + config = ci_config[platform][triple] + default_attrs.append( + ( + platform, + config["arch"], + config.get("arch_variant"), + config.get("libc"), + ) + ) + + # Include any triple whose non-expanded dimensions match a default. + allowed = set(default_triples) + for platform, platform_config in ci_config.items(): + for triple, config in platform_config.items(): + for d_platform, d_arch, d_arch_variant, d_libc in default_attrs: + if not expand_platform and platform != d_platform: + continue + if not expand_arch and ( + config["arch"] != d_arch + or config.get("arch_variant") != d_arch_variant + ): + continue + if not expand_libc and config.get("libc") != d_libc: + continue + allowed.add(triple) + break + + return allowed + + +def should_include_entry( + entry: dict[str, str], + labels: dict[str, set[str]], + pull_request_defaults: dict[str, Any] | None = None, + allowed_triples: set[str] | None = None, +) -> bool: + """Check if a matrix entry should be included. + + For pull requests, entries are restricted to the allowed target set + (computed by expand_default_triples), the default python version, and + the curated build options — unless overridden by labels. For pushes + (pull_request_defaults is None), only label filters apply. + """ + if pull_request_defaults is not None: + triple = entry["target_triple"] + default_targets = pull_request_defaults["targets"] + + # Target must be in the allowed set. + if allowed_triples is not None and triple not in allowed_triples: + return False - if filters.get("platform") and entry["platform"] not in filters["platform"]: + # Python: restrict to default version unless python labels override. + if not labels.get("python"): + if entry["python"] != pull_request_defaults["python_version"]: + return False + + # Build options: restrict to curated defaults for default triples + # unless build labels override. Non-default triples (brought in by + # :all expansion) are unrestricted. + if not labels.get("build") and triple in default_targets: + if entry["build_options"] not in default_targets[triple]["build_options"]: + return False + + # Label filters + platform_filters = labels.get("platform", set()) - {"all"} + if platform_filters and entry["platform"] not in platform_filters: return False - if filters.get("python") and entry["python"] not in filters["python"]: + python_filters = labels.get("python", set()) - {"all"} + if python_filters and entry["python"] not in python_filters: return False - if filters.get("arch") and entry["arch"] not in filters["arch"]: + arch_filters = labels.get("arch", set()) - {"all"} + if arch_filters and entry["arch"] not in arch_filters: return False - if ( - filters.get("libc") - and entry.get("libc") - and entry["libc"] not in filters["libc"] - ): + libc_filters = labels.get("libc", set()) - {"all"} + if libc_filters and entry.get("libc") and entry["libc"] not in libc_filters: return False - if filters.get("build"): - build_options = set(entry.get("build_options", "").split("+")) - if not all(f in build_options for f in filters["build"]): + build_filters = labels.get("build", set()) - {"all"} + if build_filters: + build_components = set(entry.get("build_options", "").split("+")) + required = {c for f in build_filters for c in f.split("+")} + if not required.issubset(build_components): return False return True @@ -106,15 +213,25 @@ def should_include_entry(entry: dict[str, str], filters: dict[str, set[str]]) -> def generate_docker_matrix_entries( runners: dict[str, Any], + python_entries: list[dict[str, str]], platform_filter: str | None = None, ) -> list[dict[str, str]]: - """Generate matrix entries for docker image builds.""" + """Generate matrix entries for Docker image builds.""" if platform_filter and platform_filter != "linux": return [] + needed_archs = { + runners[entry["runner"]]["arch"] + for entry in python_entries + if entry.get("platform") == "linux" + } + matrix_entries = [] for image in DOCKER_BUILD_IMAGES: - # Find appropriate runner for Linux platform with the specified architecture + if image["arch"] not in needed_archs: + continue + + # Find appropriate runner for Linux platform with the specified architecture. runner = find_runner(runners, "linux", image["arch"], False) entry = { @@ -201,14 +318,6 @@ def generate_python_build_matrix_entries( label_filters.get("directives", set()) if label_filters else set(), ) - # Apply label filters if present - if label_filters: - matrix_entries = [ - entry - for entry in matrix_entries - if should_include_entry(entry, label_filters) - ] - return matrix_entries @@ -324,6 +433,50 @@ def add_python_build_entries_for_config( matrix_entries.append(entry) +def validate_pull_request_defaults( + ci_config: dict[str, Any], pull_request_defaults: dict[str, Any] +) -> None: + """Validate the pull_request defaults in ci-defaults.yaml.""" + all_triples = set() + for platform_config in ci_config.values(): + all_triples.update(platform_config.keys()) + + for triple in pull_request_defaults["targets"]: + if triple not in all_triples: + print( + f"error: target triple {triple!r} in {CI_DEFAULTS_YAML}:pull_request " + f"not found in {CI_TARGETS_YAML}", + file=sys.stderr, + ) + sys.exit(1) + + # Validate that each build option listed is valid for the target. + all_options = set(get_all_build_options(ci_config, triple)) + for option in pull_request_defaults["targets"][triple]["build_options"]: + if option not in all_options: + print( + f"error: build option {option!r} for {triple} in " + f"{CI_DEFAULTS_YAML}:pull_request not found in {CI_TARGETS_YAML} " + f"(valid: {sorted(all_options)})", + file=sys.stderr, + ) + sys.exit(1) + + # Validate that the default python version exists in ci-targets.yaml. + default_version = pull_request_defaults["python_version"] + for triple in pull_request_defaults["targets"]: + platform = find_target_platform(ci_config, triple) + ci_versions = ci_config[platform][triple]["python_versions"] + if default_version not in ci_versions: + print( + f"error: python version {default_version!r} in " + f"{CI_DEFAULTS_YAML}:pull_request not available for {triple} in " + f"{CI_TARGETS_YAML} (valid: {ci_versions})", + file=sys.stderr, + ) + sys.exit(1) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Generate a JSON matrix for building distributions in CI" @@ -341,7 +494,12 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( "--labels", - help="Comma-separated list of labels to filter by (e.g., 'platform:darwin,python:3.13,build:debug'), all must match.", + help="Comma-separated list of labels to filter by (e.g., 'platform:linux,python:3.13')", + ) + parser.add_argument( + "--event", + choices=["pull_request", "push"], + help="The GitHub event type. When 'pull_request', uses ci-defaults.yaml for the default subset.", ) parser.add_argument( "--free-runners", @@ -367,11 +525,14 @@ def main() -> None: labels = parse_labels(args.labels) with open(CI_TARGETS_YAML) as f: - config = yaml.safe_load(f) + ci_config = yaml.safe_load(f) with open(CI_RUNNERS_YAML) as f: runners = yaml.safe_load(f) + with open(CI_DEFAULTS_YAML) as f: + ci_defaults = yaml.safe_load(f) or {} + # If only free runners are allowed, reduce to a subset if args.free_runners: runners = { @@ -380,15 +541,47 @@ def main() -> None: if runner_config.get("free") } + # Check for skip directive + if labels.get("directives") and "skip" in labels["directives"]: + # Emit empty matrices + result = {} + if args.matrix_type in ["python-build", "all"]: + if args.max_shards: + result["python-build"] = { + str(i): {"include": []} for i in range(args.max_shards) + } + else: + result["python-build"] = {"include": []} + if args.matrix_type in ["docker-build", "all"]: + result["docker-build"] = {"include": []} + if args.matrix_type in ["crate-build", "all"]: + result["crate-build"] = {"include": []} + print(json.dumps(result)) + return + + event_defaults = ci_defaults.get(args.event) if args.event else None + if "all-targets" in labels.get("directives", set()): + event_defaults = None + + allowed_triples = None + if event_defaults is not None: + validate_pull_request_defaults(ci_config, event_defaults) + allowed_triples = expand_default_triples(ci_config, event_defaults, labels) + result = {} - # Generate python build entries + # Generate all python build entries, then filter python_entries = generate_python_build_matrix_entries( - config, + ci_config, runners, args.platform, labels, ) + python_entries = [ + entry + for entry in python_entries + if should_include_entry(entry, labels, event_defaults, allowed_triples) + ] # Output python-build matrix if requested if args.matrix_type in ["python-build", "all"]: @@ -416,18 +609,19 @@ def main() -> None: result["python-build"] = {"include": python_entries} # Generate docker-build matrix if requested - # Only include docker builds if there are Linux python builds + # Only include docker builds if there are Linux python builds. if args.matrix_type in ["docker-build", "all"]: - # Check if we have any Linux python builds + # Check if we have any Linux python builds. has_linux_builds = any( entry.get("platform") == "linux" for entry in python_entries ) - # If no platform filter or explicitly requesting docker-build only, include docker builds - # Otherwise, only include if there are Linux python builds + # If no platform filter or explicitly requesting docker-build only, include docker builds. + # Otherwise, only include if there are Linux python builds. if args.matrix_type == "docker-build" or has_linux_builds: docker_entries = generate_docker_matrix_entries( runners, + python_entries, args.platform, ) result["docker-build"] = {"include": docker_entries} @@ -437,7 +631,7 @@ def main() -> None: crate_entries = generate_crate_build_matrix_entries( python_entries, runners, - config, + ci_config, # Use the full target config so --force-crate-build adds all native crate builds. args.force_crate_build, args.platform, ) From 88877940356a5eda237dff1b074837176437e386 Mon Sep 17 00:00:00 2001 From: Mohammad Miadh Angkad Date: Tue, 28 Apr 2026 00:25:19 +0800 Subject: [PATCH 03/11] Bump pip from 26.0.1 -> 26.1 (#1109) --- pythonbuild/downloads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 8a28180e8..5b8431f91 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -279,10 +279,10 @@ "version": "0.13.1", }, "pip": { - "url": "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", - "size": 1787723, - "sha256": "bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", - "version": "26.0.1", + "url": "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", + "size": 1812804, + "sha256": "4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", + "version": "26.1", }, "readline": { # Mirrored from https://ftp.gnu.org/gnu/readline/readline-8.2.tar.gz From 90a92476491e477e63ee6121640243c7b0921902 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Tue, 28 Apr 2026 10:03:57 -0500 Subject: [PATCH 04/11] statically link libpython on macOS (#1105) Statically link `python` to `libpython` on macOS. A dynamic `libpython` is still built and included in the distribution for embedded use. Remove patches that disable linking the libraries in statically linked Python stdlib modules to python as these are now needed. closes #636 --- cpython-unix/build-cpython.sh | 31 +++---------------- .../patch-python-link-modules-3.10.patch | 12 ------- .../patch-python-link-modules-3.11.patch | 13 -------- .../patch-python-link-modules-3.15.patch | 13 -------- 4 files changed, 5 insertions(+), 64 deletions(-) delete mode 100644 cpython-unix/patch-python-link-modules-3.10.patch delete mode 100644 cpython-unix/patch-python-link-modules-3.11.patch delete mode 100644 cpython-unix/patch-python-link-modules-3.15.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 77f9ec5cd..3a0fdbcbf 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -147,20 +147,6 @@ else patch -p1 -i "${ROOT}/patch-macos-link-extension-modules.patch" fi -# Also on macOS, the `python` executable is linked against libraries defined by statically -# linked modules. But those libraries should only get linked into libpython, not the -# executable. This behavior is kinda suspect on all platforms, as it could be adding -# library dependencies that shouldn't need to be there. -if [[ "${PYBUILD_PLATFORM}" = macos* ]]; then - if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_15}" ]; then - patch -p1 -i "${ROOT}/patch-python-link-modules-3.15.patch" - elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]; then - patch -p1 -i "${ROOT}/patch-python-link-modules-3.11.patch" - elif [ "${PYTHON_MAJMIN_VERSION}" = "3.10" ]; then - patch -p1 -i "${ROOT}/patch-python-link-modules-3.10.patch" - fi -fi - # The macOS code for sniffing for _dyld_shared_cache_contains_path falls back on a # possibly inappropriate code path if a configure time check fails. This is not # appropriate for certain cross-compiling scenarios. See discussion at @@ -429,19 +415,12 @@ CONFIGURE_FLAGS=" # Build a libpython3.x.so, but statically link the interpreter against # libpython. -# -# For now skip this on macos, because it causes some linker failures. Note that -# this patch mildly conflicts with the macos-only patch-python-link-modules -# applied above, so you will need to resolve that conflict if you re-enable -# this for macos. -if [[ "${PYBUILD_PLATFORM}" != macos* ]]; 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 - CONFIGURE_FLAGS="${CONFIGURE_FLAGS} --enable-static-libpython-for-interpreter" +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 +CONFIGURE_FLAGS="${CONFIGURE_FLAGS} --enable-static-libpython-for-interpreter" if [ "${CC}" = "musl-clang" ]; then # In order to build the _blake2 extension module with SSE3+ instructions, we need diff --git a/cpython-unix/patch-python-link-modules-3.10.patch b/cpython-unix/patch-python-link-modules-3.10.patch deleted file mode 100644 index 58247aebd..000000000 --- a/cpython-unix/patch-python-link-modules-3.10.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/Makefile.pre.in b/Makefile.pre.in ---- a/Makefile.pre.in -+++ b/Makefile.pre.in -@@ -563,7 +563,7 @@ clinic: check-clean-src $(srcdir)/Modules/_blake2/blake2s_impl.c - - # Build the interpreter - $(BUILDPYTHON): Programs/python.o $(LIBRARY_DEPS) -- $(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(BLDLIBRARY) $(LIBS) $(MODLIBS) $(SYSLIBS) -+ $(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(BLDLIBRARY) $(LIBS) $(SYSLIBS) - - platform: $(BUILDPYTHON) pybuilddir.txt - $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print("%s-%d.%d" % (get_platform(), *sys.version_info[:2]))' >platform diff --git a/cpython-unix/patch-python-link-modules-3.11.patch b/cpython-unix/patch-python-link-modules-3.11.patch deleted file mode 100644 index 8bc7aee63..000000000 --- a/cpython-unix/patch-python-link-modules-3.11.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/Makefile.pre.in b/Makefile.pre.in -index b356f6293e..89fddd4d4e 100644 ---- a/Makefile.pre.in -+++ b/Makefile.pre.in -@@ -702,7 +702,7 @@ clinic: check-clean-src $(srcdir)/Modules/_blake2/blake2s_impl.c - - # Build the interpreter - $(BUILDPYTHON): Programs/python.o $(LINK_PYTHON_DEPS) -- $(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS) -+ $(LINKCC) $(PY_CORE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(LINK_PYTHON_OBJS) $(LIBS) $(SYSLIBS) - - platform: $(PYTHON_FOR_BUILD_DEPS) pybuilddir.txt - $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print("%s-%d.%d" % (get_platform(), *sys.version_info[:2]))' >platform diff --git a/cpython-unix/patch-python-link-modules-3.15.patch b/cpython-unix/patch-python-link-modules-3.15.patch deleted file mode 100644 index 5225bb6f9..000000000 --- a/cpython-unix/patch-python-link-modules-3.15.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/Makefile.pre.in b/Makefile.pre.in -index 120a6add385..4d8abc5256a 100644 ---- a/Makefile.pre.in -+++ b/Makefile.pre.in -@@ -990,7 +990,7 @@ clinic-tests: check-clean-src $(srcdir)/Lib/test/clinic.test.c - - # Build the interpreter - $(BUILDPYTHON): Programs/python.o $(LINK_PYTHON_DEPS) -- $(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(LINK_PYTHON_OBJS) $(LIBS) $(MODLIBS) $(SYSLIBS) -+ $(LINKCC) $(PY_CORE_EXE_LDFLAGS) $(LINKFORSHARED) -o $@ Programs/python.o $(LINK_PYTHON_OBJS) $(LIBS) $(SYSLIBS) - - platform: $(PYTHON_FOR_BUILD_DEPS) pybuilddir.txt - $(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import sys ; from sysconfig import get_platform ; print("%s-%d.%d" % (get_platform(), *sys.version_info[:2]))' >platform From 7baf76fc1a8e4540abf0609b1724186445c2fd59 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Wed, 29 Apr 2026 11:46:34 -0500 Subject: [PATCH 05/11] remove tcl/tk translations and demo files from distributions (#1104) Remove the Tcl/Tk translation files (.msg) and the Tk demos from the distribution artifacts. The former would only be needed in edge cases and the later is never be needed. closes #1095 --- cpython-unix/build-cpython.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 3a0fdbcbf..7f7873455 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -1355,6 +1355,13 @@ if [ -d "${TOOLS_PATH}/deps/lib/tcl9" ]; then ) fi +# Prune the tk demos which are > 1 MB and not used +rm -rf "${ROOT}/out/python/install/lib/tk9.0/demos" + +# Prune the tcl/tk translations +rm -rf "${ROOT}/out/python/install/lib/tcl9.0/msgs" +rm -rf "${ROOT}/out/python/install/lib/tk9.0/msgs" + # Copy the terminfo database if present. if [ -d "${TOOLS_PATH}/deps/usr/share/terminfo" ]; then cp -av "${TOOLS_PATH}/deps/usr/share/terminfo" "${ROOT}/out/python/install/share/" From a21e242a2521c3054867a1cc531b88ef555af366 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 30 Apr 2026 10:37:25 -0500 Subject: [PATCH 06/11] Fix expansion of ci targets when labels are outside the default set (#1113) --- ci-matrix.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/ci-matrix.py b/ci-matrix.py index 2dabb8586..9fde57623 100644 --- a/ci-matrix.py +++ b/ci-matrix.py @@ -110,9 +110,17 @@ def expand_default_triples( """ default_triples = set(pull_request_defaults["targets"]) - expand_platform = "all" in labels.get("platform", set()) - expand_arch = "all" in labels.get("arch", set()) - expand_libc = "all" in labels.get("libc", set()) + platform_labels = labels.get("platform", set()) + arch_labels = labels.get("arch", set()) + libc_labels = labels.get("libc", set()) + + platform_filters = platform_labels - {"all"} + arch_filters = arch_labels - {"all"} + libc_filters = libc_labels - {"all"} + + expand_platform = "all" in platform_labels or bool(platform_filters) + expand_arch = "all" in arch_labels or bool(arch_filters) + expand_libc = "all" in libc_labels or bool(libc_filters) if not (expand_platform or expand_arch or expand_libc): return default_triples @@ -136,14 +144,27 @@ def expand_default_triples( for platform, platform_config in ci_config.items(): for triple, config in platform_config.items(): for d_platform, d_arch, d_arch_variant, d_libc in default_attrs: - if not expand_platform and platform != d_platform: + if platform_filters: + if platform not in platform_filters: + continue + elif "all" not in platform_labels and platform != d_platform: continue - if not expand_arch and ( + + if arch_filters: + if config["arch"] not in arch_filters: + continue + if config.get("arch_variant") != d_arch_variant: + continue + elif "all" not in arch_labels and ( config["arch"] != d_arch or config.get("arch_variant") != d_arch_variant ): continue - if not expand_libc and config.get("libc") != d_libc: + + if libc_filters: + if config.get("libc") not in libc_filters: + continue + elif "all" not in libc_labels and config.get("libc") != d_libc: continue allowed.add(triple) break From b8a06273d5287f2e6d5180e6d4224ca4b819d264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sat, 2 May 2026 05:21:15 +0800 Subject: [PATCH 07/11] Force patchelf to use 64KB page size on loongarch64 (#1107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch addresses a segmentation fault issue on `LoongArch64` platforms caused by incorrect ELF segment alignment during the build process. - fix: #1106 Signed-off-by: 吴小白 <296015668@qq.com> --- cpython-unix/build-patchelf.sh | 31 +++++++++++++++++++++++++++++++ cpython-unix/targets.yml | 2 ++ 2 files changed, 33 insertions(+) diff --git a/cpython-unix/build-patchelf.sh b/cpython-unix/build-patchelf.sh index c21663303..1acf6c559 100755 --- a/cpython-unix/build-patchelf.sh +++ b/cpython-unix/build-patchelf.sh @@ -13,6 +13,37 @@ tar -xf "patchelf-${PATCHELF_VERSION}.tar.bz2" pushd patchelf-0.13.1.20211127.72b6d44 +# TODO: Drop this patch once patchelf is updated to 0.14.0 or newer, +# which includes native LoongArch64 support. +# See: https://github.com/astral-sh/python-build-standalone/issues/1106 +if [[ "${TARGET_TRIPLE}" = loongarch64* ]]; then + patch -p1 << 'EOF' +diff --git a/src/patchelf.cc b/src/patchelf.cc +index 2b7ec8b9..06f41c6f 100644 +--- a/src/patchelf.cc ++++ b/src/patchelf.cc +@@ -57,6 +57,10 @@ static int forcedPageSize = DEFAULT_PAGESIZE; + static int forcedPageSize = -1; + #endif + ++#ifndef EM_LOONGARCH ++#define EM_LOONGARCH 258 ++#endif ++ + using FileContents = std::shared_ptr>; + + #define ElfFileParams class Elf_Ehdr, class Elf_Phdr, class Elf_Shdr, class Elf_Addr, class Elf_Off, class Elf_Dyn, class Elf_Sym, class Elf_Verneed, class Elf_Versym +@@ -460,6 +464,7 @@ unsigned int ElfFile::getPageSize() const + case EM_PPC64: + case EM_AARCH64: + case EM_TILEGX: ++ case EM_LOONGARCH: + return 0x10000; + default: + return 0x1000; +EOF +fi + CC="${HOST_CC}" CXX="${HOST_CXX}" CFLAGS="${EXTRA_HOST_CFLAGS} -fPIC" CPPFLAGS="${EXTRA_HOST_CFLAGS} -fPIC" \ ./configure \ --build="${BUILD_TRIPLE}" \ diff --git a/cpython-unix/targets.yml b/cpython-unix/targets.yml index 588c2db79..d0db3e39e 100644 --- a/cpython-unix/targets.yml +++ b/cpython-unix/targets.yml @@ -267,6 +267,7 @@ loongarch64-unknown-linux-gnu: - '3.12' - '3.13' - '3.14' + - '3.15' docker_image_suffix: .cross-loongarch64 host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -275,6 +276,7 @@ loongarch64-unknown-linux-gnu: target_ldflags: # Hardening - '-Wl,-z,noexecstack' + - '-Wl,-z,max-page-size=0x10000' needs: - autoconf - bdb From f6ed9c1881dae7b982b8b19b17ecfcdbd8de4f9e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 2 May 2026 11:00:05 -0500 Subject: [PATCH 08/11] Bump 3.14 to 3.14.5rc1 (#1119) --- cpython-unix/build-cpython.sh | 11 ++++----- ...-etree-deepcopy-recursion-depth-3.14.patch | 23 +++++++++++++++++++ pythonbuild/downloads.py | 8 +++---- 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 cpython-unix/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 7f7873455..6eccc09e0 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -595,12 +595,6 @@ if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" && -n "${CROSS_COMPILING}" && "$ PROFILE_TASK="${PROFILE_TASK} --ignore test_strftime_y2k" fi -# On 3.14+ `test_json.test_recursion.TestCRecursion.test_highly_nested_objects_decoding` fails during -# PGO due to RecursionError not being raised as expected. See https://github.com/python/cpython/issues/140125 -if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]]; then - PROFILE_TASK="${PROFILE_TASK} --ignore test_json" -fi - # PGO optimized / BOLT instrumented binaries segfault in a test_bytes test. Skip it. if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" && "${TARGET_TRIPLE}" == x86_64* ]]; then PROFILE_TASK="${PROFILE_TASK} --ignore test.test_bytes.BytesTest.test_from_format" @@ -613,6 +607,11 @@ if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]]; then patch -p1 -i "${ROOT}/patch-python-configure-hacl-no-simd.patch" fi +# See https://github.com/python/cpython/issues/149285 +if [[ "${PYTHON_MAJMIN_VERSION}" = "3.14" ]]; then + patch -p1 -i "${ROOT}/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch" +fi + # We use ndbm on macOS and BerkeleyDB elsewhere. if [[ "${PYBUILD_PLATFORM}" = macos* ]]; then CONFIGURE_FLAGS="${CONFIGURE_FLAGS} --with-dbmliborder=ndbm" diff --git a/cpython-unix/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch b/cpython-unix/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch new file mode 100644 index 000000000..69f0401ae --- /dev/null +++ b/cpython-unix/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Zanie Blue +Date: Sat, 2 May 2026 00:00:00 +0000 +Subject: [PATCH] gh-149285: Increase ElementTree deepcopy recursion test depth + (#149286) + +Backport of https://github.com/python/cpython/pull/149286. +--- + Lib/test/test_xml_etree.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py +--- a/Lib/test/test_xml_etree.py ++++ b/Lib/test/test_xml_etree.py +@@ -3105,7 +3105,7 @@ class BadElementTest(unittest.TestCase): + # This should raise a RecursionError and not crash. + # See https://github.com/python/cpython/issues/148801. + root = cur = ET.Element('s') +- for _ in range(150_000): ++ for _ in range(500_000): + cur = ET.SubElement(cur, 'u') + with support.infinite_recursion(): + with self.assertRaises(RecursionError): diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 5b8431f91..4ecb628c8 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -84,10 +84,10 @@ "python_tag": "cp313", }, "cpython-3.14": { - "url": "https://www.python.org/ftp/python/3.14.4/Python-3.14.4.tar.xz", - "size": 23855332, - "sha256": "d923c51303e38e249136fc1bdf3568d56ecb03214efdef48516176d3d7faaef8", - "version": "3.14.4", + "url": "https://www.python.org/ftp/python/3.14.5/Python-3.14.5rc1.tar.xz", + "size": 23895772, + "sha256": "f767b809df4376dd3dffd62a2e3ce90646a84cac04d0235943bc1f0c44c95780", + "version": "3.14.5rc1", "licenses": ["Python-2.0", "CNRI-Python"], "license_file": "LICENSE.cpython.txt", "python_tag": "cp314", From 20f6240400c4c5b9f8cbf115e022f1e92042c575 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 4 May 2026 15:56:21 -0500 Subject: [PATCH 09/11] Update the target for 3.14rc1 (#1120) --- cpython-unix/build-cpython.sh | 5 ---- .../patch-jit-llvm-version-3.14.patch | 12 +++++----- ...-etree-deepcopy-recursion-depth-3.14.patch | 23 ------------------- pythonbuild/downloads.py | 4 ++-- 4 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 cpython-unix/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 6eccc09e0..8367f9cda 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -607,11 +607,6 @@ if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]]; then patch -p1 -i "${ROOT}/patch-python-configure-hacl-no-simd.patch" fi -# See https://github.com/python/cpython/issues/149285 -if [[ "${PYTHON_MAJMIN_VERSION}" = "3.14" ]]; then - patch -p1 -i "${ROOT}/patch-test-xml-etree-deepcopy-recursion-depth-3.14.patch" -fi - # We use ndbm on macOS and BerkeleyDB elsewhere. if [[ "${PYBUILD_PLATFORM}" = macos* ]]; then CONFIGURE_FLAGS="${CONFIGURE_FLAGS} --with-dbmliborder=ndbm" diff --git a/cpython-unix/patch-jit-llvm-version-3.14.patch b/cpython-unix/patch-jit-llvm-version-3.14.patch index 48d5e63bd..a68bfadd2 100644 --- a/cpython-unix/patch-jit-llvm-version-3.14.patch +++ b/cpython-unix/patch-jit-llvm-version-3.14.patch @@ -1,12 +1,12 @@ diff --git a/Tools/jit/_llvm.py b/Tools/jit/_llvm.py --- a/Tools/jit/_llvm.py +++ b/Tools/jit/_llvm.py -@@ -8,7 +8,7 @@ - import subprocess - import typing +@@ -10,7 +10,7 @@ import typing + + import _targets -_LLVM_VERSION = 19 +_LLVM_VERSION = 22 - _LLVM_VERSION_PATTERN = re.compile(rf"version\s+{_LLVM_VERSION}\.\d+\.\d+\S*\s+") - - _P = typing.ParamSpec("_P") + _LLVM_VERSION_PATTERN = re.compile( + rf"(? -Date: Sat, 2 May 2026 00:00:00 +0000 -Subject: [PATCH] gh-149285: Increase ElementTree deepcopy recursion test depth - (#149286) - -Backport of https://github.com/python/cpython/pull/149286. ---- - Lib/test/test_xml_etree.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py ---- a/Lib/test/test_xml_etree.py -+++ b/Lib/test/test_xml_etree.py -@@ -3105,7 +3105,7 @@ class BadElementTest(unittest.TestCase): - # This should raise a RecursionError and not crash. - # See https://github.com/python/cpython/issues/148801. - root = cur = ET.Element('s') -- for _ in range(150_000): -+ for _ in range(500_000): - cur = ET.SubElement(cur, 'u') - with support.infinite_recursion(): - with self.assertRaises(RecursionError): diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 4ecb628c8..92466247b 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -85,8 +85,8 @@ }, "cpython-3.14": { "url": "https://www.python.org/ftp/python/3.14.5/Python-3.14.5rc1.tar.xz", - "size": 23895772, - "sha256": "f767b809df4376dd3dffd62a2e3ce90646a84cac04d0235943bc1f0c44c95780", + "size": 23894408, + "sha256": "67ee56f36fc22e5ada84d452430362e71081804c4f85c33dc5bf4206c27f973c", "version": "3.14.5rc1", "licenses": ["Python-2.0", "CNRI-Python"], "license_file": "LICENSE.cpython.txt", From 1e42e613c355e0271b4a2ed0a69b35f9415c25b9 Mon Sep 17 00:00:00 2001 From: Mohammad Miadh Angkad Date: Wed, 6 May 2026 23:36:10 +0800 Subject: [PATCH 10/11] Bump pip from 26.1 -> 26.1.1 (#1121) --- pythonbuild/downloads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 92466247b..c53264b17 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -279,10 +279,10 @@ "version": "0.13.1", }, "pip": { - "url": "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", - "size": 1812804, - "sha256": "4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", - "version": "26.1", + "url": "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", + "size": 1812777, + "sha256": "99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", + "version": "26.1.1", }, "readline": { # Mirrored from https://ftp.gnu.org/gnu/readline/readline-8.2.tar.gz From 6888ae472d6386ed0bcdd8d7a4e62f1bcd5e51be Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Fri, 8 May 2026 11:18:25 -0700 Subject: [PATCH 11/11] CPython 3.15.0a8 -> 3.15.0b1 (#1123) * Update CPython 3.15 to 3.15.0b1 --- cpython-unix/build-cpython.sh | 17 +- cpython-unix/extension-modules.yml | 4 + ...ch-site-reentrant-startup-files-3.15.patch | 158 ++++++++++++++++++ cpython-windows/build.py | 62 ++++++- ...ch-site-reentrant-startup-files-3.15.patch | 145 ++++++++++++++++ pythonbuild/downloads.py | 8 +- src/validation.rs | 22 ++- 7 files changed, 397 insertions(+), 19 deletions(-) create mode 100644 cpython-unix/patch-site-reentrant-startup-files-3.15.patch create 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 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