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,182 @@
1
+ """Validate ``[tool.kivy.<platform>].find_links`` before lock resolution.
2
+
3
+ ``find_links`` is a shared surface across iOS/macOS/Linux, so every diagnostic
4
+ here is parameterized by ``platform`` (the ``[tool.kivy.<platform>]`` key path
5
+ and platform-appropriate wording). ``platform`` defaults to ``"ios"`` for
6
+ backward compatibility with the original iOS-only call sites.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+
15
+ class FindLinksError(Exception):
16
+ """A find_links directory is missing or has no usable wheels."""
17
+
18
+
19
+ def resolve_find_links(project_root: Path, entries: tuple[str, ...]) -> list[str]:
20
+ """Resolve repo-relative ``find_links`` entries to absolute paths."""
21
+ root = project_root.resolve()
22
+ return [str((root / entry).resolve()) for entry in entries]
23
+
24
+
25
+ def validate_find_links(
26
+ project_root: Path, entries: tuple[str, ...], *, platform: str = "ios"
27
+ ) -> None:
28
+ """Raise ``FindLinksError`` when a find_links directory is missing or unusable."""
29
+ if not entries:
30
+ return
31
+
32
+ key = f"[tool.kivy.{platform}].find_links"
33
+ root = project_root.resolve()
34
+ lines: list[str] = []
35
+ for entry in entries:
36
+ path = (root / entry).resolve()
37
+ rel = _display_path(root, path, entry)
38
+ if not path.exists():
39
+ lines.append(f"{key} entry {entry!r} does not exist ({rel}).")
40
+ continue
41
+ if not path.is_dir():
42
+ lines.append(f"{key} entry {entry!r} is not a directory ({rel}).")
43
+ continue
44
+ if not any(path.glob("*.whl")):
45
+ lines.append(f"{key} entry {entry!r} contains no .whl files ({rel}).")
46
+
47
+ if not lines:
48
+ return
49
+
50
+ hint = _find_links_hint(root, entries, platform)
51
+ message = "\n".join(lines)
52
+ if hint:
53
+ message = f"{message}\n{hint}"
54
+ raise FindLinksError(message)
55
+
56
+
57
+ def wheel_path_from_project_root(project_root: Path, resolved: Path) -> str:
58
+ """Return a project-root-relative wheel path (may contain ``..``)."""
59
+ root = project_root.resolve()
60
+ resolved = resolved.resolve()
61
+ _ensure_find_link_scope(root, resolved)
62
+ return Path(os.path.relpath(resolved, root)).as_posix()
63
+
64
+
65
+ def find_links_resolution_hint(
66
+ project_root: Path,
67
+ entries: tuple[str, ...],
68
+ *,
69
+ pip_stderr: str = "",
70
+ platform: str = "ios",
71
+ ) -> str | None:
72
+ """Return extra guidance when pip resolution likely failed for find_links reasons."""
73
+ if not entries:
74
+ return None
75
+
76
+ root = project_root.resolve()
77
+ pip_text = pip_stderr.lower()
78
+ if (
79
+ "no matching distribution" not in pip_text
80
+ and "could not find a version" not in pip_text
81
+ ):
82
+ return None
83
+
84
+ lines: list[str] = []
85
+ for entry in entries:
86
+ path = (root / entry).resolve()
87
+ rel = _display_path(root, path, entry)
88
+ if not path.exists():
89
+ lines.append(f" • {entry!r} → missing ({rel})")
90
+ elif not path.is_dir():
91
+ lines.append(f" • {entry!r} → not a directory ({rel})")
92
+ elif not any(path.glob("*.whl")):
93
+ lines.append(
94
+ f" • {entry!r} → directory exists but has no .whl files ({rel})"
95
+ )
96
+ else:
97
+ count = len(list(path.glob("*.whl")))
98
+ lines.append(
99
+ f" • {entry!r} → {count} wheel(s) present ({rel}); "
100
+ "none matched this target's platform tags or a dependency."
101
+ )
102
+
103
+ if not lines:
104
+ return None
105
+
106
+ return (
107
+ "find_links check:\n"
108
+ + "\n".join(lines)
109
+ + "\n"
110
+ + _find_links_hint(root, entries, platform)
111
+ )
112
+
113
+
114
+ def find_links_doctor_detail(
115
+ project_root: Path, entry: str, path: Path, *, platform: str = "ios"
116
+ ) -> tuple[str, str | None]:
117
+ """Return (detail, hint) for a single find_links entry."""
118
+ rel = _display_path(project_root.resolve(), path, entry)
119
+ if not path.exists():
120
+ return f"{entry!r} missing ({rel})", _find_links_hint(
121
+ project_root, (entry,), platform
122
+ )
123
+ if not path.is_dir():
124
+ return f"{entry!r} is not a directory ({rel})", None
125
+ wheels = list(path.glob("*.whl"))
126
+ if not wheels:
127
+ return f"{entry!r} has no .whl files ({rel})", _find_links_hint(
128
+ project_root, (entry,), platform
129
+ )
130
+ return f"{entry!r} ({len(wheels)} wheel(s) in {rel})", None
131
+
132
+
133
+ def _find_links_hint(
134
+ project_root: Path, entries: tuple[str, ...], platform: str = "ios"
135
+ ) -> str:
136
+ del entries # reserved for future per-entry hints
137
+ shared_note = ""
138
+ if "examples" in project_root.parts and project_root.name != "wheels":
139
+ if platform == "ios":
140
+ # iOS ships a helper script to build its examples wheelhouse.
141
+ shared_note = (
142
+ " Shared example wheels live under examples/wheels/ios/ — run "
143
+ "`scripts/build_ios_wheels.sh` from the repo root.\n"
144
+ )
145
+ else:
146
+ shared_note = (
147
+ f" Shared example wheels live under examples/wheels/{platform}/.\n"
148
+ )
149
+ return (
150
+ f"{shared_note}"
151
+ f" Add locally built {platform} wheels to the directory, then re-run "
152
+ "`kivyforge lock`."
153
+ )
154
+
155
+
156
+ def _display_path(project_root: Path, path: Path, entry: str) -> str:
157
+ try:
158
+ return path.relative_to(project_root).as_posix()
159
+ except ValueError:
160
+ return entry
161
+
162
+
163
+ def _repo_root(start: Path) -> Path | None:
164
+ """Nearest ancestor (inclusive) containing a ``.git`` marker, else None."""
165
+ for candidate in (start, *start.parents):
166
+ if (candidate / ".git").exists():
167
+ return candidate
168
+ return None
169
+
170
+
171
+ def _ensure_find_link_scope(project_root: Path, resolved: Path) -> None:
172
+ allowed = [project_root, project_root.parent]
173
+ repo = _repo_root(project_root)
174
+ if repo is not None:
175
+ allowed.append(repo)
176
+ for base in allowed:
177
+ if resolved == base or base in resolved.parents:
178
+ return
179
+ raise FindLinksError(
180
+ f"find_links path {resolved} is outside the project directory, its "
181
+ "parent, and the enclosing repository"
182
+ )
@@ -0,0 +1,69 @@
1
+ """Shared, platform-neutral ``[[packages]]`` model (PEP 751).
2
+
3
+ The per-slice wheel + package dataclasses here are consumed by every backend
4
+ (iOS, macOS, Linux) and the generic wheel+runtime engine. Platform-specific
5
+ lockfile models (the iOS ``Lockfile`` + xcframework/swift-package entries, the
6
+ macOS/Linux ``WheelRuntimeLock``) build on top of these.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from dataclasses import dataclass
13
+
14
+ LOCK_VERSION = "1.0"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class LockedWheel:
19
+ """One ``[[packages.wheels]]`` entry (a single platform slice)."""
20
+
21
+ name: str # wheel filename; platform tag is parsed back out of this
22
+ sha256: str
23
+ url: str | None = None
24
+ path: str | None = None
25
+ upload_time: str | None = None
26
+ size: int | None = None
27
+
28
+ def __post_init__(self) -> None:
29
+ if bool(self.url) == bool(self.path):
30
+ raise ValueError(f"wheel {self.name!r} must have exactly one of url/path")
31
+
32
+ @property
33
+ def platform_tag(self) -> str:
34
+ """The platform tag parsed from the wheel filename (last tag segment)."""
35
+ stem = self.name[:-4] if self.name.endswith(".whl") else self.name
36
+ return stem.rsplit("-", 1)[-1]
37
+
38
+ @property
39
+ def is_pure_python(self) -> bool:
40
+ return "none-any" in self.name
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class PackageDep:
45
+ name: str
46
+ marker: str | None = None
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class LockedPackage:
51
+ """One ``[[packages]]`` entry."""
52
+
53
+ name: str
54
+ version: str
55
+ wheels: tuple[LockedWheel, ...]
56
+ requires_python: str | None = None
57
+ dependencies: tuple[PackageDep, ...] = ()
58
+ marker: str | None = None
59
+ direct_requirement: bool = False
60
+ source_index: str | None = None
61
+
62
+ @property
63
+ def sort_key(self) -> tuple[str, str]:
64
+ return (canonical_name(self.name), self.version)
65
+
66
+
67
+ def canonical_name(name: str) -> str:
68
+ """PEP 503 normalization for package-name comparison."""
69
+ return re.sub(r"[-_.]+", "-", name).lower()
@@ -0,0 +1,111 @@
1
+ """Shared, platform-neutral PEP 751 serialization helpers.
2
+
3
+ Both the iOS and macOS lock writers/readers emit and parse the ``[[packages]]``
4
+ wheels section identically; only the per-platform ``[tool.kivyforge]`` extension
5
+ (the iOS ``python_xcframework``/xcframeworks/swift_packages vs. the macOS python
6
+ runtime) differs. Keeping the ``[[packages]]`` machinery here means a single
7
+ source of truth for the PEP 751 layout and string escaping.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .model import LockedPackage, LockedWheel, PackageDep
13
+
14
+
15
+ def s(value: str) -> str:
16
+ """Emit a TOML basic string with minimal escaping."""
17
+ escaped = (
18
+ value.replace("\\", "\\\\")
19
+ .replace('"', '\\"')
20
+ .replace("\n", "\\n")
21
+ .replace("\t", "\\t")
22
+ )
23
+ return f'"{escaped}"'
24
+
25
+
26
+ def arr(values) -> str:
27
+ if not values:
28
+ return "[]"
29
+ return "[" + ", ".join(s(v) for v in values) + "]"
30
+
31
+
32
+ def b(value: bool) -> str:
33
+ return "true" if value else "false"
34
+
35
+
36
+ def emit_package(lines: list[str], pkg: LockedPackage) -> None:
37
+ lines.append("[[packages]]")
38
+ lines.append(f"name = {s(pkg.name)}")
39
+ lines.append(f"version = {s(pkg.version)}")
40
+ if pkg.requires_python:
41
+ lines.append(f"requires-python = {s(pkg.requires_python)}")
42
+ if pkg.marker:
43
+ lines.append(f"marker = {s(pkg.marker)}")
44
+ if pkg.dependencies:
45
+ deps = ", ".join(_dep_inline(d) for d in pkg.dependencies)
46
+ lines.append(f"dependencies = [{deps}]")
47
+
48
+ if pkg.direct_requirement or pkg.source_index:
49
+ lines.append("")
50
+ lines.append("[packages.tool.kivyforge]")
51
+ if pkg.direct_requirement:
52
+ lines.append("direct_requirement = true")
53
+ if pkg.source_index:
54
+ lines.append(f"source_index = {s(pkg.source_index)}")
55
+
56
+ for wheel in sorted(pkg.wheels, key=lambda w: w.name):
57
+ lines.append("")
58
+ emit_wheel(lines, wheel)
59
+ lines.append("")
60
+
61
+
62
+ def emit_wheel(lines: list[str], wheel: LockedWheel) -> None:
63
+ lines.append("[[packages.wheels]]")
64
+ lines.append(f"name = {s(wheel.name)}")
65
+ if wheel.upload_time:
66
+ lines.append(f"upload-time = {s(wheel.upload_time)}")
67
+ if wheel.url:
68
+ lines.append(f"url = {s(wheel.url)}")
69
+ else:
70
+ path = wheel.path
71
+ assert path is not None
72
+ lines.append(f"path = {s(path)}")
73
+ lines.append(f"hashes = {{ sha256 = {s(wheel.sha256)} }}")
74
+ if wheel.size is not None:
75
+ lines.append(f"size = {wheel.size}")
76
+
77
+
78
+ def parse_package(p: dict) -> LockedPackage:
79
+ tool = p.get("tool", {}).get("kivyforge", {})
80
+ deps = tuple(
81
+ PackageDep(name=d["name"], marker=d.get("marker"))
82
+ for d in p.get("dependencies", [])
83
+ )
84
+ wheels = tuple(parse_wheel(w) for w in p.get("wheels", []))
85
+ return LockedPackage(
86
+ name=p["name"],
87
+ version=p["version"],
88
+ wheels=wheels,
89
+ requires_python=p.get("requires-python"),
90
+ dependencies=deps,
91
+ marker=p.get("marker"),
92
+ direct_requirement=bool(tool.get("direct_requirement", False)),
93
+ source_index=tool.get("source_index"),
94
+ )
95
+
96
+
97
+ def parse_wheel(w: dict) -> LockedWheel:
98
+ return LockedWheel(
99
+ name=w["name"],
100
+ sha256=w.get("hashes", {}).get("sha256", ""),
101
+ url=w.get("url"),
102
+ path=w.get("path"),
103
+ upload_time=w.get("upload-time"),
104
+ size=w.get("size"),
105
+ )
106
+
107
+
108
+ def _dep_inline(dep: PackageDep) -> str:
109
+ if dep.marker:
110
+ return f"{{ name = {s(dep.name)}, marker = {s(dep.marker)} }}"
111
+ return f"{{ name = {s(dep.name)} }}"
@@ -0,0 +1,30 @@
1
+ """Shared lockfile drift detection + the ``LockError`` type.
2
+
3
+ Every backend records the pyproject SHA-256 in its lockfile and compares it on
4
+ build to detect drift; those neutral helpers live here so iOS, macOS, and Linux
5
+ readers all reuse them. The iOS ``pylock.ios.toml`` parser lives in
6
+ ``kivyforge.platforms.ios.lock.reader``; the wheel+runtime parser lives in
7
+ ``kivyforge.lock.wheelruntime.serialize``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from typing import Protocol
14
+
15
+
16
+ class LockError(Exception):
17
+ """A malformed or unreadable lockfile."""
18
+
19
+
20
+ class _HasPyprojectSha(Protocol):
21
+ pyproject_sha256: str
22
+
23
+
24
+ def compute_pyproject_sha256(text: str) -> str:
25
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
26
+
27
+
28
+ def is_in_sync(lock: _HasPyprojectSha, pyproject_text: str) -> bool:
29
+ """True if the lock's recorded pyproject hash matches the current pyproject."""
30
+ return lock.pyproject_sha256 == compute_pyproject_sha256(pyproject_text)
@@ -0,0 +1,124 @@
1
+ """Platform-neutral pip helpers shared by every wheel resolver (spec 02).
2
+
3
+ The iOS resolver (``kivyforge.platforms.ios.lock.resolver``) and the generic
4
+ wheel+runtime resolver (``kivyforge.lock.wheelruntime.resolver``) both drive pip
5
+ in dry-run ``--report`` mode per platform tag; the pip-version gate, version
6
+ formatting, ABI tag list, ``--python-version`` normalization, and requires-dist
7
+ name parsing are identical and live here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import subprocess
13
+
14
+ from packaging.requirements import InvalidRequirement, Requirement
15
+
16
+ # Minimum host pip that understands PEP 730 iOS platform tags (pip 24.3, 2024-10-27).
17
+ # Earlier pip cannot match an ``ios_<dt>_*`` --platform request against a wheel
18
+ # tagged at a lower floor (e.g. ios_13_0_*), so compatible wheels are reported as
19
+ # missing. kivyforge targets Python that ships iOS support, so requiring a current
20
+ # pip is simpler — and far less surprising — than reimplementing tag expansion.
21
+ MIN_PIP_VERSION = (24, 3)
22
+
23
+
24
+ def version_str(version: tuple[int, ...]) -> str:
25
+ return ".".join(str(part) for part in version)
26
+
27
+
28
+ def pip_version(python_executable: str) -> tuple[int, ...] | None:
29
+ """Best-effort ``(major, minor, ...)`` version of pip for ``python_executable``.
30
+
31
+ Returns ``None`` when pip can't be queried — treated as "unknown" (don't
32
+ block) rather than "too old", since a genuinely broken pip surfaces its own
33
+ error when resolution runs.
34
+ """
35
+ try:
36
+ proc = subprocess.run(
37
+ [python_executable, "-m", "pip", "--version"],
38
+ capture_output=True,
39
+ text=True,
40
+ )
41
+ except (OSError, ValueError):
42
+ return None
43
+ if proc.returncode != 0:
44
+ return None
45
+ # Format: "pip 24.3.1 from /path/site-packages/pip (python 3.15)".
46
+ tokens = proc.stdout.split()
47
+ if len(tokens) < 2 or tokens[0] != "pip":
48
+ return None
49
+ return _parse_version(tokens[1])
50
+
51
+
52
+ def _parse_version(text: str) -> tuple[int, ...] | None:
53
+ parts: list[int] = []
54
+ for chunk in text.split("."):
55
+ digits = "".join(c for c in chunk if c.isdigit())
56
+ if not digits:
57
+ break
58
+ parts.append(int(digits))
59
+ return tuple(parts) or None
60
+
61
+
62
+ def pip_python_version(python_version: str) -> str:
63
+ """``pip install --python-version`` accepts only integer dotted parts."""
64
+ parts: list[str] = []
65
+ for chunk in python_version.split("."):
66
+ digits = "".join(c for c in chunk if c.isdigit())
67
+ if not digits:
68
+ break
69
+ parts.append(digits)
70
+ if len(parts) >= 2:
71
+ return f"{parts[0]}.{parts[1]}"
72
+ return python_version
73
+
74
+
75
+ def abi_tags(python_version: str) -> tuple[str, ...]:
76
+ """ABI tags to offer pip; cp<XY> first, then the limited-API/none fallbacks.
77
+
78
+ Per the Phase 0 findings we must not over-constrain abi (abi3 wheels would
79
+ be missed otherwise).
80
+ """
81
+ major_minor = "".join(python_version.split(".")[:2])
82
+ return (f"cp{major_minor}", "abi3", "none")
83
+
84
+
85
+ def dep_names_for_environment(
86
+ requires_dist: list[str], environment: dict[str, str]
87
+ ) -> list[str]:
88
+ """The dependency edges that actually apply on the target platform.
89
+
90
+ ``Requires-Dist`` lists every edge a distribution *might* have, gated by
91
+ markers: Kivy's raw list names ``pytest``, ``sphinx`` and
92
+ ``kivy-deps.angle`` alongside its real runtime dependencies. Recording it
93
+ unfiltered would describe a dependency graph the lock does not install, so
94
+ each marker is evaluated against the target environment. ``extra`` is empty
95
+ there, which drops extra-gated edges too.
96
+ """
97
+ names: list[str] = []
98
+ for raw in requires_dist or []:
99
+ try:
100
+ requirement = Requirement(raw)
101
+ except InvalidRequirement:
102
+ # Unparseable metadata is upstream's problem, not a reason to fail
103
+ # the lock; fall back to the permissive name-only reading.
104
+ names.extend(_dep_names([raw]))
105
+ continue
106
+ if requirement.marker is not None and not requirement.marker.evaluate(
107
+ environment
108
+ ):
109
+ continue
110
+ names.append(requirement.name)
111
+ return names
112
+
113
+
114
+ def _dep_names(requires_dist: list[str]) -> list[str]:
115
+ names: list[str] = []
116
+ for raw in requires_dist or []:
117
+ # Strip everything after the name: extras, specifiers, markers.
118
+ name = raw.split(";", 1)[0].split("[", 1)[0]
119
+ for sep in ("==", ">=", "<=", "~=", "!=", ">", "<", "(", " "):
120
+ name = name.split(sep, 1)[0]
121
+ name = name.strip()
122
+ if name:
123
+ names.append(name)
124
+ return names
@@ -0,0 +1,70 @@
1
+ """Generic ``wheel + bundled-runtime`` lock engine.
2
+
3
+ The reusable core for every platform whose lock is "platform-tagged wheels plus
4
+ a bundled CPython runtime": macOS now; Linux, Windows, Android later. Each such
5
+ platform is a thin :class:`PlatformLockProfile` over this engine — no new
6
+ builder, resolver, model, or serializer. iOS is intentionally *not* in this
7
+ family (its per-slice framework/xcframework/SPM lock is a different shape).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .builder import (
13
+ WheelRuntimeBuildError,
14
+ build_wheel_runtime_lock,
15
+ diff_summary,
16
+ semantic_equal,
17
+ )
18
+ from .model import (
19
+ LockedNativeBinary,
20
+ PythonRuntime,
21
+ RuntimeArtifact,
22
+ WheelRuntimeLock,
23
+ )
24
+ from .native_binaries import resolve_native_binaries
25
+ from .profile import PlatformLockProfile
26
+ from .resolver import (
27
+ PipWheelResolver,
28
+ ResolvedPackage,
29
+ ResolvedWheel,
30
+ Variant,
31
+ WheelResolver,
32
+ WheelResolverError,
33
+ get_wheel_resolver,
34
+ )
35
+ from .runtime import (
36
+ PbsProvider,
37
+ ReleaseAsset,
38
+ RuntimeProvider,
39
+ RuntimeProviderError,
40
+ pbs_asset_glob,
41
+ )
42
+ from .serialize import dumps, load, loads
43
+
44
+ __all__ = [
45
+ "LockedNativeBinary",
46
+ "PbsProvider",
47
+ "PipWheelResolver",
48
+ "PlatformLockProfile",
49
+ "PythonRuntime",
50
+ "ReleaseAsset",
51
+ "ResolvedPackage",
52
+ "ResolvedWheel",
53
+ "RuntimeArtifact",
54
+ "RuntimeProvider",
55
+ "RuntimeProviderError",
56
+ "Variant",
57
+ "WheelResolver",
58
+ "WheelResolverError",
59
+ "WheelRuntimeBuildError",
60
+ "WheelRuntimeLock",
61
+ "build_wheel_runtime_lock",
62
+ "diff_summary",
63
+ "dumps",
64
+ "get_wheel_resolver",
65
+ "load",
66
+ "loads",
67
+ "pbs_asset_glob",
68
+ "resolve_native_binaries",
69
+ "semantic_equal",
70
+ ]