From 0a1c6052df85023e3b29c610ccf5a4c2a10c4c09 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Sep 2025 16:49:15 -0400 Subject: [PATCH 1/5] feat: support uv with iOS Signed-off-by: Henry Schreiner --- cibuildwheel/platforms/ios.py | 137 +++++++++++++++++++++++----------- test/test_ios.py | 8 +- 2 files changed, 97 insertions(+), 48 deletions(-) diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index d7c7f3ccd..5ba1f8b30 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -31,7 +31,7 @@ from ..util.packaging import ( find_compatible_wheel, ) -from ..venv import constraint_flags, virtualenv +from ..venv import constraint_flags, find_uv, virtualenv from .macos import install_cpython as install_build_cpython if TYPE_CHECKING: @@ -153,6 +153,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. @@ -183,13 +184,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 @@ -284,9 +287,12 @@ def setup_python( build_frontend: BuildFrontendName, xbuild_tools: Sequence[str] | None, ) -> tuple[Path, dict[str, str]]: - if build_frontend == "build[uv]": - msg = "uv doesn't support iOS" - 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 @@ -336,6 +342,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() @@ -345,16 +352,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) @@ -372,17 +380,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") @@ -394,13 +403,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) @@ -440,10 +458,12 @@ def build(options: Options, tmp_path: Path) -> None: for config in python_configurations: build_options = options.build_options(config.identifier) build_frontend = build_options.build_frontend - # uv doesn't support iOS - if build_frontend.name == "build[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) @@ -517,6 +537,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) @@ -576,20 +608,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 be64e36f2..94440aa25 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -55,10 +55,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 b839dec517d75d4e32174847ae55387b9676b841 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 10 Nov 2025 23:57:20 -0500 Subject: [PATCH 2/5] 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 5ba1f8b30..02565cd64 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -72,6 +72,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 @@ -415,6 +420,8 @@ def setup_python( *pip, "install", "--upgrade", + "--python-platform", + python_configuration.python_platform, "build", *constraint_flags(dependency_constraint), env=env, @@ -613,8 +620,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 605c0fac1928a31757e3fea8d4f15e3470cce7a8 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 11 Nov 2025 09:22:07 -0500 Subject: [PATCH 3/5] 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 6b9f3c8fc..dbe3667fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,7 +98,15 @@ jobs: python-version: ${{ matrix.python_version }} allow-prereleases: true - - uses: astral-sh/setup-uv@v7 + - 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' From ecc0e4b877f4f63aa8b23e3326081b70105266b6 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 12 Nov 2025 13:24:39 -0500 Subject: [PATCH 4/5] 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 dbe3667fd..b67ed41b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,7 +101,6 @@ jobs: - run: | gh repo clone astral-sh/uv cd uv - gh co 16686 cargo build --release env: GH_TOKEN: ${{ github.token }} From 39feb380ede086d58262e74c23122f047052760a Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 12 Nov 2025 14:17:30 -0500 Subject: [PATCH 5/5] Replace manual UV setup with action setup-uv@v7 --- .github/workflows/test.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b67ed41b0..ba4f61ed3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,14 +98,9 @@ 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@v7 + with: + version: "0.9.9" - name: Free up disk space if: runner.os == 'Linux'