Skip to content

Commit 97c36a3

Browse files
zaniebadiantek
andauthored
Add ARM64 Windows builds for Python 3.11+ (#387)
Picking up #93 --------- Co-authored-by: Adrian Antkowiak <[email protected]>
1 parent 4c3dfd8 commit 97c36a3

File tree

5 files changed

+143
-36
lines changed

5 files changed

+143
-36
lines changed

.github/workflows/windows.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ jobs:
9292
fi
9393
9494
build:
95+
timeout-minutes: 60
9596
needs:
9697
- generate-matrix
9798
- pythonbuild

ci-runners.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,8 @@ windows-latest:
4040
arch: x86_64
4141
platform: windows
4242
free: true
43+
44+
windows-11-arm:
45+
arch: aarch64
46+
platform: windows
47+
free: false

ci-targets.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,21 @@ windows:
384384
- options:
385385
- freethreaded+pgo
386386
minimum-python-version: "3.13"
387+
388+
aarch64-pc-windows-msvc:
389+
arch: aarch64
390+
vcvars: vcvarsamd64_arm64.bat
391+
python_versions:
392+
# On 3.9 / 3.10, `_tkinter` is failing to be included in the build
393+
# - "3.9"
394+
# - "3.10"
395+
- "3.11"
396+
- "3.12"
397+
- "3.13"
398+
- "3.14"
399+
build_options:
400+
- pgo
401+
build_options_conditional:
402+
- options:
403+
- freethreaded+pgo
404+
minimum-python-version: "3.13"

cpython-windows/build.py

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def hack_props(
370370

371371
mpdecimal_version = DOWNLOADS["mpdecimal"]["version"]
372372

373-
if meets_python_minimum_version(python_version, "3.14"):
373+
if meets_python_minimum_version(python_version, "3.14") or arch == "arm64":
374374
tcltk_commit = DOWNLOADS["tk-windows-bin"]["git_commit"]
375375
else:
376376
tcltk_commit = DOWNLOADS["tk-windows-bin-8612"]["git_commit"]
@@ -464,6 +464,8 @@ def hack_props(
464464
suffix = b"-x64"
465465
elif arch == "win32":
466466
suffix = b""
467+
elif arch == "arm64":
468+
suffix = b""
467469
else:
468470
raise Exception("unhandled architecture: %s" % arch)
469471

@@ -505,6 +507,7 @@ def hack_project_files(
505507
build_directory: str,
506508
python_version: str,
507509
zlib_entry: str,
510+
arch: str,
508511
):
509512
"""Hacks Visual Studio project files to work with our build."""
510513

@@ -518,6 +521,17 @@ def hack_project_files(
518521
zlib_entry,
519522
)
520523

524+
# `--include-tcltk` is forced off on arm64, undo that
525+
# See https://github.com/python/cpython/pull/132650
526+
try:
527+
static_replace_in_file(
528+
cpython_source_path / "PC" / "layout" / "main.py",
529+
rb'if ns.arch in ("arm32", "arm64"):',
530+
rb'if ns.arch == "arm32":',
531+
)
532+
except NoSearchStringError:
533+
pass
534+
521535
# Our SQLite directory is named weirdly. This throws off version detection
522536
# in the project file. Replace the parsing logic with a static string.
523537
sqlite3_version = DOWNLOADS["sqlite"]["actual_version"].encode("ascii")
@@ -603,14 +617,18 @@ def hack_project_files(
603617
# have a standalone zlib DLL, so we remove references to it. For Python
604618
# 3.14+, we're using tk-windows-bin 8.6.14 which includes a prebuilt zlib
605619
# DLL, so we skip this patch there.
606-
if meets_python_minimum_version(
607-
python_version, "3.12"
608-
) and meets_python_maximum_version(python_version, "3.13"):
609-
static_replace_in_file(
610-
pcbuild_path / "_tkinter.vcxproj",
611-
rb'<_TclTkDLL Include="$(tcltkdir)\bin\$(tclZlibDllName)" />',
612-
rb"",
613-
)
620+
# On arm64, we use the new version of tk-windows-bin for all versions.
621+
if meets_python_minimum_version(python_version, "3.12") and (
622+
meets_python_maximum_version(python_version, "3.13") or arch == "arm64"
623+
):
624+
try:
625+
static_replace_in_file(
626+
pcbuild_path / "_tkinter.vcxproj",
627+
rb'<_TclTkDLL Include="$(tcltkdir)\bin\$(tclZlibDllName)" />',
628+
rb"",
629+
)
630+
except NoSearchStringError:
631+
pass
614632

615633
# We don't need to produce python_uwp.exe and its *w variant. Or the
616634
# python3.dll, pyshellext, or pylauncher.
@@ -730,9 +748,11 @@ def build_openssl_for_arch(
730748
elif arch == "amd64":
731749
configure = "VC-WIN64A"
732750
prefix = "64"
751+
elif arch == "arm64":
752+
configure = "VC-WIN64-ARM"
753+
prefix = "arm64"
733754
else:
734-
print("invalid architecture: %s" % arch)
735-
sys.exit(1)
755+
raise Exception("unhandled architecture: %s" % arch)
736756

737757
# The official CPython OpenSSL builds hack ms/uplink.c to change the
738758
# ``GetModuleHandle(NULL)`` invocation to load things from _ssl.pyd
@@ -780,6 +800,12 @@ def build_openssl_for_arch(
780800
log("copying %s to %s" % (source, dest))
781801
shutil.copyfile(source, dest)
782802

803+
# Copy `applink.c` to the include directory.
804+
source_applink = source_root / "ms" / "applink.c"
805+
dest_applink = install_root / "include" / "openssl" / "applink.c"
806+
log("copying %s to %s" % (source_applink, dest_applink))
807+
shutil.copyfile(source_applink, dest_applink)
808+
783809

784810
def build_openssl(
785811
entry: str,
@@ -801,6 +827,7 @@ def build_openssl(
801827

802828
root_32 = td / "x86"
803829
root_64 = td / "x64"
830+
root_arm64 = td / "arm64"
804831

805832
if arch == "x86":
806833
root_32.mkdir()
@@ -824,13 +851,28 @@ def build_openssl(
824851
root_64,
825852
jom_archive=jom_archive,
826853
)
854+
elif arch == "arm64":
855+
root_arm64.mkdir()
856+
build_openssl_for_arch(
857+
perl_path,
858+
"arm64",
859+
openssl_archive,
860+
openssl_version,
861+
nasm_archive,
862+
root_arm64,
863+
jom_archive=jom_archive,
864+
)
827865
else:
828-
raise ValueError("unhandled arch: %s" % arch)
866+
raise Exception("unhandled architecture: %s" % arch)
829867

830868
install = td / "out"
831869

832870
if arch == "x86":
833871
shutil.copytree(root_32 / "install" / "32", install / "openssl" / "win32")
872+
elif arch == "arm64":
873+
shutil.copytree(
874+
root_arm64 / "install" / "arm64", install / "openssl" / "arm64"
875+
)
834876
else:
835877
shutil.copytree(root_64 / "install" / "64", install / "openssl" / "amd64")
836878

@@ -901,9 +943,14 @@ def build_libffi(
901943
if arch == "x86":
902944
args.append("-x86")
903945
artifacts_path = ffi_source_path / "i686-pc-cygwin"
904-
else:
946+
elif arch == "arm64":
947+
args.append("-arm64")
948+
artifacts_path = ffi_source_path / "aarch64-w64-cygwin"
949+
elif arch == "amd64":
905950
args.append("-x64")
906951
artifacts_path = ffi_source_path / "x86_64-w64-cygwin"
952+
else:
953+
raise Exception("unhandled architecture: %s" % arch)
907954

908955
subprocess.run(args, env=env, check=True)
909956

@@ -1069,8 +1116,10 @@ def find_additional_dependencies(project: pathlib.Path):
10691116
abi_platform = "win_amd64"
10701117
elif arch == "win32":
10711118
abi_platform = "win32"
1119+
elif arch == "arm64":
1120+
abi_platform = "win_arm64"
10721121
else:
1073-
raise ValueError("unhandled arch: %s" % arch)
1122+
raise Exception("unhandled architecture: %s" % arch)
10741123

10751124
if freethreaded:
10761125
abi_tag = ".cp%st-%s" % (python_majmin, abi_platform)
@@ -1171,8 +1220,8 @@ def find_additional_dependencies(project: pathlib.Path):
11711220
if name == "zlib":
11721221
name = zlib_entry
11731222

1174-
# On 3.14+, we use the latest tcl/tk version
1175-
if ext == "_tkinter" and python_majmin == "314":
1223+
# On 3.14+ and aarch64, we use the latest tcl/tk version
1224+
if ext == "_tkinter" and (python_majmin == "314" or arch == "arm64"):
11761225
name = name.replace("-8612", "")
11771226

11781227
download_entry = DOWNLOADS[name]
@@ -1258,16 +1307,18 @@ def build_cpython(
12581307
setuptools_wheel = download_entry("setuptools", BUILD)
12591308
pip_wheel = download_entry("pip", BUILD)
12601309

1261-
# On CPython 3.14+, we use the latest tcl/tk version which has additional runtime
1262-
# dependencies, so we are conservative and use the old version elsewhere.
1263-
if meets_python_minimum_version(python_version, "3.14"):
1264-
tk_bin_archive = download_entry(
1265-
"tk-windows-bin", BUILD, local_name="tk-windows-bin.tar.gz"
1266-
)
1267-
else:
1268-
tk_bin_archive = download_entry(
1269-
"tk-windows-bin-8612", BUILD, local_name="tk-windows-bin.tar.gz"
1270-
)
1310+
# On CPython 3.14+, we use the latest tcl/tk version which has additional
1311+
# runtime dependencies, so we are conservative and use the old version
1312+
# elsewhere. The old version isn't built for arm64, so we use the new
1313+
# version there too
1314+
tk_bin_entry = (
1315+
"tk-windows-bin"
1316+
if meets_python_minimum_version(python_version, "3.14") or arch == "arm64"
1317+
else "tk-windows-bin-8612"
1318+
)
1319+
tk_bin_archive = download_entry(
1320+
tk_bin_entry, BUILD, local_name="tk-windows-bin.tar.gz"
1321+
)
12711322

12721323
# On CPython 3.14+, zstd is included
12731324
if meets_python_minimum_version(python_version, "3.14"):
@@ -1297,8 +1348,11 @@ def build_cpython(
12971348
elif arch == "x86":
12981349
build_platform = "win32"
12991350
build_directory = "win32"
1351+
elif arch == "arm64":
1352+
build_platform = "arm64"
1353+
build_directory = "arm64"
13001354
else:
1301-
raise ValueError("unhandled arch: %s" % arch)
1355+
raise Exception("unhandled architecture: %s" % arch)
13021356

13031357
tempdir_opts = (
13041358
{"ignore_cleanup_errors": True} if sys.version_info >= (3, 12) else {}
@@ -1332,7 +1386,7 @@ def build_cpython(
13321386

13331387
# We need all the OpenSSL library files in the same directory to appease
13341388
# install rules.
1335-
openssl_arch = {"amd64": "amd64", "x86": "win32"}[arch]
1389+
openssl_arch = {"amd64": "amd64", "x86": "win32", "arm64": "arm64"}[arch]
13361390
openssl_root = td / "openssl" / openssl_arch
13371391
openssl_bin_path = openssl_root / "bin"
13381392
openssl_lib_path = openssl_root / "lib"
@@ -1346,6 +1400,17 @@ def build_cpython(
13461400
log("copying %s to %s" % (source, dest))
13471401
shutil.copyfile(source, dest)
13481402

1403+
# Delete the tk nmake helper, it's not needed and links msvc
1404+
tcltk_commit: str = DOWNLOADS[tk_bin_entry]["git_commit"]
1405+
tcltk_path = td / ("cpython-bin-deps-%s" % tcltk_commit)
1406+
(
1407+
tcltk_path
1408+
/ build_directory
1409+
/ "lib"
1410+
/ "nmake"
1411+
/ "x86_64-w64-mingw32-nmakehlp.exe"
1412+
).unlink()
1413+
13491414
cpython_source_path = td / ("Python-%s" % python_version)
13501415
pcbuild_path = cpython_source_path / "PCbuild"
13511416

@@ -1368,6 +1433,7 @@ def build_cpython(
13681433
build_directory,
13691434
python_version=python_version,
13701435
zlib_entry=zlib_entry,
1436+
arch=arch,
13711437
)
13721438

13731439
if pgo:
@@ -1790,9 +1856,14 @@ def main() -> None:
17901856
if os.environ.get("Platform") == "x86":
17911857
target_triple = "i686-pc-windows-msvc"
17921858
arch = "x86"
1793-
else:
1859+
elif os.environ.get("Platform") == "arm64":
1860+
target_triple = "aarch64-pc-windows-msvc"
1861+
arch = "arm64"
1862+
elif os.environ.get("Platform") == "x64":
17941863
target_triple = "x86_64-pc-windows-msvc"
17951864
arch = "amd64"
1865+
else:
1866+
raise Exception("unhandled architecture: %s" % os.environ.get("Platform"))
17961867

17971868
# TODO need better dependency checking.
17981869

src/validation.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use {
1818
macho::{LoadCommandVariant, MachHeader, Nlist},
1919
pe::{ImageNtHeaders, PeFile, PeFile32, PeFile64},
2020
},
21-
Endianness, FileKind, Object, SectionIndex, SymbolScope,
21+
Architecture, Endianness, FileKind, Object, SectionIndex, SymbolScope,
2222
},
2323
once_cell::sync::Lazy,
2424
std::{
@@ -33,6 +33,7 @@ use {
3333
const RECOGNIZED_TRIPLES: &[&str] = &[
3434
"aarch64-apple-darwin",
3535
"aarch64-apple-ios",
36+
"aarch64-pc-windows-msvc",
3637
"aarch64-unknown-linux-gnu",
3738
"armv7-unknown-linux-gnueabi",
3839
"armv7-unknown-linux-gnueabihf",
@@ -117,11 +118,13 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[
117118
"libcrypto-1_1.dll",
118119
"libcrypto-1_1-x64.dll",
119120
"libcrypto-3.dll",
121+
"libcrypto-3-arm64.dll",
120122
"libcrypto-3-x64.dll",
121123
"libffi-8.dll",
122124
"libssl-1_1.dll",
123125
"libssl-1_1-x64.dll",
124126
"libssl-3.dll",
127+
"libssl-3-arm64.dll",
125128
"libssl-3-x64.dll",
126129
"python3.dll",
127130
"python39.dll",
@@ -137,8 +140,14 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[
137140
"tk86t.dll",
138141
];
139142

140-
// CPython 3.14 uses tcl/tk 8.6.14+ which includes a bundled zlib and dynamically links to msvcrt.
141-
const PE_ALLOWED_LIBRARIES_314: &[&str] = &["msvcrt.dll", "zlib1.dll"];
143+
// CPython 3.14 and ARM64 use a newer version of tcl/tk (8.6.14+) which includes a bundled zlib that
144+
// dynamically links some system libraries
145+
const PE_ALLOWED_LIBRARIES_314: &[&str] = &[
146+
"zlib1.dll",
147+
"api-ms-win-crt-private-l1-1-0.dll", // zlib loads this library on arm64, 3.14+
148+
"msvcrt.dll", // zlib loads this library
149+
];
150+
const PE_ALLOWED_LIBRARIES_ARM64: &[&str] = &["msvcrt.dll", "zlib1.dll"];
142151

143152
static GLIBC_MAX_VERSION_BY_TRIPLE: Lazy<HashMap<&'static str, version_compare::Version<'static>>> =
144153
Lazy::new(|| {
@@ -496,6 +505,7 @@ static PLATFORM_TAG_BY_TRIPLE: Lazy<HashMap<&'static str, &'static str>> = Lazy:
496505
[
497506
("aarch64-apple-darwin", "macosx-11.0-arm64"),
498507
("aarch64-apple-ios", "iOS-aarch64"),
508+
("aarch64-pc-windows-msvc", "win-arm64"),
499509
("aarch64-unknown-linux-gnu", "linux-aarch64"),
500510
("armv7-unknown-linux-gnueabi", "linux-arm"),
501511
("armv7-unknown-linux-gnueabihf", "linux-arm"),
@@ -1375,15 +1385,17 @@ fn validate_pe<'data, Pe: ImageNtHeaders>(
13751385
let lib = String::from_utf8(lib.to_vec())?;
13761386

13771387
match python_major_minor {
1378-
"3.9" | "3.10" | "3.11" | "3.12" | "3.13" => {}
1388+
"3.11" | "3.12" | "3.13" if pe.architecture() == Architecture::Aarch64 => {
1389+
if PE_ALLOWED_LIBRARIES_ARM64.contains(&lib.as_str()) {
1390+
continue;
1391+
}
1392+
}
13791393
"3.14" => {
13801394
if PE_ALLOWED_LIBRARIES_314.contains(&lib.as_str()) {
13811395
continue;
13821396
}
13831397
}
1384-
_ => {
1385-
panic!("unhandled Python version: {}", python_major_minor);
1386-
}
1398+
_ => {}
13871399
}
13881400

13891401
if !PE_ALLOWED_LIBRARIES.contains(&lib.as_str()) {

0 commit comments

Comments
 (0)