diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a7535c68..ad8bbfe3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -92,6 +92,7 @@ jobs: fi build: + timeout-minutes: 60 needs: - generate-matrix - pythonbuild diff --git a/ci-runners.yaml b/ci-runners.yaml index aeb0e5ca..27a3b84e 100644 --- a/ci-runners.yaml +++ b/ci-runners.yaml @@ -35,3 +35,8 @@ windows-latest: arch: x86_64 platform: windows free: true + +windows-11-arm: + arch: aarch64 + platform: windows + free: false diff --git a/ci-targets.yaml b/ci-targets.yaml index 70078e87..c7b4bd12 100644 --- a/ci-targets.yaml +++ b/ci-targets.yaml @@ -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" diff --git a/cpython-windows/build.py b/cpython-windows/build.py index 7e4d78ac..72d62c9d 100644 --- a/cpython-windows/build.py +++ b/cpython-windows/build.py @@ -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"] @@ -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) @@ -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.""" @@ -496,6 +499,14 @@ def hack_project_files( python_version, ) + # `--include-tcltk` is forced off on arm64, undo that + # 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") @@ -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. @@ -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 @@ -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) + def build_openssl( entry: str, @@ -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() @@ -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") @@ -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) @@ -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) @@ -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] @@ -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"): @@ -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 {} @@ -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" @@ -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" @@ -1328,6 +1390,7 @@ def build_cpython( cpython_source_path, build_directory, python_version=python_version, + arch=arch, ) if pgo: @@ -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. diff --git a/src/validation.rs b/src/validation.rs index 747cdaed..415f2d30 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -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::{ @@ -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", @@ -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", @@ -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>> = Lazy::new(|| { @@ -496,6 +505,7 @@ static PLATFORM_TAG_BY_TRIPLE: Lazy> = 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"), @@ -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()) {