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,393 @@
1
+ """Assemble a ``Lockfile`` from a validated ``Config`` (spec 02 §"Resolution semantics")."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+ from urllib.parse import urlparse
10
+ from urllib.request import url2pathname
11
+
12
+ from kivyforge import __version__
13
+ from kivyforge.artifacts.verify import sha256_file
14
+ from kivyforge.config.model import Config, SwiftPackageDep, XcframeworkDep
15
+ from kivyforge.lock.find_links import (
16
+ FindLinksError,
17
+ find_links_resolution_hint,
18
+ resolve_find_links,
19
+ validate_find_links,
20
+ wheel_path_from_project_root,
21
+ )
22
+ from kivyforge.lock.model import (
23
+ LockedPackage,
24
+ LockedWheel,
25
+ PackageDep,
26
+ canonical_name,
27
+ )
28
+ from kivyforge.lock.reader import compute_pyproject_sha256
29
+
30
+ from .model import (
31
+ LockedSwiftPackage,
32
+ Lockfile,
33
+ PythonXcframework,
34
+ )
35
+ from .python_meta import PythonOrgProvider, PythonXcframeworkProvider
36
+ from .resolver import Resolver, ResolverError, get_resolver
37
+ from .spm import (
38
+ ResolvedSwiftPackage,
39
+ SpmResolver,
40
+ SpmResolverError,
41
+ get_spm_resolver,
42
+ )
43
+ from .xcframework import XcframeworkResolverError, resolve_xcframeworks
44
+
45
+ if TYPE_CHECKING:
46
+ from kivyforge.artifacts.download import Downloader
47
+
48
+
49
+ class BuildError(Exception):
50
+ """A lock build failure surfaced with an actionable message."""
51
+
52
+
53
+ def build_lockfile(
54
+ config: Config,
55
+ pyproject_text: str,
56
+ *,
57
+ project_root: Path | None = None,
58
+ resolver: Resolver | None = None,
59
+ python_provider: PythonXcframeworkProvider | None = None,
60
+ spm_resolver: SpmResolver | None = None,
61
+ xcframework_downloader: Downloader | None = None,
62
+ offline: bool = False,
63
+ now: datetime | None = None,
64
+ ) -> Lockfile:
65
+ if config.ios is None:
66
+ raise BuildError(
67
+ "pyproject.toml has no [tool.kivy.ios] table; nothing to lock."
68
+ )
69
+
70
+ resolver = resolver or get_resolver("pip")
71
+ python_provider = python_provider or PythonOrgProvider()
72
+ root = (project_root or Path.cwd()).resolve()
73
+
74
+ # [tool.kivy.ios.python].version is required (the loader raises a ConfigError
75
+ # when it is missing), so it is always set here. Never silently substitute a
76
+ # hidden default — that would pin an unexpected (and possibly unreleased)
77
+ # Python instead of surfacing the misconfig.
78
+ python_version = config.ios.python_version
79
+ if not python_version:
80
+ raise BuildError("missing required [tool.kivy.ios.python].version")
81
+ find_links_entries = config.ios.find_links
82
+ try:
83
+ validate_find_links(root, find_links_entries)
84
+ except FindLinksError as exc:
85
+ raise BuildError(str(exc)) from exc
86
+ find_links = resolve_find_links(root, find_links_entries)
87
+ py_info = python_provider.get(python_version, offline=offline)
88
+
89
+ # Rule 11: deployment_target must not be below the xcframework floor.
90
+ if _version_tuple(config.ios.deployment_target) < _version_tuple(py_info.ios_floor):
91
+ raise BuildError(
92
+ f"deployment_target {config.ios.deployment_target} is below the "
93
+ f"minimum iOS {py_info.ios_floor} required by Python "
94
+ f"{python_version}'s xcframework.\n"
95
+ f" Raise [tool.kivy.ios].deployment_target to at least "
96
+ f"{py_info.ios_floor}."
97
+ )
98
+
99
+ direct = {canonical_name(_req_name(d)) for d in config.project.dependencies}
100
+ excluded = {canonical_name(e) for e in config.ios.exclude}
101
+
102
+ # Excluded names that are also direct requirements are silently dropped from
103
+ # the exclusion set — you can't exclude what you explicitly depend on.
104
+ excluded -= direct
105
+
106
+ try:
107
+ resolved = resolver.resolve(
108
+ list(config.project.dependencies),
109
+ python_version=python_version,
110
+ deployment_target=config.ios.deployment_target,
111
+ extra_index_urls=list(config.ios.extra_index_urls),
112
+ find_links=find_links,
113
+ offline=offline,
114
+ simulator_archs=tuple(config.ios.simulator_archs),
115
+ )
116
+ except ResolverError as exc:
117
+ hint = find_links_resolution_hint(
118
+ root,
119
+ find_links_entries,
120
+ pip_stderr=str(exc),
121
+ )
122
+ if hint:
123
+ raise BuildError(f"{exc}\n{hint}") from exc
124
+ raise BuildError(str(exc)) from exc
125
+
126
+ packages = []
127
+ for rp in resolved:
128
+ if canonical_name(rp.name) in excluded:
129
+ continue
130
+ wheels = tuple(
131
+ _locked_wheel_from_resolved(w, project_root=root) for w in rp.wheels
132
+ )
133
+ _check_slices_complete(
134
+ rp.name,
135
+ wheels,
136
+ config.ios.deployment_target,
137
+ config.ios.simulator_archs,
138
+ )
139
+ packages.append(
140
+ LockedPackage(
141
+ name=rp.name,
142
+ version=rp.version,
143
+ wheels=wheels,
144
+ requires_python=rp.requires_python,
145
+ dependencies=tuple(PackageDep(name=d) for d in rp.dependencies),
146
+ direct_requirement=canonical_name(rp.name) in direct,
147
+ source_index=rp.source_index,
148
+ )
149
+ )
150
+
151
+ xcframeworks = _resolve_native_xcframeworks(
152
+ config.ios.xcframeworks, xcframework_downloader, root, offline
153
+ )
154
+
155
+ swift_packages = _resolve_swift_packages(
156
+ config.ios.swift_packages, spm_resolver, root, offline
157
+ )
158
+
159
+ return Lockfile(
160
+ requires_python=config.project.requires_python or ">=3.15",
161
+ packages=tuple(packages),
162
+ python_xcframework=PythonXcframework(
163
+ version=py_info.version, url=py_info.url, sha256=py_info.sha256
164
+ ),
165
+ kivyforge_version=__version__,
166
+ generated_at=(now or datetime.now(UTC)).strftime("%Y-%m-%dT%H:%M:%SZ"),
167
+ pyproject_sha256=compute_pyproject_sha256(pyproject_text),
168
+ tool_kivyforge_schema_version=config.ios.schema_version,
169
+ xcframeworks=tuple(xcframeworks),
170
+ swift_packages=tuple(swift_packages),
171
+ )
172
+
173
+
174
+ def _resolve_native_xcframeworks(
175
+ declared: tuple[XcframeworkDep, ...],
176
+ downloader: Downloader | None,
177
+ project_root: Path,
178
+ offline: bool,
179
+ ):
180
+ """Resolve declared native xcframeworks; surface failures as ``BuildError``.
181
+
182
+ The resolver fetches/reads each archive, pins its SHA-256, and enumerates the
183
+ slices it ships, so the lock fully captures user-declared native SDKs instead
184
+ of silently dropping them.
185
+ """
186
+ try:
187
+ return resolve_xcframeworks(
188
+ declared,
189
+ project_root=project_root,
190
+ downloader=downloader,
191
+ offline=offline,
192
+ )
193
+ except XcframeworkResolverError as exc:
194
+ raise BuildError(str(exc)) from exc
195
+
196
+
197
+ def _resolve_swift_packages(
198
+ declared: tuple[SwiftPackageDep, ...],
199
+ spm_resolver: SpmResolver | None,
200
+ project_root: Path,
201
+ offline: bool,
202
+ ) -> list[LockedSwiftPackage]:
203
+ """Resolve remote SPM packages to pins; assemble lock entries (sorted by name).
204
+
205
+ Local (``path``) packages need no resolution. The resolver is only built and
206
+ invoked when at least one remote package is declared, so a pure-wheel (or
207
+ local-only) project never requires the Swift toolchain.
208
+ """
209
+ if not declared:
210
+ return []
211
+ remote = [p for p in declared if p.url]
212
+ resolved: dict[str, ResolvedSwiftPackage] = {}
213
+ if remote:
214
+ resolver = spm_resolver or get_spm_resolver()
215
+ try:
216
+ for r in resolver.resolve(
217
+ remote, project_root=project_root, offline=offline
218
+ ):
219
+ resolved[r.name] = r
220
+ except SpmResolverError as exc:
221
+ raise BuildError(str(exc)) from exc
222
+
223
+ out: list[LockedSwiftPackage] = []
224
+ for pkg in declared:
225
+ pin = resolved.get(pkg.name)
226
+ if pkg.url and pin is None:
227
+ raise BuildError(
228
+ f"swift package {pkg.name!r} resolved to no revision; re-run lock "
229
+ f"with network access."
230
+ )
231
+ out.append(
232
+ LockedSwiftPackage(
233
+ name=pkg.name,
234
+ products=pkg.products,
235
+ url=pkg.url,
236
+ path=pkg.path,
237
+ requirement=pkg.requirement if pkg.url else None,
238
+ revision=pin.revision if pin else None,
239
+ version=pin.version if pin else None,
240
+ link=pkg.link,
241
+ embed=pkg.embed,
242
+ )
243
+ )
244
+ out.sort(key=lambda s: s.name)
245
+ return out
246
+
247
+
248
+ def _locked_wheel_from_resolved(w, *, project_root: Path) -> LockedWheel:
249
+ url, path = _normalize_wheel_source(w.url, project_root=project_root)
250
+ sha256 = w.sha256
251
+ if not sha256 and path:
252
+ sha256 = sha256_file((project_root / path).resolve())
253
+ if not sha256:
254
+ raise BuildError(
255
+ f"could not determine SHA-256 for wheel {w.filename!r}; "
256
+ f"re-run lock with network access or check the vendored file."
257
+ )
258
+ return LockedWheel(
259
+ name=w.filename,
260
+ url=url,
261
+ path=path,
262
+ sha256=sha256,
263
+ upload_time=w.upload_time,
264
+ size=w.size,
265
+ )
266
+
267
+
268
+ def _normalize_wheel_source(
269
+ url: str, *, project_root: Path
270
+ ) -> tuple[str | None, str | None]:
271
+ if url.startswith(("http://", "https://")):
272
+ return url, None
273
+ if url.startswith("file:"):
274
+ # url2pathname turns a file URL path into a native path on every host
275
+ # (e.g. "/C:/x" -> "C:\\x" on Windows), which plain unquote+Path does not.
276
+ local = Path(url2pathname(urlparse(url).path))
277
+ else:
278
+ local = Path(url)
279
+ resolved = local.resolve()
280
+ root = project_root.resolve()
281
+ try:
282
+ rel = wheel_path_from_project_root(root, resolved)
283
+ except FindLinksError as exc:
284
+ raise BuildError(
285
+ f"wheel resolved to {resolved}, which is outside the allowed "
286
+ f"find_links scope for project directory {root}.\n"
287
+ f" Vendored wheels must live under the project directory, a "
288
+ f"sibling directory, or the enclosing repository "
289
+ f"(e.g. examples/wheels/ios/)."
290
+ ) from exc
291
+ return None, rel
292
+
293
+
294
+ def _check_slices_complete(
295
+ name, wheels, deployment_target, simulator_archs=None
296
+ ) -> None:
297
+ """Fail fast if a compiled package is missing an iOS slice (spec 02).
298
+
299
+ The required slices are the device slice plus one per configured simulator
300
+ architecture (``[tool.kivy.ios].simulator_archs``), so a project that drops
301
+ ``x86_64`` is not held to a slice it no longer targets.
302
+
303
+ Compatibility rule: a wheel tagged ios_<M>_<N>_<arch>_<sdk> satisfies a
304
+ required slice when M.N <= deployment_target AND the arch+sdk suffix
305
+ matches exactly. This mirrors select_wheel's logic: python.org's iOS
306
+ Python binary always tags wheels at its own minimum OS (currently 13.0)
307
+ regardless of IPHONEOS_DEPLOYMENT_TARGET used when compiling the extension.
308
+ """
309
+ if any(w.is_pure_python for w in wheels):
310
+ return # pure-Python: single py3-none-any entry is complete
311
+
312
+ from .resolver import slice_suffixes
313
+
314
+ required = slice_suffixes(simulator_archs)
315
+ dt_parts = _version_tuple(deployment_target)
316
+ covered: set[str] = set()
317
+ for wheel in wheels:
318
+ tag = wheel.platform_tag # e.g. "ios_13_0_arm64_iphoneos"
319
+ parts = tag.split("_") # ["ios", "13", "0", "arm64", "iphoneos"]
320
+ if parts[0] != "ios" or len(parts) < 3:
321
+ continue
322
+ # Find where the arch+sdk suffix starts (last two underscore-joined segments).
323
+ for suffix in required:
324
+ if tag.endswith(f"_{suffix}"):
325
+ prefix_parts = tag[: -(len(suffix) + 1)].split("_") # ["ios","13","0"]
326
+ try:
327
+ wheel_ver = tuple(int(x) for x in prefix_parts[1:])
328
+ except ValueError:
329
+ break
330
+ if wheel_ver <= dt_parts:
331
+ covered.add(suffix)
332
+ break
333
+
334
+ missing_suffixes = set(required) - covered
335
+ if missing_suffixes:
336
+ dt = deployment_target.replace(".", "_")
337
+ missing_tags = sorted(f"ios_{dt}_{s}" for s in missing_suffixes)
338
+ raise BuildError(
339
+ f"{name} is missing iOS wheel slice(s): {', '.join(missing_tags)}.\n"
340
+ f" A compiled package must publish every targeted iOS slice "
341
+ f"(device + each configured simulator arch) to be locked reproducibly."
342
+ )
343
+
344
+
345
+ def semantic_equal(a: Lockfile, b: Lockfile) -> bool:
346
+ """Compare two lockfiles ignoring the volatile ``generated_at`` field.
347
+
348
+ Used by ``lock --check`` so a re-resolve that produces identical pins (but
349
+ a new timestamp) still reports "in sync".
350
+ """
351
+ return dataclasses.replace(a, generated_at="") == dataclasses.replace(
352
+ b, generated_at=""
353
+ )
354
+
355
+
356
+ def diff_summary(old: Lockfile, new: Lockfile) -> list[str]:
357
+ """Human-readable summary of what changed between two locks (for --check)."""
358
+ out: list[str] = []
359
+ old_pkgs = {canonical_name(p.name): p for p in old.packages}
360
+ new_pkgs = {canonical_name(p.name): p for p in new.packages}
361
+ for name in sorted(new_pkgs.keys() - old_pkgs.keys()):
362
+ out.append(f" + {new_pkgs[name].name} {new_pkgs[name].version} (added)")
363
+ for name in sorted(old_pkgs.keys() - new_pkgs.keys()):
364
+ out.append(f" - {old_pkgs[name].name} {old_pkgs[name].version} (removed)")
365
+ for name in sorted(old_pkgs.keys() & new_pkgs.keys()):
366
+ if old_pkgs[name].version != new_pkgs[name].version:
367
+ out.append(
368
+ f" ~ {new_pkgs[name].name}: "
369
+ f"{old_pkgs[name].version} -> {new_pkgs[name].version}"
370
+ )
371
+ if old.python_xcframework.version != new.python_xcframework.version:
372
+ out.append(
373
+ f" ~ Python.xcframework: {old.python_xcframework.version} -> "
374
+ f"{new.python_xcframework.version}"
375
+ )
376
+ return out
377
+
378
+
379
+ def _req_name(requirement: str) -> str:
380
+ from packaging.requirements import InvalidRequirement, Requirement
381
+
382
+ try:
383
+ return Requirement(requirement).name
384
+ except InvalidRequirement:
385
+ return requirement
386
+
387
+
388
+ def _version_tuple(v: str) -> tuple[int, ...]:
389
+ parts = []
390
+ for chunk in v.split("."):
391
+ digits = "".join(c for c in chunk if c.isdigit())
392
+ parts.append(int(digits) if digits else 0)
393
+ return tuple(parts)
@@ -0,0 +1,69 @@
1
+ """The PEP 508 marker environment of the *target*, not the lock host (spec 02).
2
+
3
+ pip's ``--platform`` decides which wheel *tags* are acceptable. It does **not**
4
+ change how environment markers are evaluated: pip evaluates every
5
+ ``; sys_platform == "darwin"``-style marker against the interpreter running
6
+ pip. Because iOS is always cross-resolved from macOS, that silently answers
7
+ every marker as **macOS** — so a dependency gated on ``sys_platform ==
8
+ "darwin"`` is wrongly pulled into an iOS lock, and one gated on
9
+ ``sys_platform == "ios"`` is wrongly dropped.
10
+
11
+ That second case is the dangerous one: nothing fails at lock time, and the app
12
+ is simply missing a dependency at runtime. Unlike the Android equivalent (where
13
+ a Windows host produced a loud "no matching distribution" error), this failure
14
+ is silent on the only supported host, so restricting the workflow to macOS does
15
+ not fix it — the environment has to be described explicitly.
16
+
17
+ Values follow CPython's own iOS build (PEP 730): ``sys.platform`` is ``"ios"``
18
+ and ``platform.system()`` is ``"iOS"`` on both device and simulator. Markers
19
+ carry no device/simulator distinction — that lives in
20
+ ``sys.implementation._multiarch``, which is not a marker name — so the two are
21
+ told apart here only by ``platform_machine``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+
27
+ class MarkerEnvironmentError(Exception):
28
+ """The target marker environment could not be described."""
29
+
30
+
31
+ def machine_for_slice(slice_suffix: str) -> str:
32
+ """``arm64_iphoneos`` -> ``arm64``; ``x86_64_iphonesimulator`` -> ``x86_64``."""
33
+ if slice_suffix.endswith("_iphoneos"):
34
+ return slice_suffix[: -len("_iphoneos")]
35
+ if slice_suffix.endswith("_iphonesimulator"):
36
+ return slice_suffix[: -len("_iphonesimulator")]
37
+ raise MarkerEnvironmentError(
38
+ f"unrecognised iOS slice {slice_suffix!r}; expected an "
39
+ "``<arch>_iphoneos`` or ``<arch>_iphonesimulator`` suffix."
40
+ )
41
+
42
+
43
+ def ios_marker_environment(*, python_version: str, slice_suffix: str) -> dict[str, str]:
44
+ """The PEP 508 environment a wheel would see on the given iOS slice.
45
+
46
+ ``python_version`` is the full runtime version (e.g. ``3.14.6``).
47
+ """
48
+ machine = machine_for_slice(slice_suffix)
49
+ parts = python_version.split(".")
50
+ if len(parts) < 2:
51
+ raise MarkerEnvironmentError(
52
+ f"python version {python_version!r} is not a full X.Y.Z version; "
53
+ "cannot describe the target marker environment."
54
+ )
55
+ return {
56
+ "implementation_name": "cpython",
57
+ "implementation_version": python_version,
58
+ "os_name": "posix",
59
+ "platform_machine": machine,
60
+ "platform_python_implementation": "CPython",
61
+ # Device kernel details are unknowable at lock time, and no sane wheel
62
+ # gates on them.
63
+ "platform_release": "",
64
+ "platform_system": "iOS",
65
+ "platform_version": "",
66
+ "python_full_version": python_version,
67
+ "python_version": ".".join(parts[:2]),
68
+ "sys_platform": "ios",
69
+ }
@@ -0,0 +1,95 @@
1
+ """In-memory model of ``pylock.ios.toml`` (spec 02).
2
+
3
+ PEP 751-shaped ``[[packages]]`` plus the iOS ``[tool.kivyforge]`` extension
4
+ (``python_xcframework``, xcframeworks, swift_packages). These dataclasses are
5
+ produced by the builder/resolver and serialized by ``writer``. The shared
6
+ ``[[packages]]`` model (``LockedPackage``/``LockedWheel``) lives in
7
+ ``kivyforge.lock.model``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ from kivyforge.lock.model import LOCK_VERSION, LockedPackage
15
+
16
+ CREATED_BY = "kivyforge"
17
+ TOOL_SCHEMA_VERSION = 1
18
+ DEFAULT_REQUIRES_PYTHON = ">=3.15"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class PythonXcframework:
23
+ version: str
24
+ url: str
25
+ sha256: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class LockedXcframework:
30
+ """One ``[[tool.kivyforge.xcframeworks]]`` entry."""
31
+
32
+ name: str
33
+ version: str
34
+ sha256: str
35
+ slices: tuple[str, ...]
36
+ url: str | None = None
37
+ path: str | None = None
38
+ archive_format: str = "zip"
39
+ archive_member: str | None = None
40
+ privacy_manifest_path: str | None = None
41
+ link: bool = True
42
+ embed: bool = True
43
+ source: str | None = None
44
+
45
+ def __post_init__(self) -> None:
46
+ if bool(self.url) == bool(self.path):
47
+ raise ValueError(
48
+ f"xcframework {self.name!r} must have exactly one of url/path"
49
+ )
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class LockedSwiftPackage:
54
+ """One ``[[tool.kivyforge.swift_packages]]`` entry (spec 07).
55
+
56
+ Unlike xcframeworks, kivyforge computes no output hash here: Xcode owns the
57
+ SPM lifecycle, so reproducibility is the pinned ``revision`` (+ the generated
58
+ ``Package.resolved``). Exactly one of ``url`` (remote) or ``path`` (local) is
59
+ set; ``requirement``/``revision``/``version`` are remote-only.
60
+ """
61
+
62
+ name: str
63
+ products: tuple[str, ...]
64
+ url: str | None = None
65
+ path: str | None = None
66
+ requirement: dict[str, object] | None = None
67
+ revision: str | None = None
68
+ version: str | None = None
69
+ link: bool = True
70
+ embed: bool = True
71
+
72
+ def __post_init__(self) -> None:
73
+ if bool(self.url) == bool(self.path):
74
+ raise ValueError(
75
+ f"swift package {self.name!r} must have exactly one of url/path"
76
+ )
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class Lockfile:
81
+ requires_python: str
82
+ packages: tuple[LockedPackage, ...]
83
+ python_xcframework: PythonXcframework
84
+ kivyforge_version: str
85
+ generated_at: str
86
+ pyproject_sha256: str
87
+ tool_kivyforge_schema_version: int
88
+ xcframeworks: tuple[LockedXcframework, ...] = ()
89
+ swift_packages: tuple[LockedSwiftPackage, ...] = ()
90
+ schema_version: int = TOOL_SCHEMA_VERSION
91
+ lock_version: str = LOCK_VERSION
92
+ created_by: str = CREATED_BY
93
+ extras: tuple[str, ...] = ()
94
+ dependency_groups: tuple[str, ...] = ()
95
+ default_groups: tuple[str, ...] = ()