Files
cibuildwheel/cibuildwheel/__main__.py
T

431 lines
15 KiB
Python
Raw Normal View History

from __future__ import annotations
2019-11-12 23:51:27 +00:00
import argparse
import dataclasses
2019-11-12 23:51:27 +00:00
import os
import shutil
2019-11-12 23:51:27 +00:00
import sys
2022-04-26 22:21:27 -04:00
import tarfile
2019-11-12 23:51:27 +00:00
import textwrap
import traceback
2022-09-09 08:34:47 -04:00
import typing
from collections.abc import Iterable, Sequence, Set
from pathlib import Path
from tempfile import mkdtemp
from typing import Protocol
2021-01-31 17:14:15 -05:00
2017-04-13 15:02:04 +01:00
import cibuildwheel
2019-11-12 23:51:27 +00:00
import cibuildwheel.linux
import cibuildwheel.macos
2024-05-28 05:31:36 -07:00
import cibuildwheel.pyodide
2021-02-04 14:27:54 -05:00
import cibuildwheel.util
2019-11-12 23:51:27 +00:00
import cibuildwheel.windows
from cibuildwheel import errors
from cibuildwheel._compat.typing import assert_never
2023-04-18 22:02:47 -04:00
from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.logger import log
from cibuildwheel.options import CommandLineArguments, Options, compute_options
2023-04-18 23:05:34 -04:00
from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
from cibuildwheel.util import (
CIBW_CACHE_PATH,
BuildSelector,
2022-12-04 13:51:56 +00:00
CIProvider,
Unbuffered,
2022-04-26 22:21:27 -04:00
chdir,
detect_ci_provider,
2022-12-04 13:51:56 +00:00
fix_ansi_codes_for_github_actions,
strtobool,
)
@dataclasses.dataclass
class GlobalOptions:
print_traceback_on_error: bool = True # decides what happens when errors are hit.
def main() -> None:
global_options = GlobalOptions()
try:
main_inner(global_options)
except errors.FatalError as e:
message = e.args[0]
if log.step_active:
log.step_end_with_error(message)
else:
print(f"cibuildwheel: {message}", file=sys.stderr)
if global_options.print_traceback_on_error:
traceback.print_exc(file=sys.stderr)
sys.exit(e.return_code)
def main_inner(global_options: GlobalOptions) -> None:
"""
`main_inner` is the same as `main`, but it raises FatalError exceptions
rather than exiting directly.
"""
2017-03-19 20:57:51 +00:00
parser = argparse.ArgumentParser(
2021-05-03 11:45:43 -04:00
description="Build wheels for all the platforms.",
epilog="""
2021-06-21 12:26:46 -04:00
Most options are supplied via environment variables or in
--config-file (pyproject.toml usually). See
https://github.com/pypa/cibuildwheel#options for info.
2021-05-03 11:45:43 -04:00
""",
2021-04-30 17:56:34 -04:00
)
2017-03-19 20:57:51 +00:00
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"--platform",
2024-05-28 05:31:36 -07:00
choices=["auto", "linux", "macos", "windows", "pyodide"],
2022-09-09 08:34:47 -04:00
default=None,
2021-05-03 11:45:43 -04:00
help="""
Platform to build for. Use this option to override the
auto-detected platform. Specifying "macos" or "windows" only works
on that operating system, but "linux" works on all three, as long
as Docker/Podman is installed. Default: auto.
2021-05-03 11:45:43 -04:00
""",
2021-04-30 17:56:34 -04:00
)
2020-12-16 23:17:51 +00:00
2021-04-29 20:21:42 -04:00
arch_list_str = ", ".join(a.name for a in Architecture)
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"--archs",
2021-04-30 17:56:34 -04:00
default=None,
2021-05-03 11:45:43 -04:00
help=f"""
2021-05-02 16:37:38 +02:00
Comma-separated list of CPU architectures to build for.
When set to 'auto', builds the architectures natively supported
on this machine. Set this option to build an architecture
via emulation, for example, using binfmt_misc and QEMU.
Default: auto.
Choices: auto, auto64, auto32, native, all, {arch_list_str}
2021-05-03 11:45:43 -04:00
""",
2021-04-30 17:56:34 -04:00
)
2021-04-29 20:21:42 -04:00
2022-09-09 08:34:47 -04:00
parser.add_argument(
"--only",
default=None,
help="""
Force a single wheel build when given an identifier. Overrides
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
if this is given.
""",
)
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"--output-dir",
2022-04-26 22:21:27 -04:00
type=Path,
2022-04-27 17:08:46 -04:00
default=Path(os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse")),
help="Destination folder for the wheels. Default: wheelhouse.",
2021-04-30 17:56:34 -04:00
)
2021-04-29 20:21:42 -04:00
2021-06-21 12:26:46 -04:00
parser.add_argument(
"--config-file",
default="",
2021-06-21 12:26:46 -04:00
help="""
2022-04-27 12:31:42 -04:00
TOML config file. Default: "", meaning {package}/pyproject.toml, if
it exists. To refer to a project inside your project, use {package};
this matters if you build from an SDist.
2021-06-21 12:26:46 -04:00
""",
)
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"package_dir",
2022-04-27 17:08:46 -04:00
metavar="PACKAGE",
2022-04-26 22:21:27 -04:00
default=Path("."),
type=Path,
2021-05-03 11:45:43 -04:00
nargs="?",
help="""
2022-04-27 17:08:46 -04:00
Path to the package that you want wheels for. Default: the working
directory. Can be a directory inside the working directory, or an
sdist. When set to a directory, the working directory is still
considered the 'project' and is copied into the build container
2022-04-27 17:08:46 -04:00
on Linux. When set to a tar.gz sdist file, --config-file
and --output-dir are relative to the current directory, and other
paths are relative to the expanded SDist directory.
2021-05-03 11:45:43 -04:00
""",
2021-04-30 17:56:34 -04:00
)
2017-03-19 20:57:51 +00:00
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"--print-build-identifiers",
action="store_true",
help="Print the build identifiers matched by the current invocation and exit.",
2021-04-30 17:56:34 -04:00
)
2021-04-29 20:21:42 -04:00
2021-04-30 17:56:34 -04:00
parser.add_argument(
2021-05-03 11:45:43 -04:00
"--allow-empty",
action="store_true",
help="Do not report an error code if the build does not match any wheels.",
2021-04-30 17:56:34 -04:00
)
parser.add_argument(
"--prerelease-pythons",
action="store_true",
help="Enable pre-release Python versions if available.",
)
parser.add_argument(
"--debug-traceback",
action="store_true",
default=strtobool(os.environ.get("CIBW_DEBUG_TRACEBACK", "0")),
help="Print a full traceback for all errors",
)
2022-04-28 09:19:27 -04:00
args = CommandLineArguments(**vars(parser.parse_args()))
2017-03-19 20:57:51 +00:00
global_options.print_traceback_on_error = args.debug_traceback
2022-04-26 22:21:27 -04:00
args.package_dir = args.package_dir.resolve()
2022-04-27 12:31:42 -04:00
# This are always relative to the base directory, even in SDist builds
2022-04-27 17:08:46 -04:00
args.output_dir = args.output_dir.resolve()
2022-04-26 22:21:27 -04:00
# Standard builds if a directory or non-existent path is given
if not args.package_dir.is_file() and not args.package_dir.name.endswith("tar.gz"):
build_in_directory(args)
return
# Tarfile builds require extraction and changing the directory
2022-09-03 11:21:23 +02:00
temp_dir = Path(mkdtemp(prefix="cibw-sdist-")).resolve(strict=True)
try:
2022-04-26 22:21:27 -04:00
with tarfile.open(args.package_dir) as tar:
tar.extractall(path=temp_dir)
# The extract directory is now the project dir
try:
(project_dir,) = temp_dir.iterdir()
except ValueError:
2022-09-05 13:11:46 -04:00
msg = "invalid sdist: didn't contain a single dir"
raise SystemExit(msg) from None
2022-04-26 22:21:27 -04:00
2022-04-27 12:31:42 -04:00
# This is now the new package dir
2022-04-26 22:21:27 -04:00
args.package_dir = project_dir.resolve()
with chdir(project_dir):
2022-04-26 22:21:27 -04:00
build_in_directory(args)
2022-09-03 11:21:23 +02:00
finally:
# avoid https://github.com/python/cpython/issues/86962 by performing
# cleanup manually
shutil.rmtree(temp_dir, ignore_errors=sys.platform.startswith("win"))
if temp_dir.exists():
2022-10-07 08:47:31 -04:00
log.warning(f"Can't delete temporary folder '{temp_dir}'")
2022-04-26 22:21:27 -04:00
2023-04-07 13:56:49 -07:00
def _compute_platform_only(only: str) -> PlatformName:
if "linux_" in only:
return "linux"
if "macosx_" in only:
return "macos"
if "win_" in only or "win32" in only:
return "windows"
2024-05-28 05:31:36 -07:00
if "pyodide_" in only:
return "pyodide"
msg = f"Invalid --only='{only}', must be a build selector with a known platform"
raise errors.ConfigurationError(msg)
def _compute_platform_auto() -> PlatformName:
if sys.platform.startswith("linux"):
return "linux"
2023-04-07 13:56:49 -07:00
elif sys.platform == "darwin":
return "macos"
elif sys.platform == "win32":
return "windows"
2023-04-07 13:56:49 -07:00
else:
msg = (
2024-02-28 09:11:06 +00:00
'cibuildwheel: Unable to detect platform from "sys.platform". cibuildwheel doesn\'t '
"support building wheels for this platform. You might be able to build for a different "
"platform using the --platform argument. Check --help output for more information."
2023-04-07 13:56:49 -07:00
)
raise errors.ConfigurationError(msg)
def _compute_platform(args: CommandLineArguments) -> PlatformName:
2024-08-14 15:56:06 -04:00
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "") or "auto"
if args.only and args.platform is not None:
msg = "--platform cannot be specified with --only, it is computed from --only"
raise errors.ConfigurationError(msg)
if args.only and args.archs is not None:
msg = "--arch cannot be specified with --only, it is computed from --only"
raise errors.ConfigurationError(msg)
if platform_option_value not in PLATFORMS | {"auto"}:
msg = f"Unsupported platform: {platform_option_value}"
raise errors.ConfigurationError(msg)
2022-09-09 08:34:47 -04:00
if args.only:
return _compute_platform_only(args.only)
2022-09-09 08:34:47 -04:00
elif platform_option_value != "auto":
return typing.cast(PlatformName, platform_option_value)
2022-09-09 08:34:47 -04:00
return _compute_platform_auto()
2023-04-18 22:02:47 -04:00
class PlatformModule(Protocol):
# note that as per PEP544, the self argument is ignored when the protocol
# is applied to a module
def get_python_configurations(
self, build_selector: BuildSelector, architectures: Set[Architecture]
) -> Sequence[GenericPythonConfiguration]: ...
def build(self, options: Options, tmp_path: Path) -> None: ...
def get_platform_module(platform: PlatformName) -> PlatformModule:
2023-04-10 22:24:20 +01:00
if platform == "linux":
return cibuildwheel.linux
2023-04-10 22:24:20 +01:00
if platform == "windows":
return cibuildwheel.windows
2023-04-10 22:24:20 +01:00
if platform == "macos":
return cibuildwheel.macos
2024-05-28 05:31:36 -07:00
if platform == "pyodide":
return cibuildwheel.pyodide
2023-04-20 12:44:19 -07:00
assert_never(platform)
def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args)
2024-05-28 05:31:36 -07:00
if platform == "pyodide" and sys.platform == "win32":
msg = "cibuildwheel: Building for pyodide is not supported on Windows"
print(msg, file=sys.stderr)
sys.exit(2)
2022-12-05 19:18:54 +00:00
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)
2021-10-12 02:05:47 +01:00
package_dir = options.globals.package_dir
package_files = {"setup.py", "setup.cfg", "pyproject.toml"}
if not any(package_dir.joinpath(name).exists() for name in package_files):
names = ", ".join(sorted(package_files, reverse=True))
msg = f"Could not find any of {{{names}}} at root of package"
raise errors.ConfigurationError(msg)
platform_module = get_platform_module(platform)
identifiers = get_build_identifiers(
platform_module=platform_module,
2021-10-12 02:05:47 +01:00
build_selector=options.globals.build_selector,
architectures=options.globals.architectures,
)
2021-01-18 00:09:11 -05:00
if args.print_build_identifiers:
2021-01-18 00:09:11 -05:00
for identifier in identifiers:
print(identifier)
2021-01-17 14:13:26 -05:00
sys.exit(0)
2021-09-19 00:19:28 -04:00
# Add CIBUILDWHEEL environment variable
os.environ["CIBUILDWHEEL"] = "1"
2017-03-19 20:57:51 +00:00
2024-02-28 09:11:06 +00:00
# Python is buffering by default when running on the CI platforms, giving
# problems interleaving subprocess call output with unflushed calls to
# 'print'
sys.stdout = Unbuffered(sys.stdout)
# create the cache dir before it gets printed & builds performed
CIBW_CACHE_PATH.mkdir(parents=True, exist_ok=True)
2021-10-12 02:05:47 +01:00
print_preamble(platform=platform, options=options, identifiers=identifiers)
2017-04-13 15:02:04 +01:00
2021-01-22 09:23:52 -05:00
try:
2021-10-12 02:05:47 +01:00
options.check_for_invalid_configuration(identifiers)
allowed_architectures_check(platform, options.globals.architectures)
2021-01-22 09:23:52 -05:00
except ValueError as err:
raise errors.DeprecationError(*err.args) from err
2021-01-22 09:23:52 -05:00
2021-01-22 08:54:19 -05:00
if not identifiers:
message = f"No build identifiers selected: {options.globals.build_selector}"
if options.globals.allow_empty:
print(f"cibuildwheel: {message}", file=sys.stderr)
else:
raise errors.NothingToDoError(message)
2021-01-22 08:54:19 -05:00
2021-10-12 02:05:47 +01:00
output_dir = options.globals.output_dir
2020-06-15 01:53:31 +02:00
if not output_dir.exists():
output_dir.mkdir(parents=True)
2017-07-02 18:02:27 -05:00
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
try:
with cibuildwheel.util.print_new_wheels(
"\n{n} wheels produced in {m:.0f} minutes:", output_dir
):
platform_module.build(options, tmp_path)
finally:
2022-09-03 11:21:23 +02:00
# avoid https://github.com/python/cpython/issues/86962 by performing
# cleanup manually
shutil.rmtree(tmp_path, ignore_errors=sys.platform.startswith("win"))
if tmp_path.exists():
2022-10-07 08:47:31 -04:00
log.warning(f"Can't delete temporary folder '{tmp_path}'")
2017-03-19 20:57:51 +00:00
def print_preamble(platform: str, options: Options, identifiers: Sequence[str]) -> None:
2021-04-30 17:56:34 -04:00
print(
textwrap.dedent(
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
_ _ _ _ _ _ _
___|_| |_ _ _|_| |_| |_ _ _| |_ ___ ___| |
| _| | . | | | | | . | | | | | -_| -_| |
|___|_|___|___|_|_|___|_____|_|_|___|___|_|
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
)
)
2017-04-13 15:02:04 +01:00
2021-05-03 11:45:43 -04:00
print(f"cibuildwheel version {cibuildwheel.__version__}\n")
2017-04-13 15:02:04 +01:00
2021-05-03 11:45:43 -04:00
print("Build options:")
2022-11-26 15:54:08 +00:00
print(f" platform: {platform}")
2022-12-04 13:51:56 +00:00
options_summary = textwrap.indent(options.summary(identifiers), " ")
if detect_ci_provider() == CIProvider.github_actions:
options_summary = fix_ansi_codes_for_github_actions(options_summary)
print(options_summary)
2017-04-13 15:02:04 +01:00
2022-11-26 15:54:08 +00:00
print()
print(f"Cache folder: {CIBW_CACHE_PATH}")
print()
warnings = detect_warnings(options=options, identifiers=identifiers)
for warning in warnings:
log.warning(warning)
print("Here we go!\n")
2017-04-13 15:02:04 +01:00
2021-01-18 00:09:11 -05:00
def get_build_identifiers(
platform_module: PlatformModule,
build_selector: BuildSelector,
architectures: Set[Architecture],
) -> list[str]:
python_configurations = platform_module.get_python_configurations(build_selector, architectures)
2021-01-18 00:09:11 -05:00
return [config.identifier for config in python_configurations]
def detect_warnings(*, options: Options, identifiers: Iterable[str]) -> list[str]:
warnings = []
python_version_deprecation = ((3, 11), 3)
if sys.version_info[:2] < python_version_deprecation[0]:
python_version = ".".join(map(str, python_version_deprecation[0]))
msg = (
f"cibuildwheel {python_version_deprecation[1]} will require Python {python_version}+, "
"please upgrade the Python version used to run cibuildwheel. "
"This does not affect the versions you can target when building wheels. See: https://cibuildwheel.pypa.io/en/stable/#what-does-it-do"
)
warnings.append(msg)
# warn about deprecated {python} and {pip}
2021-10-12 02:05:47 +01:00
for option_name in ["test_command", "before_build"]:
option_values = [getattr(options.build_options(i), option_name) for i in identifiers]
2021-10-12 02:05:47 +01:00
if any(o and ("{python}" in o or "{pip}" in o) for o in option_values):
# Reminder: in an f-string, double braces means literal single brace
msg = (
f"{option_name}: '{{python}}' and '{{pip}}' are no longer needed, "
"and will be removed in cibuildwheel 3. Simply use 'python' or 'pip' instead."
2021-10-12 02:05:47 +01:00
)
warnings.append(msg)
return warnings
2021-05-03 11:45:43 -04:00
if __name__ == "__main__":
2017-03-19 20:57:51 +00:00
main()