Files
cibuildwheel/cibuildwheel/windows.py
T

361 lines
13 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
2019-11-12 23:51:27 +00:00
import tempfile
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
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-05-09 10:15:26 +01:00
from .typing import PathOrStr
2021-01-06 13:50:58 -05:00
from .util import (
BuildOptions,
BuildSelector,
NonPlatformWheelError,
download,
get_build_verbosity_extra_flags,
prepare_command,
2021-01-09 15:40:40 -05:00
read_python_configs,
2021-01-06 13:50:58 -05:00
)
2021-04-30 17:56:34 -04:00
def call(
2021-05-09 10:15:26 +01:00
args: Sequence[PathOrStr], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None
) -> None:
2021-05-03 11:45:43 -04:00
print("+ " + " ".join(str(a) for a in args))
# we use shell=True here, even though we don't need a shell due to a bug
# https://bugs.python.org/issue8557
2021-05-09 10:15:26 +01:00
subprocess.run([str(a) for a in args], env=env, cwd=cwd, shell=True, check=True)
def shell(command: str, env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None) -> None:
2021-05-03 11:45:43 -04:00
print(f"+ {command}")
2021-02-14 12:56:33 -05:00
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
def get_nuget_args(version: str, arch: str) -> List[str]:
2021-02-14 20:44:20 +01:00
python_name = "python"
2021-05-03 11:45:43 -04:00
if arch == "32":
python_name += "x86"
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",
"C:\\cibw\\python",
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]
2021-01-01 16:53:45 -05:00
map_arch = {
2021-05-03 11:45:43 -04:00
"32": Architecture.x86,
"64": Architecture.AMD64,
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)
2020-06-15 01:53:31 +02:00
def install_cpython(version: str, arch: str, nuget: Path) -> Path:
nuget_args = get_nuget_args(version, arch)
2021-05-03 11:45:43 -04:00
installation_path = Path(nuget_args[-1]) / (nuget_args[0] + "." + version) / "tools"
call([nuget, "install", *nuget_args])
return installation_path
2020-06-15 01:53:31 +02:00
def install_pypy(version: str, arch: str, url: str) -> Path:
2021-05-03 11:45:43 -04:00
assert arch == "32"
# 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)
2021-05-03 11:45:43 -04:00
installation_path = Path("C:\\cibw") / zip_filename[: -len(extension)]
2020-06-15 01:53:31 +02:00
if not installation_path.exists():
2021-05-03 11:45:43 -04:00
pypy_zip = Path("C:\\cibw") / zip_filename
download(url, pypy_zip)
# Extract to the parent directory because the zip file still contains a directory
2020-06-15 01:53:31 +02:00
extract_zip(pypy_zip, installation_path.parent)
2021-02-14 20:44:20 +01:00
(installation_path / "python.exe").symlink_to(installation_path / "pypy3.exe")
return installation_path
2021-04-30 17:56:34 -04:00
def setup_python(
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
) -> Dict[str, str]:
2021-05-03 11:45:43 -04:00
nuget = Path("C:\\cibw\\nuget.exe")
2020-06-15 01:53:31 +02:00
if not nuget.exists():
2021-05-03 11:45:43 -04:00
log.step("Downloading nuget...")
download("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", nuget)
2020-04-05 16:03:35 +02:00
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}...")
2020-11-13 16:30:27 +00:00
2021-05-03 11:45:43 -04:00
if implementation_id.startswith("cp"):
2021-04-30 17:56:34 -04:00
installation_path = install_cpython(
python_configuration.version, python_configuration.arch, nuget
)
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
2021-04-30 17:56:34 -04:00
installation_path = install_pypy(
python_configuration.version, python_configuration.arch, python_configuration.url
)
2020-04-05 16:03:35 +02:00
else:
raise ValueError("Unknown Python implementation")
2021-05-03 11:45:43 -04:00
assert (installation_path / "python.exe").exists()
2020-04-05 16:03:35 +02:00
2021-05-03 11:45:43 -04:00
log.step("Setting up build environment...")
2020-11-13 16:30:27 +00:00
2020-04-05 16:03:35 +02:00
# set up PATH and environment variables for run_with_env
env = os.environ.copy()
2021-05-03 11:45:43 -04:00
env["PYTHON_VERSION"] = python_configuration.version
env["PYTHON_ARCH"] = python_configuration.arch
env["PATH"] = os.pathsep.join(
[str(installation_path), str(installation_path / "Scripts"), env["PATH"]]
2021-04-30 17:56:34 -04:00
)
2021-05-03 11:45:43 -04:00
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
2021-01-24 15:08:46 +00:00
2020-04-05 16:03:35 +02:00
# update env with results from CIBW_ENVIRONMENT
env = environment.as_dictionary(prev_environment=env)
# for the logs - check we're running the right version of python
2021-05-03 11:45:43 -04:00
call(["where", "python"], env=env)
call(["python", "--version"], env=env)
call(["python", "-c", "\"import struct; print(struct.calcsize('P') * 8)\""], env=env)
2021-04-30 17:56:34 -04:00
where_python = (
subprocess.run(
2021-05-03 11:45:43 -04:00
["where", "python"],
2021-04-30 17:56:34 -04:00
env=env,
universal_newlines=True,
check=True,
stdout=subprocess.PIPE,
)
.stdout.splitlines()[0]
.strip()
)
2021-05-03 11:45:43 -04:00
if where_python != str(installation_path / "python.exe"):
2021-04-30 17:56:34 -04:00
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,
)
2021-01-17 14:13:26 -05:00
sys.exit(1)
2020-04-05 16:03:35 +02:00
2021-05-08 18:03:35 +01:00
log.step("Installing build tools...")
2021-05-13 20:32:38 +02:00
# ensure pip is installed
2021-05-16 10:12:08 +01:00
call(["python", "-m", "ensurepip"], env=env, cwd="C:\\cibw")
2021-05-16 10:12:08 +01:00
# upgrade to the version matching our constraints
# if necessary, reinstall it to ensure that it's installed as 'pip.exe'
requires_reinstall = not (installation_path / "Scripts" / "pip.exe").exists()
2021-05-13 20:32:38 +02:00
call(
2021-05-16 10:12:08 +01:00
[
"python",
"-m",
"pip",
"install",
"--force-reinstall" if requires_reinstall else "--upgrade",
"pip",
*dependency_constraint_flags,
],
2021-05-13 20:32:38 +02:00
env=env,
)
2021-05-08 18:03:35 +01:00
2021-05-03 11:45:43 -04:00
assert (installation_path / "Scripts" / "pip.exe").exists()
2021-04-30 17:56:34 -04:00
where_pip = (
subprocess.run(
2021-05-03 11:45:43 -04:00
["where", "pip"], env=env, universal_newlines=True, check=True, stdout=subprocess.PIPE
2021-04-30 17:56:34 -04:00
)
.stdout.splitlines()[0]
.strip()
)
2021-05-03 11:45:43 -04:00
if where_pip.strip() != str(installation_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
2021-05-03 11:45:43 -04:00
call(["pip", "--version"], env=env)
2021-04-30 17:56:34 -04:00
call(
2021-05-03 11:45:43 -04:00
["pip", "install", "--upgrade", "setuptools", "wheel", *dependency_constraint_flags],
2021-04-30 17:56:34 -04:00
env=env,
)
2020-04-05 16:03:35 +02:00
2020-06-14 11:25:14 -04:00
return env
2020-04-10 01:44:02 +02:00
def build(options: BuildOptions) -> None:
2021-05-03 11:45:43 -04:00
temp_dir = Path(tempfile.mkdtemp(prefix="cibuildwheel"))
built_wheel_dir = temp_dir / "built_wheel"
repaired_wheel_dir = temp_dir / "repaired_wheel"
2017-07-02 18:02:27 -05:00
2020-11-13 16:30:27 +00:00
try:
if options.before_all:
2021-05-03 11:45:43 -04:00
log.step("Running before_all...")
2020-11-13 16:30:27 +00:00
env = options.environment.as_dictionary(prev_environment=os.environ)
2021-04-30 17:56:34 -04:00
before_all_prepared = prepare_command(
2021-05-03 11:45:43 -04:00
options.before_all, project=".", package=options.package_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell(before_all_prepared, env=env)
2021-04-30 17:56:34 -04:00
python_configurations = get_python_configurations(
options.build_selector, options.architectures
)
2020-05-11 17:09:54 +02:00
2020-11-13 16:30:27 +00:00
for config in python_configurations:
log.build_start(config.identifier)
dependency_constraint_flags: Sequence[PathOrStr] = []
2020-11-13 16:30:27 +00:00
if options.dependency_constraints:
dependency_constraint_flags = [
2021-05-03 11:45:43 -04:00
"-c",
2021-04-30 17:56:34 -04:00
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
env = setup_python(config, dependency_constraint_flags, options.environment)
2020-11-13 16:30:27 +00:00
# run the before_build command
if 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-05-03 11:45:43 -04:00
options.before_build, project=".", package=options.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...")
2020-11-13 16:30:27 +00:00
if built_wheel_dir.exists():
shutil.rmtree(built_wheel_dir)
built_wheel_dir.mkdir(parents=True)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369
2021-04-30 17:56:34 -04:00
call(
[
2021-05-03 11:45:43 -04:00
"pip",
"wheel",
2021-04-30 17:56:34 -04:00
options.package_dir.resolve(),
2021-05-03 11:45:43 -04:00
"-w",
2021-04-30 17:56:34 -04:00
built_wheel_dir,
2021-05-03 11:45:43 -04:00
"--no-deps",
2021-04-30 17:56:34 -04:00
*get_build_verbosity_extra_flags(options.build_verbosity),
],
env=env,
)
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
if repaired_wheel_dir.exists():
shutil.rmtree(repaired_wheel_dir)
repaired_wheel_dir.mkdir(parents=True)
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()
2020-11-13 16:30:27 +00:00
if 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(
options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir
)
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-01-20 21:22:48 -05:00
if options.test_command and options.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.
2021-05-03 11:45:43 -04:00
call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env)
2020-11-13 16:30:27 +00:00
venv_dir = Path(tempfile.mkdtemp())
2020-11-13 16:30:27 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
2021-05-03 11:45:43 -04:00
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
2021-05-03 11:45:43 -04:00
call(["where", "python"], env=virtualenv_env)
2020-11-13 16:30:27 +00:00
if options.before_test:
before_test_prepared = prepare_command(
options.before_test,
2021-05-03 11:45:43 -04:00
project=".",
2021-04-29 20:21:42 -04:00
package=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(
2021-05-03 11:45:43 -04:00
["pip", "install", str(repaired_wheel) + 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
if options.test_requires:
2021-05-03 11:45:43 -04:00
call(["pip", "install"] + 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(
options.test_command,
2021-05-03 11:45:43 -04:00
project=Path(".").resolve(),
2021-04-29 20:21:42 -04:00
package=options.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
# clean up
shutil.rmtree(venv_dir)
2020-11-13 16:30:27 +00:00
# we're all done here; move it to output (remove if already exists)
shutil.move(str(repaired_wheel), options.output_dir)
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)