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.
Files changed (225) hide show
  1. kivyforge/__init__.py +9 -0
  2. kivyforge/artifacts/__init__.py +41 -0
  3. kivyforge/artifacts/cache.py +81 -0
  4. kivyforge/artifacts/collect.py +354 -0
  5. kivyforge/artifacts/download.py +125 -0
  6. kivyforge/artifacts/frameworks.py +240 -0
  7. kivyforge/artifacts/native_stage_util.py +346 -0
  8. kivyforge/artifacts/runtime.py +65 -0
  9. kivyforge/artifacts/verify.py +45 -0
  10. kivyforge/artifacts/wheels.py +89 -0
  11. kivyforge/bundle/__init__.py +1 -0
  12. kivyforge/bundle/pycompile.py +161 -0
  13. kivyforge/cli/__init__.py +52 -0
  14. kivyforge/cli/_common.py +62 -0
  15. kivyforge/cli/_platform.py +84 -0
  16. kivyforge/cli/build.py +111 -0
  17. kivyforge/cli/clean.py +135 -0
  18. kivyforge/cli/doctor.py +52 -0
  19. kivyforge/cli/init.py +612 -0
  20. kivyforge/cli/init_writer.py +716 -0
  21. kivyforge/cli/lock.py +261 -0
  22. kivyforge/cli/open_cmd.py +15 -0
  23. kivyforge/cli/package.py +134 -0
  24. kivyforge/cli/run.py +162 -0
  25. kivyforge/cli/status.py +20 -0
  26. kivyforge/cli/upgrade.py +314 -0
  27. kivyforge/config/__init__.py +46 -0
  28. kivyforge/config/errors.py +43 -0
  29. kivyforge/config/icons.py +60 -0
  30. kivyforge/config/loader.py +3079 -0
  31. kivyforge/config/model.py +817 -0
  32. kivyforge/doctor/__init__.py +19 -0
  33. kivyforge/doctor/checks_common.py +95 -0
  34. kivyforge/doctor/probe.py +357 -0
  35. kivyforge/doctor/result.py +41 -0
  36. kivyforge/lock/__init__.py +42 -0
  37. kivyforge/lock/_pip_shim.py +63 -0
  38. kivyforge/lock/find_links.py +182 -0
  39. kivyforge/lock/model.py +69 -0
  40. kivyforge/lock/pep751.py +111 -0
  41. kivyforge/lock/reader.py +30 -0
  42. kivyforge/lock/resolver.py +124 -0
  43. kivyforge/lock/wheelruntime/__init__.py +70 -0
  44. kivyforge/lock/wheelruntime/builder.py +280 -0
  45. kivyforge/lock/wheelruntime/model.py +94 -0
  46. kivyforge/lock/wheelruntime/native_binaries.py +127 -0
  47. kivyforge/lock/wheelruntime/pbs_github.py +133 -0
  48. kivyforge/lock/wheelruntime/profile.py +104 -0
  49. kivyforge/lock/wheelruntime/resolver.py +248 -0
  50. kivyforge/lock/wheelruntime/runtime.py +161 -0
  51. kivyforge/lock/wheelruntime/serialize.py +245 -0
  52. kivyforge/platforms/__init__.py +114 -0
  53. kivyforge/platforms/android/__init__.py +140 -0
  54. kivyforge/platforms/android/adb.py +215 -0
  55. kivyforge/platforms/android/bootstrap/__init__.py +7 -0
  56. kivyforge/platforms/android/bootstrap/contract.py +166 -0
  57. kivyforge/platforms/android/bootstrap/render.py +244 -0
  58. kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +76 -0
  59. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +50 -0
  60. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
  61. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +97 -0
  62. kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
  63. kivyforge/platforms/android/bootstrap/templates/cpp/main.c +150 -0
  64. kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
  65. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +75 -0
  66. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +132 -0
  67. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +189 -0
  68. kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +276 -0
  69. kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
  70. kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
  71. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
  72. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
  73. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
  74. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
  75. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
  76. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2120 -0
  77. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
  78. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
  79. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
  80. kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
  81. kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
  82. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
  83. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
  84. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
  85. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
  86. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
  87. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
  88. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
  89. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
  90. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
  91. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
  92. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
  93. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
  94. kivyforge/platforms/android/cli.py +1023 -0
  95. kivyforge/platforms/android/doctor.py +899 -0
  96. kivyforge/platforms/android/elf.py +148 -0
  97. kivyforge/platforms/android/generate/__init__.py +1 -0
  98. kivyforge/platforms/android/generate/manifest.py +261 -0
  99. kivyforge/platforms/android/generate/project.py +462 -0
  100. kivyforge/platforms/android/generate/services.py +187 -0
  101. kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
  102. kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
  103. kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
  104. kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
  105. kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
  106. kivyforge/platforms/android/gradlew.py +60 -0
  107. kivyforge/platforms/android/icons.py +237 -0
  108. kivyforge/platforms/android/lock/__init__.py +1 -0
  109. kivyforge/platforms/android/lock/archives.py +109 -0
  110. kivyforge/platforms/android/lock/builder.py +387 -0
  111. kivyforge/platforms/android/lock/markers.py +71 -0
  112. kivyforge/platforms/android/lock/maven.py +252 -0
  113. kivyforge/platforms/android/lock/model.py +113 -0
  114. kivyforge/platforms/android/lock/python_meta.py +199 -0
  115. kivyforge/platforms/android/lock/reader.py +206 -0
  116. kivyforge/platforms/android/lock/resolver.py +276 -0
  117. kivyforge/platforms/android/lock/writer.py +121 -0
  118. kivyforge/platforms/android/policy.py +214 -0
  119. kivyforge/platforms/android/signing.py +145 -0
  120. kivyforge/platforms/android/smoke.py +43 -0
  121. kivyforge/platforms/android/splash.py +293 -0
  122. kivyforge/platforms/android/stage/__init__.py +1 -0
  123. kivyforge/platforms/android/stage/bundle.py +295 -0
  124. kivyforge/platforms/android/stage/jnilibs.py +186 -0
  125. kivyforge/platforms/android/stage/runtime.py +61 -0
  126. kivyforge/platforms/android/stage/wheels.py +106 -0
  127. kivyforge/platforms/android/toolchain.py +29 -0
  128. kivyforge/platforms/base.py +157 -0
  129. kivyforge/platforms/ios/__init__.py +129 -0
  130. kivyforge/platforms/ios/assets.py +193 -0
  131. kivyforge/platforms/ios/buildsettings.py +128 -0
  132. kivyforge/platforms/ios/cli.py +543 -0
  133. kivyforge/platforms/ios/doctor.py +410 -0
  134. kivyforge/platforms/ios/entitlements.py +34 -0
  135. kivyforge/platforms/ios/generator.py +318 -0
  136. kivyforge/platforms/ios/lock/__init__.py +71 -0
  137. kivyforge/platforms/ios/lock/builder.py +393 -0
  138. kivyforge/platforms/ios/lock/markers.py +69 -0
  139. kivyforge/platforms/ios/lock/model.py +95 -0
  140. kivyforge/platforms/ios/lock/python_meta.py +187 -0
  141. kivyforge/platforms/ios/lock/reader.py +191 -0
  142. kivyforge/platforms/ios/lock/resolver.py +304 -0
  143. kivyforge/platforms/ios/lock/spm.py +217 -0
  144. kivyforge/platforms/ios/lock/writer.py +114 -0
  145. kivyforge/platforms/ios/lock/xcframework.py +188 -0
  146. kivyforge/platforms/ios/materialize.py +59 -0
  147. kivyforge/platforms/ios/plist.py +91 -0
  148. kivyforge/platforms/ios/privacy.py +39 -0
  149. kivyforge/platforms/ios/skeleton.py +165 -0
  150. kivyforge/platforms/ios/sources.py +63 -0
  151. kivyforge/platforms/ios/staging.py +225 -0
  152. kivyforge/platforms/ios/swift_packages.py +324 -0
  153. kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +34 -0
  154. kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +146 -0
  155. kivyforge/platforms/ios/templates/main.m +15 -0
  156. kivyforge/platforms/ios/templates/main_config.h.in +12 -0
  157. kivyforge/platforms/ios/xcframeworks.py +194 -0
  158. kivyforge/platforms/ios/xcode/__init__.py +76 -0
  159. kivyforge/platforms/ios/xcode/commands.py +296 -0
  160. kivyforge/platforms/ios/xcode/runner.py +316 -0
  161. kivyforge/platforms/linux/__init__.py +120 -0
  162. kivyforge/platforms/linux/appimage.py +169 -0
  163. kivyforge/platforms/linux/bundle.py +235 -0
  164. kivyforge/platforms/linux/cli.py +232 -0
  165. kivyforge/platforms/linux/desktop.py +71 -0
  166. kivyforge/platforms/linux/doctor.py +438 -0
  167. kivyforge/platforms/linux/elftools.py +95 -0
  168. kivyforge/platforms/linux/icons.py +129 -0
  169. kivyforge/platforms/linux/launcher.py +107 -0
  170. kivyforge/platforms/linux/lock/__init__.py +126 -0
  171. kivyforge/platforms/linux/lock/profile.py +254 -0
  172. kivyforge/platforms/linux/lock/runtime.py +75 -0
  173. kivyforge/platforms/linux/native_stage.py +47 -0
  174. kivyforge/platforms/linux/runtime_stage.py +107 -0
  175. kivyforge/platforms/linux/wheels_stage.py +103 -0
  176. kivyforge/platforms/macos/__init__.py +121 -0
  177. kivyforge/platforms/macos/bundle.py +255 -0
  178. kivyforge/platforms/macos/cli.py +269 -0
  179. kivyforge/platforms/macos/doctor.py +480 -0
  180. kivyforge/platforms/macos/icns.py +75 -0
  181. kivyforge/platforms/macos/launcher.py +118 -0
  182. kivyforge/platforms/macos/lock/__init__.py +112 -0
  183. kivyforge/platforms/macos/lock/profile.py +119 -0
  184. kivyforge/platforms/macos/lock/runtime.py +58 -0
  185. kivyforge/platforms/macos/machotools.py +138 -0
  186. kivyforge/platforms/macos/native_stage.py +47 -0
  187. kivyforge/platforms/macos/notarize.py +143 -0
  188. kivyforge/platforms/macos/plist.py +37 -0
  189. kivyforge/platforms/macos/runtime_stage.py +110 -0
  190. kivyforge/platforms/macos/signing.py +78 -0
  191. kivyforge/platforms/macos/wheels_stage.py +108 -0
  192. kivyforge/platforms/windows/__init__.py +133 -0
  193. kivyforge/platforms/windows/assets.py +76 -0
  194. kivyforge/platforms/windows/bundle.py +298 -0
  195. kivyforge/platforms/windows/cli.py +314 -0
  196. kivyforge/platforms/windows/doctor.py +563 -0
  197. kivyforge/platforms/windows/fsswap.py +87 -0
  198. kivyforge/platforms/windows/icons.py +64 -0
  199. kivyforge/platforms/windows/launcher/__init__.py +193 -0
  200. kivyforge/platforms/windows/launcher/build_launcher.py +507 -0
  201. kivyforge/platforms/windows/launcher/launcher.c +379 -0
  202. kivyforge/platforms/windows/lock/__init__.py +112 -0
  203. kivyforge/platforms/windows/lock/profile.py +101 -0
  204. kivyforge/platforms/windows/lock/runtime.py +64 -0
  205. kivyforge/platforms/windows/naming.py +52 -0
  206. kivyforge/platforms/windows/native_stage.py +66 -0
  207. kivyforge/platforms/windows/petools.py +98 -0
  208. kivyforge/platforms/windows/rcedit.py +116 -0
  209. kivyforge/platforms/windows/runtime_stage.py +221 -0
  210. kivyforge/platforms/windows/signing.py +168 -0
  211. kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
  212. kivyforge/platforms/windows/vendor/NOTICE +17 -0
  213. kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
  214. kivyforge/platforms/windows/vendor/TOOLSET.txt +4 -0
  215. kivyforge/platforms/windows/vendor/__init__.py +12 -0
  216. kivyforge/platforms/windows/vendor/fetch_rcedit.py +97 -0
  217. kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
  218. kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
  219. kivyforge/platforms/windows/wheels_stage.py +139 -0
  220. kivyforge-3.0.0.dev0.dist-info/METADATA +609 -0
  221. kivyforge-3.0.0.dev0.dist-info/RECORD +225 -0
  222. kivyforge-3.0.0.dev0.dist-info/WHEEL +5 -0
  223. kivyforge-3.0.0.dev0.dist-info/entry_points.txt +3 -0
  224. kivyforge-3.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  225. 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())