diff --git a/ci-matrix.py b/ci-matrix.py index 2f7fc193..21363cc2 100644 --- a/ci-matrix.py +++ b/ci-matrix.py @@ -71,7 +71,11 @@ def should_include_entry(entry: dict[str, str], filters: dict[str, set[str]]) -> if filters.get("arch") and entry["arch"] not in filters["arch"]: return False - if filters.get("libc") and entry.get("libc") not in filters["libc"]: + if ( + filters.get("libc") + and entry.get("libc") + and entry["libc"] not in filters["libc"] + ): return False if filters.get("build"): diff --git a/ci-targets.yaml b/ci-targets.yaml index 940f1454..fa20735e 100644 --- a/ci-targets.yaml +++ b/ci-targets.yaml @@ -9,6 +9,7 @@ darwin: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - pgo+lto @@ -26,6 +27,7 @@ darwin: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - pgo+lto @@ -45,6 +47,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt @@ -65,6 +68,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt @@ -85,6 +89,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt @@ -105,6 +110,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt @@ -125,6 +131,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt @@ -145,6 +152,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - pgo+lto @@ -165,6 +173,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - pgo+lto @@ -185,6 +194,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - pgo+lto @@ -205,6 +215,7 @@ linux: - "3.11" - "3.12" - "3.13" + - "3.14" build_options: - debug - noopt diff --git a/cpython-unix/Makefile b/cpython-unix/Makefile index 779bc12e..0e859784 100644 --- a/cpython-unix/Makefile +++ b/cpython-unix/Makefile @@ -6,7 +6,7 @@ BUILD := $(HERE)/build.py NULL := SPACE := $(subst ,, ) -ALL_PYTHON_VERSIONS := 3.9 3.10 3.11 3.12 3.13 +ALL_PYTHON_VERSIONS := 3.9 3.10 3.11 3.12 3.13 3.14 ifndef PYBUILD_TARGET_TRIPLE $(error PYBUILD_TARGET_TRIPLE not defined) diff --git a/cpython-unix/build-cpython.sh b/cpython-unix/build-cpython.sh index 671ce34c..fae8cd31 100755 --- a/cpython-unix/build-cpython.sh +++ b/cpython-unix/build-cpython.sh @@ -70,12 +70,14 @@ cat Makefile.extra pushd Python-${PYTHON_VERSION} # configure doesn't support cross-compiling on Apple. Teach it. -if [ "${PYTHON_MAJMIN_VERSION}" = "3.13" ]; then - patch -p1 -i ${ROOT}/patch-apple-cross-3.13.patch -elif [ "${PYTHON_MAJMIN_VERSION}" = "3.12" ]; then - patch -p1 -i ${ROOT}/patch-apple-cross-3.12.patch -else - patch -p1 -i ${ROOT}/patch-apple-cross.patch +if [ "${PYBUILD_PLATFORM}" = "macos" ]; then + if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" ]; then + patch -p1 -i ${ROOT}/patch-apple-cross-3.13.patch + elif [ "${PYTHON_MAJMIN_VERSION}" = "3.12" ]; then + patch -p1 -i ${ROOT}/patch-apple-cross-3.12.patch + else + patch -p1 -i ${ROOT}/patch-apple-cross.patch + fi fi # This patch is slightly different on Python 3.10+. @@ -94,7 +96,9 @@ fi # Configure nerfs RUNSHARED when cross-compiling, which prevents PGO from running when # we can in fact run the target binaries (e.g. x86_64 host and i686 target). Undo that. if [ -n "${CROSS_COMPILING}" ]; then - if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" ]; then + if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]; then + patch -p1 -i ${ROOT}/patch-dont-clear-runshared-14.patch + elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_13}" ]; then patch -p1 -i ${ROOT}/patch-dont-clear-runshared-13.patch elif [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_11}" ]; then patch -p1 -i ${ROOT}/patch-dont-clear-runshared.patch @@ -471,6 +475,18 @@ if [ "${PYBUILD_PLATFORM}" = "macos" ]; then CONFIGURE_FLAGS="${CONFIGURE_FLAGS} ac_cv_func_ptsname_r=no" fi +# explicit_bzero is only available in glibc 2.25+, but we target a lower version for compatibility. +# it's only needed for the HACL Blake2 implementation in Python 3.14+ +if [ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" ]; then + CONFIGURE_FLAGS="${CONFIGURE_FLAGS} ac_cv_func_explicit_bzero=no" +fi + +# On 3.14+ `test_strftime_y2k` fails when cross-compiling for `x86_64_v2` and `x86_64_v3` targets on +# Linux, so we ignore it. See https://github.com/python/cpython/issues/128104 +if [[ -n "${PYTHON_MEETS_MINIMUM_VERSION_3_14}" && -n "${CROSS_COMPILING}" && "${PYBUILD_PLATFORM}" != "macos" ]]; then + export PROFILE_TASK='-m test --pgo --ignore test_strftime_y2k' +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/build-main.py b/cpython-unix/build-main.py index e837dd5d..a41e333b 100755 --- a/cpython-unix/build-main.py +++ b/cpython-unix/build-main.py @@ -11,6 +11,7 @@ import subprocess import sys +from pythonbuild.cpython import meets_python_minimum_version from pythonbuild.downloads import DOWNLOADS from pythonbuild.utils import ( compress_python_archive, @@ -68,6 +69,7 @@ def main(): "cpython-3.11", "cpython-3.12", "cpython-3.13", + "cpython-3.14", }, default="cpython-3.11", help="Python distribution to build", @@ -164,7 +166,9 @@ def main(): release_tag = release_tag_from_git() # Guard against accidental misuse of the free-threaded flag with older versions - if "freethreaded" in args.options and python_majmin not in ("3.13",): + if "freethreaded" in args.options and not meets_python_minimum_version( + python_majmin, "3.13" + ): print( "Invalid build option: 'freethreaded' is only compatible with CPython 3.13+ (got %s)" % cpython_version diff --git a/cpython-unix/build.py b/cpython-unix/build.py index 9bfc93ef..f7ee174e 100755 --- a/cpython-unix/build.py +++ b/cpython-unix/build.py @@ -465,7 +465,7 @@ def build_cpython_host( # Set environment variables allowing convenient testing for Python # version ranges. - for v in ("3.9", "3.10", "3.11", "3.12", "3.13"): + for v in ("3.9", "3.10", "3.11", "3.12", "3.13", "3.14"): normal_version = v.replace(".", "_") if meets_python_minimum_version(python_version, v): @@ -801,7 +801,7 @@ def build_cpython( # Set environment variables allowing convenient testing for Python # version ranges. - for v in ("3.9", "3.10", "3.11", "3.12", "3.13"): + for v in ("3.9", "3.10", "3.11", "3.12", "3.13", "3.14"): normal_version = v.replace(".", "_") if meets_python_minimum_version(python_version, v): @@ -1223,6 +1223,7 @@ def main(): "cpython-3.11", "cpython-3.12", "cpython-3.13", + "cpython-3.14", ): build_cpython( settings, diff --git a/cpython-unix/extension-modules.yml b/cpython-unix/extension-modules.yml index 36831666..d7d3f5a8 100644 --- a/cpython-unix/extension-modules.yml +++ b/cpython-unix/extension-modules.yml @@ -19,10 +19,36 @@ _bisect: - _bisectmodule.c _blake2: - sources: - - _blake2/blake2module.c - - _blake2/blake2b_impl.c - - _blake2/blake2s_impl.c + # In 3.14+, Blake2 is provided by Hacl* + sources-conditional: + - sources: + - _blake2/blake2module.c + - _blake2/blake2b_impl.c + - _blake2/blake2s_impl.c + maximum-python-version: "3.13" + - sources: + - blake2module.c + - _hacl/Hacl_Hash_Blake2s.c + - _hacl/Hacl_Hash_Blake2b.c + - _hacl/Lib_Memzero0.c + minimum-python-version: "3.14" + includes-conditional: + - includes: + - Modules/_hacl + - Modules/_hacl/include + - Modules/_hacl/internal + minimum-python-version: "3.14" + links-conditional: + - name: :libHacl_Hash_Blake2.a + minimum-python-version: "3.14" + defines-conditional: + - define: _BSD_SOURCE + minimum-python-version: "3.14" + - define: _DEFAULT_SOURCE + minimum-python-version: "3.14" + # Disable `explicit_bzero`, it requires glib 2.25+ + - define: LINUX_NO_EXPLICIT_BZERO + minimum-python-version: "3.14" _bz2: sources: @@ -355,6 +381,9 @@ _multiprocessing: _opcode: sources: - _opcode.c + setup-enabled-conditional: + - enabled: true + minimum-python-version: "3.14" _operator: setup-enabled: true diff --git a/cpython-unix/patch-dont-clear-runshared-14.patch b/cpython-unix/patch-dont-clear-runshared-14.patch new file mode 100644 index 00000000..e0862660 --- /dev/null +++ b/cpython-unix/patch-dont-clear-runshared-14.patch @@ -0,0 +1,15 @@ +diff --git a/configure.ac b/configure.ac +index bd0221481c5..f2fb52c1efc 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -1605,10 +1605,6 @@ else # shared is disabled + fi + AC_MSG_RESULT([$LDLIBRARY]) + +-if test "$cross_compiling" = yes; then +- RUNSHARED= +-fi +- + # HOSTRUNNER - Program to run CPython for the host platform + AC_MSG_CHECKING([HOSTRUNNER]) + if test -z "$HOSTRUNNER" diff --git a/cpython-unix/targets.yml b/cpython-unix/targets.yml index c01d93e2..a442cc87 100644 --- a/cpython-unix/targets.yml +++ b/cpython-unix/targets.yml @@ -64,6 +64,7 @@ aarch64-apple-darwin: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -153,6 +154,7 @@ aarch64-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -232,6 +234,7 @@ armv7-unknown-linux-gnueabi: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -271,6 +274,7 @@ armv7-unknown-linux-gnueabihf: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -310,6 +314,7 @@ i686-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -354,6 +359,7 @@ mips-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -393,6 +399,7 @@ mipsel-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -432,6 +439,7 @@ ppc64le-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -471,6 +479,7 @@ s390x-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' docker_image_suffix: .cross host_cc: /usr/bin/x86_64-linux-gnu-gcc host_cxx: /usr/bin/x86_64-linux-gnu-g++ @@ -554,6 +563,7 @@ x86_64-apple-darwin: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true apple_sdk_platform: macosx host_cc: clang @@ -562,7 +572,7 @@ x86_64-apple-darwin: target_cflags: - '-arch' - 'x86_64' - - '-mmacosx-version-min=10.9' + - '-mmacosx-version-min=10.15' # Suppress extremely verbose warnings we see with LLVM 10. - '-Wno-nullability-completeness' - '-Wno-expansion-to-defined' @@ -577,7 +587,7 @@ x86_64-apple-darwin: target_ldflags: - '-arch' - 'x86_64' - - '-mmacosx-version-min=10.9' + - '-mmacosx-version-min=10.15' needs: - autoconf - bzip2 @@ -723,6 +733,7 @@ x86_64-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -767,6 +778,7 @@ x86_64_v2-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -812,6 +824,7 @@ x86_64_v3-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -857,6 +870,7 @@ x86_64_v4-unknown-linux-gnu: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -902,6 +916,7 @@ x86_64-unknown-linux-musl: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -944,6 +959,7 @@ x86_64_v2-unknown-linux-musl: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -987,6 +1003,7 @@ x86_64_v3-unknown-linux-musl: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ @@ -1030,6 +1047,7 @@ x86_64_v4-unknown-linux-musl: - '3.11' - '3.12' - '3.13' + - '3.14' needs_toolchain: true host_cc: clang host_cxx: clang++ diff --git a/pythonbuild/cpython.py b/pythonbuild/cpython.py index c499cc0f..50ad6f70 100644 --- a/pythonbuild/cpython.py +++ b/pythonbuild/cpython.py @@ -511,7 +511,10 @@ def derive_setup_local( ) if target_match and (python_min_match and python_max_match): - line += f" {entry['source']}" + if source := entry.get("source"): + line += f" {source}" + for source in entry.get("sources", []): + line += f" {source}" for define in info.get("defines", []): line += f" -D{define}" @@ -549,7 +552,11 @@ def derive_setup_local( ) if target_match and (python_min_match and python_max_match): - line += f" -I{entry['path']}" + # TODO: Change to `include` and drop support for `path` + if include := entry.get("path"): + line += f" -I{include}" + for include in entry.get("includes", []): + line += f" -I{include}" for path in info.get("includes-deps", []): # Includes are added to global search path. @@ -562,7 +569,19 @@ def derive_setup_local( line += " %s" % link_for_target(lib, target_triple) for entry in info.get("links-conditional", []): - if any(re.match(p, target_triple) for p in entry["targets"]): + if targets := entry.get("targets", []): + target_match = any(re.match(p, target_triple) for p in targets) + else: + target_match = True + + python_min_match = meets_python_minimum_version( + python_version, entry.get("minimum-python-version", "1.0") + ) + python_max_match = meets_python_maximum_version( + python_version, entry.get("maximum-python-version", "100.0") + ) + + if target_match and (python_min_match and python_max_match): line += " %s" % link_for_target(entry["name"], target_triple) if "-apple-" in target_triple: diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index f444a4e8..6505c811 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -79,6 +79,15 @@ "license_file": "LICENSE.cpython.txt", "python_tag": "cp313", }, + "cpython-3.14": { + "url": "https://www.python.org/ftp/python/3.14.0/Python-3.14.0a3.tar.xz", + "size": 22749680, + "sha256": "94349df207456a575a8867c20b4ca434f870e1920dcdcc8fdf797e1af49abe90", + "version": "3.14.0a3", + "licenses": ["Python-2.0", "CNRI-Python"], + "license_file": "LICENSE.cpython.txt", + "python_tag": "cp314", + }, "expat": { "url": "https://github.com/libexpat/libexpat/releases/download/R_2_6_3/expat-2.6.3.tar.xz", "size": 485600, diff --git a/src/validation.rs b/src/validation.rs index 7f5e367c..fa57aec6 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -129,6 +129,8 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[ "python312.dll", "python313.dll", "python313t.dll", + "python314.dll", + "python314t.dll", "sqlite3.dll", "tcl86t.dll", "tk86t.dll", @@ -304,6 +306,26 @@ static DARWIN_ALLOWED_DYLIBS: Lazy> = Lazy::new(|| { max_compatibility_version: "3.13.0".try_into().unwrap(), required: false, }, + MachOAllowedDylib { + name: "@executable_path/../lib/libpython3.14.dylib".to_string(), + max_compatibility_version: "3.14.0".try_into().unwrap(), + required: false, + }, + MachOAllowedDylib { + name: "@executable_path/../lib/libpython3.14d.dylib".to_string(), + max_compatibility_version: "3.14.0".try_into().unwrap(), + required: false, + }, + MachOAllowedDylib { + name: "@executable_path/../lib/libpython3.14t.dylib".to_string(), + max_compatibility_version: "3.14.0".try_into().unwrap(), + required: false, + }, + MachOAllowedDylib { + name: "@executable_path/../lib/libpython3.14td.dylib".to_string(), + max_compatibility_version: "3.14.0".try_into().unwrap(), + required: false, + }, MachOAllowedDylib { name: "/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit".to_string(), max_compatibility_version: "45.0.0".try_into().unwrap(), @@ -467,7 +489,7 @@ static PLATFORM_TAG_BY_TRIPLE: Lazy> = Lazy: ("mips64el-unknown-linux-gnuabi64", "todo"), ("ppc64le-unknown-linux-gnu", "linux-powerpc64le"), ("s390x-unknown-linux-gnu", "linux-s390x"), - ("x86_64-apple-darwin", "macosx-10.9-x86_64"), + ("x86_64-apple-darwin", "macosx-10.15-x86_64"), ("x86_64-apple-ios", "iOS-x86_64"), ("x86_64-pc-windows-msvc", "win-amd64"), ("x86_64-unknown-linux-gnu", "linux-x86_64"), @@ -714,10 +736,20 @@ const GLOBAL_EXTENSIONS_PYTHON_3_13: &[&str] = &[ "_zoneinfo", ]; +const GLOBAL_EXTENSIONS_PYTHON_3_14: &[&str] = &[ + "_interpchannels", + "_interpqueues", + "_interpreters", + "_sha2", + "_sysconfig", + "_tokenize", + "_typing", + "_zoneinfo", +]; + const GLOBAL_EXTENSIONS_MACOS: &[&str] = &["_scproxy"]; const GLOBAL_EXTENSIONS_POSIX: &[&str] = &[ - "_crypt", "_ctypes_test", "_curses", "_curses_panel", @@ -735,6 +767,8 @@ const GLOBAL_EXTENSIONS_POSIX: &[&str] = &[ "termios", ]; +const GLOBAL_EXTENSIONS_POSIX_PRE_3_13: &[&str] = &["_crypt"]; + const GLOBAL_EXTENSIONS_LINUX_PRE_3_13: &[&str] = &["spwd"]; const GLOBAL_EXTENSIONS_WINDOWS: &[&str] = &[ @@ -1070,7 +1104,6 @@ fn parse_version_nibbles(v: u32) -> semver::Version { fn validate_macho>( context: &mut ValidationContext, target_triple: &str, - python_major_minor: &str, advertised_target_version: &str, advertised_sdk_version: &str, path: &Path, @@ -1365,7 +1398,6 @@ fn validate_possible_object_file( validate_macho( &mut context, triple, - python_major_minor, json.apple_sdk_deployment_target .as_ref() .expect("apple_sdk_deployment_target should be set"), @@ -1383,7 +1415,6 @@ fn validate_possible_object_file( validate_macho( &mut context, triple, - python_major_minor, json.apple_sdk_deployment_target .as_ref() .expect("apple_sdk_deployment_target should be set"), @@ -1454,6 +1485,9 @@ fn validate_extension_modules( "3.13" => { wanted.extend(GLOBAL_EXTENSIONS_PYTHON_3_13); } + "3.14" => { + wanted.extend(GLOBAL_EXTENSIONS_PYTHON_3_14); + } _ => { panic!("unhandled Python version: {}", python_major_minor); } @@ -1461,9 +1495,11 @@ fn validate_extension_modules( if is_macos { wanted.extend(GLOBAL_EXTENSIONS_POSIX); - if python_major_minor == "3.13" { - wanted.remove("_crypt"); + + if matches!(python_major_minor, "3.9" | "3.10" | "3.11" | "3.12") { + wanted.extend(GLOBAL_EXTENSIONS_POSIX_PRE_3_13); } + wanted.extend(GLOBAL_EXTENSIONS_MACOS); } @@ -1483,11 +1519,11 @@ fn validate_extension_modules( if is_linux { wanted.extend(GLOBAL_EXTENSIONS_POSIX); - // TODO: If there are more differences for `GLOBAL_EXTENSIONS_POSIX` in future Python - // versions, we should move the `_crypt` special-case into a constant - if python_major_minor == "3.13" { - wanted.remove("_crypt"); + + if matches!(python_major_minor, "3.9" | "3.10" | "3.11" | "3.12") { + wanted.extend(GLOBAL_EXTENSIONS_POSIX_PRE_3_13); } + if matches!(python_major_minor, "3.9" | "3.10" | "3.11" | "3.12") { wanted.extend(GLOBAL_EXTENSIONS_LINUX_PRE_3_13); } @@ -1497,7 +1533,7 @@ fn validate_extension_modules( } } - if (is_linux || is_macos) { + if is_linux || is_macos { wanted.extend([ "_testbuffer", "_testimportmultiple", @@ -1506,11 +1542,11 @@ fn validate_extension_modules( ]); } - if (is_linux || is_macos) && python_major_minor == "3.13" { + if (is_linux || is_macos) && matches!(python_major_minor, "3.13" | "3.14") { wanted.extend(["_suggestions", "_testexternalinspection"]); } - if (is_linux || is_macos) && matches!(python_major_minor, "3.12" | "3.13") { + if (is_linux || is_macos) && matches!(python_major_minor, "3.12" | "3.13" | "3.14") { wanted.insert("_testsinglephase"); } @@ -1638,6 +1674,8 @@ fn validate_distribution( "3.12" } else if dist_filename.starts_with("cpython-3.13.") { "3.13" + } else if dist_filename.starts_with("cpython-3.14.") { + "3.14" } else { return Err(anyhow!("could not parse Python version from filename")); };