Skip to content
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

Enable building for Windows ARM64 on a non-ARM64 device #1144

Merged
merged 48 commits into from Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ea559af
Enable building for Windows ARM64 on a non-ARM64 device
zooba Jun 17, 2022
310cfda
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 17, 2022
6bcca2d
Satisfy MyPy
zooba Jun 17, 2022
e18c21a
Merge branch 'arm64' of https://github.com/zooba/cibuildwheel into arm64
zooba Jun 17, 2022
dc14bd1
Add cross-compiler test
zooba Jun 17, 2022
0704190
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 17, 2022
222d56b
Allow more arch values in get_nuget_args
zooba Jun 17, 2022
36442d5
Merge branch 'arm64' of https://github.com/zooba/cibuildwheel into arm64
zooba Jun 17, 2022
b5b8e12
Add a pyproject.toml to the windows arm64 cross compile test
joerick Jul 4, 2022
71df33d
Merge remote-tracking branch 'upstream/main' into arm64
zooba Aug 1, 2022
59febc1
Add setup_rust_cross_compile
zooba Aug 1, 2022
a2386a2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 1, 2022
94a053e
Apply some PR feedback
zooba Aug 1, 2022
9405d82
Merge branch 'arm64' of https://github.com/zooba/cibuildwheel into arm64
zooba Aug 1, 2022
4c73245
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 1, 2022
c3dcb07
Revert type names
zooba Aug 1, 2022
46cc262
Merge branch 'main' into arm64
zooba Aug 15, 2022
15288be
Merge branch 'main' into arm64
zooba Aug 22, 2022
5f391d6
Merge branch 'main' into arm64
zooba Aug 22, 2022
634a3f7
Expect correct wheel tag
zooba Aug 23, 2022
f5c82fc
Merge branch 'main' into arm64
zooba Aug 23, 2022
09e08f6
Write to ~/pydistutils.cfg instead, preserving any existing file
zooba Aug 23, 2022
b8ac98e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 23, 2022
dcc5c00
Ensure cfg is removed, and add a message
zooba Aug 23, 2022
9e07dae
Skip ARM64 test run when not running on ARM64
zooba Aug 24, 2022
9a4e8f6
Actually call unlink, and add more typing
zooba Aug 25, 2022
f277099
Skip ARM64 tests automatically with warning, and improved cleanup
zooba Aug 31, 2022
7b0ac9e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2022
b94aceb
Merge remote-tracking branch 'upstream/main' into arm64
zooba Aug 31, 2022
4242fe9
Merge branch 'arm64' of https://github.com/zooba/cibuildwheel into arm64
zooba Aug 31, 2022
9dc5512
Fix typing
zooba Aug 31, 2022
44ebbc4
Remove unused import
zooba Aug 31, 2022
1d7054c
Switch to potential new distutils environment variable
zooba Sep 1, 2022
ff2d9de
Flow through tmp directory
zooba Sep 1, 2022
5030c2c
Remove unused names
zooba Sep 2, 2022
9815b9c
Merge remote-tracking branch 'upstream/main' into arm64
zooba Sep 9, 2022
10a4e30
Merge remote-tracking branch 'upstream/main' into arm64
zooba Sep 26, 2022
4531370
Rename variable to match one added to setuptools
zooba Sep 26, 2022
063b8e4
Merge branch 'main' into arm64
zooba Sep 28, 2022
b8e4789
Add skip for missing ARM64 tools
zooba Sep 28, 2022
7fd4a22
Merge branch 'arm64' of https://github.com/zooba/cibuildwheel into arm64
zooba Sep 28, 2022
7a9bd57
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 28, 2022
ea1d15b
For mypy
zooba Sep 28, 2022
18ced3c
Update docs and fix default architectures
zooba Oct 3, 2022
ae58080
Merge remote-tracking branch 'upstream/main' into arm64
zooba Oct 3, 2022
aa79b6e
Revert default archs on ARM64 change
zooba Oct 4, 2022
9128465
Merge remote-tracking branch 'upstream/main' into arm64
zooba Oct 4, 2022
afdae3f
AppVeyor supports ARM64 builds
zooba Oct 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Expand Up @@ -51,19 +51,20 @@ Usage

`cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using:

| | Linux | macOS | Windows | Linux ARM | macOS ARM |
|-----------------|-------|-------|---------|-----------|-----------|
| GitHub Actions | ✅ | ✅ | ✅ | ✅¹ | ✅² |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅² |
| Travis CI | ✅ | | ✅ | ✅ | |
| AppVeyor | ✅ | ✅ | ✅ | | ✅² |
| CircleCI | ✅ | ✅ | | | ✅² |
| Gitlab CI | ✅ | | | | |
| Cirrus CI | ✅ | ✅³ | ✅ | ✅ | ✅ |
| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM |
|-----------------|-------|-------|---------|-----------|-----------|-------------|
| GitHub Actions | ✅ | ✅ | ✅ | ✅¹ | ✅² | ✅⁴ |
| Azure Pipelines | ✅ | ✅ | ✅ | | ✅² | ✅⁴ |
joerick marked this conversation as resolved.
Show resolved Hide resolved
| Travis CI | ✅ | | ✅ | ✅ | | |
| AppVeyor | ✅ | ✅ | ✅ | | ✅² | ✅⁴ |
| CircleCI | ✅ | ✅ | | | ✅² | |
| Gitlab CI | ✅ | | | | | |
| Cirrus CI | ✅ | ✅³ | ✅ | ✅ | ✅ | |

<sup>¹ [Requires emulation](https://cibuildwheel.readthedocs.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.</sup><br>
<sup>² [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#universal2). It is not possible to test `arm64` and the `arm64` part of a `universal2` wheel on this CI platform.</sup><br>
<sup>³ [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#universal2). Thanks to Rosetta 2 emulation, it is possible to test `x86_64` and both parts of a `universal2` wheel on this CI platform.</sup><br>
<sup>⁴ [Uses cross-compilation](https://cibuildwheel.readthedocs.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.</sup>

<!--intro-end-->

Expand Down
7 changes: 7 additions & 0 deletions cibuildwheel/logger.py
Expand Up @@ -126,6 +126,13 @@ def step_end_with_error(self, error: BaseException | str) -> None:
self.step_end(success=False)
self.error(error)

def notice(self, message: str) -> None:
if self.fold_mode == "github":
print(f"::notice::{message}\n", file=sys.stderr)
else:
c = self.colors
print(f"{c.bold}Note{c.end}: {message}\n", file=sys.stderr)

def warning(self, message: str) -> None:
if self.fold_mode == "github":
print(f"::warning::{message}\n", file=sys.stderr)
Expand Down
132 changes: 126 additions & 6 deletions cibuildwheel/windows.py
@@ -1,9 +1,11 @@
from __future__ import annotations

import os
import platform as platform_module
import shutil
import subprocess
import sys
import textwrap
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
Expand Down Expand Up @@ -33,15 +35,22 @@
read_python_configs,
shell,
split_config_settings,
unwrap,
virtualenv,
)


def get_nuget_args(version: str, arch: str, output_directory: Path) -> list[str]:
platform_suffix = {"32": "x86", "64": "", "ARM64": "arm64"}
python_name = "python" + platform_suffix[arch]
package_name = {
"32": "pythonx86",
"64": "python",
"ARM64": "pythonarm64",
# Aliases for platform.machine() return values
"x86": "pythonx86",
"AMD64": "python",
}[arch]
return [
python_name,
package_name,
"-Version",
version,
"-FallbackSource",
Expand Down Expand Up @@ -121,6 +130,82 @@ def install_pypy(tmp: Path, arch: str, url: str) -> Path:
return installation_path / "python.exe"


def setup_setuptools_cross_compile(
tmp: Path,
python_configuration: PythonConfiguration,
python_libs_base: Path,
env: dict[str, str],
) -> None:
distutils_cfg = tmp / "extra-setup.cfg"
env["DIST_EXTRA_CONFIG"] = str(distutils_cfg)
log.notice(f"Setting DIST_EXTRA_CONFIG={distutils_cfg} for cross-compilation")

# Ensure our additional import libraries are made available, and explicitly
# set the platform name
map_plat = {"32": "win32", "64": "win-amd64", "ARM64": "win-arm64"}
plat_name = map_plat[python_configuration.arch]
# (This file must be default/locale encoding, so we can't pass 'encoding')
distutils_cfg.write_text(
textwrap.dedent(
f"""\
[build]
plat_name={plat_name}
[build_ext]
library_dirs={python_libs_base}
plat_name={plat_name}
[bdist_wheel]
plat_name={plat_name}
"""
)
)

# setuptools builds require explicit override of PYD extension
# This is because it always gets the extension from the running
# interpreter, and has no logic to construct it. Currently, CPython's
# extensions follow our identifiers, but if they ever diverge in the
# future, we will need to store new data
log.notice(
f"Setting SETUPTOOLS_EXT_SUFFIX=.{python_configuration.identifier}.pyd for cross-compilation"
)
env["SETUPTOOLS_EXT_SUFFIX"] = f".{python_configuration.identifier}.pyd"

# Cross-compilation requires fixes that only exist in setuptools's copy of
# distutils, so ensure that it is activated
# Since not all projects can handle the newer distutils, display a warning
# to help them figure out what may have gone wrong if this breaks for them
log.notice("Setting SETUPTOOLS_USE_DISTUTILS=local as it is required for cross-compilation")
env["SETUPTOOLS_USE_DISTUTILS"] = "local"


def setup_rust_cross_compile(
tmp: Path,
python_configuration: PythonConfiguration,
python_libs_base: Path,
env: dict[str, str],
) -> None:
# Assume that MSVC will be used, because we already know that we are
# cross-compiling. MinGW users can set CARGO_BUILD_TARGET themselves
# and we will respect the existing value.
cargo_target = {
"64": "x86_64-pc-windows-msvc",
"32": "i686-pc-windows-msvc",
"ARM64": "aarch64-pc-windows-msvc",
}.get(python_configuration.arch)

# CARGO_BUILD_TARGET is the variable used by Cargo and setuptools_rust
if env.get("CARGO_BUILD_TARGET"):
if env["CARGO_BUILD_TARGET"] != cargo_target:
log.notice("Not overriding CARGO_BUILD_TARGET as it has already been set")
# No message if it was set to what we were planning to set it to
elif cargo_target:
log.notice(f"Setting CARGO_BUILD_TARGET={cargo_target} for cross-compilation")
env["CARGO_BUILD_TARGET"] = cargo_target
else:
log.warning(
f"Unable to configure Rust cross-compilation for architecture {python_configuration.arch}"
)


def setup_python(
tmp: Path,
python_configuration: PythonConfiguration,
Expand All @@ -130,9 +215,22 @@ def setup_python(
) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
python_libs_base = None
log.step(f"Installing Python {implementation_id}...")
if implementation_id.startswith("cp"):
base_python = install_cpython(python_configuration.version, python_configuration.arch)
native_arch = platform_module.machine()
if python_configuration.arch == "ARM64" != native_arch:
# To cross-compile for ARM64, we need a native CPython to run the
# build, and a copy of the ARM64 import libraries ('.\libs\*.lib')
# for any extension modules.
python_libs_base = install_cpython(
python_configuration.version, python_configuration.arch
)
python_libs_base = python_libs_base.parent / "libs"
log.step(f"Installing native Python {native_arch} for cross-compilation...")
base_python = install_cpython(python_configuration.version, native_arch)
else:
base_python = install_cpython(python_configuration.version, python_configuration.arch)
elif implementation_id.startswith("pp"):
assert python_configuration.url is not None
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
Expand Down Expand Up @@ -234,6 +332,11 @@ def setup_python(
else:
assert_never(build_frontend)

if python_libs_base:
# Set up the environment for various backends to enable cross-compilation
joerick marked this conversation as resolved.
Show resolved Hide resolved
setup_setuptools_cross_compile(tmp, python_configuration, python_libs_base, env)
setup_rust_cross_compile(tmp, python_configuration, python_libs_base, env)

return env


Expand Down Expand Up @@ -296,7 +399,9 @@ def build(options: Options, tmp_path: Path) -> None:
if build_options.before_build:
log.step("Running before_build...")
before_build_prepared = prepare_command(
build_options.before_build, project=".", package=options.globals.package_dir
build_options.before_build,
project=".",
package=options.globals.package_dir,
)
shell(before_build_prepared, env=env)

Expand Down Expand Up @@ -367,7 +472,9 @@ def build(options: Options, tmp_path: Path) -> None:
if build_options.repair_command:
log.step("Repairing wheel...")
repair_command_prepared = prepare_command(
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
build_options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
)
shell(repair_command_prepared, env=env)
else:
Expand All @@ -379,6 +486,19 @@ def build(options: Options, tmp_path: Path) -> None:
raise AlreadyBuiltWheelError(repaired_wheel.name)

if build_options.test_command and options.globals.test_selector(config.identifier):
if config.arch == "ARM64" != platform_module.machine():
log.warning(
unwrap(
"""
While arm64 wheels can be built on other platforms, they cannot
be tested. An arm64 runner is required. To silence this warning,
set `CIBW_TEST_SKIP: *-win_arm64`.
"""
)
)
# skip this test
continue

log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
Expand Down
12 changes: 10 additions & 2 deletions docs/faq.md
Expand Up @@ -341,7 +341,7 @@ See [#816](https://github.com/pypa/cibuildwheel/issues/816), thanks to @phoeriou

### Windows: 'ImportError: DLL load failed: The specific module could not be found'

Visual Studio and MSVC link the compiled binary wheels to the Microsoft Visual C++ Runtime. Normally, these are included with Python, but when compiling with a newer version of Visual Studio, it is possible users will run into problems on systems that do not have these runtime libraries installed. The solution is to ask users to download the corresponding Visual C++ Redistributable from the [Microsoft website](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) and install it. Since a Python installation normally includes these VC++ Redistributable files for [the version of the MSVC compiler used to compile Python](https://wiki.python.org/moin/WindowsCompilers), this is typically only a problem when compiling a Python C extension with a newer compiler.
Visual Studio and MSVC link the compiled binary wheels to the Microsoft Visual C++ Runtime. Normally, the C parts of the runtime are included with Python, but the C++ components are not. When compiling modules using C++, it is possible users will run into problems on systems that do not have the full set of runtime libraries installed. The solution is to ask users to download the corresponding Visual C++ Redistributable from the [Microsoft website](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads) and install it.

Additionally, Visual Studio 2019 started linking to an even newer DLL, `VCRUNTIME140_1.dll`, besides the `VCRUNTIME140.dll` that is included with recent Python versions (starting from Python 3.5; see [here](https://wiki.python.org/moin/WindowsCompilers) for more details on the corresponding Visual Studio & MSVC versions used to compile the different Python versions). To avoid this extra dependency on `VCRUNTIME140_1.dll`, the [`/d2FH4-` flag](https://devblogs.microsoft.com/cppblog/making-cpp-exception-handling-smaller-x64/) can be added to the MSVC invocations (check out [this issue](https://github.com/pypa/cibuildwheel/issues/423) for details and references). CPython 3.8.3 and all versions after it have this extra DLL, so it is only needed for 3.8 and earlier.

Expand All @@ -357,4 +357,12 @@ To add the `/d2FH4-` flag to a standard `setup.py` using `setuptools`, the `extr
],
```

To investigate the dependencies of a C extension (i.e., the `.pyd` file, a DLL in disguise) on Windows, [Dependency Walker](http://www.dependencywalker.com/) is a great tool.
To investigate the dependencies of a C extension (i.e., the `.pyd` file, a DLL in disguise) on Windows, [Dependency Walker](http://www.dependencywalker.com/) is a great tool. For diagnosing a failing import, the [dlltracer](https://pypi.org/project/dlltracer/) tool may also provide additional details.

### Windows ARM64 builds {: #windows-arm64}

`cibuildwheel` supports cross-compiling `ARM64` wheels on all Windows runners, but a native ARM64 runner is required for testing. On non-native runners, tests for ARM64 wheels will be automatically skipped with a warning. Add `*-win_arm64` to your `CIBW_TEST_SKIP` setting to suppress the warning.

Cross-compilation on Windows relies on a supported build backend. Supported backends use an environment variable to specify their target platform (the one they are compiling native modules for, as opposed to the one they are running on), which is set in [cibuildwheels/windows.py](https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/windows.py) before building. Currently, `setuptools>=65.4.1` and `setuptools_rust` are the only supported backends.

By default, `ARM64` is not enabled when running on non-ARM64 runners. Use [`CIBW_ARCHS`](options.md#archs) to select it.
6 changes: 5 additions & 1 deletion docs/options.md
Expand Up @@ -347,11 +347,14 @@ On macOS, this option can be used to cross-compile between `x86_64`,
On Linux, this option can be used to build non-native architectures under
emulation. See [this guide](faq.md#emulation) for more information.

On Windows, this option can be used to compile for `ARM64` from an Intel
machine, provided the cross-compiling tools are installed.

Options:

- Linux: `x86_64` `i686` `aarch64` `ppc64le` `s390x`
- macOS: `x86_64` `arm64` `universal2`
- Windows: `AMD64` `x86`
- Windows: `AMD64` `x86` `ARM64`
- `auto`: The default archs for your machine - see the table below.
- `auto64`: Just the 64-bit auto archs
- `auto32`: Just the 32-bit auto archs
Expand All @@ -366,6 +369,7 @@ Default: `auto`
|---|---|---|---|---|
| Linux / Intel | `x86_64` | `x86_64` `i686` | `x86_64` | `i686` |
| Windows / Intel | `AMD64` | `AMD64` `x86` | `AMD64` | `x86` |
| Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | |
| macOS / Intel | `x86_64` | `x86_64` | `x86_64` | |
| macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | |

Expand Down
77 changes: 77 additions & 0 deletions test/test_windows.py
@@ -0,0 +1,77 @@
from __future__ import annotations

import os
import subprocess
import textwrap

import pytest

from . import test_projects, utils

basic_project = test_projects.new_c_project()
zooba marked this conversation as resolved.
Show resolved Hide resolved


def skip_if_no_msvc(arm64=False):
programfiles = os.getenv("ProgramFiles(x86)", "") or os.getenv("ProgramFiles", "")
if not programfiles:
pytest.skip("Requires %ProgramFiles(x86)% variable to be set")

vswhere = os.path.join(programfiles, "Microsoft Visual Studio", "Installer", "vswhere.exe")
if not os.path.isfile(vswhere):
pytest.skip("Requires Visual Studio installation")

require = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
if arm64:
require = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"

if not subprocess.check_output(
[
vswhere,
"-latest",
"-prerelease",
"-property",
"installationPath",
"-requires",
require,
]
):
pytest.skip("Requires ARM64 compiler to be installed")


@pytest.mark.parametrize("use_pyproject_toml", [True, False])
def test_wheel_tag_is_correct_when_using_windows_cross_compile(tmp_path, use_pyproject_toml):
if utils.platform != "windows":
pytest.skip("This test is only relevant to Windows")

skip_if_no_msvc(arm64=True)

if use_pyproject_toml:
basic_project.files["pyproject.toml"] = textwrap.dedent(
"""
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""
)

project_dir = tmp_path / "project"
basic_project.generate(project_dir)

# build the wheels
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_BUILD": "cp310-*",
},
add_args=["--archs", "ARM64"],
)

# check that the expected wheels are produced
expected_wheels = [
"spam-0.1.0-cp310-cp310-win_arm64.whl",
]

print("actual_wheels", actual_wheels)
print("expected_wheels", expected_wheels)

assert set(actual_wheels) == set(expected_wheels)