Files
cibuildwheel/cibuildwheel/__main__.py
T

400 lines
13 KiB
Python
Raw Normal View History

from __future__ import annotations
2019-11-12 23:51:27 +00:00
import argparse
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
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
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
2021-02-04 14:27:54 -05:00
import cibuildwheel.util
2019-11-12 23:51:27 +00:00
import cibuildwheel.windows
2021-01-22 09:33:22 -05:00
from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.logger import log
2021-10-12 02:05:47 +01:00
from cibuildwheel.options import CommandLineArguments, Options, compute_options
from cibuildwheel.typing import (
PLATFORMS,
GenericPythonConfiguration,
PlatformName,
assert_never,
)
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,
)
def main() -> None:
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",
choices=["auto", "linux", "macos", "windows"],
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 or to run cibuildwheel on your development
machine. 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.",
)
2022-04-28 09:19:27 -04:00
args = CommandLineArguments(**vars(parser.parse_args()))
2017-03-19 20:57:51 +00:00
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(temp_dir):
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"
print(
f"Invalid --only='{only}', must be a build selector with a known platform",
file=sys.stderr,
)
sys.exit(2)
2023-04-07 13:56:49 -07:00
def _compute_platform_ci() -> PlatformName:
if detect_ci_provider() is None:
print(
textwrap.dedent(
"""
cibuildwheel: Unable to detect platform. cibuildwheel should run on your CI server;
Travis CI, AppVeyor, Azure Pipelines, GitHub Actions, CircleCI, Gitlab, and Cirrus CI
are supported. You can run on your development machine or other CI providers
using the --platform argument. Check --help output for more information.
"""
),
file=sys.stderr,
)
sys.exit(2)
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:
print(
'cibuildwheel: Unable to detect platform from "sys.platform" in a CI environment. You can run '
"cibuildwheel using the --platform argument. Check --help output for more information.",
file=sys.stderr,
)
sys.exit(2)
def _compute_platform(args: CommandLineArguments) -> PlatformName:
2022-09-09 08:34:47 -04:00
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto")
if args.only and args.platform is not None:
print(
"--platform cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
if args.only and args.archs is not None:
print(
"--arch cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
if platform_option_value not in PLATFORMS | {"auto"}:
print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
sys.exit(2)
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
2023-04-07 13:56:49 -07:00
return _compute_platform_ci()
class PlatformModule(typing.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:
...
2023-04-18 06:06:18 -07:00
# pylint: disable-next=inconsistent-return-statements
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
2023-04-10 22:24:20 +01:00
assert_never(platform) # noqa: RET503
def build_in_directory(args: CommandLineArguments) -> None:
platform: PlatformName = _compute_platform(args)
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"cibuildwheel: Could not find any of {{{names}}} at root of package"
print(msg, file=sys.stderr)
sys.exit(2)
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
# 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) # type: ignore[assignment]
# 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:
print("cibuildwheel:", *err.args, file=sys.stderr)
sys.exit(4)
2021-01-22 08:54:19 -05:00
if not identifiers:
print(
2021-10-12 02:05:47 +01:00
f"cibuildwheel: No build identifiers selected: {options.globals.build_selector}",
file=sys.stderr,
)
2021-01-22 08:54:19 -05:00
if not args.allow_empty:
sys.exit(3)
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}")
warnings = detect_warnings(options=options, identifiers=identifiers)
if warnings:
2021-05-03 11:45:43 -04:00
print("\nWarnings:")
for warning in warnings:
2021-05-03 11:45:43 -04:00
print(" " + warning)
2021-05-03 11:45:43 -04:00
print("\nHere 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 = []
# 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 a future release. Simply use 'python' or 'pip' instead."
)
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()