diff --git a/action.yml b/action.yml index 21268f9cf..97877f305 100644 --- a/action.yml +++ b/action.yml @@ -47,8 +47,6 @@ runs: from subprocess import run EXTRAS = set(e.strip() for e in "${{ inputs.extras }}".split(",") if e.strip()) - if sys.platform == "linux": - EXTRAS.discard("uv") class EnvBuilder(venv.EnvBuilder): @@ -73,47 +71,41 @@ runs: shutil.rmtree(venv_path) builder = EnvBuilder() builder.create(venv_path) - exposed_binaries = {"cibuildwheel"} - if "uv" in EXTRAS: - exposed_binaries.add("uv") - clean_bin_path = builder.bin_path.parent / f"{builder.bin_path.name}.clean" - clean_bin_path.mkdir() - for path in list(builder.bin_path.iterdir()): - if path.stem in exposed_binaries: - try: - os.symlink(path, clean_bin_path / path.name) - except OSError: - import shutil + cibw_bin = [p for p in builder.bin_path.glob("cibuildwheel*") if p.stem == "cibuildwheel"][0] - shutil.copy2(path, clean_bin_path / path.name) - full_path = f"{clean_bin_path}{os.pathsep}{os.environ['PATH']}" with open(os.environ["GITHUB_OUTPUT"], "at") as f: - f.write(f"updated-path={full_path}\n") + f.write(f"cibw-bin={cibw_bin}\n") + f.write(f"prepend-path={builder.bin_path if 'uv' in EXTRAS else ''}\n") + print("::endgroup::") EOF shell: bash # Redirecting stderr to stdout to fix interleaving issue in Actions. - - run: > - cibuildwheel - "${{ inputs.package-dir }}" - ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} - ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} - ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} - 2>&1 - env: - PATH: "${{ steps.cibw.outputs.updated-path }}" + - run: | + prepend_path="${{ steps.cibw.outputs.prepend-path }}" + if [ -n "$prepend_path" ]; then + export PATH="$prepend_path:$PATH" + fi + "${{ steps.cibw.outputs.cibw-bin }}" \ + "${{ inputs.package-dir }}" \ + ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} \ + ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} \ + ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} \ + 2>&1 shell: bash if: runner.os != 'Windows' # Windows needs powershell to interact nicely with Meson - - run: > - cibuildwheel - "${{ inputs.package-dir }}" - ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} - ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} - ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} - env: - PATH: "${{ steps.cibw.outputs.updated-path }}" + - run: | + $PrependPath = "${{ steps.cibw.outputs.prepend-path }}" + if ($PrependPath) { + $env:PATH = "$PrependPath;$env:PATH" + } + & "${{ steps.cibw.outputs.cibw-bin }}" ` + "${{ inputs.package-dir }}" ` + ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} ` + ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} ` + ${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} shell: pwsh if: runner.os == 'Windows' diff --git a/cibuildwheel/platforms/android.py b/cibuildwheel/platforms/android.py index 229401e4f..e02e8af63 100644 --- a/cibuildwheel/platforms/android.py +++ b/cibuildwheel/platforms/android.py @@ -34,7 +34,7 @@ from ..util.helpers import prepare_command from ..util.packaging import find_compatible_wheel from ..util.python_build_standalone import create_python_build_standalone_environment -from ..venv import constraint_flags, find_uv, virtualenv +from ..venv import constraint_flags, ensure_uv, virtualenv ANDROID_TRIPLET = { "arm64_v8a": "aarch64-linux-android", @@ -191,11 +191,9 @@ def setup_env( log.step("Setting up build environment...") build_frontend = build_options.build_frontend.name use_uv = build_frontend == "build[uv]" - 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"] + if use_uv: + ensure_uv() + pip = ["pip"] if not use_uv else ["uv", "pip"] # Create virtual environment python_exe = create_python_build_standalone_environment( @@ -578,11 +576,9 @@ def test_wheel(state: BuildState, wheel: Path, *, build_frontend: str) -> None: log.step("Testing wheel...") use_uv = build_frontend == "build[uv]" - 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"] + if use_uv: + ensure_uv() + pip = ["pip"] if not use_uv else ["uv", "pip"] native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android") if state.config.arch != native_arch: diff --git a/cibuildwheel/platforms/macos.py b/cibuildwheel/platforms/macos.py index 32f3d81b6..2807d5e4a 100644 --- a/cibuildwheel/platforms/macos.py +++ b/cibuildwheel/platforms/macos.py @@ -33,7 +33,7 @@ ) from ..util.helpers import prepare_command, unwrap from ..util.packaging import find_compatible_wheel, get_pip_version -from ..venv import constraint_flags, find_uv, virtualenv +from ..venv import constraint_flags, ensure_uv, virtualenv @functools.cache @@ -217,8 +217,9 @@ def setup_python( environment: ParsedEnvironment, build_frontend: BuildFrontendName, ) -> tuple[Path, dict[str, str]]: - uv_path = find_uv() use_uv = build_frontend == "build[uv]" + if use_uv: + ensure_uv() tmp.mkdir() implementation_id = python_configuration.identifier.split("-")[0] @@ -370,14 +371,13 @@ def setup_python( env=env, ) case "build[uv]": - assert uv_path is not None call( - uv_path, + "uv", "pip", "install", "--upgrade", "delocate", - "build[virtualenv, uv]", + "build", *constraint_flags(dependency_constraint), env=env, ) @@ -414,11 +414,9 @@ def build(options: Options, tmp_path: Path) -> None: build_options = options.build_options(config.identifier) build_frontend = build_options.build_frontend use_uv = build_frontend.name == "build[uv]" - 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"] + if use_uv: + ensure_uv() + pip = ["pip"] if not use_uv else ["uv", "pip"] log.build_start(config.identifier) identifier_tmp_dir = tmp_path / config.identifier diff --git a/cibuildwheel/platforms/windows.py b/cibuildwheel/platforms/windows.py index 7a38fd228..a16acb76b 100644 --- a/cibuildwheel/platforms/windows.py +++ b/cibuildwheel/platforms/windows.py @@ -24,7 +24,7 @@ from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file from ..util.helpers import prepare_command, unwrap from ..util.packaging import find_compatible_wheel, get_pip_version -from ..venv import constraint_flags, find_uv, virtualenv +from ..venv import constraint_flags, ensure_uv, virtualenv def get_nuget_args( @@ -272,7 +272,8 @@ def setup_python( build_frontend = "build" use_uv = build_frontend == "build[uv]" - uv_path = find_uv() + if use_uv: + ensure_uv() log.step("Setting up build environment...") venv_path = tmp / "venv" @@ -323,13 +324,12 @@ def setup_python( env=env, ) case "build[uv]": - assert uv_path is not None call( - uv_path, + "uv", "pip", "install", "--upgrade", - "build[virtualenv]", + "build", *constraint_flags(dependency_constraint), env=env, ) diff --git a/cibuildwheel/venv.py b/cibuildwheel/venv.py index e4d3c8720..fc9d7a856 100644 --- a/cibuildwheel/venv.py +++ b/cibuildwheel/venv.py @@ -1,4 +1,3 @@ -import contextlib import functools import os import shutil @@ -108,6 +107,7 @@ def virtualenv( assert python.exists() if use_uv: + ensure_uv() call("uv", "venv", venv_path, "--python", python) else: virtualenv_app, virtualenv_version = _ensure_virtualenv(version) @@ -156,12 +156,17 @@ def virtualenv( return venv_env -def find_uv() -> Path | None: - # Prefer uv in our environment - with contextlib.suppress(ImportError, FileNotFoundError): - from uv import find_uv_bin # noqa: PLC0415 - - return Path(find_uv_bin()) +def ensure_uv() -> None: + """ + Ensures uv is available on PATH. Raises an error with a helpful message if not found. - uv_on_path = shutil.which("uv") - return Path(uv_on_path) if uv_on_path else None + When using build-frontend=build[uv], callers must ensure uv is available + on PATH before invoking cibuildwheel. + """ + if shutil.which("uv") is None: + msg = ( + "uv not found on PATH. When using build-frontend=build[uv], " + "ensure uv is installed and available on PATH. " + "You can install it with 'pip install uv' or see https://docs.astral.sh/uv/getting-started/installation/" + ) + raise FileNotFoundError(msg) diff --git a/docs/ci-services.md b/docs/ci-services.md index 4a1e2009c..4ce7044f9 100644 --- a/docs/ci-services.md +++ b/docs/ci-services.md @@ -25,7 +25,7 @@ To build Linux, macOS, and Windows wheels using GitHub Actions, create a `.githu `package-dir: .`, `output-dir: wheelhouse` and `config-file: ''` locations (those values are the defaults). You can also pass a comma-separated list of extras to install additional packages. - For example, `extras: "uv"` to install UV into the virtual environment. + For example, `extras: "uv"` to make uv available for cibuildwheel to use. !!! tab "pipx" The GitHub Actions runners have pipx installed, so you can easily build in diff --git a/docs/options.md b/docs/options.md index c94cb0424..c91548d3a 100644 --- a/docs/options.md +++ b/docs/options.md @@ -470,13 +470,19 @@ Default: `build` Choose which build frontend to use. -You can 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, which is -possible by manually passing `cibuildwheel[uv]` to installers or by using the -`extras` option in the [cibuildwheel action](ci-services.md#github-actions). +You can use "build\[uv\]", which will use [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. + +When using `build[uv]`, ensure uv is available on PATH. You can do this by: + +- Pre-installing uv (e.g., via `pip install uv` or using `astral-sh/setup-uv` + in GitHub Actions) +- Installing cibuildwheel with the `uv` extra by manually passing + `cibuildwheel[uv]` to installers +- Using the `extras: "uv"` option in the [cibuildwheel action](ci-services.md#github-actions), + which installs uv in an isolated environment + uv currently does not support iOS or musllinux on s390x, ppc64le and riscv64. On Android and Pyodide, the "pip" frontend is not supported. diff --git a/test/conftest.py b/test/conftest.py index e05ef82d9..1ce8f8c9d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import json import os +import shutil import subprocess from collections.abc import Generator @@ -11,7 +12,6 @@ from cibuildwheel.options import CommandLineArguments, Options from cibuildwheel.selector import EnableGroup from cibuildwheel.typing import PLATFORMS -from cibuildwheel.venv import find_uv from .utils import DEFAULT_CIBW_ENABLE, EMULATED_ARCHS, get_platform @@ -180,10 +180,10 @@ def build_frontend_env(request: pytest.FixtureRequest) -> dict[str, str]: pytest.skip(f"Can't use pip as build frontend for {platform}") if platform == "pyodide" and frontend == "build[uv]": pytest.skip("Can't use uv with pyodide yet") - uv_path = find_uv() - if uv_path is None and frontend == "build[uv]": + uv_available = shutil.which("uv") is not None + if not uv_available and frontend == "build[uv]": pytest.skip("Can't find uv, so skipping uv tests") - if uv_path is not None and frontend == "build" and platform not in {"android", "ios"}: + if uv_available and frontend == "build" and platform not in {"android", "ios"}: pytest.skip("No need to check build when uv is present") return {"CIBW_BUILD_FRONTEND": frontend}