From b91818f545353cb5274eeb7b59f2c2932e87c585 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 16:49:15 -0400 Subject: [PATCH 1/6] feat: support uv with iOS Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/ios.py | 139 +++++++++++++++++++++++----------- test/test_ios.py | 8 +- 2 files changed, 101 insertions(+), 46 deletions(-) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 9e2474483..ee6bec398 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -25,7 +25,7 @@ from cibuildwheel.util.file import CIBW_CACHE_PATH, copy_test_sources, download, move_file from cibuildwheel.util.helpers import prepare_command, unwrap_preserving_paragraphs from cibuildwheel.util.packaging import find_compatible_wheel -from cibuildwheel.venv import constraint_flags, virtualenv +from cibuildwheel.venv import constraint_flags, find_uv, virtualenv @dataclasses.dataclass(frozen=True, kw_only=True) @@ -138,6 +138,7 @@ def cross_virtualenv( venv_path: Path, dependency_constraint: Path | None, xbuild_tools: Sequence[str] | None, + build_frontend: str, ) -> dict[str, str]: """Create a cross-compilation virtual environment. @@ -168,13 +169,15 @@ def cross_virtualenv( :param xbuild_tools: A list of executable names (without paths) that are on the path, but must be preserved in the cross environment. """ + use_uv = build_frontend == "build[uv]" + # Create an initial macOS virtual environment env = virtualenv( py_version, build_python, venv_path, dependency_constraint, - use_uv=False, + use_uv=use_uv, ) # Convert the macOS virtual environment into an iOS virtual environment @@ -269,10 +272,15 @@ def setup_python( build_frontend: BuildFrontendName, xbuild_tools: Sequence[str] | None, ) -> tuple[Path, dict[str, str]]: - # Not using set because mypy can't narrow it - if build_frontend == "build[uv]" or build_frontend == "uv": # noqa: PLR1714 - msg = "uv doesn't support iOS" + if build_frontend == "uv": + msg = "uv doesn't support iOS, use build[uv] for now" raise errors.FatalError(msg) + 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"] # An iOS environment requires 2 python installs - one for the build machine # (macOS), and one for the target (iOS). We'll only ever interact with the @@ -322,6 +330,7 @@ def setup_python( venv_path=venv_path, dependency_constraint=dependency_constraint, xbuild_tools=xbuild_tools, + build_frontend=build_frontend, ) venv_bin_path = venv_path / "bin" assert venv_bin_path.exists() @@ -331,16 +340,17 @@ def setup_python( # upgrade pip to the version matching our constraints # if necessary, reinstall it to ensure that it's available on PATH as 'pip' - pip = ["python", "-m", "pip"] - call( - *pip, - "install", - "--upgrade", - "pip", - *constraint_flags(dependency_constraint), - env=env, - cwd=venv_path, - ) + if not use_uv: + pip = ["python", "-m", "pip"] + call( + *pip, + "install", + "--upgrade", + "pip", + *constraint_flags(dependency_constraint), + env=env, + cwd=venv_path, + ) # Apply our environment after pip is ready env = environment.as_dictionary(prev_environment=env) @@ -358,17 +368,18 @@ def setup_python( call("python", "--version", env=env) # Check what pip version we're on - assert (venv_bin_path / "pip").exists() - which_pip = call("which", "pip", env=env, capture_stdout=True).strip() - print(which_pip) - if which_pip != str(venv_bin_path / "pip"): - msg = ( - "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." - ) - raise errors.FatalError(msg) - call("pip", "--version", env=env) + if not use_uv: + assert (venv_bin_path / "pip").exists() + which_pip = call("which", "pip", env=env, capture_stdout=True).strip() + print(which_pip) + if which_pip != str(venv_bin_path / "pip"): + msg = ( + "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." + ) + raise errors.FatalError(msg) + call("pip", "--version", env=env) # Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0") @@ -380,13 +391,22 @@ def setup_python( pass case "build": call( - "pip", + *pip, "install", "--upgrade", "build[virtualenv]", *constraint_flags(dependency_constraint), env=env, ) + case "build[uv]": + call( + *pip, + "install", + "--upgrade", + "build", + *constraint_flags(dependency_constraint), + env=env, + ) case _: assert_never(build_frontend) @@ -427,11 +447,17 @@ def build(options: Options, tmp_path: Path) -> None: build_options = options.build_options(config.identifier) build_frontend = build_options.build_frontend # uv doesn't support iOS - # Not using set because mypy can't narrow it - if build_frontend.name == "build[uv]" or build_frontend.name == "uv": # noqa: PLR1714 + if build_frontend.name == "uv": msg = "uv doesn't support iOS" raise errors.FatalError(msg) + 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"] + log.build_start(config.identifier) identifier_tmp_dir = tmp_path / config.identifier @@ -508,6 +534,18 @@ def build(options: Options, tmp_path: Path) -> None: *extra_flags, env=env, ) + case "build[uv]": + call( + "python", + "-m", + "build", + build_options.package_dir, + "--wheel", + "--installer=uv", + f"--outdir={built_wheel_dir}", + *extra_flags, + env=env, + ) case _: assert_never(build_frontend) @@ -592,20 +630,35 @@ def build(options: Options, tmp_path: Path) -> None: ios_version = test_env["IPHONEOS_DEPLOYMENT_TARGET"] platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}" - call( - "python", - "-m", - "pip", - "install", - "--only-binary=:all:", - "--platform", - platform_tag, - "--target", - testbed_path / "iOSTestbed" / "app_packages", - f"{test_wheel}{build_options.test_extras}", - *build_options.test_requires, - env=test_env, - ) + if use_uv: + call( + *pip, + "install", + "--only-binary=:all:", + "--platform", + platform_tag, + "--target", + testbed_path / "iOSTestbed" / "app_packages", + f"{test_wheel}{build_options.test_extras}", + *build_options.test_requires, + env=test_env, + ) + + else: + call( + "python", + "-m", + "pip", + "install", + "--only-binary=:all:", + "--platform", + platform_tag, + "--target", + testbed_path / "iOSTestbed" / "app_packages", + f"{test_wheel}{build_options.test_extras}", + *build_options.test_requires, + env=test_env, + ) log.step("Running test suite...") diff --git a/test/test_ios.py b/test/test_ios.py index ebf30c511..0ca4b9720 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -53,10 +53,12 @@ def skip_if_ios_testing_not_supported() -> None: @pytest.mark.parametrize( "build_config", [ - # Default to the pip build frontend + # Check the default build frontend {"CIBW_PLATFORM": "ios"}, - # Also check the build frontend - {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"}, + # Check the build[uv] frontend + {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build[uv]"}, + # Check the pip frontend + {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "pip"}, ], ) def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd): From f7a36523c7657bae7639de02e6105299426479d8 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 10 Nov 2025 23:57:20 -0500 Subject: [PATCH 2/6] fix: pass through python-platform to uv Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/ios.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index ee6bec398..07b38bf29 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -57,6 +57,11 @@ def xcframework_slice(self) -> str: "XCframeworks include binaries for multiple ABIs; which ABI section should be used?" return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64" + @property + def python_platform(self) -> str: + sim = "-simulator" if self.is_simulator else "" + return f"{self.arch}-apple-ios{sim}" + def all_python_configurations() -> list[PythonConfiguration]: # iOS builds are always cross builds; we need to install a macOS Python as @@ -403,6 +408,8 @@ def setup_python( *pip, "install", "--upgrade", + "--python-platform", + python_configuration.python_platform, "build", *constraint_flags(dependency_constraint), env=env, @@ -635,8 +642,8 @@ def build(options: Options, tmp_path: Path) -> None: *pip, "install", "--only-binary=:all:", - "--platform", - platform_tag, + "--python-platform", + config.python_platform, "--target", testbed_path / "iOSTestbed" / "app_packages", f"{test_wheel}{build_options.test_extras}", From 2a940bc13a30f8aaa590236a502cd88cf901275a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 11 Nov 2025 09:22:07 -0500 Subject: [PATCH 3/6] WIP: try uv PR Signed-off-by: Henry Schreiner --- .github/workflows/test.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52c7a8db2..52502b9ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,7 +128,15 @@ jobs: python-version: ${{ matrix.python_version }} allow-prereleases: true - - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + - run: | + gh repo clone astral-sh/uv + cd uv + gh co 16686 + cargo build --release + env: + GH_TOKEN: ${{ github.token }} + + - run: echo "${{ github.workspace }}/uv/target/release" >> $GITHUB_PATH - name: Free up disk space if: runner.os == 'Linux' && matrix.test_select != 'android' From 14843d0ee1f2f3b75093e36f7acaf1d162801e32 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 12 Nov 2025 13:24:39 -0500 Subject: [PATCH 4/6] Remove checkout command for pull request 16686 Removed the command to checkout a specific pull request. --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52502b9ed..102bc65f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -131,7 +131,6 @@ jobs: - run: | gh repo clone astral-sh/uv cd uv - gh co 16686 cargo build --release env: GH_TOKEN: ${{ github.token }} From 1ed0a7a90d5c458c7cacc614fd1b3512dee41f14 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 12 Nov 2025 14:17:30 -0500 Subject: [PATCH 5/6] Replace manual UV setup with action setup-uv@v7 Update .github/workflows/test.yml --- .github/workflows/test.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 102bc65f5..52c7a8db2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,14 +128,7 @@ jobs: python-version: ${{ matrix.python_version }} allow-prereleases: true - - run: | - gh repo clone astral-sh/uv - cd uv - cargo build --release - env: - GH_TOKEN: ${{ github.token }} - - - run: echo "${{ github.workspace }}/uv/target/release" >> $GITHUB_PATH + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Free up disk space if: runner.os == 'Linux' && matrix.test_select != 'android' From e6926186f8aa1fbedffe23646f383dcd1d19a76e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 25 Mar 2026 13:01:07 -0400 Subject: [PATCH 6/6] fix: make sure build can access uv Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/ios.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 07b38bf29..7a3cc4727 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -245,6 +245,13 @@ def cross_virtualenv( print(f"{tool!r} will be included in the cross-build environment (using {original})") (xbuild_tools_path / tool).symlink_to(original) + # build[uv] runs `python -m build --installer=uv`, and build resolves `uv` + # from PATH. Ensure uv is available within the isolated tool path. + if use_uv: + uv_binary = find_uv() + if uv_binary is not None: + (xbuild_tools_path / "uv").symlink_to(uv_binary.resolve()) + env["PATH"] = os.pathsep.join( [ # The target python's binary directory