From e3ff027131daedebf01cd8227c5872d1d83f7c0f Mon Sep 17 00:00:00 2001 From: mayeut Date: Mon, 18 Apr 2022 14:24:53 +0200 Subject: [PATCH 01/45] feature: add support for ABI3 wheels --- cibuildwheel/linux.py | 142 +++++++++++++++++++---------------- cibuildwheel/macos.py | 155 +++++++++++++++++++++------------------ cibuildwheel/util.py | 41 ++++++++++- cibuildwheel/windows.py | 147 ++++++++++++++++++++----------------- setup.cfg | 2 +- test/test_limited_api.py | 50 +++++++++++++ test/test_projects/c.py | 3 + 7 files changed, 339 insertions(+), 201 deletions(-) create mode 100644 test/test_limited_api.py diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 8478bf09..7be028a1 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -12,6 +12,7 @@ from .typing import OrderedDict, PathOrStr, assert_never from .util import ( BuildSelector, NonPlatformWheelError, + find_compatible_abi3_wheel, get_build_verbosity_extra_flags, prepare_command, read_python_configs, @@ -132,6 +133,8 @@ def build_on_docker( ) docker.call(["sh", "-c", before_all_prepared], env=env) + built_wheels: List[PurePath] = [] + for config in platform_configs: log.build_start(config.identifier) build_options = options.build_options(config.identifier) @@ -174,74 +177,83 @@ def build_on_docker( ) sys.exit(1) - if build_options.before_build: - log.step("Running before_build...") - before_build_prepared = prepare_command( - build_options.before_build, - project=container_project_path, - package=container_package_dir, - ) - docker.call(["sh", "-c", before_build_prepared], env=env) - - log.step("Building wheel...") - - temp_dir = PurePath("/tmp/cibuildwheel") - built_wheel_dir = temp_dir / "built_wheel" - docker.call(["rm", "-rf", built_wheel_dir]) - docker.call(["mkdir", "-p", built_wheel_dir]) - - verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) - - if build_options.build_frontend == "pip": - docker.call( - [ - "python", - "-m", - "pip", - "wheel", - container_package_dir, - f"--wheel-dir={built_wheel_dir}", - "--no-deps", - *verbosity_flags, - ], - env=env, - ) - elif build_options.build_frontend == "build": - config_setting = " ".join(verbosity_flags) - docker.call( - [ - "python", - "-m", - "build", - container_package_dir, - "--wheel", - f"--outdir={built_wheel_dir}", - f"--config-setting={config_setting}", - ], - env=env, + abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier) + if abi3_wheel: + log.step_end() + print( + f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) + repaired_wheels = [abi3_wheel] else: - assert_never(build_options.build_frontend) - built_wheel = docker.glob(built_wheel_dir, "*.whl")[0] + if build_options.before_build: + log.step("Running before_build...") + before_build_prepared = prepare_command( + build_options.before_build, + project=container_project_path, + package=container_package_dir, + ) + docker.call(["sh", "-c", before_build_prepared], env=env) - repaired_wheel_dir = temp_dir / "repaired_wheel" - docker.call(["rm", "-rf", repaired_wheel_dir]) - docker.call(["mkdir", "-p", repaired_wheel_dir]) + log.step("Building wheel...") - if built_wheel.name.endswith("none-any.whl"): - raise NonPlatformWheelError() + temp_dir = PurePath("/tmp/cibuildwheel") + built_wheel_dir = temp_dir / "built_wheel" + docker.call(["rm", "-rf", built_wheel_dir]) + docker.call(["mkdir", "-p", built_wheel_dir]) - if build_options.repair_command: - log.step("Repairing wheel...") - repair_command_prepared = prepare_command( - build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir - ) - docker.call(["sh", "-c", repair_command_prepared], env=env) - else: - docker.call(["mv", built_wheel, repaired_wheel_dir]) + verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) - repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl") + if build_options.build_frontend == "pip": + docker.call( + [ + "python", + "-m", + "pip", + "wheel", + container_package_dir, + f"--wheel-dir={built_wheel_dir}", + "--no-deps", + *verbosity_flags, + ], + env=env, + ) + elif build_options.build_frontend == "build": + config_setting = " ".join(verbosity_flags) + docker.call( + [ + "python", + "-m", + "build", + container_package_dir, + "--wheel", + f"--outdir={built_wheel_dir}", + f"--config-setting={config_setting}", + ], + env=env, + ) + else: + assert_never(build_options.build_frontend) + + built_wheel = docker.glob(built_wheel_dir, "*.whl")[0] + + repaired_wheel_dir = temp_dir / "repaired_wheel" + docker.call(["rm", "-rf", repaired_wheel_dir]) + docker.call(["mkdir", "-p", repaired_wheel_dir]) + + if built_wheel.name.endswith("none-any.whl"): + raise NonPlatformWheelError() + + if build_options.repair_command: + log.step("Repairing wheel...") + repair_command_prepared = prepare_command( + build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir + ) + docker.call(["sh", "-c", repair_command_prepared], env=env) + else: + docker.call(["mv", built_wheel, repaired_wheel_dir]) + + repaired_wheels = docker.glob(repaired_wheel_dir, "*.whl") if build_options.test_command and build_options.test_selector(config.identifier): log.step("Testing wheel...") @@ -292,8 +304,12 @@ def build_on_docker( docker.call(["rm", "-rf", venv_dir]) # move repaired wheels to output - docker.call(["mkdir", "-p", container_output_dir]) - docker.call(["mv", *repaired_wheels, container_output_dir]) + if abi3_wheel is None: + docker.call(["mkdir", "-p", container_output_dir]) + docker.call(["mv", *repaired_wheels, container_output_dir]) + built_wheels.extend( + container_output_dir / repaired_wheel.name for repaired_wheel in repaired_wheels + ) log.build_end() diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index d2770315..aed483ad 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -23,6 +23,7 @@ from .util import ( call, detect_ci_provider, download, + find_compatible_abi3_wheel, get_build_verbosity_extra_flags, get_pip_version, install_certifi_script, @@ -291,6 +292,8 @@ def build(options: Options, tmp_path: Path) -> None: ) shell(before_all_prepared, env=env) + built_wheels: List[Path] = [] + for config in python_configurations: build_options = options.build_options(config.identifier) log.build_start(config.identifier) @@ -318,84 +321,94 @@ def build(options: Options, tmp_path: Path) -> None: build_options.build_frontend, ) - if build_options.before_build: - log.step("Running before_build...") - before_build_prepared = prepare_command( - build_options.before_build, project=".", package=build_options.package_dir + abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier) + if abi3_wheel: + log.step_end() + print( + f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) - shell(before_build_prepared, env=env) - - log.step("Building wheel...") - built_wheel_dir.mkdir() - - verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) - - if build_options.build_frontend == "pip": - # 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", - build_options.package_dir.resolve(), - f"--wheel-dir={built_wheel_dir}", - "--no-deps", - *verbosity_flags, - env=env, - ) - elif build_options.build_frontend == "build": - config_setting = " ".join(verbosity_flags) - build_env = env.copy() - if build_options.dependency_constraints: - constraint_path = build_options.dependency_constraints.get_for_python_version( - config.version + repaired_wheel = abi3_wheel + else: + if build_options.before_build: + log.step("Running before_build...") + before_build_prepared = prepare_command( + build_options.before_build, project=".", package=build_options.package_dir ) - build_env["PIP_CONSTRAINT"] = constraint_path.as_uri() - 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, - ) - else: - assert_never(build_options.build_frontend) + shell(before_build_prepared, env=env) - built_wheel = next(built_wheel_dir.glob("*.whl")) + log.step("Building wheel...") + built_wheel_dir.mkdir() - repaired_wheel_dir.mkdir() + verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) - if built_wheel.name.endswith("none-any.whl"): - raise NonPlatformWheelError() - - if build_options.repair_command: - log.step("Repairing wheel...") - - if config_is_universal2: - delocate_archs = "x86_64,arm64" - elif config_is_arm64: - delocate_archs = "arm64" + if build_options.build_frontend == "pip": + # 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", + build_options.package_dir.resolve(), + f"--wheel-dir={built_wheel_dir}", + "--no-deps", + *verbosity_flags, + env=env, + ) + elif build_options.build_frontend == "build": + config_setting = " ".join(verbosity_flags) + build_env = env.copy() + if build_options.dependency_constraints: + constraint_path = ( + build_options.dependency_constraints.get_for_python_version( + config.version + ) + ) + build_env["PIP_CONSTRAINT"] = constraint_path.as_uri() + 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, + ) else: - delocate_archs = "x86_64" + assert_never(build_options.build_frontend) - repair_command_prepared = prepare_command( - build_options.repair_command, - wheel=built_wheel, - dest_dir=repaired_wheel_dir, - delocate_archs=delocate_archs, - ) - shell(repair_command_prepared, env=env) - else: - shutil.move(str(built_wheel), repaired_wheel_dir) + built_wheel = next(built_wheel_dir.glob("*.whl")) - repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) + repaired_wheel_dir.mkdir() - log.step_end() + if built_wheel.name.endswith("none-any.whl"): + raise NonPlatformWheelError() + + if build_options.repair_command: + log.step("Repairing wheel...") + + if config_is_universal2: + delocate_archs = "x86_64,arm64" + elif config_is_arm64: + delocate_archs = "arm64" + else: + delocate_archs = "x86_64" + + repair_command_prepared = prepare_command( + build_options.repair_command, + wheel=built_wheel, + dest_dir=repaired_wheel_dir, + delocate_archs=delocate_archs, + ) + shell(repair_command_prepared, env=env) + else: + shutil.move(str(built_wheel), repaired_wheel_dir) + + repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) + + log.step_end() if build_options.test_command and build_options.test_selector(config.identifier): machine_arch = platform.machine() @@ -521,7 +534,9 @@ def build(options: Options, tmp_path: Path) -> None: ) # we're all done here; move it to output (overwrite existing) - shutil.move(str(repaired_wheel), build_options.output_dir) + if abi3_wheel is None: + shutil.move(str(repaired_wheel), build_options.output_dir) + built_wheels.append(build_options.output_dir / repaired_wheel.name) # clean up shutil.rmtree(identifier_tmp_dir) diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 5015d348..1ebffee3 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -13,7 +13,7 @@ import time import urllib.request from enum import Enum from functools import lru_cache -from pathlib import Path +from pathlib import Path, PurePath from time import sleep from typing import ( Any, @@ -26,6 +26,7 @@ from typing import ( Optional, Sequence, TextIO, + TypeVar, cast, overload, ) @@ -41,6 +42,7 @@ else: from filelock import FileLock from packaging.requirements import InvalidRequirement, Requirement from packaging.specifiers import SpecifierSet +from packaging.utils import parse_wheel_filename from packaging.version import Version from platformdirs import user_cache_path @@ -51,6 +53,7 @@ __all__ = [ "MANYLINUX_ARCHS", "call", "shell", + "find_compatible_abi3_wheel", "format_safe", "prepare_command", "get_build_verbosity_extra_flags", @@ -566,6 +569,42 @@ def virtualenv( return env +T = TypeVar("T", bound=PurePath) + + +def find_compatible_abi3_wheel(wheels: Sequence[T], identifier: str) -> Optional[T]: + """ + Finds an ABI3 wheel in `wheels` compatible with the Python interpreter + specified by `identifier`. + """ + + interpreter, platform = identifier.split("-") + if not interpreter.startswith("cp3"): + return None + for wheel in wheels: + _, _, _, tags = parse_wheel_filename(wheel.name) + for tag in tags: + if tag.abi != "abi3": + continue + if not tag.interpreter.startswith("cp3"): + continue + if int(tag.interpreter[3:]) > int(interpreter[3:]): + continue + if platform.startswith(("manylinux", "musllinux", "macosx")): + # Linux, macOS + os_, arch = platform.split("_", 1) + if not tag.platform.startswith(os_): + continue + if not tag.platform.endswith("_" + arch): + continue + else: + # Windows + if not tag.platform == platform: + continue + return wheel + return None + + if sys.version_info >= (3, 8): from functools import cached_property else: diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 13b5c0c3..85f76a04 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -22,6 +22,7 @@ from .util import ( NonPlatformWheelError, call, download, + find_compatible_abi3_wheel, get_build_verbosity_extra_flags, get_pip_version, prepare_command, @@ -249,6 +250,8 @@ def build(options: Options, tmp_path: Path) -> None: ) shell(before_all_prepared, env=env) + built_wheels: List[Path] = [] + for config in python_configurations: build_options = options.build_options(config.identifier) log.build_start(config.identifier) @@ -274,83 +277,93 @@ def build(options: Options, tmp_path: Path) -> None: build_options.build_frontend, ) - # run the before_build command - if build_options.before_build: - log.step("Running before_build...") - before_build_prepared = prepare_command( - build_options.before_build, project=".", package=options.globals.package_dir + abi3_wheel = find_compatible_abi3_wheel(built_wheels, config.identifier) + if abi3_wheel: + log.step_end() + print( + f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) - shell(before_build_prepared, env=env) - - log.step("Building wheel...") - built_wheel_dir.mkdir() - - verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) - - if build_options.build_frontend == "pip": - # 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), - env=env, - ) - elif build_options.build_frontend == "build": - config_setting = " ".join(verbosity_flags) - build_env = env.copy() - if build_options.dependency_constraints: - constraints_path = build_options.dependency_constraints.get_for_python_version( - config.version + repaired_wheel = abi3_wheel + else: + # run the before_build command + if build_options.before_build: + log.step("Running before_build...") + before_build_prepared = prepare_command( + build_options.before_build, project=".", package=options.globals.package_dir ) - # 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 + shell(before_build_prepared, env=env) - build_env["PIP_CONSTRAINT"] = str(constraints_path) - build_env["VIRTUALENV_PIP"] = get_pip_version(env) + log.step("Building wheel...") + built_wheel_dir.mkdir() + + verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity) + + if build_options.build_frontend == "pip": + # 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", - "build", - build_options.package_dir, - "--wheel", - f"--outdir={built_wheel_dir}", - f"--config-setting={config_setting}", - env=build_env, + "pip", + "wheel", + options.globals.package_dir.resolve(), + f"--wheel-dir={built_wheel_dir}", + "--no-deps", + *get_build_verbosity_extra_flags(build_options.build_verbosity), + env=env, ) - else: - assert_never(build_options.build_frontend) + elif build_options.build_frontend == "build": + config_setting = " ".join(verbosity_flags) + build_env = env.copy() + 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 - built_wheel = next(built_wheel_dir.glob("*.whl")) + 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, + ) + else: + assert_never(build_options.build_frontend) - # repair the wheel - repaired_wheel_dir.mkdir() + built_wheel = next(built_wheel_dir.glob("*.whl")) - if built_wheel.name.endswith("none-any.whl"): - raise NonPlatformWheelError() + # repair the wheel + repaired_wheel_dir.mkdir() - if build_options.repair_command: - log.step("Repairing wheel...") - repair_command_prepared = prepare_command( - build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir - ) - shell(repair_command_prepared, env=env) - else: - shutil.move(str(built_wheel), repaired_wheel_dir) + if built_wheel.name.endswith("none-any.whl"): + raise NonPlatformWheelError() - repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) + if build_options.repair_command: + log.step("Repairing wheel...") + repair_command_prepared = prepare_command( + build_options.repair_command, wheel=built_wheel, dest_dir=repaired_wheel_dir + ) + shell(repair_command_prepared, env=env) + else: + shutil.move(str(built_wheel), repaired_wheel_dir) + + repaired_wheel = next(repaired_wheel_dir.glob("*.whl")) if build_options.test_command and options.globals.test_selector(config.identifier): log.step("Testing wheel...") @@ -405,7 +418,9 @@ def build(options: Options, tmp_path: Path) -> None: shell(test_command_prepared, cwd="c:\\", env=virtualenv_env) # we're all done here; move it to output (remove if already exists) - shutil.move(str(repaired_wheel), build_options.output_dir) + if abi3_wheel is None: + shutil.move(str(repaired_wheel), build_options.output_dir) + built_wheels.append(build_options.output_dir / repaired_wheel.name) # clean up # (we ignore errors because occasionally Windows fails to unlink a file and we diff --git a/setup.cfg b/setup.cfg index a6773844..1fa219fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = bracex certifi filelock - packaging + packaging>=20.9 platformdirs dataclasses;python_version < '3.7' tomli;python_version < '3.11' diff --git a/test/test_limited_api.py b/test/test_limited_api.py new file mode 100644 index 00000000..34978c92 --- /dev/null +++ b/test/test_limited_api.py @@ -0,0 +1,50 @@ +import textwrap + +from . import test_projects, utils + +limited_api_project = test_projects.new_c_project( + setup_py_add=textwrap.dedent( + r""" + cmdclass = {} + extension_kwargs = {} + if sys.version_info[:2] >= (3, 8): + from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + + class bdist_wheel_abi3(_bdist_wheel): + def finalize_options(self): + _bdist_wheel.finalize_options(self) + self.root_is_pure = False + + def get_tag(self): + python, abi, plat = _bdist_wheel.get_tag(self) + return python, "abi3", plat + + cmdclass["bdist_wheel"] = bdist_wheel_abi3 + extension_kwargs["define_macros"] = [("Py_LIMITED_API", "0x03080000")] + extension_kwargs["py_limited_api"] = True + """ + ), + setup_py_extension_args_add="**extension_kwargs", + setup_py_setup_args_add="cmdclass=cmdclass", +) + + +def test(tmp_path): + project_dir = tmp_path / "project" + limited_api_project.generate(project_dir) + + # build the wheels + actual_wheels = utils.cibuildwheel_run( + project_dir, + add_env={ + "CIBW_SKIP": "pp* ", # PyPy does not have a Py_LIMITED_API equivalent + }, + ) + + # check that the expected wheels are produced + expected_wheels = [ + w.replace("cp38-cp38", "cp38-abi3") + for w in utils.expected_wheels("spam", "0.1.0") + if "-pp" not in w and "-cp39" not in w and "-cp310" not in w + ] + assert set(actual_wheels) == set(expected_wheels) diff --git a/test/test_projects/c.py b/test/test_projects/c.py index 41b72958..d4c37c69 100644 --- a/test/test_projects/c.py +++ b/test/test_projects/c.py @@ -54,6 +54,7 @@ setup( 'spam', sources=['spam.c'], libraries=libraries, + {{ setup_py_extension_args_add | indent(8) }} )], {{ setup_py_setup_args_add | indent(4) }} ) @@ -73,6 +74,7 @@ def new_c_project( spam_c_top_level_add="", spam_c_function_add="", setup_py_add="", + setup_py_extension_args_add="", setup_py_setup_args_add="", setup_cfg_add="", ): @@ -91,6 +93,7 @@ def new_c_project( "spam_c_top_level_add": spam_c_top_level_add, "spam_c_function_add": spam_c_function_add, "setup_py_add": setup_py_add, + "setup_py_extension_args_add": setup_py_extension_args_add, "setup_py_setup_args_add": setup_py_setup_args_add, "setup_cfg_add": setup_cfg_add, } From 763cd15ae22a32c7babbaa706bfbdc6a62890e1e Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 30 Mar 2022 09:20:26 +0100 Subject: [PATCH 02/45] Add from_sdist script --- cibuildwheel/from_sdist.py | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 cibuildwheel/from_sdist.py diff --git a/cibuildwheel/from_sdist.py b/cibuildwheel/from_sdist.py new file mode 100644 index 00000000..d0084b58 --- /dev/null +++ b/cibuildwheel/from_sdist.py @@ -0,0 +1,102 @@ +import argparse +import subprocess +import sys +import tarfile +import tempfile +import textwrap +from pathlib import Path + +from cibuildwheel.util import format_safe + + +def main() -> None: + parser = argparse.ArgumentParser( + description=textwrap.dedent( + """ + Build wheels from an sdist archive. + + Extracts the sdist to a temp dir and calls cibuildwheel on the + resulting package directory. Note that cibuildwheel will be + invoked with its working directory as the package directory, so + options aside from --output-dir and --config-file are relative to + the package directory. + """, + ), + epilog="""Any further arguments will be passed on to cibuildwheel.""", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--output-dir", + default="wheelhouse", + help=""" + Destination folder for the wheels. Default: wheelhouse. + """, + ) + + parser.add_argument( + "--config-file", + default="", + help=""" + TOML config file. To refer to a file inside the sdist, use the + `{project}` or `{package}` placeholder. e.g. `--config-file + {project}/config/cibuildwheel.toml` Default: "", meaning the + pyproject.toml inside the sdist, if it exists. + """, + ) + + parser.add_argument( + "package", + help=""" + Path to the sdist archive that you want wheels for. Must be a + tar.gz archive file. + """, + ) + + args, passthrough_args = parser.parse_known_args() + + output_dir = Path(args.output_dir).resolve() + + with tempfile.TemporaryDirectory(prefix="cibw-sdist-") as temp_dir_str: + temp_dir = Path(temp_dir_str) + + with tarfile.open(args.package) as tar: + tar.extractall(path=temp_dir) + + temp_dir_contents = list(temp_dir.iterdir()) + + if len(temp_dir_contents) != 1 or not temp_dir_contents[0].is_dir: + exit("invalid sdist: didn't contain a single dir") + + project_dir = temp_dir_contents[0] + + if args.config_file: + # expand the placeholders if they're used + config_file_path = format_safe( + args.config_file, + project=project_dir, + package=project_dir, + ) + config_file = Path(config_file_path).resolve() + else: + config_file = None + + exit( + subprocess.call( + [ + sys.executable, + "-m", + "cibuildwheel", + *(["--config-file", str(config_file)] if config_file else []), + "--output-dir", + output_dir, + *passthrough_args, + ".", + ], + cwd=project_dir, + ) + ) + + +if __name__ == "__main__": + main() From e887d4bbf8a86bea2da39648c594650bfbea7d12 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 30 Mar 2022 09:29:43 +0100 Subject: [PATCH 03/45] Add entrypoint --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index a6773844..9642cf9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ include = [options.entry_points] console_scripts = cibuildwheel = cibuildwheel.__main__:main + cibuildwheel-from-sdist = cibuildwheel.from_sdist:main [options.package_data] cibuildwheel = resources/* From c6c5e6e59b214128b97661a96868d07468827f24 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 1 Apr 2022 16:50:01 +0100 Subject: [PATCH 04/45] Add test for cibuildwheel.from-sdist --- setup.py | 1 + test/test_from_sdist.py | 177 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 test/test_from_sdist.py diff --git a/setup.py b/setup.py index 96b4d10c..7a9eee68 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ extras = { "pytest>=6", "pytest-timeout", "pytest-xdist", + "build", ], "bin": [ "click", diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py new file mode 100644 index 00000000..c3eeda49 --- /dev/null +++ b/test/test_from_sdist.py @@ -0,0 +1,177 @@ +import os +import subprocess +import sys +import textwrap +from pathlib import Path +from tempfile import TemporaryDirectory +from test.test_projects.base import TestProject + +from . import test_projects, utils + +basic_project = test_projects.new_c_project() + + +# utilities + + +def make_sdist(project: TestProject, working_dir: Path) -> Path: + project_dir = working_dir / "project" + project_dir.mkdir(parents=True, exist_ok=True) + project.generate(project_dir) + + sdist_dir = working_dir / "sdist" + subprocess.run( + [sys.executable, "-m", "build", "--sdist", "--outdir", sdist_dir, project_dir], check=True + ) + + return next(sdist_dir.glob("*.tar.gz")) + + +def cibuildwheel_from_sdist_run(sdist_path, add_env=None, config_file=None): + env = os.environ.copy() + + if add_env: + env.update(add_env) + + with TemporaryDirectory() as tmp_output_dir: + subprocess.run( + [ + sys.executable, + "-m", + "cibuildwheel.from_sdist", + *(["--config-file", config_file] if config_file else []), + "--output-dir", + tmp_output_dir, + sdist_path, + ], + env=env, + check=True, + ) + return os.listdir(tmp_output_dir) + + +# tests + + +def test_simple(tmp_path): + # make an sdist of the project + sdist_dir = tmp_path / "sdist" + sdist_dir.mkdir() + sdist_path = make_sdist(basic_project, sdist_dir) + + # build the wheels from sdist + actual_wheels = cibuildwheel_from_sdist_run( + sdist_path, + add_env={ + "CIBW_BUILD": "cp39-*", + }, + ) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w] + assert set(actual_wheels) == set(expected_wheels) + + +def test_external_config_file_argument(tmp_path, capfd): + # make an sdist of the project + sdist_dir = tmp_path / "sdist" + sdist_dir.mkdir() + sdist_path = make_sdist(basic_project, sdist_dir) + + # add a config file + config_file = tmp_path / "config.toml" + config_file.write_text( + textwrap.dedent( + """ + [tool.cibuildwheel] + before-all = 'echo "test log statement from before-all"' + """ + ) + ) + + # build the wheels from sdist + actual_wheels = cibuildwheel_from_sdist_run( + sdist_path, + add_env={ + "CIBW_BUILD": "cp39-*", + }, + config_file=config_file, + ) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w] + assert set(actual_wheels) == set(expected_wheels) + + # check that before-all was run + captured = capfd.readouterr() + assert "test log statement from before-all" in captured.out + + +def test_config_in_pyproject_toml(tmp_path, capfd): + # make a project with a pyproject.toml + project = test_projects.new_c_project() + project.files["pyproject.toml"] = textwrap.dedent( + """ + [tool.cibuildwheel] + before-build = 'echo "test log statement from before-build 8419"' + """ + ) + + # make an sdist of the project + sdist_dir = tmp_path / "sdist" + sdist_dir.mkdir() + sdist_path = make_sdist(project, sdist_dir) + + # build the wheels from sdist + actual_wheels = cibuildwheel_from_sdist_run( + sdist_path, + add_env={"CIBW_BUILD": "cp39-*"}, + ) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w] + assert set(actual_wheels) == set(expected_wheels) + + # check that before-build was run + captured = capfd.readouterr() + assert "test log statement from before-build 8419" in captured.out + + +def test_internal_config_file_argument(tmp_path, capfd): + # make a project with a config file inside + project = test_projects.new_c_project( + setup_cfg_add="include_package_data = True", + ) + project.files["wheel_build_config.toml"] = textwrap.dedent( + """ + [tool.cibuildwheel] + before-all = 'echo "test log statement from before-all 1829"' + """ + ) + project.files["MANIFEST.in"] = textwrap.dedent( + """ + include wheel_build_config.toml + """ + ) + + # make an sdist of the project + sdist_dir = tmp_path / "sdist" + sdist_dir.mkdir() + sdist_path = make_sdist(project, sdist_dir) + + # build the wheels from sdist + actual_wheels = cibuildwheel_from_sdist_run( + sdist_path, + add_env={ + "CIBW_BUILD": "cp39-*", + }, + config_file="{project}/wheel_build_config.toml", + ) + + # check that the expected wheels are produced + expected_wheels = [w for w in utils.expected_wheels("spam", "0.1.0") if "cp39" in w] + assert set(actual_wheels) == set(expected_wheels) + + # check that before-all was run + captured = capfd.readouterr() + assert "test log statement from before-all 1829" in captured.out From 898dd620223d2726fe299b2a6bf7957b7c9ae513 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 3 Apr 2022 10:46:10 +0100 Subject: [PATCH 05/45] Add test for argument passthrough --- test/test_from_sdist.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py index c3eeda49..fe312fd9 100644 --- a/test/test_from_sdist.py +++ b/test/test_from_sdist.py @@ -175,3 +175,44 @@ def test_internal_config_file_argument(tmp_path, capfd): # check that before-all was run captured = capfd.readouterr() assert "test log statement from before-all 1829" in captured.out + + +def test_argument_passthrough(tmp_path, capfd): + basic_project = test_projects.new_c_project() + + # make an sdist of a project + sdist_dir = tmp_path / "sdist" + sdist_dir.mkdir() + sdist_path = make_sdist(basic_project, sdist_dir) + + # make a call that should pass some args through to cibuildwheel + # this asks cibuildwheel to print the ppc64le build identifiers + process = subprocess.run( + [ + sys.executable, + "-m", + "cibuildwheel.from_sdist", + sdist_path, + "--platform", + "linux", + "--archs", + "ppc64le", + "--print-build-identifiers", + ], + env={ + **os.environ, + "CIBW_BUILD": "cp38-*", + }, + check=True, + stdout=subprocess.PIPE, + universal_newlines=True, + ) + + # fmt: off + assert process.stdout == textwrap.dedent( + """ + cp38-manylinux_ppc64le + cp38-musllinux_ppc64le + """ + ).lstrip() + # fmt: on From 8c77d5ed410ef9ac8c5aeab28cf121b41bff5456 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 3 Apr 2022 10:51:59 +0100 Subject: [PATCH 06/45] Tidy ups, bug fixes --- cibuildwheel/from_sdist.py | 4 ++-- test/test_from_sdist.py | 21 ++++++++------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/cibuildwheel/from_sdist.py b/cibuildwheel/from_sdist.py index d0084b58..8bff69a0 100644 --- a/cibuildwheel/from_sdist.py +++ b/cibuildwheel/from_sdist.py @@ -22,7 +22,7 @@ def main() -> None: the package directory. """, ), - epilog="""Any further arguments will be passed on to cibuildwheel.""", + epilog="Any further arguments will be passed on to cibuildwheel.", formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -65,7 +65,7 @@ def main() -> None: temp_dir_contents = list(temp_dir.iterdir()) - if len(temp_dir_contents) != 1 or not temp_dir_contents[0].is_dir: + if len(temp_dir_contents) != 1 or not temp_dir_contents[0].is_dir(): exit("invalid sdist: didn't contain a single dir") project_dir = temp_dir_contents[0] diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py index fe312fd9..73cc1c04 100644 --- a/test/test_from_sdist.py +++ b/test/test_from_sdist.py @@ -8,9 +8,6 @@ from test.test_projects.base import TestProject from . import test_projects, utils -basic_project = test_projects.new_c_project() - - # utilities @@ -54,6 +51,8 @@ def cibuildwheel_from_sdist_run(sdist_path, add_env=None, config_file=None): def test_simple(tmp_path): + basic_project = test_projects.new_c_project() + # make an sdist of the project sdist_dir = tmp_path / "sdist" sdist_dir.mkdir() @@ -62,9 +61,7 @@ def test_simple(tmp_path): # build the wheels from sdist actual_wheels = cibuildwheel_from_sdist_run( sdist_path, - add_env={ - "CIBW_BUILD": "cp39-*", - }, + add_env={"CIBW_BUILD": "cp39-*"}, ) # check that the expected wheels are produced @@ -73,6 +70,8 @@ def test_simple(tmp_path): def test_external_config_file_argument(tmp_path, capfd): + basic_project = test_projects.new_c_project() + # make an sdist of the project sdist_dir = tmp_path / "sdist" sdist_dir.mkdir() @@ -92,9 +91,7 @@ def test_external_config_file_argument(tmp_path, capfd): # build the wheels from sdist actual_wheels = cibuildwheel_from_sdist_run( sdist_path, - add_env={ - "CIBW_BUILD": "cp39-*", - }, + add_env={"CIBW_BUILD": "cp39-*"}, config_file=config_file, ) @@ -159,12 +156,10 @@ def test_internal_config_file_argument(tmp_path, capfd): sdist_dir.mkdir() sdist_path = make_sdist(project, sdist_dir) - # build the wheels from sdist + # build the wheels from sdist, referencing the config file inside actual_wheels = cibuildwheel_from_sdist_run( sdist_path, - add_env={ - "CIBW_BUILD": "cp39-*", - }, + add_env={"CIBW_BUILD": "cp39-*"}, config_file="{project}/wheel_build_config.toml", ) From 91fae8268eb35c67d63af3d4d46a51e507928e4c Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 3 Apr 2022 11:01:01 +0100 Subject: [PATCH 07/45] Add prog arguments to ArgumentParser to prevent the wrong inferred name when calling like 'python -m cibuildwheel', we get errors like usage: __main__.py [-h] [--platform {auto,linux,macos,windows}] [--archs ARCHS] [--output-dir OUTPUT_DIR] [--config-file CONFIG_FILE] [--print-build-identifiers] [--allow-empty] [--prerelease-pythons] [package_dir] __main__.py: error: unrecognized arguments: --sad With this change, we get error outputs like: usage: cibuildwheel [-h] [--platform {auto,linux,macos,windows}] [--archs ARCHS] [--output-dir OUTPUT_DIR] [--config-file CONFIG_FILE] [--print-build-identifiers] [--allow-empty] [--prerelease-pythons] [package_dir] cibuildwheel: error: unrecognized arguments: --asda --- cibuildwheel/__main__.py | 1 + cibuildwheel/from_sdist.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 306e73ab..5ddd2b63 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -28,6 +28,7 @@ def main() -> None: platform: PlatformName parser = argparse.ArgumentParser( + prog="cibuildwheel", description="Build wheels for all the platforms.", epilog=""" Most options are supplied via environment variables or in diff --git a/cibuildwheel/from_sdist.py b/cibuildwheel/from_sdist.py index 8bff69a0..f24834f5 100644 --- a/cibuildwheel/from_sdist.py +++ b/cibuildwheel/from_sdist.py @@ -11,6 +11,7 @@ from cibuildwheel.util import format_safe def main() -> None: parser = argparse.ArgumentParser( + prog="cibuildwheel-from-sdist", description=textwrap.dedent( """ Build wheels from an sdist archive. From 958a7c32c1da6cc8adf50613e56eab75b1cb2e88 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 26 Apr 2022 22:21:27 -0400 Subject: [PATCH 08/45] refactor: use single entry for SDist builds Signed-off-by: Henry Schreiner --- cibuildwheel/__main__.py | 69 ++++++++++++-- cibuildwheel/from_sdist.py | 103 --------------------- cibuildwheel/options.py | 13 +-- cibuildwheel/util.py | 19 +++- setup.cfg | 1 - test/test_from_sdist.py | 15 +-- unit_test/conftest.py | 2 +- unit_test/main_tests/main_options_test.py | 6 +- unit_test/main_tests/main_platform_test.py | 4 +- unit_test/options_test.py | 6 +- unit_test/utils.py | 6 +- 11 files changed, 103 insertions(+), 141 deletions(-) delete mode 100644 cibuildwheel/from_sdist.py diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 5ddd2b63..0e407456 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -2,6 +2,8 @@ import argparse import os import shutil import sys +import tarfile +import tempfile import textwrap from pathlib import Path from tempfile import mkdtemp @@ -20,15 +22,14 @@ from cibuildwheel.util import ( CIBW_CACHE_PATH, BuildSelector, Unbuffered, + chdir, detect_ci_provider, + format_safe, ) def main() -> None: - platform: PlatformName - parser = argparse.ArgumentParser( - prog="cibuildwheel", description="Build wheels for all the platforms.", epilog=""" Most options are supplied via environment variables or in @@ -66,6 +67,7 @@ def main() -> None: parser.add_argument( "--output-dir", + type=Path, help="Destination folder for the wheels. Default: wheelhouse.", ) @@ -74,19 +76,24 @@ def main() -> None: default="", help=""" TOML config file. Default: "", meaning {package}/pyproject.toml, - if it exists. + if it exists. To refer to a project inside your project, use {package} + or {project}. """, ) parser.add_argument( "package_dir", - default=".", + default=Path("."), + type=Path, nargs="?", help=""" - Path to the package that you want wheels for. Must be a subdirectory of - the working directory. When set, the working directory is still - considered the 'project' and is copied into the Docker container on - Linux. Default: the working directory. + Path to the package that you want wheels for. Must be a + subdirectory of the working directory. When set, the working + directory is still considered the 'project' and is copied into the + Docker container on Linux. Default: the working directory. This can + also be a tar.gz file - if it is, then --config-file and + --output-dir are relative to the current directory, and other paths + are relative to the expanded SDist directory. """, ) @@ -110,6 +117,50 @@ def main() -> None: args = parser.parse_args(namespace=CommandLineArguments()) + # These are always relative to the base directory, even in SDist builds + args.package_dir = args.package_dir.resolve() + args.output_dir = Path( + args.output_dir + if args.output_dir is not None + else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse") + ).resolve() + + # 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 + + if not args.package_dir.name.endswith("tar.gz"): + raise SystemExit("Must be a tar.gz file if a file is given.") + + # Tarfile builds require extraction and changing the directory + with tempfile.TemporaryDirectory(prefix="cibw-sdist-") as temp_dir_str: + temp_dir = Path(temp_dir_str) + 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: + raise SystemExit("invalid sdist: didn't contain a single dir") from None + + args.package_dir = project_dir.resolve() + + if args.config_file: + # expand the placeholders if they're used + config_file_path = format_safe( + args.config_file, + project=project_dir, + package=project_dir, + ) + args.config_file = str(Path(config_file_path).resolve()) + + with chdir(temp_dir): + build_in_directory(args) + + +def build_in_directory(args: CommandLineArguments) -> None: if args.platform != "auto": platform = args.platform else: diff --git a/cibuildwheel/from_sdist.py b/cibuildwheel/from_sdist.py deleted file mode 100644 index f24834f5..00000000 --- a/cibuildwheel/from_sdist.py +++ /dev/null @@ -1,103 +0,0 @@ -import argparse -import subprocess -import sys -import tarfile -import tempfile -import textwrap -from pathlib import Path - -from cibuildwheel.util import format_safe - - -def main() -> None: - parser = argparse.ArgumentParser( - prog="cibuildwheel-from-sdist", - description=textwrap.dedent( - """ - Build wheels from an sdist archive. - - Extracts the sdist to a temp dir and calls cibuildwheel on the - resulting package directory. Note that cibuildwheel will be - invoked with its working directory as the package directory, so - options aside from --output-dir and --config-file are relative to - the package directory. - """, - ), - epilog="Any further arguments will be passed on to cibuildwheel.", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - parser.add_argument( - "--output-dir", - default="wheelhouse", - help=""" - Destination folder for the wheels. Default: wheelhouse. - """, - ) - - parser.add_argument( - "--config-file", - default="", - help=""" - TOML config file. To refer to a file inside the sdist, use the - `{project}` or `{package}` placeholder. e.g. `--config-file - {project}/config/cibuildwheel.toml` Default: "", meaning the - pyproject.toml inside the sdist, if it exists. - """, - ) - - parser.add_argument( - "package", - help=""" - Path to the sdist archive that you want wheels for. Must be a - tar.gz archive file. - """, - ) - - args, passthrough_args = parser.parse_known_args() - - output_dir = Path(args.output_dir).resolve() - - with tempfile.TemporaryDirectory(prefix="cibw-sdist-") as temp_dir_str: - temp_dir = Path(temp_dir_str) - - with tarfile.open(args.package) as tar: - tar.extractall(path=temp_dir) - - temp_dir_contents = list(temp_dir.iterdir()) - - if len(temp_dir_contents) != 1 or not temp_dir_contents[0].is_dir(): - exit("invalid sdist: didn't contain a single dir") - - project_dir = temp_dir_contents[0] - - if args.config_file: - # expand the placeholders if they're used - config_file_path = format_safe( - args.config_file, - project=project_dir, - package=project_dir, - ) - config_file = Path(config_file_path).resolve() - else: - config_file = None - - exit( - subprocess.call( - [ - sys.executable, - "-m", - "cibuildwheel", - *(["--config-file", str(config_file)] if config_file else []), - "--output-dir", - output_dir, - *passthrough_args, - ".", - ], - cwd=project_dir, - ) - ) - - -if __name__ == "__main__": - main() diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index ce4f5a77..6f249d7e 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -46,9 +46,9 @@ from .util import ( class CommandLineArguments: platform: Literal["auto", "linux", "macos", "windows"] archs: Optional[str] - output_dir: Optional[str] + output_dir: Optional[Path] config_file: str - package_dir: str + package_dir: Path print_build_identifiers: bool allow_empty: bool prerelease_pythons: bool @@ -361,12 +361,9 @@ class Options: @property def globals(self) -> GlobalOptions: args = self.command_line_arguments - package_dir = Path(args.package_dir) - output_dir = Path( - args.output_dir - if args.output_dir is not None - else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse") - ) + assert args.output_dir is not None, "Must be resolved" + package_dir = args.package_dir + output_dir = args.output_dir build_config = self.reader.get("build", env_plat=False, sep=" ") or "*" skip_config = self.reader.get("skip", env_plat=False, sep=" ") diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 5015d348..d15df3f2 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -19,13 +19,14 @@ from typing import ( Any, ClassVar, Dict, + Generator, Iterable, - Iterator, List, NamedTuple, Optional, Sequence, TextIO, + Union, cast, overload, ) @@ -58,6 +59,7 @@ __all__ = [ "selector_matches", "strtobool", "cached_property", + "chdir", ] resources_dir: Final = Path(__file__).parent / "resources" @@ -414,7 +416,7 @@ def unwrap(text: str) -> str: @contextlib.contextmanager -def print_new_wheels(msg: str, output_dir: Path) -> Iterator[None]: +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, @@ -570,3 +572,16 @@ if sys.version_info >= (3, 8): from functools import cached_property else: from .functools_cached_property_38 import cached_property + + +# Can be replaced by contextlib.chdir in Python 3.11 +@contextlib.contextmanager +def chdir(new_path: Union[Path, str]) -> Generator[None, None, None]: + """Non thread-safe context manager to change the current working directory.""" + + cwd = os.getcwd() + try: + os.chdir(new_path) + yield + finally: + os.chdir(cwd) diff --git a/setup.cfg b/setup.cfg index 9642cf9f..a6773844 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,6 @@ include = [options.entry_points] console_scripts = cibuildwheel = cibuildwheel.__main__:main - cibuildwheel-from-sdist = cibuildwheel.from_sdist:main [options.package_data] cibuildwheel = resources/* diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py index 73cc1c04..31eb718c 100644 --- a/test/test_from_sdist.py +++ b/test/test_from_sdist.py @@ -18,7 +18,8 @@ def make_sdist(project: TestProject, working_dir: Path) -> Path: sdist_dir = working_dir / "sdist" subprocess.run( - [sys.executable, "-m", "build", "--sdist", "--outdir", sdist_dir, project_dir], check=True + [sys.executable, "-m", "build", "--sdist", "--outdir", str(sdist_dir), str(project_dir)], + check=True, ) return next(sdist_dir.glob("*.tar.gz")) @@ -35,11 +36,11 @@ def cibuildwheel_from_sdist_run(sdist_path, add_env=None, config_file=None): [ sys.executable, "-m", - "cibuildwheel.from_sdist", + "cibuildwheel", *(["--config-file", config_file] if config_file else []), "--output-dir", - tmp_output_dir, - sdist_path, + str(tmp_output_dir), + str(sdist_path), ], env=env, check=True, @@ -92,7 +93,7 @@ def test_external_config_file_argument(tmp_path, capfd): actual_wheels = cibuildwheel_from_sdist_run( sdist_path, add_env={"CIBW_BUILD": "cp39-*"}, - config_file=config_file, + config_file=str(config_file), ) # check that the expected wheels are produced @@ -186,8 +187,8 @@ def test_argument_passthrough(tmp_path, capfd): [ sys.executable, "-m", - "cibuildwheel.from_sdist", - sdist_path, + "cibuildwheel", + str(sdist_path), "--platform", "linux", "--archs", diff --git a/unit_test/conftest.py b/unit_test/conftest.py index 26a28c47..2f794a21 100644 --- a/unit_test/conftest.py +++ b/unit_test/conftest.py @@ -32,7 +32,7 @@ def fake_package_dir(monkeypatch): real_path_exists = Path.exists def mock_path_exists(path): - if path == MOCK_PACKAGE_DIR / "setup.py": + if str(path).endswith(str(MOCK_PACKAGE_DIR / "setup.py")): return True else: return real_path_exists(path) diff --git a/unit_test/main_tests/main_options_test.py b/unit_test/main_tests/main_options_test.py index 2152ff3d..977fc378 100644 --- a/unit_test/main_tests/main_options_test.py +++ b/unit_test/main_tests/main_options_test.py @@ -24,13 +24,13 @@ def test_output_dir(platform, intercepted_build_args, monkeypatch): main() - assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR + assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve() def test_output_dir_default(platform, intercepted_build_args, monkeypatch): main() - assert intercepted_build_args.args[0].globals.output_dir == Path("wheelhouse") + assert intercepted_build_args.args[0].globals.output_dir == Path("wheelhouse").resolve() @pytest.mark.parametrize("also_set_environment", [False, True]) @@ -43,7 +43,7 @@ def test_output_dir_argument(also_set_environment, platform, intercepted_build_a main() - assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR + assert intercepted_build_args.args[0].globals.output_dir == OUTPUT_DIR.resolve() def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_empty): diff --git a/unit_test/main_tests/main_platform_test.py b/unit_test/main_tests/main_platform_test.py index 09f5b9db..8974a27e 100644 --- a/unit_test/main_tests/main_platform_test.py +++ b/unit_test/main_tests/main_platform_test.py @@ -60,14 +60,14 @@ def test_platform_argument(platform, intercepted_build_args, monkeypatch): options = intercepted_build_args.args[0] - assert options.globals.package_dir == MOCK_PACKAGE_DIR + assert options.globals.package_dir == MOCK_PACKAGE_DIR.resolve() def test_platform_environment(platform, intercepted_build_args, monkeypatch): main() options = intercepted_build_args.args[0] - assert options.globals.package_dir == MOCK_PACKAGE_DIR + assert options.globals.package_dir == MOCK_PACKAGE_DIR.resolve() def test_archs_default(platform, intercepted_build_args, monkeypatch): diff --git a/unit_test/options_test.py b/unit_test/options_test.py index 01a98f3b..fd8a102f 100644 --- a/unit_test/options_test.py +++ b/unit_test/options_test.py @@ -34,7 +34,7 @@ def test_options_1(tmp_path, monkeypatch): f.write(PYPROJECT_1) args = get_default_command_line_arguments() - args.package_dir = str(tmp_path) + args.package_dir = tmp_path monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") @@ -77,7 +77,7 @@ def test_passthrough(tmp_path, monkeypatch): f.write(PYPROJECT_1) args = get_default_command_line_arguments() - args.package_dir = str(tmp_path) + args.package_dir = tmp_path monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") monkeypatch.setenv("EXAMPLE_ENV", "ONE") @@ -105,7 +105,7 @@ def test_passthrough(tmp_path, monkeypatch): ) def test_passthrough_evil(tmp_path, monkeypatch, env_var_value): args = get_default_command_line_arguments() - args.package_dir = str(tmp_path) + args.package_dir = tmp_path monkeypatch.setattr(platform_module, "machine", lambda: "x86_64") monkeypatch.setenv("CIBW_ENVIRONMENT_PASS_LINUX", "ENV_VAR") diff --git a/unit_test/utils.py b/unit_test/utils.py index 61833fa2..c2d94ed0 100644 --- a/unit_test/utils.py +++ b/unit_test/utils.py @@ -1,3 +1,5 @@ +from pathlib import Path + from cibuildwheel.options import CommandLineArguments @@ -8,8 +10,8 @@ def get_default_command_line_arguments() -> CommandLineArguments: defaults.allow_empty = False defaults.archs = None defaults.config_file = "" - defaults.output_dir = None - defaults.package_dir = "." + defaults.output_dir = Path("wheelhouse") # This must be resolved from "None" before passing + defaults.package_dir = Path(".") defaults.prerelease_pythons = False defaults.print_build_identifiers = False From b1e8549bb1a7d32a21f98c2ae9167a5d4b72ee83 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 27 Apr 2022 12:31:42 -0400 Subject: [PATCH 09/45] fix: minor cleanup --- cibuildwheel/__main__.py | 29 ++++++++++------------------- cibuildwheel/options.py | 4 +++- docs/cpp_standards.md | 2 +- test/test_from_sdist.py | 2 +- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 0e407456..af10eafe 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -24,7 +24,6 @@ from cibuildwheel.util import ( Unbuffered, chdir, detect_ci_provider, - format_safe, ) @@ -75,9 +74,9 @@ def main() -> None: "--config-file", default="", help=""" - TOML config file. Default: "", meaning {package}/pyproject.toml, - if it exists. To refer to a project inside your project, use {package} - or {project}. + 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. """, ) @@ -87,8 +86,8 @@ def main() -> None: type=Path, nargs="?", help=""" - Path to the package that you want wheels for. Must be a - subdirectory of the working directory. When set, the working + Path to the package that you want wheels for. Must be a subdirectory + of the working directory. When set to a directory, the working directory is still considered the 'project' and is copied into the Docker container on Linux. Default: the working directory. This can also be a tar.gz file - if it is, then --config-file and @@ -117,8 +116,9 @@ def main() -> None: args = parser.parse_args(namespace=CommandLineArguments()) - # These are always relative to the base directory, even in SDist builds args.package_dir = args.package_dir.resolve() + + # This are always relative to the base directory, even in SDist builds args.output_dir = Path( args.output_dir if args.output_dir is not None @@ -130,9 +130,6 @@ def main() -> None: build_in_directory(args) return - if not args.package_dir.name.endswith("tar.gz"): - raise SystemExit("Must be a tar.gz file if a file is given.") - # Tarfile builds require extraction and changing the directory with tempfile.TemporaryDirectory(prefix="cibw-sdist-") as temp_dir_str: temp_dir = Path(temp_dir_str) @@ -145,22 +142,16 @@ def main() -> None: except ValueError: raise SystemExit("invalid sdist: didn't contain a single dir") from None + # This is now the new package dir args.package_dir = project_dir.resolve() - if args.config_file: - # expand the placeholders if they're used - config_file_path = format_safe( - args.config_file, - project=project_dir, - package=project_dir, - ) - args.config_file = str(Path(config_file_path).resolve()) - with chdir(temp_dir): build_in_directory(args) def build_in_directory(args: CommandLineArguments) -> None: + platform: PlatformName + if args.platform != "auto": platform = args.platform else: diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 6f249d7e..bf707b23 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -22,6 +22,7 @@ if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib + from packaging.specifiers import SpecifierSet from .architecture import Architecture @@ -36,6 +37,7 @@ from .util import ( DependencyConstraints, TestSelector, cached_property, + format_safe, resources_dir, selector_matches, strtobool, @@ -344,7 +346,7 @@ class Options: args = self.command_line_arguments if args.config_file: - return Path(args.config_file.format(package=args.package_dir)) + return Path(format_safe(args.config_file, package=args.package_dir)) # return pyproject.toml, if it's available pyproject_toml_path = Path(args.package_dir) / "pyproject.toml" diff --git a/docs/cpp_standards.md b/docs/cpp_standards.md index 1f140618..468e4eb1 100644 --- a/docs/cpp_standards.md +++ b/docs/cpp_standards.md @@ -14,7 +14,7 @@ The old `manylinux1` image (based on CentOS 5) contains a version of GCC and lib OS X/macOS allows you to specify a so-called "deployment target" version that will ensure backwards compatibility with older versions of macOS. One way to do this is by setting the `MACOSX_DEPLOYMENT_TARGET` environment variable. -However, to enable modern C++ standards, the deploment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library). +However, to enable modern C++ standards, the deployment target needs to be set high enough (since older OS X/macOS versions did not have the necessary modern C++ standard library). To get C++11 and C++14 support, `MACOSX_DEPLOYMENT_TARGET` needs to be set to (at least) `"10.9"`. By default, `cibuildwheel` already does this, building 64-bit-only wheels for macOS 10.9 and later. diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py index 31eb718c..4af67015 100644 --- a/test/test_from_sdist.py +++ b/test/test_from_sdist.py @@ -161,7 +161,7 @@ def test_internal_config_file_argument(tmp_path, capfd): actual_wheels = cibuildwheel_from_sdist_run( sdist_path, add_env={"CIBW_BUILD": "cp39-*"}, - config_file="{project}/wheel_build_config.toml", + config_file="{package}/wheel_build_config.toml", ) # check that the expected wheels are produced From 35054666bb542eae0525c59941562ab785989cfc Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 27 Apr 2022 17:08:46 -0400 Subject: [PATCH 10/45] refactor: review comments --- cibuildwheel/__main__.py | 22 ++++++++++----------- cibuildwheel/options.py | 7 +++---- test/test_from_sdist.py | 41 ---------------------------------------- unit_test/utils.py | 2 +- 4 files changed, 14 insertions(+), 58 deletions(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index af10eafe..f8158b31 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -67,6 +67,7 @@ def main() -> None: parser.add_argument( "--output-dir", type=Path, + default=Path(os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse")), help="Destination folder for the wheels. Default: wheelhouse.", ) @@ -82,17 +83,18 @@ def main() -> None: parser.add_argument( "package_dir", + metavar="PACKAGE", default=Path("."), type=Path, nargs="?", help=""" - Path to the package that you want wheels for. Must be a subdirectory - of the working directory. When set to a directory, the working - directory is still considered the 'project' and is copied into the - Docker container on Linux. Default: the working directory. This can - also be a tar.gz file - if it is, then --config-file and - --output-dir are relative to the current directory, and other paths - are relative to the expanded SDist directory. + 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 Docker container + 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. """, ) @@ -119,11 +121,7 @@ def main() -> None: args.package_dir = args.package_dir.resolve() # This are always relative to the base directory, even in SDist builds - args.output_dir = Path( - args.output_dir - if args.output_dir is not None - else os.environ.get("CIBW_OUTPUT_DIR", "wheelhouse") - ).resolve() + args.output_dir = args.output_dir.resolve() # 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"): diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index bf707b23..5d9744af 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import ( Any, Dict, - Iterator, + Generator, List, Mapping, NamedTuple, @@ -48,7 +48,7 @@ from .util import ( class CommandLineArguments: platform: Literal["auto", "linux", "macos", "windows"] archs: Optional[str] - output_dir: Optional[Path] + output_dir: Path config_file: str package_dir: Path print_build_identifiers: bool @@ -265,7 +265,7 @@ class OptionsReader: ] @contextmanager - def identifier(self, identifier: Optional[str]) -> Iterator[None]: + def identifier(self, identifier: Optional[str]) -> Generator[None, None, None]: self.current_identifier = identifier try: yield @@ -363,7 +363,6 @@ class Options: @property def globals(self) -> GlobalOptions: args = self.command_line_arguments - assert args.output_dir is not None, "Must be resolved" package_dir = args.package_dir output_dir = args.output_dir diff --git a/test/test_from_sdist.py b/test/test_from_sdist.py index 4af67015..d1d4ff11 100644 --- a/test/test_from_sdist.py +++ b/test/test_from_sdist.py @@ -171,44 +171,3 @@ def test_internal_config_file_argument(tmp_path, capfd): # check that before-all was run captured = capfd.readouterr() assert "test log statement from before-all 1829" in captured.out - - -def test_argument_passthrough(tmp_path, capfd): - basic_project = test_projects.new_c_project() - - # make an sdist of a project - sdist_dir = tmp_path / "sdist" - sdist_dir.mkdir() - sdist_path = make_sdist(basic_project, sdist_dir) - - # make a call that should pass some args through to cibuildwheel - # this asks cibuildwheel to print the ppc64le build identifiers - process = subprocess.run( - [ - sys.executable, - "-m", - "cibuildwheel", - str(sdist_path), - "--platform", - "linux", - "--archs", - "ppc64le", - "--print-build-identifiers", - ], - env={ - **os.environ, - "CIBW_BUILD": "cp38-*", - }, - check=True, - stdout=subprocess.PIPE, - universal_newlines=True, - ) - - # fmt: off - assert process.stdout == textwrap.dedent( - """ - cp38-manylinux_ppc64le - cp38-musllinux_ppc64le - """ - ).lstrip() - # fmt: on diff --git a/unit_test/utils.py b/unit_test/utils.py index c2d94ed0..ef158d55 100644 --- a/unit_test/utils.py +++ b/unit_test/utils.py @@ -10,7 +10,7 @@ def get_default_command_line_arguments() -> CommandLineArguments: defaults.allow_empty = False defaults.archs = None defaults.config_file = "" - defaults.output_dir = Path("wheelhouse") # This must be resolved from "None" before passing + defaults.output_dir = Path("wheelhouse") defaults.package_dir = Path(".") defaults.prerelease_pythons = False defaults.print_build_identifiers = False From de44c88c753da5f13a7806c0812834b20d7e6ce2 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 28 Apr 2022 19:18:49 -0400 Subject: [PATCH 11/45] docs: add some projects (#1097) * docs: add some projects * chore: update docs --- README.md | 18 +-- docs/data/projects.yml | 65 ++++++++++- docs/working-examples.md | 242 ++++++++++++++++++++++----------------- 3 files changed, 211 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index a621e4f5..56da09c4 100644 --- a/README.md +++ b/README.md @@ -142,25 +142,25 @@ Here are some repos that use cibuildwheel. |-----------------------------------|----|----|:------| | [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. | | [Tornado][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. | +| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. | | [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. | | [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions | -| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | MyPyC, the compiled component of MyPy. | +| [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS | +| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform | +| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. | +| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. | | [pydantic][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Data parsing and validation using Python type hints | -| [uvloop][] | ![github icon][] | ![apple icon][] ![linux icon][] | Ultra fast asyncio event loop. | -| [psutil][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Cross-platform lib for process and system monitoring in Python | -| [vaex][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Out-of-Core hybrid Apache Arrow/NumPy DataFrame for Python, ML, visualization and exploration of big tabular data at a billion rows per second 🚀 | -| [Google Benchmark][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A microbenchmark support library | [scikit-learn]: https://github.com/scikit-learn/scikit-learn [Tornado]: https://github.com/tornadoweb/tornado +[NumPy]: https://github.com/numpy/numpy [pytorch-fairseq]: https://github.com/pytorch/fairseq [Matplotlib]: https://github.com/matplotlib/matplotlib +[Kivy]: https://github.com/kivy/kivy +[NCNN]: https://github.com/Tencent/ncnn +[Prophet]: https://github.com/facebook/prophet [MyPy]: https://github.com/mypyc/mypy_mypyc-wheels [pydantic]: https://github.com/samuelcolvin/pydantic -[uvloop]: https://github.com/MagicStack/uvloop -[psutil]: https://github.com/giampaolo/psutil -[vaex]: https://github.com/vaexio/vaex -[Google Benchmark]: https://github.com/google/benchmark [appveyor icon]: docs/data/readme_icons/appveyor.svg [github icon]: docs/data/readme_icons/github.svg diff --git a/docs/data/projects.yml b/docs/data/projects.yml index e12bd99b..7e8c544a 100644 --- a/docs/data/projects.yml +++ b/docs/data/projects.yml @@ -287,7 +287,7 @@ pypi: mypy ci: [github] os: [apple, linux, windows] - notes: MyPyC, the compiled component of MyPy. + notes: The compiled version of MyPy using MyPyC. - name: Imagecodecs (fork) gh: czaki/imagecodecs_build @@ -521,10 +521,71 @@ gh: arbor-sim/arbor ci: [github] os: [apple, linux] - pypi: arbor notes: > Arbor is a multi-compartment neuron simulation library; compatible with next-generation accelerators; best-practices applied to research software; focused on community-driven development. Includes a [small script](https://github.com/arbor-sim/arbor/blob/master/scripts/patchwheel.py) patching `rpath` in bundled libraries. + +- name: Kivy + gh: kivy/kivy + ci: [github] + os: [windows, apple, linux] + +- name: NCNN + gh: Tencent/ncnn + ci: [github] + os: [windows, apple, linux] + +- name: Prophet + gh: facebook/prophet + ci: [github] + os: [windows, apple, linux] + +- name: MemRay + gh: bloomberg/memray + ci: [github] + os: [linux] + +- name: PyGame + gh: pygame/pygame + ci: [github] + os: [apple, linux] + +- name: UltraJSON + gh: ultrajson/ultrajson + ci: [github] + os: [windows, apple, linux] + +- name: NumPy + gh: numpy/numpy + ci: [github, travisci] + os: [windows, apple, linux] + +- name: Wrapt + gh: GrahamDumpleton/wrapt + ci: [github] + os: [windows, apple, linux] + +- name: SimpleJSON + gh: simplejson/simplejson + ci: [github] + os: [windows, apple, linux] + +- name: Implicit + gh: benfred/implicit + ci: [github] + os: [windows, apple, linux] + notes: Includes GPU support for linux wheels + +- name: power-grid-model + gh: alliander-opensource/power-grid-model + ci: [github] + os: [windows, apple, linux] + notes: Python/C++ library for distribution power system analysis + +- name: Python-WebRTC + gh: MarshalX/python-webrtc + ci: [github] + os: [windows, apple, linux] diff --git a/docs/working-examples.md b/docs/working-examples.md index ae5f5702..0f4017f9 100644 --- a/docs/working-examples.md +++ b/docs/working-examples.md @@ -10,22 +10,29 @@ title: Working examples |-----------------------------------|----|----|:------| | [scikit-learn][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The machine learning library. A complex but clean config using many of cibuildwheel's features to build a large project with Cython and C++ extensions. | | [Tornado][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. | +| [NumPy][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The fundamental package for scientific computing with Python. | | [pytorch-fairseq][] | ![github icon][] | ![apple icon][] ![linux icon][] | Facebook AI Research Sequence-to-Sequence Toolkit written in Python. | | [Matplotlib][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The venerable Matplotlib, a Python library with C++ portions | -| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | MyPyC, the compiled component of MyPy. | +| [Kivy][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Open source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS | +| [NCNN][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | ncnn is a high-performance neural network inference framework optimized for the mobile platform | +| [Prophet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Tool for producing high quality forecasts for time series data that has multiple seasonality with linear or non-linear growth. | +| [MyPy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | The compiled version of MyPy using MyPyC. | | [pydantic][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Data parsing and validation using Python type hints | | [uvloop][] | ![github icon][] | ![apple icon][] ![linux icon][] | Ultra fast asyncio event loop. | | [psutil][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Cross-platform lib for process and system monitoring in Python | +| [MemRay][] | ![github icon][] | ![linux icon][] | Memray is a memory profiler for Python | | [vaex][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Out-of-Core hybrid Apache Arrow/NumPy DataFrame for Python, ML, visualization and exploration of big tabular data at a billion rows per second 🚀 | | [Google Benchmark][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A microbenchmark support library | -| [Apache Beam][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Apache Beam is a unified programming model for Batch and Streaming | +| [Apache Beam][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Apache Beam is a unified programming model for Batch and Streaming data processing. | | [asyncpg][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A fast PostgreSQL Database Client Library for Python/asyncio. | | [scikit-image][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Image processing library. Uses cibuildwheel to build and test a project that uses Cython with platform-native code. | -| [cmake][] | ![github icon][] ![travisci icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Multitagged binary builds for all supported platforms, using cibw 2 config configuration. | | [duckdb][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | DuckDB is an in-process SQL OLAP Database Management System | +| [PyGame][] | ![github icon][] | ![apple icon][] ![linux icon][] | pygame (the library) is a Free and Open Source python programming language library for making multimedia applications like games built on top of the excellent SDL library. C, Python, Native, OpenGL. | +| [cmake][] | ![github icon][] ![travisci icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Multitagged binary builds for all supported platforms, using cibw 2 config configuration. | | [twisted-iocpsupport][] | ![github icon][] | ![windows icon][] | A submodule of Twisted that hooks into native C APIs using Cython. | | [websockets][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Library for building WebSocket servers and clients. Mostly written in Python, with a small C 'speedups' extension module. | | [cvxpy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A Python-embedded modeling language for convex optimization problems. | +| [UltraJSON][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Ultra fast JSON decoder and encoder written in C with Python bindings | | [PyOxidizer][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A modern Python application packaging and distribution tool | | [Triton][] | ![github icon][] | ![linux icon][] | Self hosted runners | | [River][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | 🌊 Online machine learning in Python | @@ -33,6 +40,7 @@ title: Working examples | [pyzmq][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Python bindings for zeromq, the networking library. Uses Cython and CFFI. | | [aiortc][] | ![github icon][] | ![apple icon][] ![linux icon][] | WebRTC and ORTC implementation for Python using asyncio. | | [vispy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Main repository for Vispy | +| [Implicit][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Includes GPU support for linux wheels | | [Confluent client for Kafka][] | ![travisci icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | setup in `tools/wheels/build-wheels.bat` | | [tinyobjloader][] | ![azurepipelines icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Tiny but powerful single file wavefront obj loader | | [Dependency Injector][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Dependency injection framework for Python, uses Windows TravisCI | @@ -41,12 +49,14 @@ title: Working examples | [PyYAML][] | ![github icon][] | ![apple icon][] | Canonical source repository for PyYAML | | [numexpr][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Fast numerical array expression evaluator for Python, NumPy, PyTables, pandas, bcolz and more | | [h5py][] | ![azurepipelines icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | HDF5 for Python -- The h5py package is a Pythonic interface to the HDF5 binary data format. | +| [Wrapt][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python module for decorators, wrappers and monkey patching. | | [PyAV][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Pythonic bindings for FFmpeg's libraries. | +| [SimpleJSON][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | simplejson is a simple, fast, extensible JSON encoder/decoder for Python | | [OpenColorIO][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | A color management framework for visual effects and animation. | | [Line Profiler][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Line-by-line profiling for Python | | [PyTables][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python package to manage extremely large amounts of data | -| [OpenTimelineIO][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Open Source API and interchange format for editorial timeline information. | | [pikepdf][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python library for reading and writing PDF, powered by qpdf | +| [OpenTimelineIO][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Open Source API and interchange format for editorial timeline information. | | [ruptures][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Extensive Cython + NumPy [pyproject.toml](https://github.com/deepcharles/ruptures/blob/master/pyproject.toml) example. | | [aioquic][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | QUIC and HTTP/3 implementation in Python | | [DeepForest][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | An Efficient, Scalable and Optimized Python Framework for Deep Forest (2021.2.1) | @@ -55,8 +65,8 @@ title: Working examples | [Parselmouth][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python interface to the Praat software package, using pybind11, C++17 and CMake, with the core Praat static library built only once and shared between wheels. | | [AutoPy][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Includes a Windows Travis build. | | [H3-py][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Python bindings for H3, a hierarchical hexagonal geospatial indexing system | -| [markupsafe][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Safely add untrusted strings to HTML/XML markup. | | [Rtree][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Rtree: spatial index for Python GIS ¶ | +| [markupsafe][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Safely add untrusted strings to HTML/XML markup. | | [python-rapidjson][] | ![travisci icon][] ![gitlab icon][] ![appveyor icon][] | ![windows icon][] ![linux icon][] | Python wrapper around rapidjson | | [python-snappy][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Python bindings for the snappy google library | | [pybind11 cmake_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Example pybind11 module built with a CMake-based build system | @@ -65,10 +75,10 @@ title: Working examples | [pybind11 python_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Example pybind11 module built with a Python-based build system | | [dd-trace-py][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Uses custom alternate arch emulation on GitHub | | [cyvcf2][] | ![github icon][] | ![apple icon][] ![linux icon][] | cython + htslib == fast VCF and BCF processing | -| [sourmash][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Quickly search, compare, and analyze genomic and metagenomic data sets. | | [time-machine][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Time mocking library using only the CPython C API. | -| [abess][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A fast best-subset selection library. It uses cibuildwheel to build a large project with C++ extensions. | +| [sourmash][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Quickly search, compare, and analyze genomic and metagenomic data sets. | | [CTranslate2][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Includes libraries from the [Intel oneAPI toolkit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit.html) and CUDA kernels compiled for multiple GPU architectures. | +| [abess][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A fast best-subset selection library. It uses cibuildwheel to build a large project with C++ extensions. | | [matrixprofile][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A Python 3 library making time series data mining tasks, utilizing matrix profile algorithms, accessible to everyone. | | [jq.py][] | ![travisci icon][] | ![apple icon][] ![linux icon][] | Python bindings for jq | | [iminuit][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Jupyter-friendly Python interface for C++ MINUIT2 | @@ -78,6 +88,7 @@ title: Working examples | [boost-histogram][] | ![github icon][] ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Supports full range of wheels, including PyPy and alternate archs. | | [iDynTree][] | ![github icon][] | ![linux icon][] | Uses manylinux_2_24 | | [TgCrypto][] | ![travisci icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Includes a Windows Travis build. | +| [Python-WebRTC][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | a Python extension that provides bindings to WebRTC M92 | | [pybase64][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Fast Base64 encoding/decoding in Python | | [Arbor][] | ![github icon][] | ![apple icon][] ![linux icon][] | Arbor is a multi-compartment neuron simulation library; compatible with next-generation accelerators; best-practices applied to research software; focused on community-driven development. Includes a [small script](https://github.com/arbor-sim/arbor/blob/master/scripts/patchwheel.py) patching `rpath` in bundled libraries. | | [etebase-py][] | ![travisci icon][] | ![linux icon][] | Python bindings to a Rust library using `setuptools-rust`, and `sccache` for improved speed. | @@ -85,10 +96,11 @@ title: Working examples | [Imagecodecs (fork)][] | ![azurepipelines icon][] | ![apple icon][] ![linux icon][] | Over 20 external dependencies in compiled libraries, custom docker image, `libomp`, `openblas` and `install_name_tool` for macOS. | | [polaroid][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Full range of wheels for setuptools rust, with auto release and PyPI deploy. | | [numpythia][] | ![github icon][] | ![apple icon][] ![linux icon][] | The interface between PYTHIA and NumPy | -| [pyjet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The interface between FastJet and NumPy | | [clang-format][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Scikit-build wrapper around LLVM's CMake, all platforms, generic wheels. | -| [ninja][] | ![github icon][] ![travisci icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Multitagged binary builds for all supported platforms, using cibw 2 config configuration. | +| [pyjet][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | The interface between FastJet and NumPy | +| [power-grid-model][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | Python/C++ library for distribution power system analysis | | [pybind11 scikit_build_example][] | ![github icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | An example combining scikit-build and pybind11 | +| [ninja][] | ![github icon][] ![travisci icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Multitagged binary builds for all supported platforms, using cibw 2 config configuration. | | [GSD][] | ![github icon][] | ![apple icon][] ![linux icon][] ![windows icon][] | Cython and NumPy project with 64-bit wheels. | | [pillow-heif][] | ![github icon][] | ![apple icon][] ![linux icon][] | Python CFFI binding to libheif library with third party dependencies like `libde265`, `x265`, `libaom` with test & publishing on PyPi. | | [pyinstrument_cext][] | ![travisci icon][] ![appveyor icon][] | ![windows icon][] ![apple icon][] ![linux icon][] | A simple C extension, without external dependencies | @@ -98,22 +110,29 @@ title: Working examples [scikit-learn]: https://github.com/scikit-learn/scikit-learn [Tornado]: https://github.com/tornadoweb/tornado +[NumPy]: https://github.com/numpy/numpy [pytorch-fairseq]: https://github.com/pytorch/fairseq [Matplotlib]: https://github.com/matplotlib/matplotlib +[Kivy]: https://github.com/kivy/kivy +[NCNN]: https://github.com/Tencent/ncnn +[Prophet]: https://github.com/facebook/prophet [MyPy]: https://github.com/mypyc/mypy_mypyc-wheels [pydantic]: https://github.com/samuelcolvin/pydantic [uvloop]: https://github.com/MagicStack/uvloop [psutil]: https://github.com/giampaolo/psutil +[MemRay]: https://github.com/bloomberg/memray [vaex]: https://github.com/vaexio/vaex [Google Benchmark]: https://github.com/google/benchmark [Apache Beam]: https://github.com/apache/beam [asyncpg]: https://github.com/MagicStack/asyncpg [scikit-image]: https://github.com/scikit-image/scikit-image -[cmake]: https://github.com/scikit-build/cmake-python-distributions [duckdb]: https://github.com/duckdb/duckdb +[PyGame]: https://github.com/pygame/pygame +[cmake]: https://github.com/scikit-build/cmake-python-distributions [twisted-iocpsupport]: https://github.com/twisted/twisted-iocpsupport [websockets]: https://github.com/aaugustin/websockets [cvxpy]: https://github.com/cvxpy/cvxpy +[UltraJSON]: https://github.com/ultrajson/ultrajson [PyOxidizer]: https://github.com/indygreg/PyOxidizer [Triton]: https://github.com/openai/triton [River]: https://github.com/online-ml/river @@ -121,6 +140,7 @@ title: Working examples [pyzmq]: https://github.com/zeromq/pyzmq [aiortc]: https://github.com/aiortc/aiortc [vispy]: https://github.com/vispy/vispy +[Implicit]: https://github.com/benfred/implicit [Confluent client for Kafka]: https://github.com/confluentinc/confluent-kafka-python [tinyobjloader]: https://github.com/tinyobjloader/tinyobjloader [Dependency Injector]: https://github.com/ets-labs/python-dependency-injector @@ -129,12 +149,14 @@ title: Working examples [PyYAML]: https://github.com/yaml/pyyaml [numexpr]: https://github.com/pydata/numexpr [h5py]: https://github.com/h5py/h5py +[Wrapt]: https://github.com/GrahamDumpleton/wrapt [PyAV]: https://github.com/PyAV-Org/PyAV +[SimpleJSON]: https://github.com/simplejson/simplejson [OpenColorIO]: https://github.com/AcademySoftwareFoundation/OpenColorIO [Line Profiler]: https://github.com/pyutils/line_profiler [PyTables]: https://github.com/PyTables/PyTables -[OpenTimelineIO]: https://github.com/PixarAnimationStudios/OpenTimelineIO [pikepdf]: https://github.com/pikepdf/pikepdf +[OpenTimelineIO]: https://github.com/PixarAnimationStudios/OpenTimelineIO [ruptures]: https://github.com/deepcharles/ruptures [aioquic]: https://github.com/aiortc/aioquic [DeepForest]: https://github.com/LAMDA-NJU/Deep-Forest @@ -143,8 +165,8 @@ title: Working examples [Parselmouth]: https://github.com/YannickJadoul/Parselmouth [AutoPy]: https://github.com/autopilot-rs/autopy [H3-py]: https://github.com/uber/h3-py -[markupsafe]: https://github.com/pallets/markupsafe [Rtree]: https://github.com/Toblerity/rtree +[markupsafe]: https://github.com/pallets/markupsafe [python-rapidjson]: https://github.com/python-rapidjson/python-rapidjson [python-snappy]: https://github.com/andrix/python-snappy [pybind11 cmake_example]: https://github.com/pybind/cmake_example @@ -153,10 +175,10 @@ title: Working examples [pybind11 python_example]: https://github.com/pybind/python_example [dd-trace-py]: https://github.com/DataDog/dd-trace-py [cyvcf2]: https://github.com/brentp/cyvcf2 -[sourmash]: https://github.com/dib-lab/sourmash [time-machine]: https://github.com/adamchainz/time-machine -[abess]: https://github.com/abess-team/abess +[sourmash]: https://github.com/dib-lab/sourmash [CTranslate2]: https://github.com/OpenNMT/CTranslate2 +[abess]: https://github.com/abess-team/abess [matrixprofile]: https://github.com/matrix-profile-foundation/matrixprofile [jq.py]: https://github.com/mwilliamson/jq.py [iminuit]: https://github.com/scikit-hep/iminuit @@ -166,6 +188,7 @@ title: Working examples [boost-histogram]: https://github.com/scikit-hep/boost-histogram [iDynTree]: https://github.com/robotology/idyntree [TgCrypto]: https://github.com/pyrogram/tgcrypto +[Python-WebRTC]: https://github.com/MarshalX/python-webrtc [pybase64]: https://github.com/mayeut/pybase64 [Arbor]: https://github.com/arbor-sim/arbor [etebase-py]: https://github.com/etesync/etebase-py @@ -173,10 +196,11 @@ title: Working examples [Imagecodecs (fork)]: https://github.com/czaki/imagecodecs_build [polaroid]: https://github.com/daggy1234/polaroid [numpythia]: https://github.com/scikit-hep/numpythia -[pyjet]: https://github.com/scikit-hep/pyjet [clang-format]: https://github.com/ssciwr/clang-format-wheel -[ninja]: https://github.com/scikit-build/ninja-python-distributions +[pyjet]: https://github.com/scikit-hep/pyjet +[power-grid-model]: https://github.com/alliander-opensource/power-grid-model [pybind11 scikit_build_example]: https://github.com/pybind/scikit_build_example +[ninja]: https://github.com/scikit-build/ninja-python-distributions [GSD]: https://github.com/glotzerlab/gsd [pillow-heif]: https://github.com/bigcat88/pillow_heif [pyinstrument_cext]: https://github.com/joerick/pyinstrument_cext @@ -194,93 +218,105 @@ title: Working examples [apple icon]: data/readme_icons/apple.svg [linux icon]: data/readme_icons/linux.svg - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 46702f581da2dde13ea3d2a49dfa55df226db9f8 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 29 Apr 2022 13:32:03 +0100 Subject: [PATCH 12/45] Space out the log statements a little --- cibuildwheel/linux.py | 2 +- cibuildwheel/macos.py | 2 +- cibuildwheel/windows.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cibuildwheel/linux.py b/cibuildwheel/linux.py index 7be028a1..036eb36f 100644 --- a/cibuildwheel/linux.py +++ b/cibuildwheel/linux.py @@ -181,7 +181,7 @@ def build_on_docker( if abi3_wheel: log.step_end() print( - f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." + f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) repaired_wheels = [abi3_wheel] else: diff --git a/cibuildwheel/macos.py b/cibuildwheel/macos.py index aed483ad..7100cd05 100644 --- a/cibuildwheel/macos.py +++ b/cibuildwheel/macos.py @@ -325,7 +325,7 @@ def build(options: Options, tmp_path: Path) -> None: if abi3_wheel: log.step_end() print( - f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." + f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) repaired_wheel = abi3_wheel else: diff --git a/cibuildwheel/windows.py b/cibuildwheel/windows.py index 85f76a04..1e95c225 100644 --- a/cibuildwheel/windows.py +++ b/cibuildwheel/windows.py @@ -281,7 +281,7 @@ def build(options: Options, tmp_path: Path) -> None: if abi3_wheel: log.step_end() print( - f"Found previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." + f"\nFound previously built wheel {abi3_wheel.name}, that's compatible with {config.identifier}. Skipping build step..." ) repaired_wheel = abi3_wheel else: From 75998410e05a2e00ff2d842ed2590830fc600b99 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 29 Apr 2022 14:11:35 +0100 Subject: [PATCH 13/45] Add tips+tricks entry for ABI3 building --- docs/faq.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 97e65feb..2364be72 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -103,6 +103,12 @@ Linux), and the other architectures are emulated automatically. {% include "../examples/github-with-qemu.yml" %} ``` +### Building CPython ABI3 wheels (Limited API) {: #abi3} + +The CPython Limited API is a subset of the Python C Extension API that's declared to be forward-compatible, meaning you can compile wheels for one version of Python, and they'll be compatible with future versions. Wheels that use the Limited API are known as ABI3 wheels. + +To create a package that builds ABI3 wheels, you'll need to configure your build backend to compile libraries correctly create wheels with the right tags. [Check this repo](https://github.com/joerick/python-abi3-package-sample) for an example of how to do this with setuptools. + ### Building packages with optional C extensions `cibuildwheel` defines the environment variable `CIBUILDWHEEL` to the value `1` allowing projects for which the C extension is optional to make it mandatory when building wheels. From 0b6307072a15a35debbb73215c012cf29c672908 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 29 Apr 2022 14:46:08 +0100 Subject: [PATCH 14/45] Bump version: v2.5.0 --- README.md | 19 +++++++++++-------- cibuildwheel/__init__.py | 2 +- docs/changelog.md | 10 ++++++++++ docs/faq.md | 4 ++-- docs/setup.md | 4 ++-- examples/appveyor-minimal.yml | 2 +- examples/azure-pipelines-minimal.yml | 6 +++--- examples/circleci-minimal.yml | 4 ++-- examples/github-apple-silicon.yml | 2 +- examples/github-deploy.yml | 2 +- examples/github-minimal.yml | 2 +- examples/github-with-qemu.yml | 2 +- examples/gitlab-minimal.yml | 2 +- examples/travis-ci-deploy.yml | 2 +- examples/travis-ci-minimal.yml | 2 +- examples/travis-ci-test-and-deploy.yml | 4 ++-- setup.cfg | 2 +- 17 files changed, 42 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 56da09c4..0e2dbac3 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ jobs: - uses: actions/setup-python@v2 - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.4.0 + run: python -m pip install cibuildwheel==2.5.0 - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse @@ -192,6 +192,16 @@ Changelog +### v2.5.0 + +_29 April 2022_ + +- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. (#1091) +- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz` (#1096) +- 🐛 Fix a bug where cibuildwheel would crash when no builds are selected and `--allow-empty` is passed (#1086) +- 🐛 Workaround a permissions issue on Linux relating to newer versions of git and setuptools_scm (#1095) +- 📚 Minor docs improvements + ### v2.4.0 _2 April 2022_ @@ -231,13 +241,6 @@ _26 October 2021_ - 🐛 Fix bug in the GitHub Action step causing a syntax error (#895) -### v2.2.1 - -_26 October 2021_ - -- 🛠 Added a `config-file` option on the GitHub Action to specify something other than pyproject.toml in your GitHub Workflow file. (#883) -- 🐛 Fix missing resources in sdist and released wheel on PyPI. We've also made some internal changes to our release processes to make them more reliable. (#893, #894) - --- diff --git a/cibuildwheel/__init__.py b/cibuildwheel/__init__.py index 3d67cd6b..50062f87 100644 --- a/cibuildwheel/__init__.py +++ b/cibuildwheel/__init__.py @@ -1 +1 @@ -__version__ = "2.4.0" +__version__ = "2.5.0" diff --git a/docs/changelog.md b/docs/changelog.md index 79b03c76..80a3148e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,16 @@ title: Changelog --- +### v2.5.0 + +_29 April 2022_ + +- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. (#1091) +- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz` (#1096) +- 🐛 Fix a bug where cibuildwheel would crash when no builds are selected and `--allow-empty` is passed (#1086) +- 🐛 Workaround a permissions issue on Linux relating to newer versions of git and setuptools_scm (#1095) +- 📚 Minor docs improvements + ### v2.4.0 _2 April 2022_ diff --git a/docs/faq.md b/docs/faq.md index 2364be72..b40fc184 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -133,7 +133,7 @@ There are two suggested methods for keeping cibuildwheel up to date that instead If you use GitHub Actions for builds, you can use cibuildwheel as an action: ```yaml -uses: pypa/cibuildwheel@v2.4.0 +uses: pypa/cibuildwheel@v2.5.0 ``` This is a composite step that just runs cibuildwheel using pipx. You can set command-line options as `with:` parameters, and use `env:` as normal. @@ -159,7 +159,7 @@ The second option, and the only one that supports other CI systems, is using a ` ```bash # requirements-cibw.txt -cibuildwheel==2.4.0 +cibuildwheel==2.5.0 ``` Then your install step would have `python -m pip install -r requirements-cibw.txt` in it. Your `.github/dependabot.yml` file could look like this: diff --git a/docs/setup.md b/docs/setup.md index fb1165fc..fec8515e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -183,7 +183,7 @@ To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/ - uses: actions/checkout@v2 - name: Build wheels - run: pipx run cibuildwheel==2.4.0 + run: pipx run cibuildwheel==2.5.0 - uses: actions/upload-artifact@v2 with: @@ -218,7 +218,7 @@ To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/ - uses: actions/setup-python@v2 - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.4.0 + run: python -m pip install cibuildwheel==2.5.0 - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse diff --git a/examples/appveyor-minimal.yml b/examples/appveyor-minimal.yml index b6162ada..cad09a14 100644 --- a/examples/appveyor-minimal.yml +++ b/examples/appveyor-minimal.yml @@ -12,7 +12,7 @@ stack: python 3.7 init: - cmd: set PATH=C:\Python37;C:\Python37\Scripts;%PATH% -install: python -m pip install cibuildwheel==2.4.0 +install: python -m pip install cibuildwheel==2.5.0 build_script: python -m cibuildwheel --output-dir wheelhouse diff --git a/examples/azure-pipelines-minimal.yml b/examples/azure-pipelines-minimal.yml index c5c0ae8c..2c7da5c6 100644 --- a/examples/azure-pipelines-minimal.yml +++ b/examples/azure-pipelines-minimal.yml @@ -6,7 +6,7 @@ jobs: - bash: | set -o errexit python3 -m pip install --upgrade pip - pip3 install cibuildwheel==2.4.0 + pip3 install cibuildwheel==2.5.0 displayName: Install dependencies - bash: cibuildwheel --output-dir wheelhouse . displayName: Build wheels @@ -20,7 +20,7 @@ jobs: - bash: | set -o errexit python3 -m pip install --upgrade pip - python3 -m pip install cibuildwheel==2.4.0 + python3 -m pip install cibuildwheel==2.5.0 displayName: Install dependencies - bash: cibuildwheel --output-dir wheelhouse . displayName: Build wheels @@ -34,7 +34,7 @@ jobs: - bash: | set -o errexit python -m pip install --upgrade pip - pip install cibuildwheel==2.4.0 + pip install cibuildwheel==2.5.0 displayName: Install dependencies - bash: cibuildwheel --output-dir wheelhouse . displayName: Build wheels diff --git a/examples/circleci-minimal.yml b/examples/circleci-minimal.yml index 19c45ad9..98c05745 100644 --- a/examples/circleci-minimal.yml +++ b/examples/circleci-minimal.yml @@ -11,7 +11,7 @@ jobs: - run: name: Build the Linux wheels. command: | - pip3 install --user cibuildwheel==2.4.0 + pip3 install --user cibuildwheel==2.5.0 cibuildwheel --output-dir wheelhouse - store_artifacts: path: wheelhouse/ @@ -25,7 +25,7 @@ jobs: - run: name: Build the OS X wheels. command: | - pip3 install cibuildwheel==2.4.0 + pip3 install cibuildwheel==2.5.0 cibuildwheel --output-dir wheelhouse - store_artifacts: path: wheelhouse/ diff --git a/examples/github-apple-silicon.yml b/examples/github-apple-silicon.yml index 2f270556..4b70087d 100644 --- a/examples/github-apple-silicon.yml +++ b/examples/github-apple-silicon.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - name: Build wheels - uses: pypa/cibuildwheel@v2.4.0 + uses: pypa/cibuildwheel@v2.5.0 env: CIBW_ARCHS_MACOS: x86_64 universal2 diff --git a/examples/github-deploy.yml b/examples/github-deploy.yml index 4f604eda..11014602 100644 --- a/examples/github-deploy.yml +++ b/examples/github-deploy.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v2 - name: Build wheels - uses: pypa/cibuildwheel@v2.4.0 + uses: pypa/cibuildwheel@v2.5.0 - uses: actions/upload-artifact@v2 with: diff --git a/examples/github-minimal.yml b/examples/github-minimal.yml index 3b9038c1..442bcdd0 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - name: Build wheels - uses: pypa/cibuildwheel@v2.4.0 + uses: pypa/cibuildwheel@v2.5.0 # to supply options, put them in 'env', like: # env: # CIBW_SOME_OPTION: value diff --git a/examples/github-with-qemu.yml b/examples/github-with-qemu.yml index c46645ec..ca541f9b 100644 --- a/examples/github-with-qemu.yml +++ b/examples/github-with-qemu.yml @@ -25,7 +25,7 @@ jobs: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.4.0 + uses: pypa/cibuildwheel@v2.5.0 env: # configure cibuildwheel to build native archs ('auto'), and some # emulated ones diff --git a/examples/gitlab-minimal.yml b/examples/gitlab-minimal.yml index de4cdb77..53516f7b 100644 --- a/examples/gitlab-minimal.yml +++ b/examples/gitlab-minimal.yml @@ -12,7 +12,7 @@ linux: DOCKER_TLS_CERTDIR: "" script: - curl -sSL https://get.docker.com/ | sh - - python -m pip install cibuildwheel==2.4.0 + - python -m pip install cibuildwheel==2.5.0 - cibuildwheel --output-dir wheelhouse artifacts: paths: diff --git a/examples/travis-ci-deploy.yml b/examples/travis-ci-deploy.yml index 101f36df..6d6d550a 100644 --- a/examples/travis-ci-deploy.yml +++ b/examples/travis-ci-deploy.yml @@ -19,7 +19,7 @@ jobs: - ln -s /c/Python38/python.exe /c/Python38/python3.exe install: - - python3 -m pip install cibuildwheel==2.4.0 + - python3 -m pip install cibuildwheel==2.5.0 script: # build the wheels, put them into './dist' diff --git a/examples/travis-ci-minimal.yml b/examples/travis-ci-minimal.yml index c0f4ab4b..7d1766db 100644 --- a/examples/travis-ci-minimal.yml +++ b/examples/travis-ci-minimal.yml @@ -25,7 +25,7 @@ jobs: - ln -s /c/Python38/python.exe /c/Python38/python3.exe install: - - python3 -m pip install cibuildwheel==2.4.0 + - python3 -m pip install cibuildwheel==2.5.0 script: # build the wheels, put them into './wheelhouse' diff --git a/examples/travis-ci-test-and-deploy.yml b/examples/travis-ci-test-and-deploy.yml index 37a24d99..5c6efece 100644 --- a/examples/travis-ci-test-and-deploy.yml +++ b/examples/travis-ci-test-and-deploy.yml @@ -55,7 +55,7 @@ jobs: - stage: deploy name: Build and deploy Linux wheels services: docker - install: python3 -m pip install cibuildwheel==2.4.0 twine + install: python3 -m pip install cibuildwheel==2.5.0 twine script: python3 -m cibuildwheel --output-dir wheelhouse after_success: python3 -m twine upload --skip-existing wheelhouse/*.whl # Deploy on windows @@ -63,7 +63,7 @@ jobs: name: Build and deploy Windows wheels os: windows language: shell - install: python3 -m pip install cibuildwheel==2.4.0 twine + install: python3 -m pip install cibuildwheel==2.5.0 twine script: python3 -m cibuildwheel --output-dir wheelhouse after_success: python3 -m twine upload --skip-existing wheelhouse/*.whl diff --git a/setup.cfg b/setup.cfg index 1fa219fc..cc0d2fbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = cibuildwheel -version = 2.4.0 +version = 2.5.0 description = Build Python wheels on CI with minimal configuration. long_description = file: README.md long_description_content_type = text/markdown From 1f1c8008d8d99248acf1d0e9ae51c7db876a52f4 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 29 Apr 2022 14:54:25 +0100 Subject: [PATCH 15/45] Changelog tweak --- README.md | 4 ++-- docs/changelog.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0e2dbac3..8fe169ee 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,8 @@ Changelog _29 April 2022_ -- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. (#1091) -- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz` (#1096) +- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. Check [this entry](https://cibuildwheel.readthedocs.io/en/stable/faq/#abi3) in the docs for more info. (#1091) +- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz`. For more details, check the [`--help` output](https://cibuildwheel.readthedocs.io/en/stable/options/#command-line) (#1096) - 🐛 Fix a bug where cibuildwheel would crash when no builds are selected and `--allow-empty` is passed (#1086) - 🐛 Workaround a permissions issue on Linux relating to newer versions of git and setuptools_scm (#1095) - 📚 Minor docs improvements diff --git a/docs/changelog.md b/docs/changelog.md index 80a3148e..721b5725 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,8 +6,8 @@ title: Changelog _29 April 2022_ -- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. (#1091) -- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz` (#1096) +- ✨ Added support for building ABI3 wheels. cibuildwheel will now recognise when an ABI3 wheel was produced, and skip subsequent build steps where the previously built wheel is compatible. Tests still will run on all selected versions of Python, using the ABI3 wheel. Check [this entry](https://cibuildwheel.readthedocs.io/en/stable/faq/#abi3) in the docs for more info. (#1091) +- ✨ You can now build wheels directly from sdist archives, in addition to source directories. Just call cibuildwheel with an sdist argument on the command line, like `cibuildwheel mypackage-1.0.0.tar.gz`. For more details, check the [`--help` output](https://cibuildwheel.readthedocs.io/en/stable/options/#command-line) (#1096) - 🐛 Fix a bug where cibuildwheel would crash when no builds are selected and `--allow-empty` is passed (#1086) - 🐛 Workaround a permissions issue on Linux relating to newer versions of git and setuptools_scm (#1095) - 📚 Minor docs improvements From 6d9a16376efdd664126543ce3b6ab4e5d805dd32 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 29 Apr 2022 12:03:08 +0100 Subject: [PATCH 16/45] Add interactive diagram to the docs --- README.md | 11 +- docs/data/how-it-works.png | Bin 0 -> 77591 bytes docs/diagram.md | 455 +++++++++++++++++++++++++++++++++++++ docs/index.md | 11 + docs/options.md | 2 +- 5 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 docs/data/how-it-works.png create mode 100644 docs/diagram.md diff --git a/README.md b/README.md index 8fe169ee..b346a91b 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,15 @@ jobs: For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.readthedocs.org) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples). +How it works +------------ + +The following diagram summarises the steps that cibuildwheel takes on each platform. + +![](docs/data/how-it-works.png) + +Explore an interactive version of this diagram [in the docs](https://cibuildwheel.readthedocs.io/en/stable/#how-it-works). + Options ------- @@ -117,7 +126,7 @@ Options | | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. | | | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. | | | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build | -| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel | +| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each built wheel | | | [`CIBW_MANYLINUX_*_IMAGE`
`CIBW_MUSLLINUX_*_IMAGE`](https://cibuildwheel.readthedocs.io/en/stable/options/#linux-image) | Specify alternative manylinux / musllinux Docker images | | | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses | | **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel | diff --git a/docs/data/how-it-works.png b/docs/data/how-it-works.png new file mode 100644 index 0000000000000000000000000000000000000000..0306280a5f003005fc70c0adb2d0847c202b5c79 GIT binary patch literal 77591 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 literal 0 HcmV?d00001 diff --git a/docs/diagram.md b/docs/diagram.md new file mode 100644 index 00000000..c7d0ecd3 --- /dev/null +++ b/docs/diagram.md @@ -0,0 +1,455 @@ +
+
+ + +
+
+
+ +
+
+ Manylinux Docker container +
+
+ +
+
Test virtualenv
+
+
+
+ + +
Linux
+
macOS
+
Windows
+ +
+
For each version of Python
+
+
+
If tests are configured
+
+ +
+ +
+
+
+
+
+
+
+ {{action.label}} +
+
+ +
+
+
+ + + + diff --git a/docs/index.md b/docs/index.md index 6cc7ccfd..897eaa10 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,3 +9,14 @@ title: Home %} To get started, head over to the [setup guide](setup.md). + +How it works +------------ + +This diagram summarises the steps that cibuildwheel takes on each platform to build your package's wheels. + +{% + include-markdown "diagram.md" +%} + +This isn't exhaustive, for a full list of the things cibuildwheel can do, check the [options](options.md) page. diff --git a/docs/options.md b/docs/options.md index c9d7b742..baf4bfb9 100644 --- a/docs/options.md +++ b/docs/options.md @@ -783,7 +783,7 @@ Platform-specific environment variables are also available:
### `CIBW_REPAIR_WHEEL_COMMAND` {: #repair-wheel-command} -> Execute a shell command to repair each (non-pure Python) built wheel +> Execute a shell command to repair each built wheel Default: From 960b069eb91dd122fcdb2e4898d041b068e8531b Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Mon, 2 May 2022 11:11:02 +0100 Subject: [PATCH 17/45] Add mobile/touch support (where hover doesn't exist) --- docs/diagram.md | 22 ++++++++++++++++++---- docs/extra.css | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/diagram.md b/docs/diagram.md index c7d0ecd3..53856cd1 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -37,7 +37,6 @@
@@ -260,7 +259,7 @@ const tooltip = action.tooltip if (tooltip) { - tippy(el, { + const tippyInstance = tippy(el, { content: `
${tooltip.title || ''} @@ -277,6 +276,21 @@ maxWidth: 'none', appendTo: document.getElementById('flow-diagram'), offset: [0, 10], + onShow() { + const stepEl = el.closest('.action') + stepEl.classList.add('tooltip-open') + }, + onHide() { + const stepEl = el.closest('.action') + stepEl.classList.remove('tooltip-open') + } + }) + + el.addEventListener('touchend', e => { + e.preventDefault() + e.stopPropagation() + tippy.hideAll() + tippyInstance.show() }) } } @@ -329,10 +343,10 @@ a.action { color: inherit; } - .action.hasHoverState:hover .dot-graphic { + .action.tooltip-open .dot-graphic { background-color: #416EDA; } - .action.hasHoverState:hover .block { + .action.tooltip-open .block { background-color: #416EDA; color: white; } diff --git a/docs/extra.css b/docs/extra.css index e3599627..a971d398 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -3,6 +3,7 @@ body { overflow-wrap: break-word; + overflow-x: hidden; } p { From 99d6c378dbf9587555424f68c2c596750dea12c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 7 May 2022 20:42:17 -0400 Subject: [PATCH 18/45] [pre-commit.ci] pre-commit autoupdate (#1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/hadialqattan/pycln: v1.3.1 → v1.3.2](https://github.com/hadialqattan/pycln/compare/v1.3.1...v1.3.2) - [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c64525ee..da8b94a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: # Autoremoves unused imports - repo: https://github.com/hadialqattan/pycln - rev: v1.3.1 + rev: v1.3.2 hooks: - id: pycln args: [--all] @@ -49,7 +49,7 @@ repos: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v0.950 hooks: - id: mypy name: mypy 3.6 on cibuildwheel/ From 2d1ad70d06148c8dab0ded921d41f012168cacd3 Mon Sep 17 00:00:00 2001 From: Matthieu Darbois Date: Mon, 9 May 2022 08:27:30 +0200 Subject: [PATCH 19/45] chore: use bot email for dependency update Pull Requests (#1105) --- .github/workflows/update-dependencies.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 70f65e2a..f9516e67 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -41,5 +41,7 @@ jobs: PR generated by "Update dependencies" [workflow](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). branch: update-dependencies-pr + committer: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>" + author: "cibuildwheel-bot[bot] <83877280+cibuildwheel-bot[bot]@users.noreply.github.com>" token: ${{ steps.generate-token.outputs.token }} delete-branch: true From a37367fb86229608e62cab8985c1b1eac74745c3 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Mon, 9 May 2022 13:31:47 +0100 Subject: [PATCH 20/45] Improve the touch support (tap the tooltip to follow link) --- docs/diagram.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/diagram.md b/docs/diagram.md index 53856cd1..19328ad5 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -255,30 +255,35 @@ info: { inserted(el, binding) { const action = binding.value - const {env, label, optional=false, description=''} = action + const {env, label, optional=false, description='', href=''} = action const tooltip = action.tooltip if (tooltip) { const tippyInstance = tippy(el, { content: ` -
- ${tooltip.title || ''} -
-
- ${tooltip.tag || ''} -
-
- ${tooltip.description} -
+
+ ${tooltip.title || ''} +
+
+ ${tooltip.tag || ''} +
+
+ ${tooltip.description} +
+ `, placement: 'right-start', allowHTML: true, maxWidth: 'none', appendTo: document.getElementById('flow-diagram'), offset: [0, 10], - onShow() { + onShow(instance) { const stepEl = el.closest('.action') stepEl.classList.add('tooltip-open') + instance.setProps({ + interactive: tippy.currentInput.isTouch + }) }, onHide() { const stepEl = el.closest('.action') @@ -286,11 +291,11 @@ } }) - el.addEventListener('touchend', e => { - e.preventDefault() - e.stopPropagation() - tippy.hideAll() - tippyInstance.show() + el.addEventListener('click', e => { + // click event should just open the tooltip on touch devices + if (tippy.currentInput.isTouch) { + e.preventDefault() + } }) } } @@ -451,6 +456,11 @@ .tippy-box[data-placement^='right'] > .tippy-arrow::before { border-right-color: white; } + a.tooltip-contents { + color: inherit; + text-decoration: none; + display: block; + } .tooltip-title { font-weight: 600; font-size: 1.1em; From dcb5be78d0c6f002ee3fec16ccb8dd2851df7f19 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Mon, 9 May 2022 14:24:03 +0100 Subject: [PATCH 21/45] Add noscript fallback --- docs/diagram.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/diagram.md b/docs/diagram.md index 19328ad5..d0557b4f 100644 --- a/docs/diagram.md +++ b/docs/diagram.md @@ -62,6 +62,11 @@
+ +