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,104 @@
1
+ """The per-platform lock *profile* for the wheel+runtime family.
2
+
3
+ A profile is the small, platform-specific seam the generic builder/resolver
4
+ depend on. It reads the platform's ``[tool.kivy.<platform>]`` overlay off the
5
+ shared ``Config`` and answers: which variants to resolve, how a wheel tag maps to
6
+ variants (the coverage rule), which runtime provider to use, and the
7
+ platform-appropriate error/hint wording. Adding Linux/Windows/Android is writing
8
+ one of these — no new builder, resolver, model, or serializer.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC, abstractmethod
14
+
15
+ from ...config.model import Config
16
+ from .resolver import Variant, WheelResolver, get_wheel_resolver
17
+ from .runtime import RuntimeProvider
18
+
19
+
20
+ class PlatformLockProfile(ABC):
21
+ """Platform-specific inputs to the generic wheel+runtime lock engine."""
22
+
23
+ #: Platform name; the ``pylock.<platform>.toml`` infix + overlay key.
24
+ platform: str
25
+
26
+ @abstractmethod
27
+ def overlay(self, config: Config):
28
+ """The platform overlay object (e.g. ``config.macos``), or ``None``."""
29
+
30
+ @abstractmethod
31
+ def missing_overlay_error(self) -> str:
32
+ """Message when the overlay is absent (nothing to lock)."""
33
+
34
+ @abstractmethod
35
+ def python_version(self, config: Config) -> str:
36
+ """The bundled CPython version to pin."""
37
+
38
+ @abstractmethod
39
+ def variants(self, config: Config) -> tuple[Variant, ...]:
40
+ """The build variants to resolve (arch/ABI + pip ``--platform`` tag)."""
41
+
42
+ @abstractmethod
43
+ def wheel_covers(self, platform_tag: str, archs: tuple[str, ...]) -> set[str]:
44
+ """The subset of ``archs`` a wheel's platform tag satisfies.
45
+
46
+ A fat wheel (e.g. macOS ``universal2``) returns every arch; a per-arch
47
+ wheel returns its own; an unrelated tag returns the empty set.
48
+ """
49
+
50
+ def wheel_coverage(
51
+ self, wheel, archs: tuple[str, ...]
52
+ ) -> tuple[set[str], str | None]:
53
+ """Archs a resolved *wheel* covers, plus an optional non-fatal warning.
54
+
55
+ Default: the pure tag→arch rule (:meth:`wheel_covers`) with no warning.
56
+ A platform may override to gate coverage on the wheel's *source* — e.g.
57
+ Linux accepts a plain ``linux_*`` (no-glibc-promise) wheel only when it
58
+ was vendored via ``find_links``, and warns when it does.
59
+ """
60
+ return self.wheel_covers(wheel.platform_tag, archs), None
61
+
62
+ @abstractmethod
63
+ def runtime_provider(self, config: Config) -> RuntimeProvider:
64
+ """The default runtime provider for this platform."""
65
+
66
+ @abstractmethod
67
+ def coverage_error(self, name: str, missing: list[str]) -> str:
68
+ """Message when a compiled package is missing a required variant."""
69
+
70
+ @abstractmethod
71
+ def wheel_scope_hint(self) -> str:
72
+ """The vendored-wheel scope hint (platform-appropriate example path)."""
73
+
74
+ # --- fields with cross-platform defaults (overlay-name agnostic accessors) ---
75
+
76
+ def wheel_resolver(self) -> WheelResolver:
77
+ return get_wheel_resolver("pip")
78
+
79
+ def extra_index_urls(self, config: Config) -> tuple[str, ...]:
80
+ return tuple(getattr(self.overlay(config), "extra_index_urls", ()) or ())
81
+
82
+ def find_links(self, config: Config) -> tuple[str, ...]:
83
+ return tuple(getattr(self.overlay(config), "find_links", ()) or ())
84
+
85
+ def exclude(self, config: Config) -> tuple[str, ...]:
86
+ return tuple(getattr(self.overlay(config), "exclude", ()) or ())
87
+
88
+ def archs(self, config: Config) -> tuple[str, ...]:
89
+ return tuple(v.arch for v in self.variants(config))
90
+
91
+ def schema_version(self, config: Config) -> int:
92
+ return int(getattr(self.overlay(config), "schema_version", 1))
93
+
94
+ def declared_floor(self, config: Config) -> str | None:
95
+ """The user-declared OS floor to validate against the runtime floor."""
96
+ return None
97
+
98
+ def floor_error(
99
+ self, declared: str, runtime_floor: str, version: str
100
+ ) -> str: # pragma: no cover - overridden where a floor exists
101
+ return (
102
+ f"the declared OS floor {declared} is below the {runtime_floor} floor "
103
+ f"required by the Python {version} runtime."
104
+ )
@@ -0,0 +1,248 @@
1
+ """Generic wheel resolver for the wheel+runtime family.
2
+
3
+ Resolves ``[project].dependencies`` to fully-pinned wheels, host-independent: pip
4
+ runs once per *variant* (an arch/ABI + its pip ``--platform`` tag) and the
5
+ results are merged. A wheel that satisfies several variants (e.g. macOS
6
+ ``universal2``) is deduplicated by filename. The pip mechanics are identical to
7
+ the iOS resolver (dry-run ``--report`` per platform tag); only the tag list — and
8
+ the coverage rule, which lives in the platform profile — differ. The
9
+ platform-neutral pip helpers are shared from ``kivyforge.lock.resolver``.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Protocol
21
+
22
+ from ..model import canonical_name
23
+ from ..resolver import (
24
+ MIN_PIP_VERSION,
25
+ _dep_names,
26
+ abi_tags,
27
+ pip_python_version,
28
+ pip_version,
29
+ version_str,
30
+ )
31
+
32
+
33
+ class WheelResolverError(Exception):
34
+ """A resolution failure (missing variant, non-wheel, backend error)."""
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Variant:
39
+ """One build variant: an arch/ABI identifier + its pip ``--platform`` tag(s).
40
+
41
+ ``platform_tag`` is the primary/identity tag. ``extra_platform_tags`` lets a
42
+ variant request additional pip ``--platform`` tags in the *same* pip
43
+ invocation — Linux needs this because pip does not expand the manylinux
44
+ hierarchy from a single explicit ``--platform`` (see linux-spec), so the
45
+ profile passes the whole compatible tag ladder and pip resolves one
46
+ consistent version accepting a wheel matching any of them. macOS variants
47
+ carry a single tag (``extra_platform_tags`` empty).
48
+ """
49
+
50
+ arch: str
51
+ platform_tag: str
52
+ extra_platform_tags: tuple[str, ...] = ()
53
+
54
+ @property
55
+ def request_tags(self) -> tuple[str, ...]:
56
+ return (self.platform_tag, *self.extra_platform_tags)
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ResolvedWheel:
61
+ filename: str
62
+ url: str
63
+ sha256: str
64
+ upload_time: str | None = None
65
+ size: int | None = None
66
+
67
+
68
+ @dataclass
69
+ class ResolvedPackage:
70
+ name: str
71
+ version: str
72
+ wheels: list[ResolvedWheel] = field(default_factory=list)
73
+ requires_python: str | None = None
74
+ dependencies: list[str] = field(default_factory=list)
75
+ source_index: str | None = None
76
+
77
+
78
+ class WheelResolver(Protocol):
79
+ def resolve(
80
+ self,
81
+ requirements: list[str],
82
+ *,
83
+ python_version: str,
84
+ variants: tuple[Variant, ...],
85
+ extra_index_urls: list[str],
86
+ find_links: list[str] | None = None,
87
+ offline: bool = False,
88
+ ) -> list[ResolvedPackage]:
89
+ """Resolve requirements to wheels covering every requested variant."""
90
+ ...
91
+
92
+
93
+ class PipWheelResolver:
94
+ """Default backend: stock pip cross-resolution via ``pip install --report``."""
95
+
96
+ def __init__(self, python_executable: str | None = None) -> None:
97
+ self._python = python_executable or sys.executable
98
+
99
+ def resolve(
100
+ self,
101
+ requirements: list[str],
102
+ *,
103
+ python_version: str,
104
+ variants: tuple[Variant, ...],
105
+ extra_index_urls: list[str],
106
+ find_links: list[str] | None = None,
107
+ offline: bool = False,
108
+ ) -> list[ResolvedPackage]:
109
+ if not requirements:
110
+ return []
111
+ self._require_modern_pip()
112
+ abis = abi_tags(python_version)
113
+ merged: dict[str, ResolvedPackage] = {}
114
+ seen_filenames: dict[str, set[str]] = {}
115
+
116
+ for variant in variants:
117
+ report = self._run_report(
118
+ requirements,
119
+ python_version=python_version,
120
+ platform_tags=variant.request_tags,
121
+ abis=abis,
122
+ extra_index_urls=extra_index_urls,
123
+ find_links=find_links or [],
124
+ offline=offline,
125
+ )
126
+ for item in report.get("install", []):
127
+ self._absorb(item, merged, seen_filenames)
128
+
129
+ return list(merged.values())
130
+
131
+ def _require_modern_pip(self) -> None:
132
+ version = pip_version(self._python)
133
+ if version is not None and version < MIN_PIP_VERSION:
134
+ raise WheelResolverError(
135
+ f"kivyforge needs pip >= {version_str(MIN_PIP_VERSION)} to resolve "
136
+ f"wheels, but {self._python} has pip {version_str(version)}.\n"
137
+ f" Upgrade it: {self._python} -m pip install --upgrade pip"
138
+ )
139
+
140
+ def _absorb(self, item, merged, seen_filenames) -> None:
141
+ meta = item.get("metadata", {})
142
+ name = meta.get("name")
143
+ version = meta.get("version")
144
+ download = item.get("download_info", {})
145
+ url = download.get("url", "")
146
+ if not url.endswith(".whl"):
147
+ raise WheelResolverError(
148
+ f"{name} {version} resolved to a non-wheel source ({url}); "
149
+ "kivyforge installs only wheels (no on-host compile)."
150
+ )
151
+ archive = download.get("archive_info", {})
152
+ hashes = archive.get("hashes", {})
153
+ sha256 = hashes.get("sha256", "")
154
+ filename = url.rsplit("/", 1)[-1]
155
+ key = canonical_name(name)
156
+ pkg = merged.get(key)
157
+ if pkg is None:
158
+ pkg = ResolvedPackage(
159
+ name=name,
160
+ version=version,
161
+ requires_python=meta.get("requires_python"),
162
+ dependencies=_dep_names(meta.get("requires_dist", [])),
163
+ )
164
+ merged[key] = pkg
165
+ seen_filenames[key] = set()
166
+ elif version != pkg.version:
167
+ raise WheelResolverError(
168
+ f"{name} resolved to inconsistent versions across variants: "
169
+ f"{pkg.version!r} and {version!r}. Every variant of a package "
170
+ "must pin the same version."
171
+ )
172
+ if filename not in seen_filenames[key]:
173
+ seen_filenames[key].add(filename)
174
+ pkg.wheels.append(ResolvedWheel(filename=filename, url=url, sha256=sha256))
175
+
176
+ def _run_report(
177
+ self,
178
+ requirements: list[str],
179
+ *,
180
+ python_version: str,
181
+ platform_tags: tuple[str, ...],
182
+ abis: tuple[str, ...],
183
+ extra_index_urls: list[str],
184
+ find_links: list[str],
185
+ offline: bool,
186
+ ) -> dict:
187
+ with tempfile.TemporaryDirectory(prefix="kivy-wheel-lock-") as tmp:
188
+ report_path = Path(tmp) / "report.json"
189
+ cmd = [
190
+ self._python,
191
+ "-m",
192
+ "pip",
193
+ "install",
194
+ "--dry-run",
195
+ "--ignore-installed",
196
+ "--only-binary=:all:",
197
+ "--python-version",
198
+ pip_python_version(python_version),
199
+ "--implementation",
200
+ "cp",
201
+ "--target",
202
+ str(Path(tmp) / "target"),
203
+ "--report",
204
+ str(report_path),
205
+ ]
206
+ # pip accepts --platform multiple times; a wheel matching ANY of the
207
+ # tags is eligible while a single consistent version set is resolved.
208
+ for tag in platform_tags:
209
+ cmd += ["--platform", tag]
210
+ for abi in abis:
211
+ cmd += ["--abi", abi]
212
+ for index in extra_index_urls:
213
+ cmd += ["--extra-index-url", index]
214
+ for link in find_links:
215
+ cmd += ["--find-links", link]
216
+ if offline:
217
+ cmd += ["--no-index"]
218
+ cmd += list(requirements)
219
+
220
+ proc = subprocess.run(
221
+ cmd,
222
+ capture_output=True,
223
+ text=True,
224
+ encoding="utf-8",
225
+ errors="replace",
226
+ )
227
+ if proc.returncode != 0:
228
+ tags = ", ".join(platform_tags)
229
+ raise WheelResolverError(
230
+ f"pip could not resolve wheels for platform tag(s) {tags}.\n"
231
+ f" This usually means a dependency has no wheel for that "
232
+ f"variant upstream.\n"
233
+ f" pip said:\n{_indent(proc.stderr or proc.stdout)}"
234
+ )
235
+ try:
236
+ return json.loads(report_path.read_text(encoding="utf-8"))
237
+ except (OSError, ValueError) as exc:
238
+ raise WheelResolverError(f"could not read pip report: {exc}") from exc
239
+
240
+
241
+ def get_wheel_resolver(backend: str = "pip", **kwargs) -> WheelResolver:
242
+ if backend == "pip":
243
+ return PipWheelResolver(**kwargs)
244
+ raise WheelResolverError(f"unknown resolver backend {backend!r} (expected pip)")
245
+
246
+
247
+ def _indent(text: str) -> str:
248
+ return "\n".join(f" {line}" for line in (text or "").splitlines())
@@ -0,0 +1,161 @@
1
+ """Swappable Python-runtime providers for the wheel+runtime family.
2
+
3
+ A ``RuntimeProvider`` resolves a CPython version + arch set to concrete, pinned
4
+ per-arch archives and normalizes them (downstream) to a canonical relocatable
5
+ layout the bundler consumes. Hiding the *source* behind this seam means switching
6
+ from ``python-build-standalone`` (PBS) to python.org's future relocatable builds
7
+ is a re-lock, not a rewrite.
8
+
9
+ ``PbsProvider`` is the generic PBS implementation, parameterized by an
10
+ arch→target-triple map so each platform (macOS darwin triples now, Linux triples
11
+ later) is a one-line specialization. PBS embeds a dated release tag in every
12
+ asset filename, so the concrete URL + SHA-256 are discovered via an injectable
13
+ ``MetadataFetcher`` (keeping the selection logic hermetically testable); the real
14
+ GitHub-backed fetcher is validated at the phase stop.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Protocol
22
+
23
+ from .model import PythonRuntime, RuntimeArtifact
24
+
25
+ PBS_PROVIDER_NAME = "python-build-standalone"
26
+
27
+ # Per-provider archive layout: the sub-directory an extracted runtime archive
28
+ # places its canonical relocatable CPython tree under. Staging (macOS/Linux)
29
+ # consults this by the runtime's recorded ``provider`` field instead of assuming
30
+ # a fixed layout, so the bundler depends only on the canonical contract and a
31
+ # new provider is registered here in one place (spec 07).
32
+ _PROVIDER_ARCHIVE_ROOTS = {
33
+ PBS_PROVIDER_NAME: "python",
34
+ }
35
+
36
+
37
+ def normalized_runtime_root(provider: str, extracted: Path) -> Path:
38
+ """Return the canonical relocatable CPython root within *extracted*.
39
+
40
+ Encapsulates each provider's archive layout so bundlers never hard-code a
41
+ specific provider's internals. Raises :class:`RuntimeProviderError` for an
42
+ unregistered provider or an archive that does not match its expected layout.
43
+ """
44
+ try:
45
+ root_name = _PROVIDER_ARCHIVE_ROOTS[provider]
46
+ except KeyError:
47
+ raise RuntimeProviderError(
48
+ f"no runtime staging layout registered for provider {provider!r}; "
49
+ f"known providers: {', '.join(sorted(_PROVIDER_ARCHIVE_ROOTS))}."
50
+ ) from None
51
+ root = extracted / root_name
52
+ if not root.is_dir():
53
+ raise RuntimeProviderError(
54
+ f"the extracted runtime is missing the expected top-level "
55
+ f"{root_name}/ directory required by the {provider!r} runtime provider."
56
+ )
57
+ return root
58
+
59
+
60
+ class RuntimeProviderError(Exception):
61
+ """A runtime could not be resolved (unknown arch, missing asset, offline)."""
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ReleaseAsset:
66
+ """One resolved downloadable archive for a single arch."""
67
+
68
+ url: str
69
+ sha256: str
70
+ archive_format: str = "tar.gz"
71
+
72
+
73
+ class MetadataFetcher(Protocol):
74
+ """Resolves a (version, target triple) to a concrete asset (URL + SHA-256)."""
75
+
76
+ def fetch(
77
+ self, version: str, target_triple: str, *, offline: bool = False
78
+ ) -> ReleaseAsset: ...
79
+
80
+
81
+ class RuntimeProvider(Protocol):
82
+ name: str
83
+
84
+ def resolve(
85
+ self, version: str, archs: tuple[str, ...], *, offline: bool = False
86
+ ) -> PythonRuntime: ...
87
+
88
+
89
+ class PbsProvider:
90
+ """python-build-standalone provider, parameterized by an arch→triple map."""
91
+
92
+ name = PBS_PROVIDER_NAME
93
+
94
+ def __init__(
95
+ self,
96
+ triples: dict[str, str],
97
+ metadata_fetcher: MetadataFetcher | None = None,
98
+ *,
99
+ floor: str | None = None,
100
+ ) -> None:
101
+ self._triples = triples
102
+ # The default GitHub-backed fetcher is created lazily so tests that
103
+ # inject a fake never import the network layer.
104
+ self._fetcher = metadata_fetcher
105
+ self._floor = floor
106
+
107
+ def resolve(
108
+ self, version: str, archs: tuple[str, ...], *, offline: bool = False
109
+ ) -> PythonRuntime:
110
+ if not archs:
111
+ raise RuntimeProviderError("no architectures requested for the runtime")
112
+ unknown = [a for a in archs if a not in self._triples]
113
+ if unknown:
114
+ valid = ", ".join(sorted(self._triples))
115
+ raise RuntimeProviderError(
116
+ f"python-build-standalone has no build for arch(es) {unknown}; "
117
+ f"valid values are: {valid}."
118
+ )
119
+ fetcher = self._fetcher or _default_fetcher()
120
+ artifacts: list[RuntimeArtifact] = []
121
+ for arch in archs:
122
+ triple = self._triples[arch]
123
+ try:
124
+ asset = fetcher.fetch(version, triple, offline=offline)
125
+ except RuntimeProviderError:
126
+ raise
127
+ except Exception as exc: # noqa: BLE001 — normalize fetcher failures
128
+ raise RuntimeProviderError(
129
+ f"could not resolve a python-build-standalone {version} build "
130
+ f"for {arch} ({triple}): {exc}"
131
+ ) from exc
132
+ artifacts.append(
133
+ RuntimeArtifact(
134
+ arch=arch,
135
+ url=asset.url,
136
+ sha256=asset.sha256,
137
+ archive_format=asset.archive_format,
138
+ )
139
+ )
140
+ return PythonRuntime(
141
+ provider=self.name,
142
+ version=version,
143
+ artifacts=tuple(artifacts),
144
+ floor=self._floor,
145
+ )
146
+
147
+
148
+ def pbs_asset_glob(version: str, target_triple: str) -> str:
149
+ """The PBS ``install_only`` archive filename pattern for a version + triple.
150
+
151
+ PBS embeds a dated release tag (e.g.
152
+ ``cpython-3.15.0+20250115-aarch64-apple-darwin-install_only.tar.gz``); this
153
+ returns the tag-globbed stem the metadata lookup matches.
154
+ """
155
+ return f"cpython-{version}+*-{target_triple}-install_only.tar.gz"
156
+
157
+
158
+ def _default_fetcher() -> MetadataFetcher:
159
+ from .pbs_github import GithubPbsMetadataFetcher
160
+
161
+ return GithubPbsMetadataFetcher()