From edbeefabf25b74a1af36cb24d81ec6ef0078871a Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:42:36 +0000 Subject: [PATCH 1/7] Add Windows ARM64EC builds --- ci-matrix.py | 5 +- ci-targets.yaml | 21 ++++++ cpython-windows/build.py | 103 ++++++++++++++++++++++++++-- cpython-windows/xz-support/config.h | 2 +- docs/building.md | 13 ++++ docs/building.rst | 11 +++ docs/running.md | 6 ++ docs/running.rst | 5 ++ src/release.rs | 13 ++++ src/validation.rs | 2 + 10 files changed, 174 insertions(+), 7 deletions(-) diff --git a/ci-matrix.py b/ci-matrix.py index c90744f59..71f84d7eb 100644 --- a/ci-matrix.py +++ b/ci-matrix.py @@ -411,10 +411,11 @@ def add_python_build_entries_for_config( python_versions = config["python_versions"] build_options = config["build_options"] arch = config["arch"] - runner = find_runner(runners, platform, arch, False) + host_arch = "aarch64" if platform == "windows" and arch == "arm64ec" else arch + runner = find_runner(runners, platform, host_arch, False) runner_arch = runners[runner]["arch"] # 64-bit Windows runners can also execute 32-bit Windows builds. - can_run = runner_arch == arch or ( + can_run = runner_arch == host_arch or ( platform == "windows" and runner_arch == "x86_64" and arch == "x86" ) diff --git a/ci-targets.yaml b/ci-targets.yaml index 83f430416..acf35e080 100644 --- a/ci-targets.yaml +++ b/ci-targets.yaml @@ -422,6 +422,27 @@ windows: - freethreaded+pgo minimum-python-version: "3.13" + arm64ec-pc-windows-msvc: + arch: arm64ec + python_versions: + - "3.11" + - "3.12" + - "3.13" + - "3.14" + - "3.15" + - "3.16" + vs_version: "2026" + platform_toolset: "v143" + vc_tools_version: "14.4" + build_options: + - debug + - pgo + build_options_conditional: + - options: + - freethreaded+debug + - freethreaded+pgo + minimum-python-version: "3.13" + aarch64-pc-windows-msvc: arch: aarch64 python_versions: diff --git a/cpython-windows/build.py b/cpython-windows/build.py index a87d9731a..2af986ca0 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -47,6 +47,7 @@ "i686-pc-windows-msvc": "x86", "x86_64-pc-windows-msvc": "amd64", "aarch64-pc-windows-msvc": "arm64", + "arm64ec-pc-windows-msvc": "arm64ec", } LOG_PREFIX = [None] @@ -734,6 +735,91 @@ def hack_project_files( pass +def prepare_arm64ec(cpython_source_path: pathlib.Path) -> None: + """Add ARM64EC configurations while retaining the x64 extension ABI.""" + pcbuild_path = cpython_source_path / "PCbuild" + + configurations = re.compile( + rb' .*?', + re.DOTALL, + ) + for project in pcbuild_path.glob("*.vcxproj"): + data = project.read_bytes() + newline = b"\r\n" if b"\r\n" in data else b"\n" + data = configurations.sub( + lambda match, newline=newline: ( + match[0] + newline + match[0].replace(b"ARM64", b"ARM64EC") + ), + data, + ) + # The stable-ABI DLLs export only forwarders and can be built for x64. + data = re.sub( + rb'(]*>)', + rb"\1\n Platform=x64", + data, + ) + project.write_bytes(data) + + # Use the x64 layout, dependency bundles, and extension suffixes. + static_replace_in_file( + pcbuild_path / "python.props", + b"and $(Platform) == 'x64'\">amd64", + b"and ($(Platform) == 'x64' or $(Platform) == 'ARM64EC')\">amd64", + ) + static_replace_in_file( + pcbuild_path / "pyproject.props", + b"MachineARM64", + b"MachineARM64\n" + b" MachineARM64EC", + ) + static_replace_in_file( + pcbuild_path / "pyproject.props", + b"$(VCRedistDir)$(Platform)\\", + b"$(VCRedistDir)$(Platform)\\\n" + b" $(VCRedistDir)x64\\", + ) + + # Use portable decimal arithmetic instead of x64 assembly. + decimal = pcbuild_path / "_decimal.vcxproj" + data = decimal.read_bytes() + for condition in (b"'$(Platform)'=='ARM64'", b"'$(Platform)' == 'ARM64'"): + data = data.replace( + condition, b"(" + condition + b" or '$(Platform)' == 'ARM64EC')" + ) + decimal.write_bytes(data) + + # The x64 PGO runtime can be loaded by an ARM64EC executable. + python_project = pcbuild_path / "python.vcxproj" + data = python_project.read_bytes().replace( + b"$(Platform) == 'x64'", + b"($(Platform) == 'x64' or $(Platform) == 'ARM64EC')", + ) + python_project.write_bytes(data) + + projects = pcbuild_path / "pcbuild.proj" + data = re.sub( + rb'', + rb'x64', + projects.read_bytes(), + ) + projects.write_bytes(data) + + # ARM64EC defines _M_X64 for ABI compatibility, but needs ARM memory barriers. + for relative_path in ( + "Include/internal/pycore_atomic.h", + "Include/cpython/pyatomic_msc.h", + "Include/internal/mimalloc/mimalloc/atomic.h", + ): + path = cpython_source_path / relative_path + if not path.exists(): + continue + data = path.read_bytes() + data = data.replace( + b"defined(_M_X64)", b"(defined(_M_X64) && !defined(_M_ARM64EC))" + ).replace(b"defined(_M_ARM64)", b"(defined(_M_ARM64) || defined(_M_ARM64EC))") + path.write_bytes(data) + + def run_msbuild( msbuild: pathlib.Path, pcbuild_path: pathlib.Path, @@ -1483,6 +1569,9 @@ def build_cpython( elif arch == "arm64": build_platform = "arm64" build_directory = "arm64" + elif arch == "arm64ec": + build_platform = "ARM64EC" + build_directory = "amd64" else: raise Exception(f"unhandled architecture: {arch}") @@ -1540,7 +1629,7 @@ def build_cpython( # We need all the OpenSSL library files in the same directory to appease # install rules. - openssl_arch = {"amd64": "amd64", "x86": "win32", "arm64": "arm64"}[arch] + openssl_arch = build_directory openssl_root = td / "openssl" / openssl_arch openssl_bin_path = openssl_root / "bin" openssl_lib_path = openssl_root / "lib" @@ -1605,6 +1694,8 @@ def build_cpython( zlib_entry=zlib_entry, tk_bin_entry=tk_bin_entry, ) + if arch == "arm64ec": + prepare_arm64ec(cpython_source_path) if pgo: run_msbuild( @@ -2044,6 +2135,8 @@ def main() -> None: build_options = args.options target_triple = args.target_triple or default_target_triple() arch = TARGET_ARCHITECTURES[target_triple] + # OpenSSL, libffi, and Tcl/Tk use the x64 ABI in ARM64EC distributions. + dependency_arch = "amd64" if arch == "arm64ec" else arch platform_toolset = args.platform_toolset or ( "v145" if arch == "amd64" @@ -2061,7 +2154,9 @@ def main() -> None: log(f"building for {target_triple}") log(f"using {platform_toolset} with MSVC {vc_tools_version}") - build_env = get_visual_studio_environment(args.vs, arch, vc_tools_version) + build_env = get_visual_studio_environment( + args.vs, dependency_arch, vc_tools_version + ) os.environ.update(build_env) compiler_cache_key = f"{platform_toolset}-{vc_tools_version}" @@ -2084,7 +2179,7 @@ def main() -> None: build_openssl( openssl_entry, perl_path, - arch, + dependency_arch, dest_archive=openssl_archive, with_uplink=openssl_with_uplink, ) @@ -2095,7 +2190,7 @@ def main() -> None: if not libffi_archive.exists(): build_libffi( args.python, - arch, + dependency_arch, pathlib.Path(args.sh), args.vs, vc_tools_version, diff --git a/cpython-windows/xz-support/config.h b/cpython-windows/xz-support/config.h index 81ddf6b7f..78fd8d182 100644 --- a/cpython-windows/xz-support/config.h +++ b/cpython-windows/xz-support/config.h @@ -31,7 +31,7 @@ This is used by CPython, and is not part of the regular xz release. #define HAVE_ENCODER_SPARC 1 #define HAVE_ENCODER_X86 1 -#if defined(_M_ARM64) +#if defined(_M_ARM64) || defined(_M_ARM64EC) #undef HAVE_IMMINTRIN_H #undef HAVE_USABLE_CLMUL diff --git a/docs/building.md b/docs/building.md index be8fc8c77..1901d68b4 100644 --- a/docs/building.md +++ b/docs/building.md @@ -174,3 +174,16 @@ so a debug distribution only runs on a machine with Visual Studio installed. To build a 32-bit x86 binary, simply use an `x86 Native Tools Command Prompt` instead of `x64`. + +To build ARM64EC, use a Windows 11 on ARM host with the Visual Studio +ARM64/ARM64EC and x64 build tools installed: + +``` +$ uv run --no-dev build.py --target-triple arm64ec-pc-windows-msvc --sh c:\cygwin\bin\sh.exe +``` + +The build runs the generated interpreter to assemble the distribution, so +an x64 Windows host cannot build this target. CPython and its bundled +extensions compile as ARM64EC. The stable-ABI forwarding DLLs and the bundled +OpenSSL, libffi, and Tcl/Tk libraries are built for x64. ARM64EC distributions use the +x64-compatible extension ABI and `win_amd64` wheel platform tag. diff --git a/docs/building.rst b/docs/building.rst index ed5e8f5fd..e8f429f9e 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -161,3 +161,14 @@ so a debug distribution only runs on a machine with Visual Studio installed. To build a 32-bit x86 binary, simply use an ``x86 Native Tools Command Prompt`` instead of ``x64``. + +To build ARM64EC, use a Windows 11 on ARM host with the Visual Studio +ARM64/ARM64EC and x64 build tools installed:: + + $ uv run --no-dev build.py --target-triple arm64ec-pc-windows-msvc --sh c:\cygwin\bin\sh.exe + +The build runs the generated interpreter to assemble the distribution, so +an x64 Windows host cannot build this target. CPython and its bundled +extensions compile as ARM64EC. The stable-ABI forwarding DLLs and the bundled +OpenSSL, libffi, and Tcl/Tk libraries are built for x64. ARM64EC distributions use the +x64-compatible extension ABI and ``win_amd64`` wheel platform tag. diff --git a/docs/running.md b/docs/running.md index e8717c781..4ae9d5fa1 100644 --- a/docs/running.md +++ b/docs/running.md @@ -77,6 +77,12 @@ Distributions are produced for the following target triples: : Windows 64-bit ARM CPUs. Available for CPython 3.11 and newer. +`arm64ec-pc-windows-msvc` + +: Windows 11 on ARM, with support for x64 extension modules and + `win_amd64` wheels. Available for CPython 3.11 and newer. These builds + cannot run on x64 Windows or load native ARM64 extension modules. + `x86_64-unknown-linux-gnu` : Linux 64-bit Intel/AMD CPUs linking against GNU libc. diff --git a/docs/running.rst b/docs/running.rst index 897dd7964..b15dc1716 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -72,6 +72,11 @@ Distributions are produced for the following target triples: ``aarch64-pc-windows-msvc`` Windows 64-bit ARM CPUs. Available for CPython 3.11 and newer. +``arm64ec-pc-windows-msvc`` + Windows 11 on ARM, with support for x64 extension modules and + ``win_amd64`` wheels. Available for CPython 3.11 and newer. These builds + cannot run on x64 Windows or load native ARM64 extension modules. + ``x86_64-unknown-linux-gnu`` Linux 64-bit Intel/AMD CPUs linking against GNU libc. diff --git a/src/release.rs b/src/release.rs index e92c73b65..c691963ba 100644 --- a/src/release.rs +++ b/src/release.rs @@ -140,6 +140,19 @@ pub static RELEASE_TRIPLES: Lazy> = Lazy:: }], }, ); + h.insert( + "arm64ec-pc-windows-msvc", + TripleRelease { + suffixes: windows_suffixes.clone(), + install_only_suffix: "pgo", + freethreaded_install_only_suffix: "freethreaded+pgo", + python_version_requirement: Some(VersionSpecifier::from_str(">=3.11").unwrap()), + conditional_suffixes: vec![ConditionalSuffixes { + python_version_requirement: VersionSpecifier::from_str(">=3.13").unwrap(), + suffixes: windows_suffixes_313.clone(), + }], + }, + ); h.insert( "aarch64-pc-windows-msvc", TripleRelease { diff --git a/src/validation.rs b/src/validation.rs index fa30a1d3e..fe919927d 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -37,6 +37,7 @@ const RECOGNIZED_TRIPLES: &[&str] = &[ "aarch64-pc-windows-msvc", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", + "arm64ec-pc-windows-msvc", "armv7-unknown-linux-gnueabi", "armv7-unknown-linux-gnueabihf", "i686-pc-windows-msvc", @@ -594,6 +595,7 @@ static PLATFORM_TAG_BY_TRIPLE: Lazy> = Lazy: [ ("aarch64-apple-darwin", "macosx-11.0-arm64"), ("aarch64-pc-windows-msvc", "win-arm64"), + ("arm64ec-pc-windows-msvc", "win-amd64"), ("aarch64-unknown-linux-gnu", "linux-aarch64"), ("aarch64-unknown-linux-musl", "linux-aarch64"), ("armv7-unknown-linux-gnueabi", "linux-arm"), From 3ed7e1c3dec10630cf0402354c8cd0e7bf730c57 Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:43:05 +0000 Subject: [PATCH 2/7] Cover Windows dependency architecture selection --- .github/workflows/check.yml | 2 +- tests/test_windows.py | 106 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/test_windows.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d70d9b54f..bccfc91c1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -33,7 +33,7 @@ jobs: - name: "Run unit tests" run: | - uv run python -m unittest tests/test_mirror.py tests/test_archive.py + uv run python -m unittest tests/test_mirror.py tests/test_archive.py tests/test_windows.py - name: "Run mirror integration test" run: | diff --git a/tests/test_windows.py b/tests/test_windows.py new file mode 100644 index 000000000..fe1ae8bdd --- /dev/null +++ b/tests/test_windows.py @@ -0,0 +1,106 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +import importlib.util +import tempfile +import unittest +from contextlib import ExitStack +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parent.parent +SPEC = importlib.util.spec_from_file_location( + "windows_build", ROOT / "cpython-windows" / "build.py" +) +assert SPEC is not None and SPEC.loader is not None +windows_build = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(windows_build) + + +class WindowsBuildTests(unittest.TestCase): + def test_dependency_architecture(self) -> None: + for target, arch, dependency_arch in ( + ("i686", "x86", "x86"), + ("x86_64", "amd64", "amd64"), + ("aarch64", "arm64", "arm64"), + ("arm64ec", "arm64ec", "amd64"), + ): + with ( + self.subTest(target=target), + tempfile.TemporaryDirectory() as directory, + ): + root = Path(directory) + triple = f"{target}-pc-windows-msvc" + with ExitStack() as stack: + stack.enter_context(patch.object(windows_build, "BUILD", root)) + stack.enter_context( + patch.object(windows_build, "DIST", root / "dist") + ) + stack.enter_context( + patch.object( + windows_build.sys, + "argv", + [ + "build.py", + "--target-triple", + triple, + "--python", + "cpython-3.14", + "--sh", + "sh.exe", + ], + ) + ) + mocks = { + name: stack.enter_context(patch.object(windows_build, name)) + for name in ( + "get_visual_studio_environment", + "fetch_strawberry_perl", + "build_openssl", + "build_libffi", + "build_cpython", + "compress_python_archive", + "release_tag_from_git", + "log", + ) + } + mocks["get_visual_studio_environment"].return_value = {} + mocks["fetch_strawberry_perl"].return_value = root + mocks["build_cpython"].return_value = root / "python.tar" + windows_build.main() + + mocks["get_visual_studio_environment"].assert_called_once_with( + "2022", dependency_arch, "14.4" + ) + self.assertEqual( + mocks["build_openssl"].call_args.args[2], dependency_arch + ) + self.assertEqual( + mocks["build_libffi"].call_args.args[1], dependency_arch + ) + self.assertEqual( + mocks["build_cpython"].call_args.args[1:3], (triple, arch) + ) + + def test_arm64ec_does_not_enable_x64_tail_call_interpreter(self) -> None: + with patch.object(windows_build, "exec_and_log") as run: + windows_build.run_msbuild( + Path("MSBuild.exe"), + Path("PCbuild"), + "Release", + "ARM64EC", + "3.15.1", + "10.0.26100.0", + "v143", + False, + ) + args = run.call_args.args[0] + self.assertIn("/property:Platform=ARM64EC", args) + self.assertNotIn("/property:UseTailCallInterp=true", args) + + +if __name__ == "__main__": + unittest.main() From acc15d54f06537ce0cbd93772250a6ff8ad2077a Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:07:53 +0000 Subject: [PATCH 3/7] Install x64 v143 tools for ARM64EC dependencies --- .github/workflows/windows.yml | 17 +++++++++++++++++ pythonbuild/disttests/__init__.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a9ad6317b..08e146bcd 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -137,6 +137,23 @@ jobs: shell: pwsh run: ./scripts/install-cygwin.ps1 -Packages autoconf,automake,libtool + - name: Install x64 MSVC v143 tools for ARM64EC dependencies + if: ${{ matrix.arch == 'arm64ec' }} + shell: pwsh + run: | + $installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer" + $installation = & "$installer\vswhere.exe" -latest -products '*' -version '[18,19)' -property installationPath + $arguments = @( + 'modify', '--installPath', "`"$installation`"", + '--add', 'Microsoft.VisualStudio.Component.VC.14.44.17.14.x86.x64', + '--quiet', '--norestart', '--nocache' + ) + $process = Start-Process "$installer\setup.exe" -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -notin @(0, 3010)) { + throw "Visual Studio setup failed with exit code $($process.ExitCode)" + } + Get-ChildItem "$installation\VC\Tools\MSVC\14.4*\lib\x64\msvcrt.lib" -ErrorAction Stop + - name: Set up uv uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: diff --git a/pythonbuild/disttests/__init__.py b/pythonbuild/disttests/__init__.py index d6ad302b8..9d2100e09 100644 --- a/pythonbuild/disttests/__init__.py +++ b/pythonbuild/disttests/__init__.py @@ -129,6 +129,22 @@ def test_ctypes(self): def error_handler(fif, message): pass + @unittest.skipUnless( + os.environ["TARGET_TRIPLE"] == "arm64ec-pc-windows-msvc", + "Requires an ARM64EC distribution", + ) + def test_arm64ec_x64_callback(self): + import ctypes + import sysconfig + + self.assertEqual(sysconfig.get_platform(), "win-amd64") + + @ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_int, ctypes.c_double) + def callback(integer, floating): + return integer + floating + + self.assertEqual(callback(40, 2.5), 42.5) + @unittest.skipIf(os.name == "nt", "curses not available on Windows") def test_curses_import(self): import curses From 15c8a12f6e1e46c0a2a857bff0da6f8d206e3081 Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:37:04 +0000 Subject: [PATCH 4/7] Rename HACL uint128_t alias for ARM64EC --- cpython-windows/build.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 2af986ca0..c403eba2e 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -819,6 +819,13 @@ def prepare_arm64ec(cpython_source_path: pathlib.Path) -> None: ).replace(b"defined(_M_ARM64)", b"(defined(_M_ARM64) || defined(_M_ARM64EC))") path.write_bytes(data) + # The Windows SDK defines `uint128_t` for ARM64EC, conflicting with HACL's definition. + for path in (cpython_source_path / "Modules" / "_hacl" / "include").rglob("*.h"): + data = path.read_bytes() + patched = re.sub(rb"\buint128_t\b", b"Hacl_uint128_t", data) + if patched != data: + path.write_bytes(patched) + def run_msbuild( msbuild: pathlib.Path, From 1eb34b70c1fbf63b32cc16f3726d23b36f1ef908 Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:12:48 +0000 Subject: [PATCH 5/7] Capture ARM64EC interpreter diagnostics --- .github/workflows/windows.yml | 8 +++++ cpython-windows/build.py | 19 +++++++++-- cpython-windows/diagnose-arm64ec.py | 51 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 cpython-windows/diagnose-arm64ec.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 08e146bcd..5bc1581fc 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -183,6 +183,14 @@ jobs: MATRIX_PLATFORM_TOOLSET: ${{ matrix.platform_toolset }} MATRIX_VC_TOOLS_VERSION: ${{ matrix.vc_tools_version }} + - name: Upload ARM64EC interpreter diagnostics + if: ${{ failure() && matrix.arch == 'arm64ec' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: arm64ec-startup-${{ matrix.python }}-${{ matrix.build_options }} + path: build/arm64ec-diagnostics + retention-days: 3 + - name: Generate attestations uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 if: ${{ github.ref == 'refs/heads/main' }} diff --git a/cpython-windows/build.py b/cpython-windows/build.py index c403eba2e..6d8b316e4 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -1740,13 +1740,28 @@ def build_cpython( / instrumented_python_exe ) - tests = subprocess.run( + discovery = subprocess.run( [str(instrumented_python), "-m", "test", "--list-tests"], cwd=cpython_source_path, env=env, check=False, stdout=subprocess.PIPE, - ).stdout + ) + if discovery.returncode or not discovery.stdout.strip(): + log( + f"PGO test discovery exited 0x{discovery.returncode & 0xFFFFFFFF:08X}" + ) + if arch == "arm64ec": + subprocess.run( + [ + sys.executable, + str(SUPPORT / "diagnose-arm64ec.py"), + str(instrumented_python), + ], + check=True, + ) + discovery.check_returncode() + tests = discovery.stdout tests = [l.strip() for l in tests.decode("utf-8").splitlines() if l.strip()] diff --git a/cpython-windows/diagnose-arm64ec.py b/cpython-windows/diagnose-arm64ec.py new file mode 100644 index 000000000..93221ab31 --- /dev/null +++ b/cpython-windows/diagnose-arm64ec.py @@ -0,0 +1,51 @@ +import os +import pathlib +import shutil +import subprocess +import sys + +executable = pathlib.Path(sys.argv[1]) +output = executable.parent +destination = pathlib.Path("build/arm64ec-diagnostics") +original = destination / "original" +original.mkdir(parents=True, exist_ok=True) +for source in output.iterdir(): + if source.suffix.lower() in {".exe", ".dll", ".pyd"}: + shutil.copy2(source, original / source.name) + +tools = pathlib.Path(os.environ["VCToolsInstallDir"]) +crt_directories = [original, None] +crt_directories.extend( + sorted((tools.parents[2] / "Redist/MSVC").glob("*/arm64/Microsoft.VC*.CRT")) +) +pgo_runtimes = [original / "pgort140.dll"] +pgo_runtimes.extend(sorted(tools.rglob("pgort140.dll"))) + +env = dict(os.environ) +env["PYTHONHOME"] = str(executable.parents[3]) +env.pop("PYTHONPATH", None) +for crt in crt_directories: + for runtime in output.glob("vcruntime140*.dll"): + runtime.unlink() + if crt is not None: + for runtime in crt.glob("vcruntime140*.dll"): + shutil.copy2(runtime, output / runtime.name) + for pgo in pgo_runtimes: + shutil.copy2(pgo, output / "pgort140.dll") + print(f"CRT={crt or 'system'} PGO={pgo}", flush=True) + try: + result = subprocess.run( + [str(executable), "-c", "import sys; print(sys.version)"], + env=env, + capture_output=True, + timeout=15, + ) + except subprocess.TimeoutExpired: + print("STARTUP TIMEOUT", flush=True) + continue + print(f"EXIT=0x{result.returncode & 0xFFFFFFFF:08X}", flush=True) + print(result.stdout.decode("utf-8", "replace"), flush=True) + print(result.stderr.decode("utf-8", "replace"), flush=True) + +for source in original.glob("*.dll"): + shutil.copy2(source, output / source.name) From 49720866c46c8c73b48f8c15e94d0c9126035e57 Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:14:35 +0000 Subject: [PATCH 6/7] Use ARM64 redistributables for ARM64EC --- cpython-windows/build.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 6d8b316e4..f79477222 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -772,11 +772,12 @@ def prepare_arm64ec(cpython_source_path: pathlib.Path) -> None: b"MachineARM64\n" b" MachineARM64EC", ) + # Use the ARM64 redistributable for its ARM64X `vcruntime140.dll`. static_replace_in_file( pcbuild_path / "pyproject.props", b"$(VCRedistDir)$(Platform)\\", b"$(VCRedistDir)$(Platform)\\\n" - b" $(VCRedistDir)x64\\", + b" $(VCRedistDir)arm64\\", ) # Use portable decimal arithmetic instead of x64 assembly. From 703327ebb6a30d7e910c900ab4223cd1e8124d23 Mon Sep 17 00:00:00 2001 From: zaniebot <242828183+zaniebot@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:44:29 +0000 Subject: [PATCH 7/7] Use the ARM64 PGO runtime for ARM64EC --- .github/workflows/windows.yml | 8 ----- cpython-windows/build.py | 32 +++++------------- cpython-windows/diagnose-arm64ec.py | 51 ----------------------------- pythonbuild/disttests/__init__.py | 12 ++++++- pythonbuild/testdist.py | 12 ++++++- 5 files changed, 30 insertions(+), 85 deletions(-) delete mode 100644 cpython-windows/diagnose-arm64ec.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 5bc1581fc..08e146bcd 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -183,14 +183,6 @@ jobs: MATRIX_PLATFORM_TOOLSET: ${{ matrix.platform_toolset }} MATRIX_VC_TOOLS_VERSION: ${{ matrix.vc_tools_version }} - - name: Upload ARM64EC interpreter diagnostics - if: ${{ failure() && matrix.arch == 'arm64ec' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: arm64ec-startup-${{ matrix.python }}-${{ matrix.build_options }} - path: build/arm64ec-diagnostics - retention-days: 3 - - name: Generate attestations uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 if: ${{ github.ref == 'refs/heads/main' }} diff --git a/cpython-windows/build.py b/cpython-windows/build.py index f79477222..02020f888 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -789,13 +789,12 @@ def prepare_arm64ec(cpython_source_path: pathlib.Path) -> None: ) decimal.write_bytes(data) - # The x64 PGO runtime can be loaded by an ARM64EC executable. - python_project = pcbuild_path / "python.vcxproj" - data = python_project.read_bytes().replace( - b"$(Platform) == 'x64'", - b"($(Platform) == 'x64' or $(Platform) == 'ARM64EC')", + # Use the PGO runtime from the `arm64` directory for ARM64EC. + static_replace_in_file( + pcbuild_path / "python.vcxproj", + b"$(Platform) == 'ARM64'", + b"($(Platform) == 'ARM64' or $(Platform) == 'ARM64EC')", ) - python_project.write_bytes(data) projects = pcbuild_path / "pcbuild.proj" data = re.sub( @@ -1741,28 +1740,13 @@ def build_cpython( / instrumented_python_exe ) - discovery = subprocess.run( + tests = subprocess.run( [str(instrumented_python), "-m", "test", "--list-tests"], cwd=cpython_source_path, env=env, - check=False, + check=True, stdout=subprocess.PIPE, - ) - if discovery.returncode or not discovery.stdout.strip(): - log( - f"PGO test discovery exited 0x{discovery.returncode & 0xFFFFFFFF:08X}" - ) - if arch == "arm64ec": - subprocess.run( - [ - sys.executable, - str(SUPPORT / "diagnose-arm64ec.py"), - str(instrumented_python), - ], - check=True, - ) - discovery.check_returncode() - tests = discovery.stdout + ).stdout tests = [l.strip() for l in tests.decode("utf-8").splitlines() if l.strip()] diff --git a/cpython-windows/diagnose-arm64ec.py b/cpython-windows/diagnose-arm64ec.py deleted file mode 100644 index 93221ab31..000000000 --- a/cpython-windows/diagnose-arm64ec.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import pathlib -import shutil -import subprocess -import sys - -executable = pathlib.Path(sys.argv[1]) -output = executable.parent -destination = pathlib.Path("build/arm64ec-diagnostics") -original = destination / "original" -original.mkdir(parents=True, exist_ok=True) -for source in output.iterdir(): - if source.suffix.lower() in {".exe", ".dll", ".pyd"}: - shutil.copy2(source, original / source.name) - -tools = pathlib.Path(os.environ["VCToolsInstallDir"]) -crt_directories = [original, None] -crt_directories.extend( - sorted((tools.parents[2] / "Redist/MSVC").glob("*/arm64/Microsoft.VC*.CRT")) -) -pgo_runtimes = [original / "pgort140.dll"] -pgo_runtimes.extend(sorted(tools.rglob("pgort140.dll"))) - -env = dict(os.environ) -env["PYTHONHOME"] = str(executable.parents[3]) -env.pop("PYTHONPATH", None) -for crt in crt_directories: - for runtime in output.glob("vcruntime140*.dll"): - runtime.unlink() - if crt is not None: - for runtime in crt.glob("vcruntime140*.dll"): - shutil.copy2(runtime, output / runtime.name) - for pgo in pgo_runtimes: - shutil.copy2(pgo, output / "pgort140.dll") - print(f"CRT={crt or 'system'} PGO={pgo}", flush=True) - try: - result = subprocess.run( - [str(executable), "-c", "import sys; print(sys.version)"], - env=env, - capture_output=True, - timeout=15, - ) - except subprocess.TimeoutExpired: - print("STARTUP TIMEOUT", flush=True) - continue - print(f"EXIT=0x{result.returncode & 0xFFFFFFFF:08X}", flush=True) - print(result.stdout.decode("utf-8", "replace"), flush=True) - print(result.stderr.decode("utf-8", "replace"), flush=True) - -for source in original.glob("*.dll"): - shutil.copy2(source, output / source.name) diff --git a/pythonbuild/disttests/__init__.py b/pythonbuild/disttests/__init__.py index 9d2100e09..ab5d670b3 100644 --- a/pythonbuild/disttests/__init__.py +++ b/pythonbuild/disttests/__init__.py @@ -133,7 +133,7 @@ def error_handler(fif, message): os.environ["TARGET_TRIPLE"] == "arm64ec-pc-windows-msvc", "Requires an ARM64EC distribution", ) - def test_arm64ec_x64_callback(self): + def test_arm64ec_x64_abi(self): import ctypes import sysconfig @@ -145,6 +145,16 @@ def callback(integer, floating): self.assertEqual(callback(40, 2.5), 42.5) + build_options = os.environ["BUILD_OPTIONS"].split("+") + suffix = "t" if "freethreaded" in build_options else "" + if "debug" in build_options: + suffix += "_d" + stable_abi = ctypes.PyDLL( + str(Path(sys.executable).parent / f"python3{suffix}.dll") + ) + stable_abi.Py_GetVersion.restype = ctypes.c_char_p + self.assertEqual(stable_abi.Py_GetVersion().decode("utf-8"), sys.version) + @unittest.skipIf(os.name == "nt", "curses not available on Windows") def test_curses_import(self): import curses diff --git a/pythonbuild/testdist.py b/pythonbuild/testdist.py index 875f00bb2..1e7cfd6f4 100644 --- a/pythonbuild/testdist.py +++ b/pythonbuild/testdist.py @@ -12,6 +12,7 @@ import shlex import subprocess import tempfile +import time from pathlib import Path from typing import Optional @@ -651,4 +652,13 @@ def main(pbs_source_dir: Path, raw_args: list[str]) -> int: finally: if td: - td.cleanup() + # DLLs can remain locked briefly after the test process exits on Windows. + for attempt in range(11): + try: + td.cleanup() + except PermissionError: + if os.name != "nt" or attempt == 10: + raise + time.sleep(1) + else: + break