Skip to content

Add ARM64 Windows builds for Python 3.11+ #387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ jobs:
fi

build:
timeout-minutes: 60
needs:
- generate-matrix
- pythonbuild
Expand Down
5 changes: 5 additions & 0 deletions ci-runners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ windows-latest:
arch: x86_64
platform: windows
free: true

windows-11-arm:
arch: aarch64
platform: windows
free: false
18 changes: 18 additions & 0 deletions ci-targets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,21 @@ windows:
- options:
- freethreaded+pgo
minimum-python-version: "3.13"

aarch64-pc-windows-msvc:
arch: aarch64
vcvars: vcvarsamd64_arm64.bat
python_versions:
# On 3.9 / 3.10, `_tkinter` is failing to be included in the build
# - "3.9"
# - "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
build_options:
- pgo
build_options_conditional:
- options:
- freethreaded+pgo
minimum-python-version: "3.13"
126 changes: 97 additions & 29 deletions cpython-windows/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def hack_props(

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

if meets_python_minimum_version(python_version, "3.14"):
if meets_python_minimum_version(python_version, "3.14") or arch == "arm64":
tcltk_commit = DOWNLOADS["tk-windows-bin"]["git_commit"]
else:
tcltk_commit = DOWNLOADS["tk-windows-bin-8612"]["git_commit"]
Expand Down Expand Up @@ -444,6 +444,8 @@ def hack_props(
suffix = b"-x64"
elif arch == "win32":
suffix = b""
elif arch == "arm64":
suffix = b""
else:
raise Exception("unhandled architecture: %s" % arch)

Expand Down Expand Up @@ -484,6 +486,7 @@ def hack_project_files(
cpython_source_path: pathlib.Path,
build_directory: str,
python_version: str,
arch: str,
):
"""Hacks Visual Studio project files to work with our build."""

Expand All @@ -496,6 +499,14 @@ def hack_project_files(
python_version,
)

# `--include-tcltk` is forced off on arm64, undo that
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot about this - glad I'm following your PR.

python/cpython#132650 will fix it in 3.13 and 3.14, FYI

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate it!

# See https://github.com/python/cpython/pull/132650
static_replace_in_file(
cpython_source_path / "PC" / "layout" / "main.py",
rb'if ns.arch in ("arm32", "arm64"):',
rb'if ns.arch == "arm32":',
)

# Our SQLite directory is named weirdly. This throws off version detection
# in the project file. Replace the parsing logic with a static string.
sqlite3_version = DOWNLOADS["sqlite"]["actual_version"].encode("ascii")
Expand Down Expand Up @@ -581,14 +592,18 @@ def hack_project_files(
# have a standalone zlib DLL, so we remove references to it. For Python
# 3.14+, we're using tk-windows-bin 8.6.14 which includes a prebuilt zlib
# DLL, so we skip this patch there.
if meets_python_minimum_version(
python_version, "3.12"
) and meets_python_maximum_version(python_version, "3.13"):
static_replace_in_file(
pcbuild_path / "_tkinter.vcxproj",
rb'<_TclTkDLL Include="$(tcltkdir)\bin\$(tclZlibDllName)" />',
rb"",
)
# On arm64, we use the new version of tk-windows-bin for all versions.
if meets_python_minimum_version(python_version, "3.12") and (
meets_python_maximum_version(python_version, "3.13") or arch == "arm64"
):
try:
static_replace_in_file(
pcbuild_path / "_tkinter.vcxproj",
rb'<_TclTkDLL Include="$(tcltkdir)\bin\$(tclZlibDllName)" />',
rb"",
)
except NoSearchStringError:
pass

# We don't need to produce python_uwp.exe and its *w variant. Or the
# python3.dll, pyshellext, or pylauncher.
Expand Down Expand Up @@ -708,9 +723,11 @@ def build_openssl_for_arch(
elif arch == "amd64":
configure = "VC-WIN64A"
prefix = "64"
elif arch == "arm64":
configure = "VC-WIN64-ARM"
prefix = "arm64"
else:
print("invalid architecture: %s" % arch)
sys.exit(1)
raise Exception("unhandled architecture: %s" % arch)

# The official CPython OpenSSL builds hack ms/uplink.c to change the
# ``GetModuleHandle(NULL)`` invocation to load things from _ssl.pyd
Expand Down Expand Up @@ -758,6 +775,12 @@ def build_openssl_for_arch(
log("copying %s to %s" % (source, dest))
shutil.copyfile(source, dest)

# Copy `applink.c` to the include directory.
source_applink = source_root / "ms" / "applink.c"
dest_applink = install_root / "include" / "openssl" / "applink.c"
log("copying %s to %s" % (source_applink, dest_applink))
shutil.copyfile(source_applink, dest_applink)

Comment on lines +778 to +783
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain in the comment what this is and why it's needed? (Why didn't we need this before?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try, but I don't fully understand.

#387 (comment)


def build_openssl(
entry: str,
Expand All @@ -779,6 +802,7 @@ def build_openssl(

root_32 = td / "x86"
root_64 = td / "x64"
root_arm64 = td / "arm64"

if arch == "x86":
root_32.mkdir()
Expand All @@ -802,13 +826,28 @@ def build_openssl(
root_64,
jom_archive=jom_archive,
)
elif arch == "arm64":
root_arm64.mkdir()
build_openssl_for_arch(
perl_path,
"arm64",
openssl_archive,
openssl_version,
nasm_archive,
root_arm64,
jom_archive=jom_archive,
)
else:
raise ValueError("unhandled arch: %s" % arch)
raise Exception("unhandled architecture: %s" % arch)

install = td / "out"

if arch == "x86":
shutil.copytree(root_32 / "install" / "32", install / "openssl" / "win32")
elif arch == "arm64":
shutil.copytree(
root_arm64 / "install" / "arm64", install / "openssl" / "arm64"
)
else:
shutil.copytree(root_64 / "install" / "64", install / "openssl" / "amd64")

Expand Down Expand Up @@ -879,9 +918,14 @@ def build_libffi(
if arch == "x86":
args.append("-x86")
artifacts_path = ffi_source_path / "i686-pc-cygwin"
else:
elif arch == "arm64":
args.append("-arm64")
artifacts_path = ffi_source_path / "aarch64-w64-cygwin"
elif arch == "amd64":
args.append("-x64")
artifacts_path = ffi_source_path / "x86_64-w64-cygwin"
else:
raise Exception("unhandled architecture: %s" % arch)

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

Expand Down Expand Up @@ -1043,8 +1087,10 @@ def find_additional_dependencies(project: pathlib.Path):
abi_platform = "win_amd64"
elif arch == "win32":
abi_platform = "win32"
elif arch == "arm64":
abi_platform = "win_arm64"
else:
raise ValueError("unhandled arch: %s" % arch)
raise Exception("unhandled architecture: %s" % arch)

if freethreaded:
abi_tag = ".cp%st-%s" % (python_majmin, abi_platform)
Expand Down Expand Up @@ -1142,8 +1188,8 @@ def find_additional_dependencies(project: pathlib.Path):
if name == "openssl":
name = openssl_entry

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

download_entry = DOWNLOADS[name]
Expand Down Expand Up @@ -1226,16 +1272,18 @@ def build_cpython(
setuptools_wheel = download_entry("setuptools", BUILD)
pip_wheel = download_entry("pip", BUILD)

# On CPython 3.14+, we use the latest tcl/tk version which has additional runtime
# dependencies, so we are conservative and use the old version elsewhere.
if meets_python_minimum_version(python_version, "3.14"):
tk_bin_archive = download_entry(
"tk-windows-bin", BUILD, local_name="tk-windows-bin.tar.gz"
)
else:
tk_bin_archive = download_entry(
"tk-windows-bin-8612", BUILD, local_name="tk-windows-bin.tar.gz"
)
# On CPython 3.14+, we use the latest tcl/tk version which has additional
# runtime dependencies, so we are conservative and use the old version
# elsewhere. The old version isn't built for arm64, so we use the new
# version there too
tk_bin_entry = (
"tk-windows-bin"
if meets_python_minimum_version(python_version, "3.14") or arch == "arm64"
else "tk-windows-bin-8612"
)
tk_bin_archive = download_entry(
tk_bin_entry, BUILD, local_name="tk-windows-bin.tar.gz"
)

# CPython 3.13+ no longer uses a bundled `mpdecimal` version so we build it
if meets_python_minimum_version(python_version, "3.13"):
Expand All @@ -1259,8 +1307,11 @@ def build_cpython(
elif arch == "x86":
build_platform = "win32"
build_directory = "win32"
elif arch == "arm64":
build_platform = "arm64"
build_directory = "arm64"
else:
raise ValueError("unhandled arch: %s" % arch)
raise Exception("unhandled architecture: %s" % arch)

tempdir_opts = (
{"ignore_cleanup_errors": True} if sys.version_info >= (3, 12) else {}
Expand Down Expand Up @@ -1293,7 +1344,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"}[arch]
openssl_arch = {"amd64": "amd64", "x86": "win32", "arm64": "arm64"}[arch]
openssl_root = td / "openssl" / openssl_arch
openssl_bin_path = openssl_root / "bin"
openssl_lib_path = openssl_root / "lib"
Expand All @@ -1307,6 +1358,17 @@ def build_cpython(
log("copying %s to %s" % (source, dest))
shutil.copyfile(source, dest)

# Delete the tk nmake helper, it's not needed and links msvc
tcltk_commit: str = DOWNLOADS[tk_bin_entry]["git_commit"]
tcltk_path = td / ("cpython-bin-deps-%s" % tcltk_commit)
(
tcltk_path
/ build_directory
/ "lib"
/ "nmake"
/ "x86_64-w64-mingw32-nmakehlp.exe"
).unlink()

cpython_source_path = td / ("Python-%s" % python_version)
pcbuild_path = cpython_source_path / "PCbuild"

Expand All @@ -1328,6 +1390,7 @@ def build_cpython(
cpython_source_path,
build_directory,
python_version=python_version,
arch=arch,
)

if pgo:
Expand Down Expand Up @@ -1749,9 +1812,14 @@ def main() -> None:
if os.environ.get("Platform") == "x86":
target_triple = "i686-pc-windows-msvc"
arch = "x86"
else:
elif os.environ.get("Platform") == "arm64":
target_triple = "aarch64-pc-windows-msvc"
arch = "arm64"
elif os.environ.get("Platform") == "x64":
target_triple = "x86_64-pc-windows-msvc"
arch = "amd64"
else:
raise Exception("unhandled architecture: %s" % os.environ.get("Platform"))

# TODO need better dependency checking.

Expand Down
26 changes: 19 additions & 7 deletions src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use {
macho::{LoadCommandVariant, MachHeader, Nlist},
pe::{ImageNtHeaders, PeFile, PeFile32, PeFile64},
},
Endianness, FileKind, Object, SectionIndex, SymbolScope,
Architecture, Endianness, FileKind, Object, SectionIndex, SymbolScope,
},
once_cell::sync::Lazy,
std::{
Expand All @@ -33,6 +33,7 @@ use {
const RECOGNIZED_TRIPLES: &[&str] = &[
"aarch64-apple-darwin",
"aarch64-apple-ios",
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"armv7-unknown-linux-gnueabi",
"armv7-unknown-linux-gnueabihf",
Expand Down Expand Up @@ -117,11 +118,13 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[
"libcrypto-1_1.dll",
"libcrypto-1_1-x64.dll",
"libcrypto-3.dll",
"libcrypto-3-arm64.dll",
"libcrypto-3-x64.dll",
"libffi-8.dll",
"libssl-1_1.dll",
"libssl-1_1-x64.dll",
"libssl-3.dll",
"libssl-3-arm64.dll",
"libssl-3-x64.dll",
"python3.dll",
"python39.dll",
Expand All @@ -137,8 +140,14 @@ const PE_ALLOWED_LIBRARIES: &[&str] = &[
"tk86t.dll",
];

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

static GLIBC_MAX_VERSION_BY_TRIPLE: Lazy<HashMap<&'static str, version_compare::Version<'static>>> =
Lazy::new(|| {
Expand Down Expand Up @@ -496,6 +505,7 @@ static PLATFORM_TAG_BY_TRIPLE: Lazy<HashMap<&'static str, &'static str>> = Lazy:
[
("aarch64-apple-darwin", "macosx-11.0-arm64"),
("aarch64-apple-ios", "iOS-aarch64"),
("aarch64-pc-windows-msvc", "win-arm64"),
("aarch64-unknown-linux-gnu", "linux-aarch64"),
("armv7-unknown-linux-gnueabi", "linux-arm"),
("armv7-unknown-linux-gnueabihf", "linux-arm"),
Expand Down Expand Up @@ -1369,15 +1379,17 @@ fn validate_pe<'data, Pe: ImageNtHeaders>(
let lib = String::from_utf8(lib.to_vec())?;

match python_major_minor {
"3.9" | "3.10" | "3.11" | "3.12" | "3.13" => {}
"3.11" | "3.12" | "3.13" if pe.architecture() == Architecture::Aarch64 => {
if PE_ALLOWED_LIBRARIES_ARM64.contains(&lib.as_str()) {
continue;
}
}
"3.14" => {
if PE_ALLOWED_LIBRARIES_314.contains(&lib.as_str()) {
continue;
}
}
_ => {
panic!("unhandled Python version: {}", python_major_minor);
}
_ => {}
}

if !PE_ALLOWED_LIBRARIES.contains(&lib.as_str()) {
Expand Down