Compare commits

...
Author SHA1 Message Date
Joe Rickerby 355c5a46b2 Fix PATH manipulation in action.yml 2025-12-21 16:05:43 +00:00
Joe Rickerby bc79c16a71 Another approach to the uv installation question
Don't get clever about it at all, instead make uv on PATH a hard
requirement.
2025-12-19 17:20:39 +00:00
8 changed files with 77 additions and 80 deletions
+25 -33
View File
@@ -47,8 +47,6 @@ runs:
from subprocess import run from subprocess import run
EXTRAS = set(e.strip() for e in "${{ inputs.extras }}".split(",") if e.strip()) EXTRAS = set(e.strip() for e in "${{ inputs.extras }}".split(",") if e.strip())
if sys.platform == "linux":
EXTRAS.discard("uv")
class EnvBuilder(venv.EnvBuilder): class EnvBuilder(venv.EnvBuilder):
@@ -73,47 +71,41 @@ runs:
shutil.rmtree(venv_path) shutil.rmtree(venv_path)
builder = EnvBuilder() builder = EnvBuilder()
builder.create(venv_path) builder.create(venv_path)
exposed_binaries = {"cibuildwheel"} cibw_bin = [p for p in builder.bin_path.glob("cibuildwheel*") if p.stem == "cibuildwheel"][0]
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
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: 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::") print("::endgroup::")
EOF EOF
shell: bash shell: bash
# Redirecting stderr to stdout to fix interleaving issue in Actions. # Redirecting stderr to stdout to fix interleaving issue in Actions.
- run: > - run: |
cibuildwheel prepend_path="${{ steps.cibw.outputs.prepend-path }}"
"${{ inputs.package-dir }}" if [ -n "$prepend_path" ]; then
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} export PATH="$prepend_path:$PATH"
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} fi
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} "${{ steps.cibw.outputs.cibw-bin }}" \
2>&1 "${{ inputs.package-dir }}" \
env: ${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} \
PATH: "${{ steps.cibw.outputs.updated-path }}" ${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} \
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} \
2>&1
shell: bash shell: bash
if: runner.os != 'Windows' if: runner.os != 'Windows'
# Windows needs powershell to interact nicely with Meson # Windows needs powershell to interact nicely with Meson
- run: > - run: |
cibuildwheel $PrependPath = "${{ steps.cibw.outputs.prepend-path }}"
"${{ inputs.package-dir }}" if ($PrependPath) {
${{ inputs.output-dir != '' && format('--output-dir "{0}"', inputs.output-dir) || ''}} $env:PATH = "$PrependPath;$env:PATH"
${{ inputs.config-file != '' && format('--config-file "{0}"', inputs.config-file) || ''}} }
${{ inputs.only != '' && format('--only "{0}"', inputs.only) || ''}} & "${{ steps.cibw.outputs.cibw-bin }}" `
env: "${{ inputs.package-dir }}" `
PATH: "${{ steps.cibw.outputs.updated-path }}" ${{ 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 shell: pwsh
if: runner.os == 'Windows' if: runner.os == 'Windows'
+7 -11
View File
@@ -34,7 +34,7 @@ from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file
from ..util.helpers import prepare_command from ..util.helpers import prepare_command
from ..util.packaging import find_compatible_wheel from ..util.packaging import find_compatible_wheel
from ..util.python_build_standalone import create_python_build_standalone_environment 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 = { ANDROID_TRIPLET = {
"arm64_v8a": "aarch64-linux-android", "arm64_v8a": "aarch64-linux-android",
@@ -191,11 +191,9 @@ def setup_env(
log.step("Setting up build environment...") log.step("Setting up build environment...")
build_frontend = build_options.build_frontend.name build_frontend = build_options.build_frontend.name
use_uv = build_frontend == "build[uv]" use_uv = build_frontend == "build[uv]"
uv_path = find_uv() if use_uv:
if use_uv and uv_path is None: ensure_uv()
msg = "uv not found" pip = ["pip"] if not use_uv else ["uv", "pip"]
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
# Create virtual environment # Create virtual environment
python_exe = create_python_build_standalone_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...") log.step("Testing wheel...")
use_uv = build_frontend == "build[uv]" use_uv = build_frontend == "build[uv]"
uv_path = find_uv() if use_uv:
if use_uv and uv_path is None: ensure_uv()
msg = "uv not found" pip = ["pip"] if not use_uv else ["uv", "pip"]
raise AssertionError(msg)
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android") native_arch = arch_synonym(platform.machine(), platforms.native_platform(), "android")
if state.config.arch != native_arch: if state.config.arch != native_arch:
+8 -10
View File
@@ -33,7 +33,7 @@ from ..util.file import (
) )
from ..util.helpers import prepare_command, unwrap from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel, get_pip_version 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 @functools.cache
@@ -217,8 +217,9 @@ def setup_python(
environment: ParsedEnvironment, environment: ParsedEnvironment,
build_frontend: BuildFrontendName, build_frontend: BuildFrontendName,
) -> tuple[Path, dict[str, str]]: ) -> tuple[Path, dict[str, str]]:
uv_path = find_uv()
use_uv = build_frontend == "build[uv]" use_uv = build_frontend == "build[uv]"
if use_uv:
ensure_uv()
tmp.mkdir() tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0] implementation_id = python_configuration.identifier.split("-")[0]
@@ -370,14 +371,13 @@ def setup_python(
env=env, env=env,
) )
case "build[uv]": case "build[uv]":
assert uv_path is not None
call( call(
uv_path, "uv",
"pip", "pip",
"install", "install",
"--upgrade", "--upgrade",
"delocate", "delocate",
"build[virtualenv, uv]", "build",
*constraint_flags(dependency_constraint), *constraint_flags(dependency_constraint),
env=env, env=env,
) )
@@ -414,11 +414,9 @@ def build(options: Options, tmp_path: Path) -> None:
build_options = options.build_options(config.identifier) build_options = options.build_options(config.identifier)
build_frontend = build_options.build_frontend build_frontend = build_options.build_frontend
use_uv = build_frontend.name == "build[uv]" use_uv = build_frontend.name == "build[uv]"
uv_path = find_uv() if use_uv:
if use_uv and uv_path is None: ensure_uv()
msg = "uv not found" pip = ["pip"] if not use_uv else ["uv", "pip"]
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
+5 -5
View File
@@ -24,7 +24,7 @@ from ..util.cmd import call, shell
from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file from ..util.file import CIBW_CACHE_PATH, copy_test_sources, download, extract_zip, move_file
from ..util.helpers import prepare_command, unwrap from ..util.helpers import prepare_command, unwrap
from ..util.packaging import find_compatible_wheel, get_pip_version 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( def get_nuget_args(
@@ -272,7 +272,8 @@ def setup_python(
build_frontend = "build" build_frontend = "build"
use_uv = build_frontend == "build[uv]" use_uv = build_frontend == "build[uv]"
uv_path = find_uv() if use_uv:
ensure_uv()
log.step("Setting up build environment...") log.step("Setting up build environment...")
venv_path = tmp / "venv" venv_path = tmp / "venv"
@@ -323,13 +324,12 @@ def setup_python(
env=env, env=env,
) )
case "build[uv]": case "build[uv]":
assert uv_path is not None
call( call(
uv_path, "uv",
"pip", "pip",
"install", "install",
"--upgrade", "--upgrade",
"build[virtualenv]", "build",
*constraint_flags(dependency_constraint), *constraint_flags(dependency_constraint),
env=env, env=env,
) )
+14 -9
View File
@@ -1,4 +1,3 @@
import contextlib
import functools import functools
import os import os
import shutil import shutil
@@ -108,6 +107,7 @@ def virtualenv(
assert python.exists() assert python.exists()
if use_uv: if use_uv:
ensure_uv()
call("uv", "venv", venv_path, "--python", python) call("uv", "venv", venv_path, "--python", python)
else: else:
virtualenv_app, virtualenv_version = _ensure_virtualenv(version) virtualenv_app, virtualenv_version = _ensure_virtualenv(version)
@@ -156,12 +156,17 @@ def virtualenv(
return venv_env return venv_env
def find_uv() -> Path | None: def ensure_uv() -> None:
# Prefer uv in our environment """
with contextlib.suppress(ImportError, FileNotFoundError): Ensures uv is available on PATH. Raises an error with a helpful message if not found.
from uv import find_uv_bin # noqa: PLC0415
return Path(find_uv_bin()) When using build-frontend=build[uv], callers must ensure uv is available
on PATH before invoking cibuildwheel.
uv_on_path = shutil.which("uv") """
return Path(uv_on_path) if uv_on_path else None 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)
+1 -1
View File
@@ -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: ''` `package-dir: .`, `output-dir: wheelhouse` and `config-file: ''`
locations (those values are the defaults). You can also pass a locations (those values are the defaults). You can also pass a
comma-separated list of extras to install additional packages. 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" !!! tab "pipx"
The GitHub Actions runners have pipx installed, so you can easily build in The GitHub Actions runners have pipx installed, so you can easily build in
+13 -7
View File
@@ -470,13 +470,19 @@ Default: `build`
Choose which build frontend to use. Choose which build frontend to use.
You can use "build\[uv\]", which will use an external [uv][] everywhere You can use "build\[uv\]", which will use [uv][] everywhere possible, both
possible, both through `--installer=uv` passed to build, as well as when making through `--installer=uv` passed to build, as well as when making all build and
all build and test environments. This will generally speed up cibuildwheel. 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 When using `build[uv]`, ensure uv is available on PATH. You can do this by:
possible by manually passing `cibuildwheel[uv]` to installers or by using the
`extras` option in the [cibuildwheel action](ci-services.md#github-actions). - 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. uv currently does not support iOS or musllinux on s390x, ppc64le and riscv64.
On Android and Pyodide, the "pip" frontend is not supported. On Android and Pyodide, the "pip" frontend is not supported.
+4 -4
View File
@@ -1,5 +1,6 @@
import json import json
import os import os
import shutil
import subprocess import subprocess
from collections.abc import Generator from collections.abc import Generator
@@ -11,7 +12,6 @@ from cibuildwheel.ci import detect_ci_provider
from cibuildwheel.options import CommandLineArguments, Options from cibuildwheel.options import CommandLineArguments, Options
from cibuildwheel.selector import EnableGroup from cibuildwheel.selector import EnableGroup
from cibuildwheel.typing import PLATFORMS from cibuildwheel.typing import PLATFORMS
from cibuildwheel.venv import find_uv
from .utils import DEFAULT_CIBW_ENABLE, EMULATED_ARCHS, get_platform 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}") pytest.skip(f"Can't use pip as build frontend for {platform}")
if platform == "pyodide" and frontend == "build[uv]": if platform == "pyodide" and frontend == "build[uv]":
pytest.skip("Can't use uv with pyodide yet") pytest.skip("Can't use uv with pyodide yet")
uv_path = find_uv() uv_available = shutil.which("uv") is not None
if uv_path is None and frontend == "build[uv]": if not uv_available and frontend == "build[uv]":
pytest.skip("Can't find uv, so skipping uv tests") 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") pytest.skip("No need to check build when uv is present")
return {"CIBW_BUILD_FRONTEND": frontend} return {"CIBW_BUILD_FRONTEND": frontend}