feat: build[uv] (#1856)

This commit is contained in:
Henry Schreiner
2024-06-09 15:45:31 -04:00
committed by GitHub
parent c37e5a2d13
commit 384c8d5c82
14 changed files with 399 additions and 180 deletions
+5 -2
View File
@@ -49,6 +49,8 @@ jobs:
with: with:
python-version: ${{ matrix.python_version }} python-version: ${{ matrix.python_version }}
- uses: yezz123/setup-uv@v4
# Install podman on this CI instance for podman tests on linux # Install podman on this CI instance for podman tests on linux
# Snippet from: https://github.com/redhat-actions/podman-login/blob/main/.github/workflows/example.yml # Snippet from: https://github.com/redhat-actions/podman-login/blob/main/.github/workflows/example.yml
- name: Install latest podman - name: Install latest podman
@@ -69,7 +71,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install ".[test]" uv pip install --system ".[test]"
- name: Generate a sample project - name: Generate a sample project
run: | run: |
@@ -82,6 +84,7 @@ jobs:
output-dir: wheelhouse output-dir: wheelhouse
env: env:
CIBW_ARCHS_MACOS: x86_64 universal2 arm64 CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_BUILD_FRONTEND: 'build[uv]'
CIBW_FREE_THREADED_SUPPORT: 1 CIBW_FREE_THREADED_SUPPORT: 1
CIBW_PRERELEASE_PYTHONS: 1 CIBW_PRERELEASE_PYTHONS: 1
@@ -161,7 +164,7 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: python -m pip install ".[test]" run: python -m pip install ".[test,uv]"
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
+1
View File
@@ -40,6 +40,7 @@ repos:
- types-jinja2 - types-jinja2
- types-pyyaml - types-pyyaml
- types-requests - types-requests
- uv
- validate-pyproject - validate-pyproject
- id: mypy - id: mypy
name: mypy 3.12 name: mypy 3.12
+5 -3
View File
@@ -48,19 +48,21 @@ properties:
type: string_array type: string_array
build-frontend: build-frontend:
default: default default: default
description: Set the tool to use to build, either "pip" (default for now) or "build" description: Set the tool to use to build, either "pip" (default for now), "build", or "build[uv]"
oneOf: oneOf:
- enum: [pip, build, default] - enum: [pip, build, "build[uv]", default]
- type: string - type: string
pattern: '^pip; ?args:' pattern: '^pip; ?args:'
- type: string - type: string
pattern: '^build; ?args:' pattern: '^build; ?args:'
- type: string
pattern: '^build\[uv\]; ?args:'
- type: object - type: object
additionalProperties: false additionalProperties: false
required: [name] required: [name]
properties: properties:
name: name:
enum: [pip, build] enum: [pip, build, "build[uv]"]
args: args:
type: array type: array
items: items:
+37 -18
View File
@@ -196,6 +196,8 @@ def build_in_container(
log.build_start(config.identifier) log.build_start(config.identifier)
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip") build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version("3.8")
pip = ["uv", "pip"] if use_uv else ["pip"]
dependency_constraint_flags: list[PathOrStr] = [] dependency_constraint_flags: list[PathOrStr] = []
@@ -229,13 +231,22 @@ def build_in_container(
) )
sys.exit(1) sys.exit(1)
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip() if use_uv:
if PurePosixPath(which_pip) != python_bin / "pip": which_uv = container.call(["which", "uv"], env=env, capture_output=True).strip()
print( if not which_uv:
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", print(
file=sys.stderr, "cibuildwheel: uv not found on PATH. You must use a supported manylinux or musllinux environment with uv.",
) file=sys.stderr,
sys.exit(1) )
sys.exit(1)
else:
which_pip = container.call(["which", "pip"], env=env, capture_output=True).strip()
if PurePosixPath(which_pip) != python_bin / "pip":
print(
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
file=sys.stderr,
)
sys.exit(1)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel: if compatible_wheel:
@@ -279,10 +290,12 @@ def build_in_container(
], ],
env=env, env=env,
) )
elif build_frontend.name == "build": elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
if use_uv:
extra_flags += ["--installer=uv"]
container.call( container.call(
[ [
"python", "python",
@@ -327,26 +340,32 @@ def build_in_container(
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
container.call(["pip", "install", "virtualenv", *dependency_constraint_flags], env=env) if not use_uv:
container.call(
["pip", "install", "virtualenv", *dependency_constraint_flags], env=env
)
testing_temp_dir = PurePosixPath( testing_temp_dir = PurePosixPath(
container.call(["mktemp", "-d"], capture_output=True).strip() container.call(["mktemp", "-d"], capture_output=True).strip()
) )
venv_dir = testing_temp_dir / "venv" venv_dir = testing_temp_dir / "venv"
# Use embedded dependencies from virtualenv to ensure determinism if use_uv:
venv_args = ["--no-periodic-update", "--pip=embed"] container.call(["uv", "venv", venv_dir], env=env)
# In Python<3.12, setuptools & wheel are installed as well else:
if Version(config.version) < Version("3.12"): # Use embedded dependencies from virtualenv to ensure determinism
venv_args.extend(("--setuptools=embed", "--wheel=embed")) venv_args = ["--no-periodic-update", "--pip=embed"]
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env) # In Python<3.12, setuptools & wheel are installed as well
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
container.call(["python", "-m", "virtualenv", *venv_args, venv_dir], env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}" virtualenv_env["PATH"] = f"{venv_dir / 'bin'}:{virtualenv_env['PATH']}"
virtualenv_env["VIRTUAL_ENV"] = str(venv_dir) virtualenv_env["VIRTUAL_ENV"] = str(venv_dir)
# TODO remove me once virtualenv provides pip>=24.1b1 # TODO remove me once virtualenv provides pip>=24.1b1
if config.version == "3.13": if config.version == "3.13" and not use_uv:
container.call(["pip", "install", "pip>=24.1b1"], env=virtualenv_env) container.call(["pip", "install", "pip>=24.1b1"], env=virtualenv_env)
if build_options.before_test: if build_options.before_test:
@@ -365,13 +384,13 @@ def build_in_container(
# Let's just pick the first one. # Let's just pick the first one.
wheel_to_test = repaired_wheels[0] wheel_to_test = repaired_wheels[0]
container.call( container.call(
["pip", "install", str(wheel_to_test) + build_options.test_extras], [*pip, "install", str(wheel_to_test) + build_options.test_extras],
env=virtualenv_env, env=virtualenv_env,
) )
# Install any requirements to run the tests # Install any requirements to run the tests
if build_options.test_requires: if build_options.test_requires:
container.call(["pip", "install", *build_options.test_requires], env=virtualenv_env) container.call([*pip, "install", *build_options.test_requires], env=virtualenv_env)
# Run the tests from a different directory # Run the tests from a different directory
test_command_prepared = prepare_command( test_command_prepared = prepare_command(
+110 -42
View File
@@ -30,9 +30,11 @@ from .util import (
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
call, call,
combine_constraints,
detect_ci_provider, detect_ci_provider,
download, download,
find_compatible_wheel, find_compatible_wheel,
find_uv,
free_thread_enable_313, free_thread_enable_313,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
get_pip_version, get_pip_version,
@@ -48,6 +50,7 @@ from .util import (
) )
@functools.lru_cache(maxsize=None)
def get_macos_version() -> tuple[int, int]: def get_macos_version() -> tuple[int, int]:
""" """
Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0) Returns the macOS major/minor version, as a tuple, e.g. (10, 15) or (11, 0)
@@ -59,9 +62,29 @@ def get_macos_version() -> tuple[int, int]:
""" """
version_str, _, _ = platform.mac_ver() version_str, _, _ = platform.mac_ver()
version = tuple(map(int, version_str.split(".")[:2])) version = tuple(map(int, version_str.split(".")[:2]))
if (10, 15) < version < (11, 0):
# When built against an older macOS SDK, Python will report macOS 10.16
# instead of the real version.
version_str = call(
sys.executable,
"-sS",
"-c",
"import platform; print(platform.mac_ver()[0])",
env={"SYSTEM_VERSION_COMPAT": "0"},
capture_stdout=True,
)
version = tuple(map(int, version_str.split(".")[:2]))
return typing.cast(Tuple[int, int], version) return typing.cast(Tuple[int, int], version)
@functools.lru_cache(maxsize=None)
def get_test_macosx_deployment_target() -> str:
version = get_macos_version()
if version >= (11, 0):
return f"{version[0]}.0"
return f"{version[0]}.{version[1]}"
def get_macos_sdks() -> list[str]: def get_macos_sdks() -> list[str]:
output = call("xcodebuild", "-showsdks", capture_stdout=True) output = call("xcodebuild", "-showsdks", capture_stdout=True)
return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)] return [m.group(1) for m in re.finditer(r"-sdk (macosx\S+)", output)]
@@ -179,6 +202,14 @@ def setup_python(
environment: ParsedEnvironment, environment: ParsedEnvironment,
build_frontend: BuildFrontendName, build_frontend: BuildFrontendName,
) -> dict[str, str]: ) -> dict[str, str]:
if build_frontend == "build[uv]" and Version(python_configuration.version) < Version("3.8"):
build_frontend = "build"
uv_path = find_uv()
use_uv = build_frontend == "build[uv]" and Version(python_configuration.version) >= Version(
"3.8"
)
tmp.mkdir() tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0] implementation_id = python_configuration.identifier.split("-")[0]
log.step(f"Installing Python {implementation_id}...") log.step(f"Installing Python {implementation_id}...")
@@ -200,7 +231,11 @@ def setup_python(
log.step("Setting up build environment...") log.step("Setting up build environment...")
venv_path = tmp / "venv" venv_path = tmp / "venv"
env = virtualenv( env = virtualenv(
python_configuration.version, base_python, venv_path, dependency_constraint_flags python_configuration.version,
base_python,
venv_path,
dependency_constraint_flags,
use_uv=use_uv,
) )
venv_bin_path = venv_path / "bin" venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists() assert venv_bin_path.exists()
@@ -217,32 +252,38 @@ def setup_python(
# upgrade pip to the version matching our constraints # upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip' # if necessary, reinstall it to ensure that it's available on PATH as 'pip'
call( if build_frontend == "build[uv]":
"python", assert uv_path is not None
"-m", pip = [str(uv_path), "pip"]
"pip", else:
"install", pip = ["python", "-m", "pip"]
"--upgrade",
"pip", if not use_uv:
*dependency_constraint_flags, call(
env=env, *pip,
cwd=venv_path, "install",
) "--upgrade",
"pip",
*dependency_constraint_flags,
env=env,
cwd=venv_path,
)
# Apply our environment after pip is ready # Apply our environment after pip is ready
env = environment.as_dictionary(prev_environment=env) env = environment.as_dictionary(prev_environment=env)
# check what pip version we're on # check what pip version we're on
assert (venv_bin_path / "pip").exists() if not use_uv:
call("which", "pip", env=env) assert (venv_bin_path / "pip").exists()
call("pip", "--version", env=env) call("which", "pip", env=env)
which_pip = call("which", "pip", env=env, capture_stdout=True).strip() call("pip", "--version", env=env)
if which_pip != str(venv_bin_path / "pip"): which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
print( if which_pip != str(venv_bin_path / "pip"):
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", print(
file=sys.stderr, "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
) file=sys.stderr,
sys.exit(1) )
sys.exit(1)
# check what Python version we're on # check what Python version we're on
call("which", "python", env=env) call("which", "python", env=env)
@@ -338,6 +379,18 @@ def setup_python(
*dependency_constraint_flags, *dependency_constraint_flags,
env=env, env=env,
) )
elif build_frontend == "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"delocate",
"build[virtualenv, uv]",
*dependency_constraint_flags,
env=env,
)
else: else:
assert_never(build_frontend) assert_never(build_frontend)
@@ -370,6 +423,14 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip") build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version(
"3.8"
)
uv_path = find_uv()
if use_uv and uv_path is None:
msg = "uv not found"
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
log.build_start(config.identifier) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
@@ -394,7 +455,8 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.environment, build_options.environment,
build_frontend.name, build_frontend.name,
) )
pip_version = get_pip_version(env) if not use_uv:
pip_version = get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel: if compatible_wheel:
@@ -420,15 +482,14 @@ def build(options: Options, tmp_path: Path) -> None:
extra_flags += build_frontend.args extra_flags += build_frontend.args
build_env = env.copy() build_env = env.copy()
build_env["VIRTUALENV_PIP"] = pip_version if not use_uv:
build_env["VIRTUALENV_PIP"] = pip_version
if build_options.dependency_constraints: if build_options.dependency_constraints:
constraint_path = build_options.dependency_constraints.get_for_python_version( constraint_path = build_options.dependency_constraints.get_for_python_version(
config.version config.version
) )
user_constraints = build_env.get("PIP_CONSTRAINT") combine_constraints(
our_constraints = constraint_path.as_uri() build_env, constraint_path, identifier_tmp_dir if use_uv else None
build_env["PIP_CONSTRAINT"] = " ".join(
c for c in [user_constraints, our_constraints] if c
) )
if build_frontend.name == "pip": if build_frontend.name == "pip":
@@ -446,10 +507,12 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=build_env, env=build_env,
) )
elif build_frontend.name == "build": elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
if use_uv:
extra_flags.append("--installer=uv")
call( call(
"python", "python",
"-m", "-m",
@@ -579,15 +642,18 @@ def build(options: Options, tmp_path: Path) -> None:
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env) if not use_uv:
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}" venv_dir = identifier_tmp_dir / f"venv-test-{testing_arch}"
arch_prefix = [] arch_prefix = []
uv_arch_args = []
if testing_arch != machine_arch: if testing_arch != machine_arch:
if machine_arch == "arm64" and testing_arch == "x86_64": if machine_arch == "arm64" and testing_arch == "x86_64":
# rosetta2 will provide the emulation with just the arch prefix. # rosetta2 will provide the emulation with just the arch prefix.
arch_prefix = ["arch", "-x86_64"] arch_prefix = ["arch", "-x86_64"]
uv_arch_args = ["--python-platform", "x86_64-apple-darwin"]
else: else:
msg = f"don't know how to emulate {testing_arch} on {machine_arch}" msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
raise RuntimeError(msg) raise RuntimeError(msg)
@@ -596,14 +662,20 @@ def build(options: Options, tmp_path: Path) -> None:
call_with_arch = functools.partial(call, *arch_prefix) call_with_arch = functools.partial(call, *arch_prefix)
shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c") shell_with_arch = functools.partial(call, *arch_prefix, "/bin/sh", "-c")
# Use pip version from the initial env to ensure determinism if use_uv:
venv_args = ["--no-periodic-update", f"--pip={pip_version}"] pip_install = functools.partial(call, *pip, "install", *uv_arch_args)
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones call("uv", "venv", venv_dir, "--python=python", env=env)
if Version(config.version) < Version("3.12"): else:
venv_args.extend(("--setuptools=embed", "--wheel=embed")) pip_install = functools.partial(call_with_arch, *pip, "install")
call_with_arch("python", "-m", "virtualenv", *venv_args, venv_dir, env=env) # Use pip version from the initial env to ensure determinism
venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
call_with_arch("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env["MACOSX_DEPLOYMENT_TARGET"] = get_test_macosx_deployment_target()
virtualenv_env["PATH"] = os.pathsep.join( virtualenv_env["PATH"] = os.pathsep.join(
[ [
str(venv_dir / "bin"), str(venv_dir / "bin"),
@@ -641,18 +713,14 @@ def build(options: Options, tmp_path: Path) -> None:
else: else:
virtualenv_env_install_wheel = virtualenv_env virtualenv_env_install_wheel = virtualenv_env
call_with_arch( pip_install(
"pip",
"install",
f"{repaired_wheel}{build_options.test_extras}", f"{repaired_wheel}{build_options.test_extras}",
env=virtualenv_env_install_wheel, env=virtualenv_env_install_wheel,
) )
# test the wheel # test the wheel
if build_options.test_requires: if build_options.test_requires:
call_with_arch( pip_install(
"pip",
"install",
*build_options.test_requires, *build_options.test_requires,
env=virtualenv_env_install_wheel, env=virtualenv_env_install_wheel,
) )
+1 -1
View File
@@ -114,7 +114,7 @@ def setup_python(
log.step("Setting up build environment...") log.step("Setting up build environment...")
venv_path = tmp / "venv" venv_path = tmp / "venv"
env = virtualenv(python_configuration.version, base_python, venv_path, []) env = virtualenv(python_configuration.version, base_python, venv_path, [], use_uv=False)
venv_bin_path = venv_path / "bin" venv_bin_path = venv_path / "bin"
assert venv_bin_path.exists() assert venv_bin_path.exists()
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
@@ -96,12 +96,13 @@
}, },
"build-frontend": { "build-frontend": {
"default": "default", "default": "default",
"description": "Set the tool to use to build, either \"pip\" (default for now) or \"build\"", "description": "Set the tool to use to build, either \"pip\" (default for now), \"build\", or \"build[uv]\"",
"oneOf": [ "oneOf": [
{ {
"enum": [ "enum": [
"pip", "pip",
"build", "build",
"build[uv]",
"default" "default"
] ]
}, },
@@ -113,6 +114,10 @@
"type": "string", "type": "string",
"pattern": "^build; ?args:" "pattern": "^build; ?args:"
}, },
{
"type": "string",
"pattern": "^build\\[uv\\]; ?args:"
},
{ {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -123,7 +128,8 @@
"name": { "name": {
"enum": [ "enum": [
"pip", "pip",
"build" "build",
"build[uv]"
] ]
}, },
"args": { "args": {
+108 -50
View File
@@ -16,7 +16,7 @@ import time
import typing import typing
import urllib.request import urllib.request
from collections import defaultdict from collections import defaultdict
from collections.abc import Generator, Iterable, Mapping, Sequence from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from functools import cached_property, lru_cache from functools import cached_property, lru_cache
@@ -40,20 +40,22 @@ from .architecture import Architecture
from .typing import PathOrStr, PlatformName from .typing import PathOrStr, PlatformName
__all__ = [ __all__ = [
"resources_dir",
"MANYLINUX_ARCHS", "MANYLINUX_ARCHS",
"call",
"shell",
"find_compatible_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
"read_python_configs",
"selector_matches",
"strtobool",
"cached_property", "cached_property",
"call",
"chdir", "chdir",
"combine_constraints",
"find_compatible_wheel",
"find_uv",
"format_safe",
"get_build_verbosity_extra_flags",
"prepare_command",
"read_python_configs",
"resources_dir",
"selector_matches",
"shell",
"split_config_settings", "split_config_settings",
"strtobool",
] ]
resources_dir: Final[Path] = Path(__file__).parent / "resources" resources_dir: Final[Path] = Path(__file__).parent / "resources"
@@ -202,7 +204,9 @@ def get_build_verbosity_extra_flags(level: int) -> list[str]:
return [] return []
def split_config_settings(config_settings: str, frontend: Literal["pip", "build"]) -> list[str]: def split_config_settings(
config_settings: str, frontend: Literal["pip", "build", "build[uv]"]
) -> list[str]:
config_settings_list = shlex.split(config_settings) config_settings_list = shlex.split(config_settings)
s = "s" if frontend == "pip" else "" s = "s" if frontend == "pip" else ""
return [f"--config-setting{s}={setting}" for setting in config_settings_list] return [f"--config-setting{s}={setting}" for setting in config_settings_list]
@@ -431,7 +435,7 @@ class DependencyConstraints:
return self.base_file_path.name return self.base_file_path.name
BuildFrontendName = Literal["pip", "build"] BuildFrontendName = Literal["pip", "build", "build[uv]"]
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -443,8 +447,8 @@ class BuildFrontendConfig:
def from_config_string(config_string: str) -> BuildFrontendConfig: def from_config_string(config_string: str) -> BuildFrontendConfig:
config_dict = parse_key_value_string(config_string, ["name"], ["args"]) config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"]) name = " ".join(config_dict["name"])
if name not in {"pip", "build"}: if name not in {"pip", "build", "build[uv]"}:
msg = f"Unrecognised build frontend {name}, only 'pip' and 'build' are supported" msg = f"Unrecognised build frontend {name}, only 'pip', 'build', and 'build[uv]' are supported"
raise ValueError(msg) raise ValueError(msg)
name = typing.cast(BuildFrontendName, name) name = typing.cast(BuildFrontendName, name)
@@ -689,45 +693,60 @@ def _parse_constraints_for_virtualenv(
def virtualenv( def virtualenv(
version: str, python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr] version: str,
python: Path,
venv_path: Path,
dependency_constraint_flags: Sequence[PathOrStr],
*,
use_uv: bool,
) -> dict[str, str]: ) -> dict[str, str]:
"""
Create a virtual environment. If `use_uv` is True,
dependency_constraint_flags are ignored since nothing is installed in the
venv. Otherwise, pip is installed, and setuptools + wheel if Python < 3.12.
"""
assert python.exists() assert python.exists()
virtualenv_app = _ensure_virtualenv(version)
allowed_seed_packages = ["pip", "setuptools", "wheel"]
constraints = _parse_constraints_for_virtualenv(
allowed_seed_packages, dependency_constraint_flags
)
additional_flags: list[str] = []
for package in allowed_seed_packages:
if package in constraints:
additional_flags.append(f"--{package}={constraints[package]}")
else:
additional_flags.append(f"--no-{package}")
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual if use_uv:
# environment. The initial cost is a bit higher but reusing is much faster. call("uv", "venv", venv_path, "--python", python)
# Windows does not always allow symlinks so just disabling for now. else:
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the virtualenv_app = _ensure_virtualenv(version)
# version of pip that will end-up installed. allowed_seed_packages = ["pip", "setuptools", "wheel"]
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder constraints = _parse_constraints_for_virtualenv(
if ( allowed_seed_packages, dependency_constraint_flags
not IS_WIN )
and constraints["pip"] != "embed" additional_flags: list[str] = []
and Version(constraints["pip"]) >= Version("19.3") for package in allowed_seed_packages:
): if package in constraints:
additional_flags.append("--symlink-app-data") additional_flags.append(f"--{package}={constraints[package]}")
else:
additional_flags.append(f"--no-{package}")
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
# environment. The initial cost is a bit higher but reusing is much faster.
# Windows does not always allow symlinks so just disabling for now.
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
# version of pip that will end-up installed.
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
if (
not IS_WIN
and constraints["pip"] != "embed"
and Version(constraints["pip"]) >= Version("19.3")
):
additional_flags.append("--symlink-app-data")
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
paths = [str(venv_path), str(venv_path / "Scripts")] if IS_WIN else [str(venv_path / "bin")] paths = [str(venv_path), str(venv_path / "Scripts")] if IS_WIN else [str(venv_path / "bin")]
env = os.environ.copy() env = os.environ.copy()
env["PATH"] = os.pathsep.join([*paths, env["PATH"]]) env["PATH"] = os.pathsep.join([*paths, env["PATH"]])
@@ -873,3 +892,42 @@ def parse_key_value_string(
result[field_name] += values result[field_name] += values
return dict(result) return dict(result)
def find_uv() -> Path | None:
# Prefer uv in our environment
with contextlib.suppress(ImportError, FileNotFoundError):
# pylint: disable-next=import-outside-toplevel
from uv import find_uv_bin
return Path(find_uv_bin())
uv_on_path = shutil.which("uv")
return Path(uv_on_path) if uv_on_path else None
def combine_constraints(
env: MutableMapping[str, str], /, constraints_path: Path, tmp_dir: Path | None
) -> None:
"""
This will workaround a bug in pip<=21.1.1 or uv<=0.2.0 if a tmp_dir is given.
If set to None, this will use the modern URI method.
"""
if tmp_dir:
if " " in str(constraints_path):
assert " " not in str(tmp_dir)
tmp_file = tmp_dir / "constraints.txt"
tmp_file.write_bytes(constraints_path.read_bytes())
constraints_path = tmp_file
our_constraints = str(constraints_path)
else:
our_constraints = (
constraints_path.as_uri() if " " in str(constraints_path) else str(constraints_path)
)
user_constraints = env.get("PIP_CONSTRAINT")
env["UV_CONSTRAINT"] = env["PIP_CONSTRAINT"] = " ".join(
c for c in [our_constraints, user_constraints] if c
)
+73 -48
View File
@@ -28,9 +28,11 @@ from .util import (
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
call, call,
combine_constraints,
download, download,
extract_zip, extract_zip,
find_compatible_wheel, find_compatible_wheel,
find_uv,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
get_pip_version, get_pip_version,
move_file, move_file,
@@ -244,10 +246,19 @@ def setup_python(
raise ValueError(msg) raise ValueError(msg)
assert base_python.exists() assert base_python.exists()
use_uv = build_frontend == "build[uv]" and Version(python_configuration.version) >= Version(
"3.8"
)
uv_path = find_uv()
log.step("Setting up build environment...") log.step("Setting up build environment...")
venv_path = tmp / "venv" venv_path = tmp / "venv"
env = virtualenv( env = virtualenv(
python_configuration.version, base_python, venv_path, dependency_constraint_flags python_configuration.version,
base_python,
venv_path,
dependency_constraint_flags,
use_uv=use_uv,
) )
# set up environment variables for run_with_env # set up environment variables for run_with_env
@@ -257,17 +268,18 @@ def setup_python(
# upgrade pip to the version matching our constraints # upgrade pip to the version matching our constraints
# if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe' # if necessary, reinstall it to ensure that it's available on PATH as 'pip.exe'
call( if not use_uv:
"python", call(
"-m", "python",
"pip", "-m",
"install", "pip",
"--upgrade", "install",
"pip", "--upgrade",
*dependency_constraint_flags, "pip",
env=env, *dependency_constraint_flags,
cwd=venv_path, env=env,
) cwd=venv_path,
)
# update env with results from CIBW_ENVIRONMENT # update env with results from CIBW_ENVIRONMENT
env = environment.as_dictionary(prev_environment=env) env = environment.as_dictionary(prev_environment=env)
@@ -285,16 +297,17 @@ def setup_python(
sys.exit(1) sys.exit(1)
# check what pip version we're on # check what pip version we're on
assert (venv_path / "Scripts" / "pip.exe").exists() if not use_uv:
where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip() assert (venv_path / "Scripts" / "pip.exe").exists()
if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"): where_pip = call("where", "pip", env=env, capture_stdout=True).splitlines()[0].strip()
print( if where_pip.strip() != str(venv_path / "Scripts" / "pip.exe"):
"cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.", print(
file=sys.stderr, "cibuildwheel: pip available on PATH doesn't match our installed instance. If you have modified PATH, ensure that you don't overwrite cibuildwheel's entry or insert pip above it.",
) file=sys.stderr,
sys.exit(1) )
sys.exit(1)
call("pip", "--version", env=env) call("pip", "--version", env=env)
log.step("Installing build tools...") log.step("Installing build tools...")
if build_frontend == "build": if build_frontend == "build":
@@ -306,6 +319,17 @@ def setup_python(
*dependency_constraint_flags, *dependency_constraint_flags,
env=env, env=env,
) )
elif build_frontend == "build[uv]":
assert uv_path is not None
call(
uv_path,
"pip",
"install",
"--upgrade",
"build[virtualenv]",
*dependency_constraint_flags,
env=env,
)
if python_libs_base: if python_libs_base:
# Set up the environment for various backends to enable cross-compilation # Set up the environment for various backends to enable cross-compilation
@@ -340,6 +364,9 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip") build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version(
"3.8"
)
log.build_start(config.identifier) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
@@ -362,7 +389,8 @@ def build(options: Options, tmp_path: Path) -> None:
build_options.environment, build_options.environment,
build_frontend.name, build_frontend.name,
) )
pip_version = get_pip_version(env) if not use_uv:
pip_version = get_pip_version(env)
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
if compatible_wheel: if compatible_wheel:
@@ -391,26 +419,14 @@ def build(options: Options, tmp_path: Path) -> None:
extra_flags += build_frontend.args extra_flags += build_frontend.args
build_env = env.copy() build_env = env.copy()
build_env["VIRTUALENV_PIP"] = pip_version if not use_uv:
build_env["VIRTUALENV_PIP"] = pip_version
if build_options.dependency_constraints: if build_options.dependency_constraints:
constraints_path = build_options.dependency_constraints.get_for_python_version( constraints_path = build_options.dependency_constraints.get_for_python_version(
config.version config.version
) )
# Bug in pip <= 21.1.3 - we can't have a space in the combine_constraints(build_env, constraints_path, identifier_tmp_dir)
# 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
our_constraints = str(constraints_path)
user_constraints = build_env.get("PIP_CONSTRAINT")
build_env["PIP_CONSTRAINT"] = " ".join(
c for c in [our_constraints, user_constraints] if c
)
if build_frontend.name == "pip": if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity) extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
@@ -427,10 +443,12 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=build_env, env=build_env,
) )
elif build_frontend.name == "build": elif build_frontend.name == "build" or build_frontend.name == "build[uv]":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
if use_uv:
extra_flags.append("--installer=uv")
call( call(
"python", "python",
"-m", "-m",
@@ -484,15 +502,20 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Testing wheel...") log.step("Testing wheel...")
# set up a virtual environment to install and test from, to make sure # set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time. # there are no dependencies that were pulled in at build time.
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env) if not use_uv:
call("pip", "install", "virtualenv", *dependency_constraint_flags, env=env)
venv_dir = identifier_tmp_dir / "venv-test" venv_dir = identifier_tmp_dir / "venv-test"
# Use pip version from the initial env to ensure determinism if use_uv:
venv_args = ["--no-periodic-update", f"--pip={pip_version}"] call("uv", "venv", venv_dir, "--python=python", env=env)
# In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones else:
if Version(config.version) < Version("3.12"): # Use pip version from the initial env to ensure determinism
venv_args.extend(("--setuptools=embed", "--wheel=embed")) venv_args = ["--no-periodic-update", f"--pip={pip_version}"]
call("python", "-m", "virtualenv", *venv_args, venv_dir, env=env) # In Python<3.12, setuptools & wheel are installed as well, use virtualenv embedded ones
if Version(config.version) < Version("3.12"):
venv_args.extend(("--setuptools=embed", "--wheel=embed"))
call("python", "-m", "virtualenv", *venv_args, venv_dir, env=env)
virtualenv_env = env.copy() virtualenv_env = env.copy()
virtualenv_env["PATH"] = os.pathsep.join( virtualenv_env["PATH"] = os.pathsep.join(
@@ -514,9 +537,11 @@ def build(options: Options, tmp_path: Path) -> None:
) )
shell(before_test_prepared, env=virtualenv_env) shell(before_test_prepared, env=virtualenv_env)
pip = ["uv", "pip"] if use_uv else ["pip"]
# install the wheel # install the wheel
call( call(
"pip", *pip,
"install", "install",
str(repaired_wheel) + build_options.test_extras, str(repaired_wheel) + build_options.test_extras,
env=virtualenv_env, env=virtualenv_env,
@@ -524,7 +549,7 @@ def build(options: Options, tmp_path: Path) -> None:
# test the wheel # test the wheel
if build_options.test_requires: if build_options.test_requires:
call("pip", "install", *build_options.test_requires, env=virtualenv_env) call(*pip, "install", *build_options.test_requires, env=virtualenv_env)
# run the tests from a temp dir, with an absolute path in the command # run the tests from a temp dir, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel # (this ensures that Python runs the tests against the installed wheel
+28 -3
View File
@@ -624,7 +624,7 @@ This option can also be set using the [command-line option](#command-line) `--pr
## Build customization ## Build customization
### `CIBW_BUILD_FRONTEND` {: #build-frontend} ### `CIBW_BUILD_FRONTEND` {: #build-frontend}
> Set the tool to use to build, either "pip" (default for now) or "build" > Set the tool to use to build, either "pip" (default for now), "build", or "build[uv]"
Options: Options:
@@ -636,17 +636,30 @@ Default: `pip`
Choose which build frontend to use. Can either be "pip", which will run Choose which build frontend to use. Can either be "pip", which will run
`python -m pip wheel`, or "build", which will run `python -m build --wheel`. `python -m pip wheel`, or "build", which will run `python -m build --wheel`.
You can also use "build[uv]", which will use an external [uv][] everywhere
possible, both through `--installer=uv` passed to build, as well as when making
all build and test environments. This will generally speed up cibuildwheel.
Make sure you have an external uv on Windows and macOS, either by
pre-installing it, or installing cibuildwheel with the uv extra,
`cibuildwheel[uv]`. `uv` will not be used for Python 3.6 or Python 3.7. You
cannot use uv currently on Windows for ARM or for musllinux on s390x as
binaries are not provided by uv. Legacy dependencies like setuptools on Python
< 3.12 and pip are not installed if using uv.
Pyodide ignores this setting, as only "build" is supported.
You can specify extra arguments to pass to `pip wheel` or `build` using the You can specify extra arguments to pass to `pip wheel` or `build` using the
optional `args` option. optional `args` option.
!!! tip !!! tip
Until v2.0.0, [pip] was the only way to build wheels, and is still the Until v2.0.0, [pip][] was the only way to build wheels, and is still the
default. However, we expect that at some point in the future, cibuildwheel default. However, we expect that at some point in the future, cibuildwheel
will change the default to [build], in line with the PyPA's recommendation. will change the default to [build][], in line with the PyPA's recommendation.
If you want to try `build` before this, you can use this option. If you want to try `build` before this, you can use this option.
[pip]: https://pip.pypa.io/en/stable/cli/pip_wheel/ [pip]: https://pip.pypa.io/en/stable/cli/pip_wheel/
[build]: https://github.com/pypa/build/ [build]: https://github.com/pypa/build/
[uv]: https://github.com/astral-sh/uv
#### Examples #### Examples
@@ -661,6 +674,12 @@ optional `args` option.
# supply an extra argument to 'pip wheel' # supply an extra argument to 'pip wheel'
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation" CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
# Use uv and build
CIBW_BUILD_FRONTEND: "build[uv]"
# Use uv and build with an argument
CIBW_BUILD_FRONTEND: "build[uv]; args: --no-isolation"
``` ```
!!! tab examples "pyproject.toml" !!! tab examples "pyproject.toml"
@@ -675,6 +694,12 @@ optional `args` option.
# supply an extra argument to 'pip wheel' # supply an extra argument to 'pip wheel'
build-frontend = { name = "pip", args = ["--no-build-isolation"] } build-frontend = { name = "pip", args = ["--no-build-isolation"] }
# Use uv and build
build-frontend = "build[uv]"
# Use uv and build with an argument
build-frontend = { name = "build[uv]", args = ["--no-isolation"] }
``` ```
### `CIBW_CONFIG_SETTINGS` {: #config-settings} ### `CIBW_CONFIG_SETTINGS` {: #config-settings}
+2
View File
@@ -78,6 +78,7 @@ test = [
"tomli_w", "tomli_w",
"validate-pyproject", "validate-pyproject",
] ]
uv = ["uv"]
[project.scripts] [project.scripts]
cibuildwheel = "cibuildwheel.__main__:main" cibuildwheel = "cibuildwheel.__main__:main"
@@ -176,6 +177,7 @@ messages_control.disable = [
"wrong-import-position", "wrong-import-position",
"unused-argument", # Handled by Ruff "unused-argument", # Handled by Ruff
"broad-exception-raised", # Could be improved eventually "broad-exception-raised", # Could be improved eventually
"consider-using-in", # MyPy can't narrow "in"
] ]
[tool.ruff] [tool.ruff]
+11 -2
View File
@@ -6,7 +6,7 @@ from typing import Generator
import pytest import pytest
from cibuildwheel.util import detect_ci_provider from cibuildwheel.util import detect_ci_provider, find_uv
from .utils import EMULATED_ARCHS, platform from .utils import EMULATED_ARCHS, platform
@@ -31,12 +31,21 @@ def pytest_addoption(parser) -> None:
@pytest.fixture( @pytest.fixture(
params=[{"CIBW_BUILD_FRONTEND": "pip"}, {"CIBW_BUILD_FRONTEND": "build"}], ids=["pip", "build"] params=[{"CIBW_BUILD_FRONTEND": "pip"}, {"CIBW_BUILD_FRONTEND": "build"}], ids=["pip", "build"]
) )
def build_frontend_env(request) -> dict[str, str]: def build_frontend_env_nouv(request) -> dict[str, str]:
if platform == "pyodide": if platform == "pyodide":
pytest.skip("Can't use pip as build frontend for pyodide platform") pytest.skip("Can't use pip as build frontend for pyodide platform")
return request.param # type: ignore[no-any-return] return request.param # type: ignore[no-any-return]
@pytest.fixture()
def build_frontend_env(build_frontend_env_nouv: dict[str, str]) -> dict[str, str]:
if build_frontend_env_nouv["CIBW_BUILD_FRONTEND"] == "build" and find_uv() is not None:
return {"CIBW_BUILD_FRONTEND": "build[uv]"}
return build_frontend_env_nouv
@pytest.fixture() @pytest.fixture()
def docker_cleanup() -> Generator[None, None, None]: def docker_cleanup() -> Generator[None, None, None]:
def get_images() -> set[str]: def get_images() -> set[str]:
+4 -4
View File
@@ -53,7 +53,7 @@ def get_versions_from_constraint_file(constraint_file):
@pytest.mark.parametrize("python_version", ["3.6", "3.8", "3.10"]) @pytest.mark.parametrize("python_version", ["3.6", "3.8", "3.10"])
def test_pinned_versions(tmp_path, python_version, build_frontend_env): def test_pinned_versions(tmp_path, python_version, build_frontend_env_nouv):
if utils.platform == "linux": if utils.platform == "linux":
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead") pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
if python_version == "3.6" and utils.platform == "macos" and platform.machine() == "arm64": if python_version == "3.6" and utils.platform == "macos" and platform.machine() == "arm64":
@@ -79,7 +79,7 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env):
add_env={ add_env={
"CIBW_BUILD": build_pattern, "CIBW_BUILD": build_pattern,
"CIBW_ENVIRONMENT": cibw_environment_option, "CIBW_ENVIRONMENT": cibw_environment_option,
**build_frontend_env, **build_frontend_env_nouv,
}, },
) )
@@ -93,7 +93,7 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env):
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
def test_dependency_constraints_file(tmp_path, build_frontend_env): def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
if utils.platform == "linux": if utils.platform == "linux":
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead") pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
@@ -130,7 +130,7 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env):
"CIBW_ENVIRONMENT": cibw_environment_option, "CIBW_ENVIRONMENT": cibw_environment_option,
"CIBW_DEPENDENCY_VERSIONS": str(constraints_file), "CIBW_DEPENDENCY_VERSIONS": str(constraints_file),
"CIBW_SKIP": "cp36-*", "CIBW_SKIP": "cp36-*",
**build_frontend_env, **build_frontend_env_nouv,
}, },
) )
+6 -5
View File
@@ -172,7 +172,7 @@ def test_universal2_testing_on_x86_64(tmp_path, capfd, skip_arm64_test):
assert set(actual_wheels) == set(expected_wheels) assert set(actual_wheels) == set(expected_wheels)
def test_universal2_testing_on_arm64(tmp_path, capfd): def test_universal2_testing_on_arm64(build_frontend_env, tmp_path, capfd):
# cibuildwheel should test the universal2 wheel on both x86_64 and arm64, when run on arm64 # cibuildwheel should test the universal2 wheel on both x86_64 and arm64, when run on arm64
if utils.platform != "macos": if utils.platform != "macos":
pytest.skip("this test is only relevant to macos") pytest.skip("this test is only relevant to macos")
@@ -187,15 +187,16 @@ def test_universal2_testing_on_arm64(tmp_path, capfd):
add_env={ add_env={
"CIBW_ARCHS": "universal2", "CIBW_ARCHS": "universal2",
# check that a native dependency is correctly installed, once per each testing arch # check that a native dependency is correctly installed, once per each testing arch
"CIBW_TEST_REQUIRES": "numpy", "CIBW_TEST_REQUIRES": "--only-binary :all: pillow>=10.3", # pillow>=10.3 provides wheels for macOS 10.10, not 10.9
"CIBW_TEST_COMMAND": '''python -c "import numpy, platform; print(f'running tests on {platform.machine()} with numpy {numpy.__version__}')"''', "CIBW_TEST_COMMAND": '''python -c "import PIL, platform; print(f'running tests on {platform.machine()} with pillow {PIL.__version__}')"''',
**build_frontend_env,
}, },
single_python=True, single_python=True,
) )
captured = capfd.readouterr() captured = capfd.readouterr()
assert "running tests on arm64" in captured.out assert "running tests on arm64 with pillow" in captured.out
assert "running tests on x86_64" in captured.out assert "running tests on x86_64 with pillow" in captured.out
python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION) python_tag = "cp{}{}".format(*utils.SINGLE_PYTHON_VERSION)
expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w] expected_wheels = [w for w in ALL_MACOS_WHEELS if python_tag in w and "universal2" in w]