kivyforge 3.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kivyforge/__init__.py +9 -0
- kivyforge/artifacts/__init__.py +41 -0
- kivyforge/artifacts/cache.py +81 -0
- kivyforge/artifacts/collect.py +354 -0
- kivyforge/artifacts/download.py +125 -0
- kivyforge/artifacts/frameworks.py +240 -0
- kivyforge/artifacts/native_stage_util.py +346 -0
- kivyforge/artifacts/runtime.py +65 -0
- kivyforge/artifacts/verify.py +45 -0
- kivyforge/artifacts/wheels.py +89 -0
- kivyforge/bundle/__init__.py +1 -0
- kivyforge/bundle/pycompile.py +161 -0
- kivyforge/cli/__init__.py +52 -0
- kivyforge/cli/_common.py +62 -0
- kivyforge/cli/_platform.py +84 -0
- kivyforge/cli/build.py +111 -0
- kivyforge/cli/clean.py +135 -0
- kivyforge/cli/doctor.py +52 -0
- kivyforge/cli/init.py +612 -0
- kivyforge/cli/init_writer.py +716 -0
- kivyforge/cli/lock.py +261 -0
- kivyforge/cli/open_cmd.py +15 -0
- kivyforge/cli/package.py +134 -0
- kivyforge/cli/run.py +162 -0
- kivyforge/cli/status.py +20 -0
- kivyforge/cli/upgrade.py +314 -0
- kivyforge/config/__init__.py +46 -0
- kivyforge/config/errors.py +43 -0
- kivyforge/config/icons.py +60 -0
- kivyforge/config/loader.py +3079 -0
- kivyforge/config/model.py +817 -0
- kivyforge/doctor/__init__.py +19 -0
- kivyforge/doctor/checks_common.py +95 -0
- kivyforge/doctor/probe.py +357 -0
- kivyforge/doctor/result.py +41 -0
- kivyforge/lock/__init__.py +42 -0
- kivyforge/lock/_pip_shim.py +63 -0
- kivyforge/lock/find_links.py +182 -0
- kivyforge/lock/model.py +69 -0
- kivyforge/lock/pep751.py +111 -0
- kivyforge/lock/reader.py +30 -0
- kivyforge/lock/resolver.py +124 -0
- kivyforge/lock/wheelruntime/__init__.py +70 -0
- kivyforge/lock/wheelruntime/builder.py +280 -0
- kivyforge/lock/wheelruntime/model.py +94 -0
- kivyforge/lock/wheelruntime/native_binaries.py +127 -0
- kivyforge/lock/wheelruntime/pbs_github.py +133 -0
- kivyforge/lock/wheelruntime/profile.py +104 -0
- kivyforge/lock/wheelruntime/resolver.py +248 -0
- kivyforge/lock/wheelruntime/runtime.py +161 -0
- kivyforge/lock/wheelruntime/serialize.py +245 -0
- kivyforge/platforms/__init__.py +114 -0
- kivyforge/platforms/android/__init__.py +140 -0
- kivyforge/platforms/android/adb.py +215 -0
- kivyforge/platforms/android/bootstrap/__init__.py +7 -0
- kivyforge/platforms/android/bootstrap/contract.py +166 -0
- kivyforge/platforms/android/bootstrap/render.py +244 -0
- kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +76 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +50 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +97 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/main.c +150 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +75 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +132 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +189 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +276 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2120 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
- kivyforge/platforms/android/cli.py +1023 -0
- kivyforge/platforms/android/doctor.py +899 -0
- kivyforge/platforms/android/elf.py +148 -0
- kivyforge/platforms/android/generate/__init__.py +1 -0
- kivyforge/platforms/android/generate/manifest.py +261 -0
- kivyforge/platforms/android/generate/project.py +462 -0
- kivyforge/platforms/android/generate/services.py +187 -0
- kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
- kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
- kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
- kivyforge/platforms/android/gradlew.py +60 -0
- kivyforge/platforms/android/icons.py +237 -0
- kivyforge/platforms/android/lock/__init__.py +1 -0
- kivyforge/platforms/android/lock/archives.py +109 -0
- kivyforge/platforms/android/lock/builder.py +387 -0
- kivyforge/platforms/android/lock/markers.py +71 -0
- kivyforge/platforms/android/lock/maven.py +252 -0
- kivyforge/platforms/android/lock/model.py +113 -0
- kivyforge/platforms/android/lock/python_meta.py +199 -0
- kivyforge/platforms/android/lock/reader.py +206 -0
- kivyforge/platforms/android/lock/resolver.py +276 -0
- kivyforge/platforms/android/lock/writer.py +121 -0
- kivyforge/platforms/android/policy.py +214 -0
- kivyforge/platforms/android/signing.py +145 -0
- kivyforge/platforms/android/smoke.py +43 -0
- kivyforge/platforms/android/splash.py +293 -0
- kivyforge/platforms/android/stage/__init__.py +1 -0
- kivyforge/platforms/android/stage/bundle.py +295 -0
- kivyforge/platforms/android/stage/jnilibs.py +186 -0
- kivyforge/platforms/android/stage/runtime.py +61 -0
- kivyforge/platforms/android/stage/wheels.py +106 -0
- kivyforge/platforms/android/toolchain.py +29 -0
- kivyforge/platforms/base.py +157 -0
- kivyforge/platforms/ios/__init__.py +129 -0
- kivyforge/platforms/ios/assets.py +193 -0
- kivyforge/platforms/ios/buildsettings.py +128 -0
- kivyforge/platforms/ios/cli.py +543 -0
- kivyforge/platforms/ios/doctor.py +410 -0
- kivyforge/platforms/ios/entitlements.py +34 -0
- kivyforge/platforms/ios/generator.py +318 -0
- kivyforge/platforms/ios/lock/__init__.py +71 -0
- kivyforge/platforms/ios/lock/builder.py +393 -0
- kivyforge/platforms/ios/lock/markers.py +69 -0
- kivyforge/platforms/ios/lock/model.py +95 -0
- kivyforge/platforms/ios/lock/python_meta.py +187 -0
- kivyforge/platforms/ios/lock/reader.py +191 -0
- kivyforge/platforms/ios/lock/resolver.py +304 -0
- kivyforge/platforms/ios/lock/spm.py +217 -0
- kivyforge/platforms/ios/lock/writer.py +114 -0
- kivyforge/platforms/ios/lock/xcframework.py +188 -0
- kivyforge/platforms/ios/materialize.py +59 -0
- kivyforge/platforms/ios/plist.py +91 -0
- kivyforge/platforms/ios/privacy.py +39 -0
- kivyforge/platforms/ios/skeleton.py +165 -0
- kivyforge/platforms/ios/sources.py +63 -0
- kivyforge/platforms/ios/staging.py +225 -0
- kivyforge/platforms/ios/swift_packages.py +324 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +34 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +146 -0
- kivyforge/platforms/ios/templates/main.m +15 -0
- kivyforge/platforms/ios/templates/main_config.h.in +12 -0
- kivyforge/platforms/ios/xcframeworks.py +194 -0
- kivyforge/platforms/ios/xcode/__init__.py +76 -0
- kivyforge/platforms/ios/xcode/commands.py +296 -0
- kivyforge/platforms/ios/xcode/runner.py +316 -0
- kivyforge/platforms/linux/__init__.py +120 -0
- kivyforge/platforms/linux/appimage.py +169 -0
- kivyforge/platforms/linux/bundle.py +235 -0
- kivyforge/platforms/linux/cli.py +232 -0
- kivyforge/platforms/linux/desktop.py +71 -0
- kivyforge/platforms/linux/doctor.py +438 -0
- kivyforge/platforms/linux/elftools.py +95 -0
- kivyforge/platforms/linux/icons.py +129 -0
- kivyforge/platforms/linux/launcher.py +107 -0
- kivyforge/platforms/linux/lock/__init__.py +126 -0
- kivyforge/platforms/linux/lock/profile.py +254 -0
- kivyforge/platforms/linux/lock/runtime.py +75 -0
- kivyforge/platforms/linux/native_stage.py +47 -0
- kivyforge/platforms/linux/runtime_stage.py +107 -0
- kivyforge/platforms/linux/wheels_stage.py +103 -0
- kivyforge/platforms/macos/__init__.py +121 -0
- kivyforge/platforms/macos/bundle.py +255 -0
- kivyforge/platforms/macos/cli.py +269 -0
- kivyforge/platforms/macos/doctor.py +480 -0
- kivyforge/platforms/macos/icns.py +75 -0
- kivyforge/platforms/macos/launcher.py +118 -0
- kivyforge/platforms/macos/lock/__init__.py +112 -0
- kivyforge/platforms/macos/lock/profile.py +119 -0
- kivyforge/platforms/macos/lock/runtime.py +58 -0
- kivyforge/platforms/macos/machotools.py +138 -0
- kivyforge/platforms/macos/native_stage.py +47 -0
- kivyforge/platforms/macos/notarize.py +143 -0
- kivyforge/platforms/macos/plist.py +37 -0
- kivyforge/platforms/macos/runtime_stage.py +110 -0
- kivyforge/platforms/macos/signing.py +78 -0
- kivyforge/platforms/macos/wheels_stage.py +108 -0
- kivyforge/platforms/windows/__init__.py +133 -0
- kivyforge/platforms/windows/assets.py +76 -0
- kivyforge/platforms/windows/bundle.py +298 -0
- kivyforge/platforms/windows/cli.py +314 -0
- kivyforge/platforms/windows/doctor.py +563 -0
- kivyforge/platforms/windows/fsswap.py +87 -0
- kivyforge/platforms/windows/icons.py +64 -0
- kivyforge/platforms/windows/launcher/__init__.py +193 -0
- kivyforge/platforms/windows/launcher/build_launcher.py +507 -0
- kivyforge/platforms/windows/launcher/launcher.c +379 -0
- kivyforge/platforms/windows/lock/__init__.py +112 -0
- kivyforge/platforms/windows/lock/profile.py +101 -0
- kivyforge/platforms/windows/lock/runtime.py +64 -0
- kivyforge/platforms/windows/naming.py +52 -0
- kivyforge/platforms/windows/native_stage.py +66 -0
- kivyforge/platforms/windows/petools.py +98 -0
- kivyforge/platforms/windows/rcedit.py +116 -0
- kivyforge/platforms/windows/runtime_stage.py +221 -0
- kivyforge/platforms/windows/signing.py +168 -0
- kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
- kivyforge/platforms/windows/vendor/NOTICE +17 -0
- kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
- kivyforge/platforms/windows/vendor/TOOLSET.txt +4 -0
- kivyforge/platforms/windows/vendor/__init__.py +12 -0
- kivyforge/platforms/windows/vendor/fetch_rcedit.py +97 -0
- kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
- kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
- kivyforge/platforms/windows/wheels_stage.py +139 -0
- kivyforge-3.0.0.dev0.dist-info/METADATA +609 -0
- kivyforge-3.0.0.dev0.dist-info/RECORD +225 -0
- kivyforge-3.0.0.dev0.dist-info/WHEEL +5 -0
- kivyforge-3.0.0.dev0.dist-info/entry_points.txt +3 -0
- kivyforge-3.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- kivyforge-3.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Resolve ``Python.xcframework`` metadata for the lock (spec 02 §python_xcframework).
|
|
2
|
+
|
|
3
|
+
``kivyforge lock`` records the pinned Python.xcframework's URL + SHA-256 and
|
|
4
|
+
uses its declared iOS floor to validate ``deployment_target`` (spec 01 rule
|
|
5
|
+
11). The provider is an injection point so unit tests stay offline.
|
|
6
|
+
|
|
7
|
+
The iOS deployment floor is read directly from ``Python.xcframework/Info.plist``
|
|
8
|
+
inside the downloaded archive, so no per-version table needs manual maintenance.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import plistlib
|
|
15
|
+
import tarfile
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
23
|
+
from kivyforge.artifacts.cache import ArtifactCache
|
|
24
|
+
from kivyforge.artifacts.verify import sha256_file
|
|
25
|
+
|
|
26
|
+
# python.org publishes the official iOS xcframework from 3.15.0b1 onward.
|
|
27
|
+
PYTHON_ORG_URL = "https://www.python.org/ftp/python/{version}/python-{version}-iOS-XCframework.tar.gz"
|
|
28
|
+
|
|
29
|
+
_DOWNLOAD_ATTEMPTS = 4
|
|
30
|
+
_RETRY_BACKOFF_SEC = (1.0, 2.0, 4.0)
|
|
31
|
+
# Returned when the Info.plist is absent or cannot be parsed.
|
|
32
|
+
_IOS_FLOOR_FALLBACK = "13.0"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def python_org_ios_url(version: str) -> str:
|
|
36
|
+
"""Return the python.org FTP URL for an iOS ``Python.xcframework`` archive.
|
|
37
|
+
|
|
38
|
+
Pre-releases (``3.15.0b2``, ``3.15.0rc1``, …) live under the base
|
|
39
|
+
``/ftp/python/3.Y.0/`` directory; final releases use ``/ftp/python/X.Y.Z/``.
|
|
40
|
+
"""
|
|
41
|
+
if _is_prerelease(version):
|
|
42
|
+
base = ".".join(version.split(".")[:2]) + ".0"
|
|
43
|
+
return (
|
|
44
|
+
f"https://www.python.org/ftp/python/{base}/"
|
|
45
|
+
f"python-{version}-iOS-XCframework.tar.gz"
|
|
46
|
+
)
|
|
47
|
+
return PYTHON_ORG_URL.format(version=version)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def archive_filename(version: str) -> str:
|
|
51
|
+
return f"python-{version}-iOS-XCframework.tar.gz"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_prerelease(version: str) -> bool:
|
|
55
|
+
tail = version.split(".")[-1]
|
|
56
|
+
return any(tag in tail for tag in ("a", "b", "rc"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_ios_floor(archive_path: str | Path) -> str:
|
|
60
|
+
"""Read ``MinimumOSVersion`` from ``Python.xcframework/Info.plist`` inside the tarball.
|
|
61
|
+
|
|
62
|
+
The XCFramework Info.plist carries an ``AvailableLibraries`` array; each
|
|
63
|
+
entry has a ``LibraryIdentifier`` (e.g. ``"ios-arm64"``) and a
|
|
64
|
+
``MinimumOSVersion`` string. We return the value for the ``ios-arm64``
|
|
65
|
+
slice, which is the device floor that kivyforge validates against.
|
|
66
|
+
|
|
67
|
+
Returns ``_IOS_FLOOR_FALLBACK`` if the plist is absent, malformed, or the
|
|
68
|
+
archive is not a valid tarball (e.g. a test fixture).
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
with open(archive_path, "rb") as f:
|
|
72
|
+
return _read_ios_floor_from_fileobj(f)
|
|
73
|
+
except OSError:
|
|
74
|
+
return _IOS_FLOOR_FALLBACK
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _read_ios_floor_from_fileobj(fileobj) -> str:
|
|
78
|
+
"""Same as :func:`read_ios_floor`, but reads an already-open, seekable file.
|
|
79
|
+
|
|
80
|
+
``_download`` reads it via its own live ``NamedTemporaryFile`` handle
|
|
81
|
+
rather than reopening the path — reopening a still-open ``NamedTemporaryFile``
|
|
82
|
+
by name is a documented POSIX-only guarantee (it silently fails on Windows).
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
with tarfile.open(fileobj=fileobj, mode="r:gz") as tf:
|
|
86
|
+
for member in tf.getmembers():
|
|
87
|
+
if not member.name.endswith("Python.xcframework/Info.plist"):
|
|
88
|
+
continue
|
|
89
|
+
f = tf.extractfile(member)
|
|
90
|
+
if f is None:
|
|
91
|
+
continue
|
|
92
|
+
info = plistlib.load(f)
|
|
93
|
+
for lib in info.get("AvailableLibraries", []):
|
|
94
|
+
if "ios-arm64" in lib.get("LibraryIdentifier", ""):
|
|
95
|
+
return lib["MinimumOSVersion"]
|
|
96
|
+
except Exception: # noqa: BLE001 — malformed archive, old format, etc.
|
|
97
|
+
pass
|
|
98
|
+
return _IOS_FLOOR_FALLBACK
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class PythonXcframeworkInfo:
|
|
103
|
+
version: str
|
|
104
|
+
url: str
|
|
105
|
+
sha256: str
|
|
106
|
+
ios_floor: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PythonXcframeworkProvider(Protocol):
|
|
110
|
+
def get(self, version: str, *, offline: bool = False) -> PythonXcframeworkInfo: ...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PythonOrgProvider:
|
|
114
|
+
"""Default provider: build the python.org URL and hash the artifact."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
url_template: str = PYTHON_ORG_URL,
|
|
120
|
+
cache: ArtifactCache | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
self._url_template = url_template
|
|
123
|
+
self._cache = cache or ArtifactCache()
|
|
124
|
+
|
|
125
|
+
def get(self, version: str, *, offline: bool = False) -> PythonXcframeworkInfo:
|
|
126
|
+
url = (
|
|
127
|
+
python_org_ios_url(version)
|
|
128
|
+
if self._url_template == PYTHON_ORG_URL
|
|
129
|
+
else self._url_template.format(version=version)
|
|
130
|
+
)
|
|
131
|
+
sha256, floor = self._resolve(url, version, offline=offline)
|
|
132
|
+
return PythonXcframeworkInfo(
|
|
133
|
+
version=version,
|
|
134
|
+
url=url,
|
|
135
|
+
sha256=sha256,
|
|
136
|
+
ios_floor=floor,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _resolve(self, url: str, version: str, *, offline: bool) -> tuple[str, str]:
|
|
140
|
+
"""Return ``(sha256, ios_floor)`` from cache or by downloading."""
|
|
141
|
+
filename = archive_filename(version)
|
|
142
|
+
cached = self._cache.find_by_filename(filename)
|
|
143
|
+
if cached is not None:
|
|
144
|
+
return sha256_file(cached), read_ios_floor(cached)
|
|
145
|
+
|
|
146
|
+
if offline:
|
|
147
|
+
raise PythonXcframeworkError(
|
|
148
|
+
f"Python.xcframework {version} is not in the artifact cache "
|
|
149
|
+
f"({filename}).\n"
|
|
150
|
+
f" Run `kivyforge lock` online once, or `kivyforge build` to "
|
|
151
|
+
f"populate the cache, then retry with --offline."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return self._download(url, version)
|
|
155
|
+
|
|
156
|
+
def _download(self, url: str, version: str) -> tuple[str, str]:
|
|
157
|
+
"""Download the archive; return ``(sha256, ios_floor)``."""
|
|
158
|
+
last_exc: OSError | None = None
|
|
159
|
+
for attempt in range(_DOWNLOAD_ATTEMPTS):
|
|
160
|
+
try:
|
|
161
|
+
digest = hashlib.sha256()
|
|
162
|
+
with tempfile.NamedTemporaryFile(suffix=".tar.gz") as tmp:
|
|
163
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
164
|
+
for chunk in iter(lambda: resp.read(1 << 20), b""):
|
|
165
|
+
digest.update(chunk)
|
|
166
|
+
tmp.write(chunk)
|
|
167
|
+
tmp.flush()
|
|
168
|
+
tmp.seek(0)
|
|
169
|
+
floor = _read_ios_floor_from_fileobj(tmp)
|
|
170
|
+
return digest.hexdigest(), floor
|
|
171
|
+
except OSError as exc:
|
|
172
|
+
last_exc = exc
|
|
173
|
+
if attempt < _DOWNLOAD_ATTEMPTS - 1:
|
|
174
|
+
time.sleep(
|
|
175
|
+
_RETRY_BACKOFF_SEC[min(attempt, len(_RETRY_BACKOFF_SEC) - 1)]
|
|
176
|
+
)
|
|
177
|
+
assert last_exc is not None
|
|
178
|
+
raise PythonXcframeworkError(
|
|
179
|
+
f"could not fetch Python.xcframework {version} from {url}: {last_exc}\n"
|
|
180
|
+
f" Check the version exists on python.org (3.15.0b1+) and that "
|
|
181
|
+
f"you are online. Transient network/SSL errors are retried "
|
|
182
|
+
f"{_DOWNLOAD_ATTEMPTS} times."
|
|
183
|
+
) from last_exc
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class PythonXcframeworkError(Exception):
|
|
187
|
+
pass
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Parse ``pylock.ios.toml`` back into a ``Lockfile`` and detect drift (spec 02).
|
|
2
|
+
|
|
3
|
+
The neutral drift helpers (``LockError``, ``compute_pyproject_sha256``,
|
|
4
|
+
``is_in_sync``) live in ``kivyforge.lock.reader``; this module adds the iOS
|
|
5
|
+
``pylock.ios.toml`` parsing on top.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from kivyforge.lock.pep751 import parse_package as _parse_package
|
|
14
|
+
from kivyforge.lock.reader import (
|
|
15
|
+
LockError,
|
|
16
|
+
compute_pyproject_sha256,
|
|
17
|
+
is_in_sync,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .model import (
|
|
21
|
+
LOCK_VERSION,
|
|
22
|
+
TOOL_SCHEMA_VERSION,
|
|
23
|
+
LockedSwiftPackage,
|
|
24
|
+
LockedXcframework,
|
|
25
|
+
Lockfile,
|
|
26
|
+
PythonXcframework,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"LockError",
|
|
31
|
+
"compute_pyproject_sha256",
|
|
32
|
+
"is_in_sync",
|
|
33
|
+
"load",
|
|
34
|
+
"loads",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Highest major lock-version (PEP 751 top-level) this reader accepts.
|
|
38
|
+
SUPPORTED_LOCK_VERSION_MAJOR = int(LOCK_VERSION.split(".", 1)[0])
|
|
39
|
+
# Highest [tool.kivyforge].schema_version (the extension's own schema) accepted.
|
|
40
|
+
SUPPORTED_TOOL_SCHEMA_VERSION = TOOL_SCHEMA_VERSION
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def loads(text: str) -> Lockfile:
|
|
44
|
+
"""Parse ``pylock.ios.toml`` text into a ``Lockfile``.
|
|
45
|
+
|
|
46
|
+
``kivyforge lock`` is the writer, so the reader's job is to fail *cleanly*
|
|
47
|
+
when the file is corrupt, hand-edited, or from a future schema — never to
|
|
48
|
+
leak a raw ``KeyError``/``TypeError``. Every shape failure surfaces as
|
|
49
|
+
``LockError``.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
raw = tomllib.loads(text)
|
|
53
|
+
except tomllib.TOMLDecodeError as exc:
|
|
54
|
+
raise LockError(f"pylock.ios.toml is not valid TOML: {exc}") from exc
|
|
55
|
+
if not isinstance(raw, dict):
|
|
56
|
+
raise LockError("pylock.ios.toml must be a TOML table.")
|
|
57
|
+
try:
|
|
58
|
+
return _from_raw(raw)
|
|
59
|
+
except LockError:
|
|
60
|
+
raise
|
|
61
|
+
except (KeyError, TypeError, AttributeError, ValueError) as exc:
|
|
62
|
+
# A missing/mistyped field anywhere in the tree — name the trigger but
|
|
63
|
+
# don't leak the raw exception type to callers.
|
|
64
|
+
raise LockError(f"pylock.ios.toml is malformed: {exc!r}") from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load(path: str | Path) -> Lockfile:
|
|
68
|
+
return loads(Path(path).read_text(encoding="utf-8"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _from_raw(raw: dict) -> Lockfile:
|
|
72
|
+
lock_version = _check_lock_version(raw)
|
|
73
|
+
|
|
74
|
+
tool_raw = raw.get("tool", {})
|
|
75
|
+
if not isinstance(tool_raw, dict):
|
|
76
|
+
raise LockError("pylock.ios.toml [tool] must be a table.")
|
|
77
|
+
tool = tool_raw.get("kivyforge", {})
|
|
78
|
+
if not isinstance(tool, dict) or not tool:
|
|
79
|
+
raise LockError("pylock.ios.toml is missing the [tool.kivyforge] table.")
|
|
80
|
+
|
|
81
|
+
schema_version = _check_tool_schema_version(tool)
|
|
82
|
+
python_xcframework = _parse_python_xcframework(tool)
|
|
83
|
+
|
|
84
|
+
packages = tuple(_parse_package(p) for p in _as_list(raw, "packages"))
|
|
85
|
+
xcframeworks = tuple(_parse_xcframework(x) for x in _as_list(tool, "xcframeworks"))
|
|
86
|
+
swift_packages = tuple(
|
|
87
|
+
_parse_swift_package(s) for s in _as_list(tool, "swift_packages")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return Lockfile(
|
|
91
|
+
requires_python=raw.get("requires-python", ">=3.15"),
|
|
92
|
+
packages=packages,
|
|
93
|
+
python_xcframework=python_xcframework,
|
|
94
|
+
kivyforge_version=tool.get("kivyforge_version", ""),
|
|
95
|
+
generated_at=tool.get("generated_at", ""),
|
|
96
|
+
pyproject_sha256=tool.get("pyproject_sha256", ""),
|
|
97
|
+
tool_kivyforge_schema_version=tool.get("tool_kivyforge_schema_version", 1),
|
|
98
|
+
xcframeworks=xcframeworks,
|
|
99
|
+
swift_packages=swift_packages,
|
|
100
|
+
schema_version=schema_version,
|
|
101
|
+
lock_version=lock_version,
|
|
102
|
+
created_by=raw.get("created-by", "kivyforge"),
|
|
103
|
+
extras=tuple(raw.get("extras", [])),
|
|
104
|
+
dependency_groups=tuple(raw.get("dependency-groups", [])),
|
|
105
|
+
default_groups=tuple(raw.get("default-groups", [])),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_lock_version(raw: dict) -> str:
|
|
110
|
+
lock_version = raw.get("lock-version", LOCK_VERSION)
|
|
111
|
+
if not isinstance(lock_version, str):
|
|
112
|
+
raise LockError("pylock.ios.toml lock-version must be a string.")
|
|
113
|
+
major = lock_version.split(".", 1)[0]
|
|
114
|
+
if not major.isdigit():
|
|
115
|
+
raise LockError(
|
|
116
|
+
f"pylock.ios.toml lock-version {lock_version!r} is not a valid version."
|
|
117
|
+
)
|
|
118
|
+
if int(major) > SUPPORTED_LOCK_VERSION_MAJOR:
|
|
119
|
+
raise LockError(
|
|
120
|
+
f"pylock.ios.toml lock-version {lock_version} is newer than this "
|
|
121
|
+
f"kivyforge understands (max {SUPPORTED_LOCK_VERSION_MAJOR}.x). "
|
|
122
|
+
"Upgrade kivyforge."
|
|
123
|
+
)
|
|
124
|
+
return lock_version
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _check_tool_schema_version(tool: dict) -> int:
|
|
128
|
+
schema_version = tool.get("schema_version", SUPPORTED_TOOL_SCHEMA_VERSION)
|
|
129
|
+
if not isinstance(schema_version, int) or isinstance(schema_version, bool):
|
|
130
|
+
raise LockError("[tool.kivyforge].schema_version must be an integer.")
|
|
131
|
+
if schema_version > SUPPORTED_TOOL_SCHEMA_VERSION:
|
|
132
|
+
raise LockError(
|
|
133
|
+
f"[tool.kivyforge].schema_version {schema_version} is newer than this "
|
|
134
|
+
f"kivyforge understands (max {SUPPORTED_TOOL_SCHEMA_VERSION}). "
|
|
135
|
+
"Upgrade kivyforge."
|
|
136
|
+
)
|
|
137
|
+
return schema_version
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_python_xcframework(tool: dict) -> PythonXcframework:
|
|
141
|
+
px = tool.get("python_xcframework")
|
|
142
|
+
if not isinstance(px, dict) or not px:
|
|
143
|
+
raise LockError(
|
|
144
|
+
"pylock.ios.toml is missing [tool.kivyforge.python_xcframework]."
|
|
145
|
+
)
|
|
146
|
+
for field in ("version", "url", "sha256"):
|
|
147
|
+
value = px.get(field)
|
|
148
|
+
if not isinstance(value, str) or not value:
|
|
149
|
+
raise LockError(
|
|
150
|
+
f"[tool.kivyforge.python_xcframework].{field} is missing or empty."
|
|
151
|
+
)
|
|
152
|
+
return PythonXcframework(version=px["version"], url=px["url"], sha256=px["sha256"])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _as_list(table: dict, key: str) -> list:
|
|
156
|
+
value = table.get(key, [])
|
|
157
|
+
if not isinstance(value, list):
|
|
158
|
+
raise LockError(f"pylock.ios.toml {key!r} must be an array of tables.")
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _parse_xcframework(x: dict) -> LockedXcframework:
|
|
163
|
+
return LockedXcframework(
|
|
164
|
+
name=x["name"],
|
|
165
|
+
version=x["version"],
|
|
166
|
+
sha256=x.get("sha256", ""),
|
|
167
|
+
slices=tuple(x.get("slices", [])),
|
|
168
|
+
url=x.get("url"),
|
|
169
|
+
path=x.get("path"),
|
|
170
|
+
archive_format=x.get("archive_format", "zip"),
|
|
171
|
+
archive_member=x.get("archive_member"),
|
|
172
|
+
privacy_manifest_path=x.get("privacy_manifest_path"),
|
|
173
|
+
link=bool(x.get("link", True)),
|
|
174
|
+
embed=bool(x.get("embed", True)),
|
|
175
|
+
source=x.get("source"),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _parse_swift_package(s: dict) -> LockedSwiftPackage:
|
|
180
|
+
requirement = s.get("requirement")
|
|
181
|
+
return LockedSwiftPackage(
|
|
182
|
+
name=s["name"],
|
|
183
|
+
products=tuple(s.get("products", [])),
|
|
184
|
+
url=s.get("url"),
|
|
185
|
+
path=s.get("path"),
|
|
186
|
+
requirement=dict(requirement) if requirement else None,
|
|
187
|
+
revision=s.get("revision"),
|
|
188
|
+
version=s.get("version"),
|
|
189
|
+
link=bool(s.get("link", True)),
|
|
190
|
+
embed=bool(s.get("embed", True)),
|
|
191
|
+
)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""iOS dependency resolver (spec 02 §"Resolution semantics").
|
|
2
|
+
|
|
3
|
+
The resolver turns ``[project].dependencies`` into a fully-pinned set of
|
|
4
|
+
per-slice iOS wheels, **host-independent** — every compiled package is pinned
|
|
5
|
+
for the device slice (arm64) plus one slice per configured simulator
|
|
6
|
+
architecture, regardless of the architecture of the host running ``lock``. The
|
|
7
|
+
simulator architectures default to ``arm64`` + ``x86_64`` and are configurable
|
|
8
|
+
via ``[tool.kivy.ios].simulator_archs`` (a project that no longer needs the
|
|
9
|
+
Intel-host simulator can drop ``x86_64``).
|
|
10
|
+
|
|
11
|
+
Host-independence covers *marker evaluation*, not just wheel tags. pip's
|
|
12
|
+
``--platform`` selects acceptable tags but leaves ``sys_platform`` and friends
|
|
13
|
+
describing the macOS host running pip, which would wrongly admit
|
|
14
|
+
``sys_platform == "darwin"`` dependencies and — silently — drop
|
|
15
|
+
``sys_platform == "ios"`` ones. Resolution therefore runs through
|
|
16
|
+
``lock/_pip_shim.py``, which retargets ``packaging``'s marker environment at
|
|
17
|
+
the iOS slice (see ``markers.py``) before handing off to pip.
|
|
18
|
+
|
|
19
|
+
``pip`` is the backend (validated by the Phase 0 spike: see
|
|
20
|
+
docs/dev/resolver-findings.md). The backend is abstracted behind the
|
|
21
|
+
``Resolver`` protocol so unit tests can inject a fake resolver and stay hermetic.
|
|
22
|
+
The platform-neutral pip helpers are shared from ``kivyforge.lock.resolver``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import tempfile
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Protocol
|
|
35
|
+
|
|
36
|
+
from kivyforge.lock import _pip_shim
|
|
37
|
+
from kivyforge.lock._pip_shim import MARKER_ENV_VAR
|
|
38
|
+
from kivyforge.lock.model import canonical_name
|
|
39
|
+
from kivyforge.lock.resolver import (
|
|
40
|
+
MIN_PIP_VERSION,
|
|
41
|
+
abi_tags,
|
|
42
|
+
dep_names_for_environment,
|
|
43
|
+
pip_python_version,
|
|
44
|
+
pip_version,
|
|
45
|
+
version_str,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from .markers import ios_marker_environment
|
|
49
|
+
|
|
50
|
+
# Run by path, not ``-m``: the shim imports nothing from kivyforge, so it also
|
|
51
|
+
# works when resolving with an interpreter that only has pip installed.
|
|
52
|
+
_SHIM = Path(_pip_shim.__file__)
|
|
53
|
+
|
|
54
|
+
# iOS build slices (spec 02). The device slice is always arm64 — there is no
|
|
55
|
+
# 32-bit iOS — while the simulator slices are driven by the project's configured
|
|
56
|
+
# simulator architectures. ``DEFAULT_SIMULATOR_ARCHS`` mirrors
|
|
57
|
+
# ``config.model.DEFAULT_SIMULATOR_ARCHS``; the builder always passes the
|
|
58
|
+
# validated config value, so this default only serves direct/test callers. Tag
|
|
59
|
+
# templates are formatted with the deployment target (dots -> underscores).
|
|
60
|
+
DEVICE_SLICE_SUFFIX = "arm64_iphoneos"
|
|
61
|
+
DEFAULT_SIMULATOR_ARCHS = ("arm64", "x86_64")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ResolverError(Exception):
|
|
65
|
+
"""A resolution failure (missing slice, no matching wheel, backend error)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class ResolvedWheel:
|
|
70
|
+
filename: str
|
|
71
|
+
url: str
|
|
72
|
+
sha256: str
|
|
73
|
+
upload_time: str | None = None
|
|
74
|
+
size: int | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ResolvedPackage:
|
|
79
|
+
name: str
|
|
80
|
+
version: str
|
|
81
|
+
wheels: list[ResolvedWheel] = field(default_factory=list)
|
|
82
|
+
requires_python: str | None = None
|
|
83
|
+
dependencies: list[str] = field(default_factory=list)
|
|
84
|
+
source_index: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def slice_suffixes(
|
|
88
|
+
simulator_archs: tuple[str, ...] | None = None,
|
|
89
|
+
) -> tuple[str, ...]:
|
|
90
|
+
"""The arch+sdk suffixes for the slices a build targets.
|
|
91
|
+
|
|
92
|
+
Always the device slice (arm64) plus one simulator slice per configured
|
|
93
|
+
simulator architecture. Single source of truth for both ``slice_tags`` (lock
|
|
94
|
+
resolution) and the builder's slice-coverage check.
|
|
95
|
+
"""
|
|
96
|
+
archs = simulator_archs if simulator_archs is not None else DEFAULT_SIMULATOR_ARCHS
|
|
97
|
+
return (DEVICE_SLICE_SUFFIX, *(f"{arch}_iphonesimulator" for arch in archs))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def slice_tags(
|
|
101
|
+
deployment_target: str,
|
|
102
|
+
simulator_archs: tuple[str, ...] | None = None,
|
|
103
|
+
) -> tuple[str, ...]:
|
|
104
|
+
dt = deployment_target.replace(".", "_")
|
|
105
|
+
return tuple(f"ios_{dt}_{suffix}" for suffix in slice_suffixes(simulator_archs))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Resolver(Protocol):
|
|
109
|
+
def resolve(
|
|
110
|
+
self,
|
|
111
|
+
requirements: list[str],
|
|
112
|
+
*,
|
|
113
|
+
python_version: str,
|
|
114
|
+
deployment_target: str,
|
|
115
|
+
extra_index_urls: list[str],
|
|
116
|
+
find_links: list[str] | None = None,
|
|
117
|
+
offline: bool = False,
|
|
118
|
+
simulator_archs: tuple[str, ...] | None = None,
|
|
119
|
+
) -> list[ResolvedPackage]:
|
|
120
|
+
"""Resolve requirements to per-slice iOS wheels (all slices pinned)."""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PipResolver:
|
|
125
|
+
"""Default backend: stock pip cross-resolution via ``pip install --report``.
|
|
126
|
+
|
|
127
|
+
For each iOS slice we run pip in dry-run report mode with the slice's
|
|
128
|
+
platform tag; the JSON report yields each resolved wheel's URL + sha256.
|
|
129
|
+
Results are merged across slices into one ``ResolvedPackage`` per
|
|
130
|
+
distribution. A package missing any compiled slice raises ``ResolverError``
|
|
131
|
+
(fail fast at lock time, host-independent).
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, python_executable: str | None = None) -> None:
|
|
135
|
+
self._python = python_executable or sys.executable
|
|
136
|
+
|
|
137
|
+
def resolve(
|
|
138
|
+
self,
|
|
139
|
+
requirements: list[str],
|
|
140
|
+
*,
|
|
141
|
+
python_version: str,
|
|
142
|
+
deployment_target: str,
|
|
143
|
+
extra_index_urls: list[str],
|
|
144
|
+
find_links: list[str] | None = None,
|
|
145
|
+
offline: bool = False,
|
|
146
|
+
simulator_archs: tuple[str, ...] | None = None,
|
|
147
|
+
) -> list[ResolvedPackage]:
|
|
148
|
+
if not requirements:
|
|
149
|
+
return []
|
|
150
|
+
self._require_modern_pip()
|
|
151
|
+
suffixes = slice_suffixes(simulator_archs)
|
|
152
|
+
tags = slice_tags(deployment_target, simulator_archs)
|
|
153
|
+
abis = abi_tags(python_version)
|
|
154
|
+
# name -> ResolvedPackage (merged across slices)
|
|
155
|
+
merged: dict[str, ResolvedPackage] = {}
|
|
156
|
+
seen_filenames: dict[str, set[str]] = {}
|
|
157
|
+
|
|
158
|
+
for suffix, tag in zip(suffixes, tags):
|
|
159
|
+
# Markers are evaluated for the *target* slice, not the macOS host
|
|
160
|
+
# running pip (see markers.py); platform_machine differs between
|
|
161
|
+
# the arm64 and x86_64 simulator slices.
|
|
162
|
+
environment = ios_marker_environment(
|
|
163
|
+
python_version=python_version, slice_suffix=suffix
|
|
164
|
+
)
|
|
165
|
+
report = self._run_report(
|
|
166
|
+
requirements,
|
|
167
|
+
python_version=python_version,
|
|
168
|
+
platform_tag=tag,
|
|
169
|
+
abis=abis,
|
|
170
|
+
extra_index_urls=extra_index_urls,
|
|
171
|
+
find_links=find_links or [],
|
|
172
|
+
offline=offline,
|
|
173
|
+
marker_environment=environment,
|
|
174
|
+
)
|
|
175
|
+
for item in report.get("install", []):
|
|
176
|
+
self._absorb(item, merged, seen_filenames, environment)
|
|
177
|
+
|
|
178
|
+
return list(merged.values())
|
|
179
|
+
|
|
180
|
+
def _require_modern_pip(self) -> None:
|
|
181
|
+
version = pip_version(self._python)
|
|
182
|
+
if version is not None and version < MIN_PIP_VERSION:
|
|
183
|
+
raise ResolverError(
|
|
184
|
+
f"kivyforge needs pip >= {version_str(MIN_PIP_VERSION)} to resolve "
|
|
185
|
+
f"iOS wheels, but {self._python} has pip {version_str(version)}.\n"
|
|
186
|
+
" pip < 24.3 predates PEP 730 iOS platform tags, so compatible "
|
|
187
|
+
"wheels (e.g. ios_13_0_*) are reported as missing.\n"
|
|
188
|
+
f" Upgrade it: {self._python} -m pip install --upgrade pip"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _absorb(self, item, merged, seen_filenames, environment) -> None:
|
|
192
|
+
meta = item.get("metadata", {})
|
|
193
|
+
name = meta.get("name")
|
|
194
|
+
version = meta.get("version")
|
|
195
|
+
download = item.get("download_info", {})
|
|
196
|
+
url = download.get("url", "")
|
|
197
|
+
if not url.endswith(".whl"):
|
|
198
|
+
# sdist/vcs/directory are refused (spec 02): wheels only.
|
|
199
|
+
raise ResolverError(
|
|
200
|
+
f"{name} {version} resolved to a non-wheel source ({url}); "
|
|
201
|
+
"kivyforge installs only iOS wheels (no on-device compile)."
|
|
202
|
+
)
|
|
203
|
+
archive = download.get("archive_info", {})
|
|
204
|
+
hashes = archive.get("hashes", {})
|
|
205
|
+
sha256 = hashes.get("sha256", "")
|
|
206
|
+
filename = url.rsplit("/", 1)[-1]
|
|
207
|
+
key = canonical_name(name)
|
|
208
|
+
pkg = merged.get(key)
|
|
209
|
+
if pkg is None:
|
|
210
|
+
pkg = ResolvedPackage(
|
|
211
|
+
name=name,
|
|
212
|
+
version=version,
|
|
213
|
+
requires_python=meta.get("requires_python"),
|
|
214
|
+
dependencies=dep_names_for_environment(
|
|
215
|
+
meta.get("requires_dist", []), environment
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
merged[key] = pkg
|
|
219
|
+
seen_filenames[key] = set()
|
|
220
|
+
elif version != pkg.version:
|
|
221
|
+
# pip resolves each iOS slice independently; if upstream only
|
|
222
|
+
# published some slices for a release, a later slice can resolve to
|
|
223
|
+
# a different version. Merging those would silently produce a
|
|
224
|
+
# lockfile that looks pinned but installs mismatched binaries across
|
|
225
|
+
# device and simulator. Fail fast, like the missing-slice case.
|
|
226
|
+
raise ResolverError(
|
|
227
|
+
f"{name} resolved to inconsistent versions across iOS slices: "
|
|
228
|
+
f"{pkg.version!r} and {version!r}. This usually means the slices "
|
|
229
|
+
"were published at different times upstream; kivyforge requires "
|
|
230
|
+
"every slice of a package to pin the same version."
|
|
231
|
+
)
|
|
232
|
+
if filename not in seen_filenames[key]:
|
|
233
|
+
seen_filenames[key].add(filename)
|
|
234
|
+
pkg.wheels.append(ResolvedWheel(filename=filename, url=url, sha256=sha256))
|
|
235
|
+
|
|
236
|
+
def _run_report(
|
|
237
|
+
self,
|
|
238
|
+
requirements: list[str],
|
|
239
|
+
*,
|
|
240
|
+
python_version: str,
|
|
241
|
+
platform_tag: str,
|
|
242
|
+
abis: tuple[str, ...],
|
|
243
|
+
extra_index_urls: list[str],
|
|
244
|
+
find_links: list[str],
|
|
245
|
+
offline: bool,
|
|
246
|
+
marker_environment: dict[str, str],
|
|
247
|
+
) -> dict:
|
|
248
|
+
with tempfile.TemporaryDirectory(prefix="kivy-lock-") as tmp:
|
|
249
|
+
report_path = Path(tmp) / "report.json"
|
|
250
|
+
# Not "-m pip": the shim retargets marker evaluation at the iOS
|
|
251
|
+
# slice first, then runs pip unchanged (see lock/_pip_shim.py).
|
|
252
|
+
cmd = [
|
|
253
|
+
self._python,
|
|
254
|
+
str(_SHIM),
|
|
255
|
+
"install",
|
|
256
|
+
"--dry-run",
|
|
257
|
+
"--ignore-installed",
|
|
258
|
+
"--only-binary=:all:",
|
|
259
|
+
"--python-version",
|
|
260
|
+
pip_python_version(python_version),
|
|
261
|
+
"--implementation",
|
|
262
|
+
"cp",
|
|
263
|
+
"--platform",
|
|
264
|
+
platform_tag,
|
|
265
|
+
"--target",
|
|
266
|
+
str(Path(tmp) / "target"),
|
|
267
|
+
"--report",
|
|
268
|
+
str(report_path),
|
|
269
|
+
]
|
|
270
|
+
for abi in abis:
|
|
271
|
+
cmd += ["--abi", abi]
|
|
272
|
+
for index in extra_index_urls:
|
|
273
|
+
cmd += ["--extra-index-url", index]
|
|
274
|
+
for link in find_links:
|
|
275
|
+
cmd += ["--find-links", link]
|
|
276
|
+
if offline:
|
|
277
|
+
cmd += ["--no-index"]
|
|
278
|
+
cmd += list(requirements)
|
|
279
|
+
|
|
280
|
+
env = dict(os.environ)
|
|
281
|
+
env[MARKER_ENV_VAR] = json.dumps(marker_environment)
|
|
282
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, env=env)
|
|
283
|
+
if proc.returncode != 0:
|
|
284
|
+
raise ResolverError(
|
|
285
|
+
f"pip could not resolve the iOS slice {platform_tag!r}.\n"
|
|
286
|
+
f" This usually means a dependency has no wheel for that "
|
|
287
|
+
f"slice upstream.\n pip said:\n{_indent(proc.stderr or proc.stdout)}"
|
|
288
|
+
)
|
|
289
|
+
try:
|
|
290
|
+
# pip writes the report as UTF-8; never trust the locale codec
|
|
291
|
+
# (cp1252 on Windows chokes on non-ASCII package metadata).
|
|
292
|
+
return json.loads(report_path.read_text(encoding="utf-8"))
|
|
293
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
294
|
+
raise ResolverError(f"could not read pip report: {exc}") from exc
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_resolver(backend: str = "pip", **kwargs) -> Resolver:
|
|
298
|
+
if backend == "pip":
|
|
299
|
+
return PipResolver(**kwargs)
|
|
300
|
+
raise ResolverError(f"unknown resolver backend {backend!r} (expected pip)")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _indent(text: str) -> str:
|
|
304
|
+
return "\n".join(f" {line}" for line in (text or "").splitlines())
|