Files
cibuildwheel/cibuildwheel/windows.py
T

421 lines
16 KiB
Python
Raw Normal View History

2019-11-12 23:51:27 +00:00
import os
import shutil
import subprocess
2020-02-19 17:56:11 +01:00
import sys
from functools import lru_cache
2020-06-15 01:53:31 +02:00
from pathlib import Path
2021-05-08 19:24:40 +01:00
from typing import Dict, List, NamedTuple, Optional, Sequence, Set
2019-11-07 00:55:58 +01:00
from zipfile import ZipFile
2020-11-13 16:30:27 +00:00
from filelock import FileLock
from packaging.version import Version
2021-01-22 09:33:22 -05:00
from .architecture import Architecture
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2020-11-13 16:30:27 +00:00
from .logger import log
2021-10-12 02:05:47 +01:00
from .options import Options
2021-06-23 10:47:18 -04:00
from .typing import PathOrStr, assert_never
2021-01-06 13:50:58 -05:00
from .util import (
CIBW_CACHE_PATH,
2021-06-23 10:47:18 -04:00
BuildFrontend,
2021-01-06 13:50:58 -05:00
BuildSelector,
NonPlatformWheelError,
call,
2021-01-06 13:50:58 -05:00
download,
get_build_verbosity_extra_flags,
2021-06-23 10:47:18 -04:00
get_pip_version,
2021-01-06 13:50:58 -05:00
prepare_command,
2021-01-09 15:40:40 -05:00
read_python_configs,
shell,
virtualenv,
2021-01-06 13:50:58 -05:00
)
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]
2021-04-30 22:41:19 +01:00
return [
python_name,
2021-05-03 11:45:43 -04:00
"-Version",
2021-04-30 17:56:34 -04:00
version,
2021-05-03 11:45:43 -04:00
"-FallbackSource",
"https://api.nuget.org/v3/index.json",
"-OutputDirectory",
str(output_directory),
2021-04-30 22:41:19 +01:00
]
2019-11-12 23:51:27 +00:00
class PythonConfiguration(NamedTuple):
version: str
arch: str
identifier: str
2021-01-09 15:40:40 -05:00
url: Optional[str] = None
2021-01-09 15:40:40 -05:00
def get_python_configurations(
2021-04-30 17:56:34 -04:00
build_selector: BuildSelector,
architectures: Set[Architecture],
2021-01-09 15:40:40 -05:00
) -> List[PythonConfiguration]:
2021-05-03 11:45:43 -04:00
full_python_configs = read_python_configs("windows")
2021-01-09 15:40:40 -05:00
python_configurations = [PythonConfiguration(**item) for item in full_python_configs]
map_arch = {"32": Architecture.x86, "64": Architecture.AMD64, "ARM64": Architecture.ARM64}
2021-01-01 16:53:45 -05:00
2019-11-12 23:51:27 +00:00
# skip builds as required
2021-01-01 16:53:45 -05:00
python_configurations = [
2021-04-30 17:56:34 -04:00
c
for c in python_configurations
2021-01-01 16:53:45 -05:00
if build_selector(c.identifier) and map_arch[c.arch] in architectures
]
2019-09-28 19:24:16 +02:00
return python_configurations
2020-06-15 01:53:31 +02:00
def extract_zip(zip_src: Path, dest: Path) -> None:
with ZipFile(zip_src) as zip_:
zip_.extractall(dest)
@lru_cache(maxsize=None)
def _ensure_nuget() -> Path:
nuget = CIBW_CACHE_PATH / "nuget.exe"
with FileLock(str(nuget) + ".lock"):
if not nuget.exists():
download("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", nuget)
return nuget
def install_cpython(version: str, arch: str) -> Path:
base_output_dir = CIBW_CACHE_PATH / "nuget-cpython"
nuget_args = get_nuget_args(version, arch, base_output_dir)
installation_path = base_output_dir / (nuget_args[0] + "." + version) / "tools"
with FileLock(str(base_output_dir) + f"-{version}-{arch}.lock"):
if not installation_path.exists():
nuget = _ensure_nuget()
call(nuget, "install", *nuget_args)
return installation_path / "python.exe"
def install_pypy(tmp: Path, arch: str, url: str) -> Path:
assert arch == "64" and "win64" in url
# Inside the PyPy zip file is a directory with the same name
2021-05-03 11:45:43 -04:00
zip_filename = url.rsplit("/", 1)[-1]
2020-06-15 01:53:31 +02:00
extension = ".zip"
assert zip_filename.endswith(extension)
installation_path = CIBW_CACHE_PATH / zip_filename[: -len(extension)]
with FileLock(str(installation_path) + ".lock"):
if not installation_path.exists():
pypy_zip = tmp / zip_filename
download(url, pypy_zip)
# Extract to the parent directory because the zip file still contains a directory
extract_zip(pypy_zip, installation_path.parent)
return installation_path / "python.exe"
2021-04-30 17:56:34 -04:00
def setup_python(
tmp: Path,
2021-04-30 17:56:34 -04:00
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
2021-06-23 10:47:18 -04:00
build_frontend: BuildFrontend,
2021-04-30 17:56:34 -04:00
) -> Dict[str, str]:
tmp.mkdir()
2020-11-13 16:30:27 +00:00
implementation_id = python_configuration.identifier.split("-")[0]
2021-05-03 11:45:43 -04:00
log.step(f"Installing Python {implementation_id}...")
if implementation_id.startswith("cp"):
base_python = install_cpython(python_configuration.version, python_configuration.arch)
2021-05-03 11:45:43 -04:00
elif implementation_id.startswith("pp"):
2020-04-10 20:24:52 +02:00
assert python_configuration.url is not None
base_python = install_pypy(tmp, python_configuration.arch, python_configuration.url)
2020-04-05 16:03:35 +02:00
else:
raise ValueError("Unknown Python implementation")
assert base_python.exists()
2020-04-05 16:03:35 +02:00
2021-05-03 11:45:43 -04:00
log.step("Setting up build environment...")
venv_path = tmp / "venv"
env = virtualenv(base_python, venv_path, dependency_constraint_flags)
2020-11-13 16:30:27 +00:00
# set up environment variables for run_with_env
2021-05-03 11:45:43 -04:00
env["PYTHON_VERSION"] = python_configuration.version
env["PYTHON_ARCH"] = python_configuration.arch
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
2021-01-24 15:08:46 +00:00
# pip older than 21.3 builds executables such as pip.exe for x64 platform.
# The first re-install of pip updates pip module but builds pip.exe using
# the old pip which still generates x64 executable. But the second
# re-install uses updated pip and correctly builds pip.exe for the target.
# This can be removed once ARM64 Pythons (currently 3.9 and 3.10) bundle
# pip versions newer than 21.3.
if python_configuration.arch == "ARM64" and Version(get_pip_version(env)) < Version("21.3"):
call(
"python",
"-m",
"pip",
"install",
"--force-reinstall",
"--upgrade",
"pip",
*dependency_constraint_flags,
env=env,
cwd=venv_path,
)
2021-05-16 10:56:19 +01:00
# upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
2021-05-13 20:32:38 +02:00
call(
"python",
"-m",
"pip",
"install",
"--upgrade",
"pip",
*dependency_constraint_flags,
2021-05-13 20:32:38 +02:00
env=env,
cwd=venv_path,
2021-05-13 20:32:38 +02:00
)
2021-05-08 18:03:35 +01:00
# update env with results from CIBW_ENVIRONMENT
env = environment.as_dictionary(prev_environment=env)
# check what Python version we're on
call("where", "python", env=env)
call("python", "--version", env=env)
call("python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\"", env=env)
where_python = call("where", "python", env=env, capture_stdout=True).splitlines()[0].strip()
if where_python != str(venv_path / "Scripts" / "python.exe"):
print(
"cibuildwheel: python available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert python above it.",
file=sys.stderr,
)
sys.exit(1)
# check what pip version we're on
assert (venv_path / "Scripts" / "pip.exe").exists()
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
2021-04-30 17:56:34 -04:00
print(
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
file=sys.stderr,
)
2021-01-17 14:13:26 -05:00
sys.exit(1)
2020-04-05 16:03:35 +02:00
call("pip", "--version", env=env)
2021-06-23 10:47:18 -04:00
log.step("Installing build tools...")
2021-06-23 10:47:18 -04:00
if build_frontend == "pip":
call(
"pip",
"install",
"--upgrade",
"setuptools",
"wheel",
*dependency_constraint_flags,
2021-06-23 10:47:18 -04:00
env=env,
)
elif build_frontend == "build":
call(
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*dependency_constraint_flags,
2021-06-23 10:47:18 -04:00
env=env,
)
else:
assert_never(build_frontend)
2020-04-05 16:03:35 +02:00
2020-06-14 11:25:14 -04:00
return env
def build(options: Options, tmp_path: Path) -> None:
2021-10-12 02:05:47 +01:00
python_configurations = get_python_configurations(
options.globals.build_selector, options.globals.architectures
)
2021-09-19 00:19:28 -04:00
2022-04-19 16:00:12 +01:00
if len(python_configurations) == 0:
return
2021-10-12 02:05:47 +01:00
2022-04-19 16:00:12 +01:00
try:
before_all_options_identifier = python_configurations[0].identifier
before_all_options = options.build_options(before_all_options_identifier)
if before_all_options.before_all:
log.step("Running before_all...")
env = before_all_options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command(
before_all_options.before_all, project=".", package=options.globals.package_dir
)
shell(before_all_prepared, env=env)
2020-11-13 16:30:27 +00:00
for config in python_configurations:
2021-10-12 02:05:47 +01:00
build_options = options.build_options(config.identifier)
2020-11-13 16:30:27 +00:00
log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier
identifier_tmp_dir.mkdir()
built_wheel_dir = identifier_tmp_dir / "built_wheel"
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
dependency_constraint_flags: Sequence[PathOrStr] = []
2021-10-12 02:05:47 +01:00
if build_options.dependency_constraints:
2020-11-13 16:30:27 +00:00
dependency_constraint_flags = [
2021-05-03 11:45:43 -04:00
"-c",
2021-10-12 02:05:47 +01:00
build_options.dependency_constraints.get_for_python_version(config.version),
2020-11-13 16:30:27 +00:00
]
2017-03-19 21:27:29 +00:00
2020-11-13 16:30:27 +00:00
# install Python
2021-06-23 10:47:18 -04:00
env = setup_python(
identifier_tmp_dir / "build",
2021-06-23 10:47:18 -04:00
config,
dependency_constraint_flags,
2021-10-12 02:05:47 +01:00
build_options.environment,
build_options.build_frontend,
2021-06-23 10:47:18 -04:00
)
2020-11-13 16:30:27 +00:00
# run the before_build command
2021-10-12 02:05:47 +01:00
if build_options.before_build:
2021-05-03 11:45:43 -04:00
log.step("Running before_build...")
2021-04-30 17:56:34 -04:00
before_build_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.before_build, project=".", package=options.globals.package_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell(before_build_prepared, env=env)
2020-06-14 11:25:14 -04:00
2021-05-03 11:45:43 -04:00
log.step("Building wheel...")
built_wheel_dir.mkdir()
2021-06-23 10:47:18 -04:00
2021-10-12 02:05:47 +01:00
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
2021-06-23 10:47:18 -04:00
2021-10-12 02:05:47 +01:00
if build_options.build_frontend == "pip":
2021-06-23 10:47:18 -04:00
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
call(
"python",
"-m",
"pip",
"wheel",
options.globals.package_dir.resolve(),
f"--wheel-dir={built_wheel_dir}",
"--no-deps",
*get_build_verbosity_extra_flags(build_options.build_verbosity),
2021-06-23 10:47:18 -04:00
env=env,
)
2021-10-12 02:05:47 +01:00
elif build_options.build_frontend == "build":
2021-06-23 10:47:18 -04:00
config_setting = " ".join(verbosity_flags)
build_env = env.copy()
2021-10-12 02:05:47 +01:00
if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version
)
# Bug in pip <= 21.1.3 - we can't have a space in the
# constraints file, and pip doesn't support drive letters
# in uhi. After probably pip 21.2, we can use uri. For
# now, use a temporary file.
if " " in str(constraints_path):
assert " " not in str(identifier_tmp_dir)
tmp_file = identifier_tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
build_env["PIP_CONSTRAINT"] = str(constraints_path)
build_env["VIRTUALENV_PIP"] = get_pip_version(env)
call(
"python",
"-m",
"build",
build_options.package_dir,
"--wheel",
f"--outdir={built_wheel_dir}",
f"--config-setting={config_setting}",
env=build_env,
2021-06-23 10:47:18 -04:00
)
else:
2021-10-12 02:05:47 +01:00
assert_never(build_options.build_frontend)
2019-11-12 23:34:59 +00:00
2021-05-03 11:45:43 -04:00
built_wheel = next(built_wheel_dir.glob("*.whl"))
2020-11-13 16:30:27 +00:00
# repair the wheel
repaired_wheel_dir.mkdir()
2021-05-03 11:45:43 -04:00
if built_wheel.name.endswith("none-any.whl"):
2020-11-13 16:30:27 +00:00
raise NonPlatformWheelError()
2021-10-12 02:05:47 +01:00
if build_options.repair_command:
2021-05-03 11:45:43 -04:00
log.step("Repairing wheel...")
2021-04-30 17:56:34 -04:00
repair_command_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell(repair_command_prepared, env=env)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
2017-03-19 21:27:29 +00:00
2021-05-03 11:45:43 -04:00
repaired_wheel = next(repaired_wheel_dir.glob("*.whl"))
2020-03-01 11:09:22 +00:00
2021-10-12 02:05:47 +01:00
if build_options.test_command and options.globals.test_selector(config.identifier):
2021-05-03 11:45:43 -04:00
log.step("Testing wheel...")
2020-11-13 16:30:27 +00:00
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / "venv-test"
2020-11-13 16:30:27 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
call("python", "-m", "virtualenv", "--no-download", venv_dir, env=env)
2020-11-13 16:30:27 +00:00
virtualenv_env = env.copy()
2021-05-03 11:45:43 -04:00
virtualenv_env["PATH"] = os.pathsep.join(
2021-04-30 17:56:34 -04:00
[
2021-05-03 11:45:43 -04:00
str(venv_dir / "Scripts"),
virtualenv_env["PATH"],
2021-04-30 17:56:34 -04:00
]
)
2020-11-13 16:30:27 +00:00
# check that we are using the Python from the virtual environment
call("where", "python", env=virtualenv_env)
2020-11-13 16:30:27 +00:00
2021-10-12 02:05:47 +01:00
if build_options.before_test:
2020-11-13 16:30:27 +00:00
before_test_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.before_test,
2021-05-03 11:45:43 -04:00
project=".",
2021-10-12 02:05:47 +01:00
package=build_options.package_dir,
2020-11-13 16:30:27 +00:00
)
shell(before_test_prepared, env=virtualenv_env)
# install the wheel
2021-04-30 17:56:34 -04:00
call(
"pip",
"install",
str(repaired_wheel) + build_options.test_extras,
2021-04-30 17:56:34 -04:00
env=virtualenv_env,
)
2020-11-13 16:30:27 +00:00
# test the wheel
2021-10-12 02:05:47 +01:00
if build_options.test_requires:
call("pip", "install", *build_options.test_requires, env=virtualenv_env)
2020-11-13 16:30:27 +00:00
# run the tests from c:\, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
test_command_prepared = prepare_command(
2021-10-12 02:05:47 +01:00
build_options.test_command,
2021-05-03 11:45:43 -04:00
project=Path(".").resolve(),
2021-10-12 02:05:47 +01:00
package=options.globals.package_dir.resolve(),
)
2021-05-03 11:45:43 -04:00
shell(test_command_prepared, cwd="c:\\", env=virtualenv_env)
2020-01-07 00:17:03 +01:00
2020-11-13 16:30:27 +00:00
# we're all done here; move it to output (remove if already exists)
2021-10-12 02:05:47 +01:00
shutil.move(str(repaired_wheel), build_options.output_dir)
# clean up
# (we ignore errors because occasionally Windows fails to unlink a file and we
# don't want to abort a build because of that)
shutil.rmtree(identifier_tmp_dir, ignore_errors=True)
2020-11-13 16:30:27 +00:00
log.build_end()
except subprocess.CalledProcessError as error:
2021-04-30 17:56:34 -04:00
log.step_end_with_error(
2021-05-03 11:45:43 -04:00
f"Command {error.cmd} failed with code {error.returncode}. {error.stdout}"
2021-04-30 17:56:34 -04:00
)
2021-01-17 14:13:26 -05:00
sys.exit(1)