Skip to content

Commit e739e56

Browse files
committed
feat: support uv with iOS
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent ccbae30 commit e739e56

File tree

2 files changed

+97
-48
lines changed

2 files changed

+97
-48
lines changed

cibuildwheel/platforms/ios.py

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ..util.packaging import (
3232
find_compatible_wheel,
3333
)
34-
from ..venv import constraint_flags, virtualenv
34+
from ..venv import constraint_flags, find_uv, virtualenv
3535
from .macos import install_cpython as install_build_cpython
3636

3737
if TYPE_CHECKING:
@@ -153,6 +153,7 @@ def cross_virtualenv(
153153
venv_path: Path,
154154
dependency_constraint: Path | None,
155155
xbuild_tools: Sequence[str] | None,
156+
build_frontend: str,
156157
) -> dict[str, str]:
157158
"""Create a cross-compilation virtual environment.
158159
@@ -183,13 +184,15 @@ def cross_virtualenv(
183184
:param xbuild_tools: A list of executable names (without paths) that are
184185
on the path, but must be preserved in the cross environment.
185186
"""
187+
use_uv = build_frontend == "build[uv]"
188+
186189
# Create an initial macOS virtual environment
187190
env = virtualenv(
188191
py_version,
189192
build_python,
190193
venv_path,
191194
dependency_constraint,
192-
use_uv=False,
195+
use_uv=use_uv,
193196
)
194197

195198
# Convert the macOS virtual environment into an iOS virtual environment
@@ -284,9 +287,12 @@ def setup_python(
284287
build_frontend: BuildFrontendName,
285288
xbuild_tools: Sequence[str] | None,
286289
) -> tuple[Path, dict[str, str]]:
287-
if build_frontend == "build[uv]":
288-
msg = "uv doesn't support iOS"
289-
raise errors.FatalError(msg)
290+
use_uv = build_frontend == "build[uv]"
291+
uv_path = find_uv()
292+
if use_uv and uv_path is None:
293+
msg = "uv not found"
294+
raise AssertionError(msg)
295+
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
290296

291297
# An iOS environment requires 2 python installs - one for the build machine
292298
# (macOS), and one for the target (iOS). We'll only ever interact with the
@@ -336,6 +342,7 @@ def setup_python(
336342
venv_path=venv_path,
337343
dependency_constraint=dependency_constraint,
338344
xbuild_tools=xbuild_tools,
345+
build_frontend=build_frontend,
339346
)
340347
venv_bin_path = venv_path / "bin"
341348
assert venv_bin_path.exists()
@@ -345,16 +352,17 @@ def setup_python(
345352

346353
# upgrade pip to the version matching our constraints
347354
# if necessary, reinstall it to ensure that it's available on PATH as 'pip'
348-
pip = ["python", "-m", "pip"]
349-
call(
350-
*pip,
351-
"install",
352-
"--upgrade",
353-
"pip",
354-
*constraint_flags(dependency_constraint),
355-
env=env,
356-
cwd=venv_path,
357-
)
355+
if not use_uv:
356+
pip = ["python", "-m", "pip"]
357+
call(
358+
*pip,
359+
"install",
360+
"--upgrade",
361+
"pip",
362+
*constraint_flags(dependency_constraint),
363+
env=env,
364+
cwd=venv_path,
365+
)
358366

359367
# Apply our environment after pip is ready
360368
env = environment.as_dictionary(prev_environment=env)
@@ -372,17 +380,18 @@ def setup_python(
372380
call("python", "--version", env=env)
373381

374382
# Check what pip version we're on
375-
assert (venv_bin_path / "pip").exists()
376-
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
377-
print(which_pip)
378-
if which_pip != str(venv_bin_path / "pip"):
379-
msg = (
380-
"cibuildwheel: pip available on PATH doesn't match our installed instance. "
381-
"If you have modified PATH, ensure that you don't overwrite cibuildwheel's "
382-
"entry or insert pip above it."
383-
)
384-
raise errors.FatalError(msg)
385-
call("pip", "--version", env=env)
383+
if not use_uv:
384+
assert (venv_bin_path / "pip").exists()
385+
which_pip = call("which", "pip", env=env, capture_stdout=True).strip()
386+
print(which_pip)
387+
if which_pip != str(venv_bin_path / "pip"):
388+
msg = (
389+
"cibuildwheel: pip available on PATH doesn't match our installed instance. "
390+
"If you have modified PATH, ensure that you don't overwrite cibuildwheel's "
391+
"entry or insert pip above it."
392+
)
393+
raise errors.FatalError(msg)
394+
call("pip", "--version", env=env)
386395

387396
# Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment
388397
env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
@@ -394,13 +403,22 @@ def setup_python(
394403
pass
395404
case "build":
396405
call(
397-
"pip",
406+
*pip,
398407
"install",
399408
"--upgrade",
400409
"build[virtualenv]",
401410
*constraint_flags(dependency_constraint),
402411
env=env,
403412
)
413+
case "build[uv]":
414+
call(
415+
*pip,
416+
"install",
417+
"--upgrade",
418+
"build",
419+
*constraint_flags(dependency_constraint),
420+
env=env,
421+
)
404422
case _:
405423
assert_never(build_frontend)
406424

@@ -440,10 +458,12 @@ def build(options: Options, tmp_path: Path) -> None:
440458
for config in python_configurations:
441459
build_options = options.build_options(config.identifier)
442460
build_frontend = build_options.build_frontend
443-
# uv doesn't support iOS
444-
if build_frontend.name == "build[uv]":
445-
msg = "uv doesn't support iOS"
446-
raise errors.FatalError(msg)
461+
use_uv = build_frontend.name == "build[uv]"
462+
uv_path = find_uv()
463+
if use_uv and uv_path is None:
464+
msg = "uv not found"
465+
raise AssertionError(msg)
466+
pip = ["pip"] if not use_uv else [str(uv_path), "pip"]
447467

448468
log.build_start(config.identifier)
449469

@@ -517,6 +537,18 @@ def build(options: Options, tmp_path: Path) -> None:
517537
*extra_flags,
518538
env=env,
519539
)
540+
case "build[uv]":
541+
call(
542+
"python",
543+
"-m",
544+
"build",
545+
build_options.package_dir,
546+
"--wheel",
547+
"--installer=uv",
548+
f"--outdir={built_wheel_dir}",
549+
*extra_flags,
550+
env=env,
551+
)
520552
case _:
521553
assert_never(build_frontend)
522554

@@ -576,20 +608,35 @@ def build(options: Options, tmp_path: Path) -> None:
576608
ios_version = test_env["IPHONEOS_DEPLOYMENT_TARGET"]
577609
platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}"
578610

579-
call(
580-
"python",
581-
"-m",
582-
"pip",
583-
"install",
584-
"--only-binary=:all:",
585-
"--platform",
586-
platform_tag,
587-
"--target",
588-
testbed_path / "iOSTestbed" / "app_packages",
589-
f"{test_wheel}{build_options.test_extras}",
590-
*build_options.test_requires,
591-
env=test_env,
592-
)
611+
if use_uv:
612+
call(
613+
*pip,
614+
"install",
615+
"--only-binary=:all:",
616+
"--platform",
617+
platform_tag,
618+
"--target",
619+
testbed_path / "iOSTestbed" / "app_packages",
620+
f"{test_wheel}{build_options.test_extras}",
621+
*build_options.test_requires,
622+
env=test_env,
623+
)
624+
625+
else:
626+
call(
627+
"python",
628+
"-m",
629+
"pip",
630+
"install",
631+
"--only-binary=:all:",
632+
"--platform",
633+
platform_tag,
634+
"--target",
635+
testbed_path / "iOSTestbed" / "app_packages",
636+
f"{test_wheel}{build_options.test_extras}",
637+
*build_options.test_requires,
638+
env=test_env,
639+
)
593640

594641
log.step("Running test suite...")
595642

test/test_ios.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ def skip_if_ios_testing_not_supported() -> None:
5555
@pytest.mark.parametrize(
5656
"build_config",
5757
[
58-
# Default to the pip build frontend
58+
# Check the default build frontend
5959
{"CIBW_PLATFORM": "ios"},
60-
# Also check the build frontend
61-
{"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"},
60+
# Check the build[uv] frontend
61+
{"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build[uv]"},
62+
# Check the pip frontend
63+
{"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "pip"},
6264
],
6365
)
6466
def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):

0 commit comments

Comments
 (0)