Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions ci-matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
21 changes: 21 additions & 0 deletions ci-targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
112 changes: 107 additions & 5 deletions cpython-windows/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -734,6 +735,98 @@ 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' <ProjectConfiguration Include="[^"]+\|ARM64">.*?</ProjectConfiguration>',
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'(<ProjectReference Include="python3t?dll.vcxproj"[^>]*>)',
rb"\1\n <SetPlatform>Platform=x64</SetPlatform>",
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</ArchName>",
b"and ($(Platform) == 'x64' or $(Platform) == 'ARM64EC')\">amd64</ArchName>",
)
static_replace_in_file(
pcbuild_path / "pyproject.props",
b"<TargetMachine Condition=\"'$(Platform)'=='ARM64'\">MachineARM64</TargetMachine>",
b"<TargetMachine Condition=\"'$(Platform)'=='ARM64'\">MachineARM64</TargetMachine>\n"
b" <TargetMachine Condition=\"'$(Platform)'=='ARM64EC'\">MachineARM64EC</TargetMachine>",
)
# Use the ARM64 redistributable for its ARM64X `vcruntime140.dll`.
static_replace_in_file(
pcbuild_path / "pyproject.props",
b"<VCRedistDir Condition=\"$(Platform) != 'Win32'\">$(VCRedistDir)$(Platform)\\</VCRedistDir>",
b"<VCRedistDir Condition=\"$(Platform) != 'Win32' and $(Platform) != 'ARM64EC'\">$(VCRedistDir)$(Platform)\\</VCRedistDir>\n"
b" <VCRedistDir Condition=\"$(Platform) == 'ARM64EC'\">$(VCRedistDir)arm64\\</VCRedistDir>",
)

# 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)

# 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')",
)

projects = pcbuild_path / "pcbuild.proj"
data = re.sub(
rb'<Projects Include="(python3t?dll.vcxproj)" />',
rb'<Projects Include="\1"><Platform>x64</Platform></Projects>',
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)

# 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,
pcbuild_path: pathlib.Path,
Expand Down Expand Up @@ -1483,6 +1576,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}")

Expand Down Expand Up @@ -1540,7 +1636,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"
Expand Down Expand Up @@ -1605,6 +1701,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(
Expand Down Expand Up @@ -1646,7 +1744,7 @@ def build_cpython(
[str(instrumented_python), "-m", "test", "--list-tests"],
cwd=cpython_source_path,
env=env,
check=False,
check=True,
stdout=subprocess.PIPE,
).stdout

Expand Down Expand Up @@ -2044,6 +2142,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"
Expand All @@ -2061,7 +2161,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}"
Expand All @@ -2084,7 +2186,7 @@ def main() -> None:
build_openssl(
openssl_entry,
perl_path,
arch,
dependency_arch,
dest_archive=openssl_archive,
with_uplink=openssl_with_uplink,
)
Expand All @@ -2095,7 +2197,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,
Expand Down
2 changes: 1 addition & 1 deletion cpython-windows/xz-support/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 11 additions & 0 deletions docs/building.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions docs/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions docs/running.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 26 additions & 0 deletions pythonbuild/disttests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ 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_abi(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)

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
Expand Down
12 changes: 11 additions & 1 deletion pythonbuild/testdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import shlex
import subprocess
import tempfile
import time
from pathlib import Path
from typing import Optional

Expand Down Expand Up @@ -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
13 changes: 13 additions & 0 deletions src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ pub static RELEASE_TRIPLES: Lazy<BTreeMap<&'static str, TripleRelease>> = 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 {
Expand Down
Loading
Loading