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,280 @@
1
+ """Assemble a ``WheelRuntimeLock`` from a ``Config`` + a platform profile.
2
+
3
+ Generic orchestration shared by every wheel+runtime platform: validate/resolve
4
+ find_links, pin the runtime, resolve wheels per variant, fail-fast on any variant
5
+ a compiled dependency does not cover, and assemble the lock. The only
6
+ platform-specific inputs come from the ``PlatformLockProfile``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from collections.abc import Callable
13
+ from datetime import UTC, datetime
14
+ from pathlib import Path
15
+ from urllib.parse import unquote, urlparse
16
+
17
+ from ... import __version__
18
+ from ...artifacts.verify import sha256_file
19
+ from ...config.model import Config, NativeBinaryDep
20
+ from ..find_links import (
21
+ FindLinksError,
22
+ find_links_resolution_hint,
23
+ resolve_find_links,
24
+ validate_find_links,
25
+ wheel_path_from_project_root,
26
+ )
27
+ from ..model import LockedPackage, LockedWheel, PackageDep, canonical_name
28
+ from ..reader import compute_pyproject_sha256
29
+ from .model import WheelRuntimeLock
30
+ from .native_binaries import NativeBinaryResolverError, resolve_native_binaries
31
+ from .profile import PlatformLockProfile
32
+ from .resolver import WheelResolver, WheelResolverError
33
+ from .runtime import RuntimeProvider, RuntimeProviderError
34
+
35
+
36
+ class WheelRuntimeBuildError(Exception):
37
+ """A wheel+runtime lock build failure surfaced with an actionable message."""
38
+
39
+
40
+ def build_wheel_runtime_lock(
41
+ profile: PlatformLockProfile,
42
+ config: Config,
43
+ pyproject_text: str,
44
+ *,
45
+ project_root: Path | None = None,
46
+ resolver: WheelResolver | None = None,
47
+ runtime_provider: RuntimeProvider | None = None,
48
+ native_binaries: tuple[NativeBinaryDep, ...] = (),
49
+ offline: bool = False,
50
+ now: datetime | None = None,
51
+ on_warning: Callable[[str], None] | None = None,
52
+ ) -> WheelRuntimeLock:
53
+ if profile.overlay(config) is None:
54
+ raise WheelRuntimeBuildError(profile.missing_overlay_error())
55
+
56
+ resolver = resolver or profile.wheel_resolver()
57
+ runtime_provider = runtime_provider or profile.runtime_provider(config)
58
+ root = (project_root or Path.cwd()).resolve()
59
+
60
+ python_version = profile.python_version(config)
61
+ variants = profile.variants(config)
62
+ archs = profile.archs(config)
63
+
64
+ find_links_entries = profile.find_links(config)
65
+ try:
66
+ validate_find_links(root, find_links_entries, platform=profile.platform)
67
+ except FindLinksError as exc:
68
+ raise WheelRuntimeBuildError(str(exc)) from exc
69
+ find_links = resolve_find_links(root, find_links_entries)
70
+
71
+ try:
72
+ runtime = runtime_provider.resolve(python_version, archs, offline=offline)
73
+ except RuntimeProviderError as exc:
74
+ raise WheelRuntimeBuildError(str(exc)) from exc
75
+
76
+ try:
77
+ locked_native_binaries = resolve_native_binaries(
78
+ native_binaries, project_root=root, offline=offline
79
+ )
80
+ except NativeBinaryResolverError as exc:
81
+ raise WheelRuntimeBuildError(str(exc)) from exc
82
+
83
+ declared_floor = profile.declared_floor(config)
84
+ if (
85
+ declared_floor
86
+ and runtime.floor
87
+ and _version_tuple(declared_floor) < _version_tuple(runtime.floor)
88
+ ):
89
+ raise WheelRuntimeBuildError(
90
+ profile.floor_error(declared_floor, runtime.floor, python_version)
91
+ )
92
+
93
+ direct = {canonical_name(_req_name(d)) for d in config.project.dependencies}
94
+ excluded = {canonical_name(e) for e in profile.exclude(config)} - direct
95
+
96
+ try:
97
+ resolved = resolver.resolve(
98
+ list(config.project.dependencies),
99
+ python_version=python_version,
100
+ variants=variants,
101
+ extra_index_urls=list(profile.extra_index_urls(config)),
102
+ find_links=find_links,
103
+ offline=offline,
104
+ )
105
+ except WheelResolverError as exc:
106
+ hint = find_links_resolution_hint(
107
+ root, find_links_entries, pip_stderr=str(exc), platform=profile.platform
108
+ )
109
+ if hint:
110
+ raise WheelRuntimeBuildError(f"{exc}\n{hint}") from exc
111
+ raise WheelRuntimeBuildError(str(exc)) from exc
112
+
113
+ packages = []
114
+ for rp in resolved:
115
+ if canonical_name(rp.name) in excluded:
116
+ continue
117
+ wheels = tuple(
118
+ _locked_wheel_from_resolved(profile, w, project_root=root)
119
+ for w in rp.wheels
120
+ )
121
+ _check_variants_complete(profile, rp.name, wheels, archs, on_warning=on_warning)
122
+ packages.append(
123
+ LockedPackage(
124
+ name=rp.name,
125
+ version=rp.version,
126
+ wheels=wheels,
127
+ requires_python=rp.requires_python,
128
+ dependencies=tuple(PackageDep(name=d) for d in rp.dependencies),
129
+ direct_requirement=canonical_name(rp.name) in direct,
130
+ source_index=rp.source_index,
131
+ )
132
+ )
133
+
134
+ return WheelRuntimeLock(
135
+ platform=profile.platform,
136
+ requires_python=config.project.requires_python or ">=3.15",
137
+ # Canonical (serializer) order so a freshly built lock compares equal to
138
+ # its round-tripped form; ``lock --check`` re-resolves and compares
139
+ # order-sensitively against the on-disk (sorted) lock.
140
+ packages=tuple(sorted(packages, key=lambda p: p.sort_key)),
141
+ python_runtime=runtime,
142
+ archs=archs,
143
+ kivyforge_version=__version__,
144
+ generated_at=(now or datetime.now(UTC)).strftime("%Y-%m-%dT%H:%M:%SZ"),
145
+ pyproject_sha256=compute_pyproject_sha256(pyproject_text),
146
+ tool_kivyforge_schema_version=profile.schema_version(config),
147
+ native_binaries=locked_native_binaries,
148
+ )
149
+
150
+
151
+ def _locked_wheel_from_resolved(
152
+ profile: PlatformLockProfile, w, *, project_root: Path
153
+ ) -> LockedWheel:
154
+ url, path = _normalize_wheel_source(profile, w.url, project_root=project_root)
155
+ sha256 = w.sha256
156
+ if not sha256 and path:
157
+ sha256 = sha256_file((project_root / path).resolve())
158
+ if not sha256:
159
+ raise WheelRuntimeBuildError(
160
+ f"could not determine SHA-256 for wheel {w.filename!r}; "
161
+ f"re-run lock with network access or check the vendored file."
162
+ )
163
+ return LockedWheel(
164
+ name=w.filename,
165
+ url=url,
166
+ path=path,
167
+ sha256=sha256,
168
+ upload_time=w.upload_time,
169
+ size=w.size,
170
+ )
171
+
172
+
173
+ def _normalize_wheel_source(
174
+ profile: PlatformLockProfile, url: str, *, project_root: Path
175
+ ) -> tuple[str | None, str | None]:
176
+ if url.startswith(("http://", "https://")):
177
+ return url, None
178
+ if url.startswith("file:"):
179
+ local = Path(unquote(urlparse(url).path))
180
+ else:
181
+ local = Path(url)
182
+ resolved = local.resolve()
183
+ root = project_root.resolve()
184
+ try:
185
+ rel = wheel_path_from_project_root(root, resolved)
186
+ except FindLinksError as exc:
187
+ raise WheelRuntimeBuildError(
188
+ f"wheel resolved to {resolved}, which is outside the allowed "
189
+ f"find_links scope for project directory {root}.\n"
190
+ f" {profile.wheel_scope_hint()}"
191
+ ) from exc
192
+ return None, rel
193
+
194
+
195
+ def _check_variants_complete(
196
+ profile: PlatformLockProfile,
197
+ name: str,
198
+ wheels: tuple[LockedWheel, ...],
199
+ archs: tuple[str, ...],
200
+ *,
201
+ on_warning: Callable[[str], None] | None = None,
202
+ ) -> None:
203
+ """Fail fast if a compiled package is missing a required variant.
204
+
205
+ Pure-Python packages (a single ``py3-none-any`` wheel) are always complete.
206
+ Coverage is source-aware: a platform may decline a wheel (and/or attach a
207
+ non-fatal warning) based on where it came from — see
208
+ :meth:`PlatformLockProfile.wheel_coverage`.
209
+ """
210
+ if any(w.is_pure_python for w in wheels):
211
+ return
212
+ covered: set[str] = set()
213
+ for wheel in wheels:
214
+ wheel_archs, warning = profile.wheel_coverage(wheel, archs)
215
+ covered.update(wheel_archs)
216
+ if warning and on_warning is not None:
217
+ on_warning(warning)
218
+ missing = [a for a in archs if a not in covered]
219
+ if missing:
220
+ raise WheelRuntimeBuildError(profile.coverage_error(name, missing))
221
+
222
+
223
+ def semantic_equal(a: WheelRuntimeLock, b: WheelRuntimeLock) -> bool:
224
+ """Compare two locks ignoring the volatile ``generated_at`` field."""
225
+ return dataclasses.replace(a, generated_at="") == dataclasses.replace(
226
+ b, generated_at=""
227
+ )
228
+
229
+
230
+ def diff_summary(old: WheelRuntimeLock, new: WheelRuntimeLock) -> list[str]:
231
+ out: list[str] = []
232
+ old_pkgs = {canonical_name(p.name): p for p in old.packages}
233
+ new_pkgs = {canonical_name(p.name): p for p in new.packages}
234
+ for name in sorted(new_pkgs.keys() - old_pkgs.keys()):
235
+ out.append(f" + {new_pkgs[name].name} {new_pkgs[name].version} (added)")
236
+ for name in sorted(old_pkgs.keys() - new_pkgs.keys()):
237
+ out.append(f" - {old_pkgs[name].name} {old_pkgs[name].version} (removed)")
238
+ for name in sorted(old_pkgs.keys() & new_pkgs.keys()):
239
+ if old_pkgs[name].version != new_pkgs[name].version:
240
+ out.append(
241
+ f" ~ {new_pkgs[name].name}: "
242
+ f"{old_pkgs[name].version} -> {new_pkgs[name].version}"
243
+ )
244
+ if old.python_runtime.version != new.python_runtime.version:
245
+ out.append(
246
+ f" ~ python runtime: {old.python_runtime.version} -> "
247
+ f"{new.python_runtime.version}"
248
+ )
249
+ old_nb = {b.name: b for b in old.native_binaries}
250
+ new_nb = {b.name: b for b in new.native_binaries}
251
+ for name in sorted(new_nb.keys() - old_nb.keys()):
252
+ out.append(
253
+ f" + native binary {new_nb[name].name} {new_nb[name].version} (added)"
254
+ )
255
+ for name in sorted(old_nb.keys() - new_nb.keys()):
256
+ out.append(
257
+ f" - native binary {old_nb[name].name} {old_nb[name].version} (removed)"
258
+ )
259
+ for name in sorted(old_nb.keys() & new_nb.keys()):
260
+ o, n = old_nb[name], new_nb[name]
261
+ if o.version != n.version or o.sha256 != n.sha256:
262
+ out.append(f" ~ native binary {n.name}: {o.version} -> {n.version}")
263
+ return out
264
+
265
+
266
+ def _req_name(requirement: str) -> str:
267
+ from packaging.requirements import InvalidRequirement, Requirement
268
+
269
+ try:
270
+ return Requirement(requirement).name
271
+ except InvalidRequirement:
272
+ return requirement
273
+
274
+
275
+ def _version_tuple(v: str) -> tuple[int, ...]:
276
+ parts = []
277
+ for chunk in v.split("."):
278
+ digits = "".join(c for c in chunk if c.isdigit())
279
+ parts.append(int(digits) if digits else 0)
280
+ return tuple(parts)
@@ -0,0 +1,94 @@
1
+ """Generic ``wheel + bundled-runtime`` lockfile model.
2
+
3
+ Shared by every platform whose lock is "platform-tagged wheels plus a bundled
4
+ CPython runtime": macOS today, and Linux/Windows/Android in later phases. Each
5
+ such platform differs only in a small *profile* (tag construction, the arch/ABI
6
+ variant set, and the runtime provider); the lockfile *shape* is identical, so it
7
+ lives here once. iOS is deliberately not part of this family — its per-slice
8
+ framework conversion, ``Python.xcframework`` runtime, and SPM extension give it a
9
+ genuinely different lock shape.
10
+
11
+ PEP 751 ``[[packages]]`` (the generic ``LockedPackage``/``LockedWheel`` from
12
+ ``kivyforge.lock.model``) plus a ``[tool.kivyforge]`` extension recording the
13
+ arch set and the runtime pin (provider + one archive per arch).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass
19
+
20
+ from ..model import LOCK_VERSION, LockedPackage
21
+
22
+ # The wheel-runtime [tool.kivyforge] extension schema version.
23
+ TOOL_SCHEMA_VERSION = 1
24
+ CREATED_BY = "kivyforge"
25
+ DEFAULT_REQUIRES_PYTHON = ">=3.15"
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RuntimeArtifact:
30
+ """One per-arch bundled Python runtime archive (pinned by URL + SHA-256).
31
+
32
+ ``arch`` is the variant identifier: a CPU architecture on desktops
33
+ (``arm64``/``x86_64``) or an ABI on Android (``arm64-v8a``/…).
34
+ """
35
+
36
+ arch: str
37
+ url: str
38
+ sha256: str
39
+ archive_format: str = "tar.gz"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class PythonRuntime:
44
+ """The bundled CPython runtime pin (provider-agnostic)."""
45
+
46
+ provider: str # e.g. "python-build-standalone"
47
+ version: str # CPython version, e.g. "3.15.0"
48
+ artifacts: tuple[RuntimeArtifact, ...] # one per arch in the lock
49
+ floor: str | None = None # runtime's own OS floor, if the provider reports it
50
+
51
+ def artifact_for(self, arch: str) -> RuntimeArtifact | None:
52
+ for art in self.artifacts:
53
+ if art.arch == arch:
54
+ return art
55
+ return None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class LockedNativeBinary:
60
+ """One pinned ``[tool.kivy.<platform>.native.binaries]`` entry.
61
+
62
+ A non-wheel native binary (vendor SDK dylib/so/DLL or a helper executable)
63
+ pinned by SHA-256. Exactly one of ``url`` (remote source) or ``path``
64
+ (repo-relative vendored source) is set — the ``LockedXcframework``
65
+ convention.
66
+ """
67
+
68
+ name: str
69
+ version: str
70
+ sha256: str
71
+ url: str | None = None
72
+ path: str | None = None
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class WheelRuntimeLock:
77
+ """A ``pylock.<platform>.toml`` for the wheel + bundled-runtime family."""
78
+
79
+ platform: str # "macos" | "linux" | "windows" | "android"
80
+ requires_python: str
81
+ packages: tuple[LockedPackage, ...]
82
+ python_runtime: PythonRuntime
83
+ archs: tuple[str, ...]
84
+ kivyforge_version: str
85
+ generated_at: str
86
+ pyproject_sha256: str
87
+ tool_kivyforge_schema_version: int
88
+ schema_version: int = TOOL_SCHEMA_VERSION
89
+ lock_version: str = LOCK_VERSION
90
+ created_by: str = CREATED_BY
91
+ extras: tuple[str, ...] = ()
92
+ dependency_groups: tuple[str, ...] = ()
93
+ default_groups: tuple[str, ...] = ()
94
+ native_binaries: tuple[LockedNativeBinary, ...] = ()
@@ -0,0 +1,127 @@
1
+ """Native-binary resolver (the ``native.binaries`` channel, macos-spec).
2
+
3
+ ``kivyforge lock`` reads each declared ``[tool.kivy.<platform>.native.binaries]``
4
+ entry's ``source`` (a single native binary or a ``.zip`` of several, referenced
5
+ by URL or repo-relative path) and computes its SHA-256. Those pins land in
6
+ ``pylock.<platform>.toml`` so ``kivyforge build`` can re-fetch the artifact by
7
+ ``url``+``sha256`` (or ``path``) and stage it into the bundle's ``bin`` directory
8
+ reproducibly.
9
+
10
+ The download backend is injectable (``Downloader``) so unit tests stay hermetic
11
+ (no network). This mirrors ``platforms/ios/lock/xcframework.py`` without the
12
+ per-slice framework enumeration — a native binary has no slices to read.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import tempfile
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING
20
+
21
+ from kivyforge.artifacts.verify import sha256_file
22
+ from kivyforge.config.model import NativeBinaryDep
23
+ from kivyforge.lock.find_links import FindLinksError, wheel_path_from_project_root
24
+
25
+ from .model import LockedNativeBinary
26
+
27
+ if TYPE_CHECKING:
28
+ from kivyforge.artifacts.cache import ArtifactCache
29
+ from kivyforge.artifacts.download import Downloader
30
+
31
+
32
+ class NativeBinaryResolverError(Exception):
33
+ """A native-binary resolution failure surfaced with an actionable message."""
34
+
35
+
36
+ def resolve_native_binaries(
37
+ declared: tuple[NativeBinaryDep, ...],
38
+ *,
39
+ project_root: Path,
40
+ downloader: Downloader | None = None,
41
+ cache: ArtifactCache | None = None,
42
+ offline: bool = False,
43
+ ) -> tuple[LockedNativeBinary, ...]:
44
+ """Resolve declared native binaries into SHA-256-pinned lock entries.
45
+
46
+ Returns entries sorted by ``(name, version)``. The downloader is only invoked
47
+ for remote (URL) sources, so a project with only vendored binaries (or none)
48
+ never needs network access. ``cache`` is accepted for call-site symmetry with
49
+ the build-time fetch path; lock resolution computes the pin from scratch.
50
+ """
51
+ if not declared:
52
+ return ()
53
+ out = [
54
+ _resolve_one(
55
+ dep, project_root=project_root, downloader=downloader, offline=offline
56
+ )
57
+ for dep in declared
58
+ ]
59
+ out.sort(key=lambda x: (x.name.lower(), x.version))
60
+ return tuple(out)
61
+
62
+
63
+ def _resolve_one(
64
+ dep: NativeBinaryDep,
65
+ *,
66
+ project_root: Path,
67
+ downloader: Downloader | None,
68
+ offline: bool,
69
+ ) -> LockedNativeBinary:
70
+ is_url = dep.source.startswith(("http://", "https://"))
71
+ if is_url:
72
+ with tempfile.TemporaryDirectory(prefix="kivy-nb-lock-") as tmp:
73
+ archive = _download(dep, Path(tmp), downloader=downloader, offline=offline)
74
+ sha256 = sha256_file(archive)
75
+ return LockedNativeBinary(
76
+ name=dep.name, version=dep.version, sha256=sha256, url=dep.source
77
+ )
78
+ resolved, rel = _resolve_local(dep, project_root=project_root)
79
+ return LockedNativeBinary(
80
+ name=dep.name, version=dep.version, sha256=sha256_file(resolved), path=rel
81
+ )
82
+
83
+
84
+ def _download(
85
+ dep: NativeBinaryDep,
86
+ tmpdir: Path,
87
+ *,
88
+ downloader: Downloader | None,
89
+ offline: bool,
90
+ ) -> Path:
91
+ # Imported lazily: artifacts.download imports lock.find_links, so a top-level
92
+ # import here would close an import cycle (lock <-> artifacts).
93
+ from kivyforge.artifacts.download import DownloadError, UrllibDownloader
94
+
95
+ if offline:
96
+ raise NativeBinaryResolverError(
97
+ f"native binary {dep.name!r}: cannot download {dep.source} while "
98
+ f"offline.\n Re-run `kivyforge lock` with network access, or vendor "
99
+ f"the file and point `source` at a repo-relative path."
100
+ )
101
+ dl = downloader or UrllibDownloader()
102
+ filename = dep.source.rsplit("/", 1)[-1] or dep.name
103
+ dest = tmpdir / filename
104
+ try:
105
+ dl.fetch_to(dep.source, dest)
106
+ except DownloadError as exc:
107
+ raise NativeBinaryResolverError(f"native binary {dep.name!r}: {exc}") from exc
108
+ return dest
109
+
110
+
111
+ def _resolve_local(dep: NativeBinaryDep, *, project_root: Path) -> tuple[Path, str]:
112
+ root = project_root.resolve()
113
+ resolved = (root / dep.source).resolve()
114
+ try:
115
+ rel = wheel_path_from_project_root(root, resolved)
116
+ except FindLinksError as exc:
117
+ raise NativeBinaryResolverError(
118
+ f"native binary {dep.name!r}: source {dep.source!r} resolves to "
119
+ f"{resolved}, which is outside the project directory and its parent.\n"
120
+ f" Vendored native binaries must live under the project directory "
121
+ f"or a shared sibling directory (e.g. examples/binaries/)."
122
+ ) from exc
123
+ if not resolved.is_file():
124
+ raise NativeBinaryResolverError(
125
+ f"native binary {dep.name!r}: vendored file not found at {dep.source!r}."
126
+ )
127
+ return resolved, rel
@@ -0,0 +1,133 @@
1
+ """GitHub-backed python-build-standalone (PBS) release metadata lookup.
2
+
3
+ PBS publishes builds as GitHub release assets on
4
+ ``astral-sh/python-build-standalone``. Each release is tagged by date and ships,
5
+ per platform, an ``install_only`` archive plus a companion ``.sha256`` file. This
6
+ fetcher finds the newest release providing the requested version + target triple
7
+ and returns its download URL + pinned SHA-256. It is triple-agnostic, so macOS
8
+ and (later) Linux share it unchanged.
9
+
10
+ This is the only networked part of the runtime provider; it is exercised against
11
+ the live API at the phase stop rather than in hermetic unit tests (which inject a
12
+ fake ``MetadataFetcher``).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import re
19
+ import urllib.request
20
+
21
+ from .runtime import ReleaseAsset, RuntimeProviderError
22
+
23
+ _API = "https://api.github.com/repos/astral-sh/python-build-standalone"
24
+ # Each PBS release bundles *every* supported version × platform (hundreds of
25
+ # assets), so listing many full release objects at once makes the API 504. The
26
+ # newest release already contains every actively-supported CPython, so we check
27
+ # `releases/latest` first and only fall back to small paged scans for an older
28
+ # pin. Keep the page size small for the same 504-avoidance reason.
29
+ _LATEST_URL = f"{_API}/releases/latest"
30
+ _RELEASES_URL = f"{_API}/releases?per_page={{per_page}}&page={{page}}"
31
+ _PAGE_SIZE = 10
32
+ _MAX_PAGES = 15
33
+
34
+
35
+ class GithubPbsMetadataFetcher:
36
+ """Resolve a PBS asset by querying the GitHub releases API."""
37
+
38
+ def __init__(self, latest_url: str = _LATEST_URL) -> None:
39
+ self._latest_url = latest_url
40
+
41
+ def fetch(
42
+ self, version: str, target_triple: str, *, offline: bool = False
43
+ ) -> ReleaseAsset:
44
+ if offline:
45
+ raise RuntimeProviderError(
46
+ "cannot resolve a python-build-standalone runtime offline; "
47
+ "re-run `kivyforge lock` with network access."
48
+ )
49
+ pattern = re.compile(
50
+ rf"^cpython-{re.escape(version)}\+\d+-{re.escape(target_triple)}"
51
+ r"-install_only\.tar\.gz$"
52
+ )
53
+ for release in self._candidate_releases():
54
+ asset = self._match_release(release, pattern)
55
+ if asset is not None:
56
+ return asset
57
+ raise RuntimeProviderError(
58
+ f"no python-build-standalone install_only build found for CPython "
59
+ f"{version} ({target_triple}).\n"
60
+ f" Check available versions at "
61
+ f"https://github.com/astral-sh/python-build-standalone/releases."
62
+ )
63
+
64
+ def _candidate_releases(self):
65
+ """Yield the latest release, then older releases page by page."""
66
+ yield self._get_json(self._latest_url)
67
+ for page in range(1, _MAX_PAGES + 1):
68
+ url = _RELEASES_URL.format(per_page=_PAGE_SIZE, page=page)
69
+ releases = self._get_json(url)
70
+ if not releases:
71
+ return
72
+ yield from releases
73
+
74
+ def _match_release(self, release, pattern) -> ReleaseAsset | None:
75
+ assets = {a["name"]: a for a in release.get("assets", [])}
76
+ match = next((n for n in assets if pattern.match(n)), None)
77
+ if match is None:
78
+ return None
79
+ url = assets[match]["browser_download_url"]
80
+ sha256 = self._read_sha256(assets, match, url)
81
+ return ReleaseAsset(url=url, sha256=sha256)
82
+
83
+ def _read_sha256(self, assets: dict, name: str, url: str) -> str:
84
+ """Pin the asset's SHA-256, tried most→least authoritative.
85
+
86
+ 1. GitHub's asset ``digest`` field (``sha256:<hex>``) — no extra fetch.
87
+ 2. A per-asset ``<name>.sha256`` companion.
88
+ 3. The release-wide ``SHA256SUMS`` manifest.
89
+ """
90
+ digest = assets[name].get("digest") or ""
91
+ if digest.startswith("sha256:"):
92
+ return digest.split(":", 1)[1].strip()
93
+
94
+ companion = f"{name}.sha256"
95
+ if companion in assets:
96
+ text = self._get_text(assets[companion]["browser_download_url"])
97
+ return text.split()[0].strip()
98
+
99
+ if "SHA256SUMS" in assets:
100
+ sha = self._sha_from_manifest(
101
+ assets["SHA256SUMS"]["browser_download_url"], name
102
+ )
103
+ if sha is not None:
104
+ return sha
105
+
106
+ raise RuntimeProviderError(
107
+ f"python-build-standalone asset {name!r} exposes no SHA-256 (no "
108
+ f"digest, .sha256, or SHA256SUMS entry); cannot pin it "
109
+ f"reproducibly ({url})."
110
+ )
111
+
112
+ def _sha_from_manifest(self, url: str, name: str) -> str | None:
113
+ for line in self._get_text(url).splitlines():
114
+ parts = line.split()
115
+ # "<hex> <filename>" — match the filename column exactly.
116
+ if len(parts) == 2 and parts[1].strip() == name:
117
+ return parts[0].strip()
118
+ return None
119
+
120
+ def _get_json(self, url: str):
121
+ return json.loads(self._get_text(url))
122
+
123
+ def _get_text(self, url: str) -> str:
124
+ req = urllib.request.Request( # noqa: S310 — https GitHub API only
125
+ url, headers={"Accept": "application/vnd.github+json"}
126
+ )
127
+ try:
128
+ with urllib.request.urlopen(req) as resp: # noqa: S310
129
+ return resp.read().decode("utf-8")
130
+ except (OSError, ValueError) as exc:
131
+ raise RuntimeProviderError(
132
+ f"failed to query python-build-standalone metadata ({url}): {exc}"
133
+ ) from exc