Files
cibuildwheel/cibuildwheel/__main__.py
T

487 lines
16 KiB
Python
Raw Normal View History

from __future__ import annotations
2019-11-12 23:51:27 +00:00
import argparse
import contextlib
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
2025-01-27 20:35:56 +01:00
import time
import traceback
2022-09-09 08:34:47 -04:00
import typing
2025-01-27 20:35:56 +01:00
from collections.abc import Generator, Iterable, Sequence, Set
from pathlib import Path
from tempfile import mkdtemp
2025-01-27 20:35:56 +01:00
from typing import Any, Protocol, TextIO, assert_never
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
2023-04-18 22:02:47 -04:00
from cibuildwheel.architecture import Architecture, allowed_architectures_check
2025-01-27 20:35:56 +01:00
from cibuildwheel.ci import CIProvider, detect_ci_provider, fix_ansi_codes_for_github_actions
2023-04-18 22:02:47 -04:00
from cibuildwheel.logger import log
from cibuildwheel.options import CommandLineArguments, Options, compute_options
2025-01-27 20:35:56 +01:00
from cibuildwheel.selector import BuildSelector, EnableGroup
2023-04-18 23:05:34 -04:00
from cibuildwheel.typing import PLATFORMS, GenericPythonConfiguration, PlatformName
2025-01-27 20:35:56 +01:00
from cibuildwheel.util.file import CIBW_CACHE_PATH
from cibuildwheel.util.helpers import strtobool
@dataclasses.dataclass
class GlobalOptions:
print_traceback_on_error: bool = True # decides what happens when errors are hit.
2025-01-27 20:35:56 +01:00
@dataclasses.dataclass(frozen=True)
class FileReport:
name: str
size: str
# Taken from https://stackoverflow.com/a/107717
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
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
enable_groups_str = ", ".join(g.value for g in EnableGroup)
parser.add_argument(
"--enable",
action="append",
default=[],
metavar="GROUP",
help=f"""
Enable an additional category of builds. Use multiple times to select multiple groups. Choices: {enable_groups_str}.
""",
)
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(
"--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 contextlib.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)
2025-01-27 20:35:56 +01:00
@contextlib.contextmanager
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
"""
Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds,
and/or {m} for total number of minutes. Does not print anything if this
exits via exception.
"""
start_time = time.time()
existing_contents = set(output_dir.iterdir())
yield
final_contents = set(output_dir.iterdir())
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
]
if not new_contents:
return
max_name_len = max(len(f.name) for f in new_contents)
max_size_len = max(len(f.size) for f in new_contents)
n = len(new_contents)
s = time.time() - start_time
m = s / 60
print(
msg.format(n=n, s=s, m=m),
*sorted(
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
),
sep="\n",
)
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
2025-02-26 10:56:35 -05:00
output_dir.mkdir(parents=True, exist_ok=True)
2017-07-02 18:02:27 -05:00
tmp_path = Path(mkdtemp(prefix="cibw-run-")).resolve(strict=True)
try:
2025-01-27 20:35:56 +01:00
with 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 supported "
"and have been removed in cibuildwheel 3. Simply use 'python' or 'pip' instead."
2021-10-12 02:05:47 +01:00
)
raise errors.ConfigurationError(msg)
return warnings
2021-05-03 11:45:43 -04:00
if __name__ == "__main__":
2017-03-19 20:57:51 +00:00
main()