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,387 @@
1
+ """Assemble an ``AndroidLockfile`` from a validated ``Config`` (android/02).
2
+
3
+ Resolution semantics, in the documented order: runtime per ABI (with the
4
+ ``min_sdk >= min_api`` check), per-ABI wheels (fail-fasts live in the
5
+ resolver + the completeness check here), ``.aar``/``.jar`` pins, Gradle/Maven
6
+ pins (skipped without declared coordinates), ``include_files`` hashes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING
15
+
16
+ from kivyforge import __version__
17
+ from kivyforge.artifacts.verify import sha256_file
18
+ from kivyforge.config.model import AndroidConfig, Config
19
+ from kivyforge.lock.find_links import (
20
+ FindLinksError,
21
+ find_links_resolution_hint,
22
+ resolve_find_links,
23
+ validate_find_links,
24
+ wheel_path_from_project_root,
25
+ )
26
+ from kivyforge.lock.model import (
27
+ LockedPackage,
28
+ LockedWheel,
29
+ PackageDep,
30
+ canonical_name,
31
+ )
32
+ from kivyforge.lock.reader import compute_pyproject_sha256
33
+
34
+ from .archives import ArchiveResolverError, resolve_android_libs
35
+ from .maven import GradleMavenResolver, MavenResolverError, resolve_gradle_pins
36
+ from .model import (
37
+ DEFAULT_REQUIRES_PYTHON,
38
+ AndroidLockfile,
39
+ LockedIncludeFile,
40
+ PythonAndroidRuntime,
41
+ )
42
+ from .python_meta import (
43
+ PythonAndroidError,
44
+ PythonAndroidProvider,
45
+ PythonOrgAndroidProvider,
46
+ )
47
+ from .resolver import Resolver, ResolverError, get_resolver
48
+
49
+ if TYPE_CHECKING:
50
+ from kivyforge.artifacts.download import Downloader
51
+
52
+
53
+ class BuildError(Exception):
54
+ """A lock build failure surfaced with an actionable message."""
55
+
56
+
57
+ def build_lockfile(
58
+ config: Config,
59
+ pyproject_text: str,
60
+ *,
61
+ project_root: Path | None = None,
62
+ resolver: Resolver | None = None,
63
+ python_provider: PythonAndroidProvider | None = None,
64
+ maven_resolver: GradleMavenResolver | None = None,
65
+ lib_downloader: Downloader | None = None,
66
+ offline: bool = False,
67
+ now: datetime | None = None,
68
+ ) -> AndroidLockfile:
69
+ if config.android is None:
70
+ raise BuildError(
71
+ "pyproject.toml has no [tool.kivy.android] table; nothing to lock."
72
+ )
73
+ android = config.android
74
+ resolver = resolver or get_resolver("pip")
75
+ python_provider = python_provider or PythonOrgAndroidProvider()
76
+ root = (project_root or Path.cwd()).resolve()
77
+
78
+ python_version = android.python_required.version
79
+
80
+ find_links_entries = android.find_links
81
+ try:
82
+ validate_find_links(root, find_links_entries, platform="android")
83
+ except FindLinksError as exc:
84
+ raise BuildError(str(exc)) from exc
85
+ find_links = resolve_find_links(root, find_links_entries)
86
+
87
+ runtimes = _resolve_runtimes(
88
+ android, python_version, python_provider, offline=offline
89
+ )
90
+
91
+ direct = {canonical_name(_req_name(d)) for d in config.project.dependencies}
92
+ excluded = {canonical_name(e) for e in android.exclude}
93
+ # You can't exclude what you explicitly depend on (android/01 §exclude).
94
+ excluded -= direct
95
+
96
+ try:
97
+ resolved = resolver.resolve(
98
+ list(config.project.dependencies),
99
+ python_version=python_version,
100
+ min_sdk=android.min_sdk,
101
+ abis=tuple(android.abis),
102
+ extra_index_urls=list(android.extra_index_urls),
103
+ find_links=find_links,
104
+ offline=offline,
105
+ )
106
+ except ResolverError as exc:
107
+ hint = find_links_resolution_hint(
108
+ root, find_links_entries, pip_stderr=str(exc), platform="android"
109
+ )
110
+ if hint:
111
+ raise BuildError(f"{exc}\n{hint}") from exc
112
+ raise BuildError(str(exc)) from exc
113
+
114
+ packages = []
115
+ for rp in resolved:
116
+ if canonical_name(rp.name) in excluded:
117
+ continue
118
+ wheels = tuple(
119
+ _locked_wheel_from_resolved(w, project_root=root) for w in rp.wheels
120
+ )
121
+ _check_abis_complete(rp.name, wheels, android.min_sdk, android.abis)
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
+ try:
135
+ android_libs = resolve_android_libs(
136
+ android.aars + android.jars,
137
+ project_root=root,
138
+ downloader=lib_downloader,
139
+ offline=offline,
140
+ )
141
+ except ArchiveResolverError as exc:
142
+ raise BuildError(str(exc)) from exc
143
+
144
+ try:
145
+ gradle_pins = resolve_gradle_pins(
146
+ android.gradle, resolver=maven_resolver, offline=offline
147
+ )
148
+ except MavenResolverError as exc:
149
+ raise BuildError(str(exc)) from exc
150
+
151
+ include_files = _hash_include_files(android, root)
152
+
153
+ return AndroidLockfile(
154
+ requires_python=config.project.requires_python or DEFAULT_REQUIRES_PYTHON,
155
+ packages=tuple(packages),
156
+ python_android=tuple(runtimes),
157
+ kivyforge_version=__version__,
158
+ generated_at=(now or datetime.now(UTC)).strftime("%Y-%m-%dT%H:%M:%SZ"),
159
+ pyproject_sha256=compute_pyproject_sha256(pyproject_text),
160
+ tool_kivy_android_schema_version=android.schema_version,
161
+ kivy_generation=android.kivy_generation,
162
+ android_libs=tuple(android_libs),
163
+ gradle=gradle_pins,
164
+ include_files=tuple(include_files),
165
+ )
166
+
167
+
168
+ def _resolve_runtimes(
169
+ android: AndroidConfig,
170
+ python_version: str,
171
+ provider: PythonAndroidProvider,
172
+ *,
173
+ offline: bool,
174
+ ) -> list[PythonAndroidRuntime]:
175
+ runtimes: list[PythonAndroidRuntime] = []
176
+ for abi in android.abis:
177
+ try:
178
+ info = provider.get(python_version, abi, offline=offline)
179
+ except PythonAndroidError as exc:
180
+ raise BuildError(str(exc)) from exc
181
+ if android.min_sdk < info.min_api:
182
+ raise BuildError(
183
+ f"[tool.kivy.android].min_sdk {android.min_sdk} is below the "
184
+ f"python.org runtime's minimum API {info.min_api} "
185
+ f"(Python {python_version}, {abi}).\n"
186
+ f" Raise min_sdk to at least {info.min_api}."
187
+ )
188
+ runtimes.append(
189
+ PythonAndroidRuntime(
190
+ version=info.version,
191
+ abi=info.abi,
192
+ url=info.url,
193
+ sha256=info.sha256,
194
+ min_api=info.min_api,
195
+ )
196
+ )
197
+ return runtimes
198
+
199
+
200
+ def _hash_include_files(android: AndroidConfig, root: Path) -> list[LockedIncludeFile]:
201
+ """One pin per staged file; a directory source expands to its files."""
202
+ pins: list[LockedIncludeFile] = []
203
+ for entry in android.include_files:
204
+ for source in entry.sources:
205
+ local = (root / source).resolve()
206
+ if local.is_dir():
207
+ for child in sorted(local.rglob("*")):
208
+ if child.is_file():
209
+ rel = (Path(source) / child.relative_to(local)).as_posix()
210
+ pins.append(
211
+ LockedIncludeFile(
212
+ source=rel,
213
+ dest=entry.dest,
214
+ sha256=sha256_file(child),
215
+ )
216
+ )
217
+ elif local.is_file():
218
+ pins.append(
219
+ LockedIncludeFile(
220
+ source=Path(source).as_posix(),
221
+ dest=entry.dest,
222
+ sha256=sha256_file(local),
223
+ )
224
+ )
225
+ else:
226
+ raise BuildError(
227
+ f"include_files source does not exist: {source} "
228
+ f"(resolved to {local})"
229
+ )
230
+ pins.sort(key=lambda p: (p.dest, p.source))
231
+ return pins
232
+
233
+
234
+ def _locked_wheel_from_resolved(w, *, project_root: Path) -> LockedWheel:
235
+ url, path = _normalize_wheel_source(w.url, project_root=project_root)
236
+ sha256 = w.sha256
237
+ if not sha256 and path:
238
+ sha256 = sha256_file((project_root / path).resolve())
239
+ if not sha256:
240
+ raise BuildError(
241
+ f"could not determine SHA-256 for wheel {w.filename!r}; "
242
+ f"re-run lock with network access or check the vendored file."
243
+ )
244
+ return LockedWheel(
245
+ name=w.filename,
246
+ url=url,
247
+ path=path,
248
+ sha256=sha256,
249
+ upload_time=w.upload_time,
250
+ size=w.size,
251
+ )
252
+
253
+
254
+ def _normalize_wheel_source(
255
+ url: str, *, project_root: Path
256
+ ) -> tuple[str | None, str | None]:
257
+ from urllib.parse import urlparse
258
+ from urllib.request import url2pathname
259
+
260
+ if url.startswith(("http://", "https://")):
261
+ return url, None
262
+ if url.startswith("file:"):
263
+ local = Path(url2pathname(urlparse(url).path))
264
+ else:
265
+ local = Path(url)
266
+ resolved = local.resolve()
267
+ root = project_root.resolve()
268
+ try:
269
+ rel = wheel_path_from_project_root(root, resolved)
270
+ except FindLinksError as exc:
271
+ raise BuildError(
272
+ f"wheel resolved to {resolved}, which is outside the allowed "
273
+ f"find_links scope for project directory {root}.\n"
274
+ f" Vendored wheels must live under the project directory, a "
275
+ f"sibling directory, or the enclosing repository."
276
+ ) from exc
277
+ return None, rel
278
+
279
+
280
+ def _check_abis_complete(
281
+ name: str,
282
+ wheels: tuple[LockedWheel, ...],
283
+ min_sdk: int,
284
+ abis: tuple[str, ...],
285
+ ) -> None:
286
+ """Fail fast if a compiled package is missing a targeted ABI (android/02).
287
+
288
+ Compatibility rule: an ``android_<api>_<abi>`` wheel covers a targeted ABI
289
+ when its tag API level is <= ``min_sdk`` (the tag is a floor, android/01
290
+ §find_links) and the ABI matches exactly. Pure-Python packages are complete
291
+ with their single ``py3-none-any`` entry.
292
+ """
293
+ if any(w.is_pure_python for w in wheels):
294
+ return
295
+ covered: set[str] = set()
296
+ for wheel in wheels:
297
+ tag = wheel.platform_tag # e.g. "android_24_arm64_v8a"
298
+ parts = tag.split("_", 2) # ["android", "24", "arm64_v8a"]
299
+ if parts[0] != "android" or len(parts) != 3:
300
+ continue
301
+ try:
302
+ api = int(parts[1])
303
+ except ValueError:
304
+ continue
305
+ if api <= min_sdk and parts[2] in abis:
306
+ covered.add(parts[2])
307
+ missing = [abi for abi in abis if abi not in covered]
308
+ if missing:
309
+ tags = ", ".join(f"android_{min_sdk}_{abi}" for abi in missing)
310
+ raise BuildError(
311
+ f"{name} is missing Android wheel slice(s): {tags}.\n"
312
+ f" A compiled package must publish (or vendor via find_links) a "
313
+ f"wheel for every targeted ABI to be locked reproducibly; narrow "
314
+ f"[tool.kivy.android].abis or supply the missing wheel (android/03)."
315
+ )
316
+
317
+
318
+ def _normalized(lock: AndroidLockfile) -> AndroidLockfile:
319
+ """Sort every resolver-derived collection, ignoring ``generated_at``.
320
+
321
+ ``writer.py`` always sorts these before serializing, but a freshly
322
+ resolved lockfile (as ``--check`` builds to compare against) reflects
323
+ resolver-graph order instead -- same content, different tuple order.
324
+ Plain dataclass equality is order-sensitive, so comparing raw objects
325
+ reports a same-content lock as stale (found comparing a written lock
326
+ against itself, re-resolved). Sorting both sides by the writer's own
327
+ keys before comparing makes the comparison mean what "in sync" should:
328
+ same content, not same resolver iteration order.
329
+ """
330
+ gradle = dataclasses.replace(
331
+ lock.gradle,
332
+ resolved=tuple(
333
+ dataclasses.replace(
334
+ module, artifacts=tuple(sorted(module.artifacts, key=lambda a: a.name))
335
+ )
336
+ for module in sorted(lock.gradle.resolved, key=lambda m: m.coordinate)
337
+ ),
338
+ )
339
+ return dataclasses.replace(
340
+ lock,
341
+ generated_at="",
342
+ packages=tuple(sorted(lock.packages, key=lambda p: p.sort_key)),
343
+ python_android=tuple(sorted(lock.python_android, key=lambda r: r.abi)),
344
+ android_libs=tuple(
345
+ sorted(lock.android_libs, key=lambda x: (x.name.lower(), x.version))
346
+ ),
347
+ include_files=tuple(
348
+ sorted(lock.include_files, key=lambda p: (p.dest, p.source))
349
+ ),
350
+ gradle=gradle,
351
+ )
352
+
353
+
354
+ def semantic_equal(a: AndroidLockfile, b: AndroidLockfile) -> bool:
355
+ """Compare two lockfiles ignoring volatile timestamps and tuple order."""
356
+ return _normalized(a) == _normalized(b)
357
+
358
+
359
+ def diff_summary(old: AndroidLockfile, new: AndroidLockfile) -> list[str]:
360
+ """Human-readable summary of what changed between two locks (for --check)."""
361
+ out: list[str] = []
362
+ old_pkgs = {canonical_name(p.name): p for p in old.packages}
363
+ new_pkgs = {canonical_name(p.name): p for p in new.packages}
364
+ for name in sorted(new_pkgs.keys() - old_pkgs.keys()):
365
+ out.append(f" + {new_pkgs[name].name} {new_pkgs[name].version} (added)")
366
+ for name in sorted(old_pkgs.keys() - new_pkgs.keys()):
367
+ out.append(f" - {old_pkgs[name].name} {old_pkgs[name].version} (removed)")
368
+ for name in sorted(old_pkgs.keys() & new_pkgs.keys()):
369
+ if old_pkgs[name].version != new_pkgs[name].version:
370
+ out.append(
371
+ f" ~ {new_pkgs[name].name}: "
372
+ f"{old_pkgs[name].version} -> {new_pkgs[name].version}"
373
+ )
374
+ old_rt = {r.abi: r.version for r in old.python_android}
375
+ new_rt = {r.abi: r.version for r in new.python_android}
376
+ if old_rt != new_rt:
377
+ out.append(f" ~ python.org Android runtime: {old_rt} -> {new_rt}")
378
+ return out
379
+
380
+
381
+ def _req_name(requirement: str) -> str:
382
+ from packaging.requirements import InvalidRequirement, Requirement
383
+
384
+ try:
385
+ return Requirement(requirement).name
386
+ except InvalidRequirement:
387
+ return requirement
@@ -0,0 +1,71 @@
1
+ """The PEP 508 marker environment of the *target*, not the lock host (android/02).
2
+
3
+ pip's ``--platform`` / ``--abi`` / ``--python-version`` decide which wheel
4
+ *tags* are acceptable. They do **not** change how environment markers are
5
+ evaluated: pip evaluates every ``; sys_platform == "win32"``-style marker
6
+ against the interpreter running pip. Cross-resolving therefore inherits the
7
+ host's identity, and the same ``kivyforge lock -p android`` produces different
8
+ locks — or fails outright — depending on the machine that ran it.
9
+
10
+ The concrete failure that motivated this: Kivy declares
11
+
12
+ kivy-deps.angle~=0.4.0; sys_platform == "win32"
13
+
14
+ so locking for Android **on Windows** kept that requirement, looked for an
15
+ ``android_24_arm64_v8a`` wheel of a Windows-only ANGLE build, and failed. The
16
+ same lock succeeded on Linux. A lock file that depends on who ran it is not a
17
+ lock file, so kivyforge supplies the environment explicitly instead.
18
+
19
+ Values follow CPython's own Android build: ``sys.platform`` is ``"android"``
20
+ (CPython 3.13+, PEP 738) and ``platform.system()`` is ``"Android"``.
21
+ ``platform_release`` / ``platform_version`` are left empty on purpose — they
22
+ describe the device kernel, which is unknowable at lock time and which no
23
+ sane wheel should gate on.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ # Android ABI -> the machine name CPython reports there (``platform.machine()``).
29
+ ANDROID_ABI_MACHINE = {
30
+ "arm64_v8a": "aarch64",
31
+ "armeabi_v7a": "armv7l",
32
+ "x86_64": "x86_64",
33
+ "x86": "i686",
34
+ }
35
+
36
+
37
+ class MarkerEnvironmentError(Exception):
38
+ """The target marker environment could not be described."""
39
+
40
+
41
+ def android_marker_environment(*, python_version: str, abi: str) -> dict[str, str]:
42
+ """The PEP 508 environment a wheel would see on ``abi`` running Android.
43
+
44
+ ``python_version`` is the full runtime version (e.g. ``3.14.6``).
45
+ """
46
+ machine = ANDROID_ABI_MACHINE.get(abi)
47
+ if machine is None:
48
+ raise MarkerEnvironmentError(
49
+ f"unknown Android ABI {abi!r}; cannot describe its marker "
50
+ f"environment (known: {', '.join(sorted(ANDROID_ABI_MACHINE))})."
51
+ )
52
+ parts = python_version.split(".")
53
+ if len(parts) < 2:
54
+ raise MarkerEnvironmentError(
55
+ f"python version {python_version!r} is not a full X.Y.Z version; "
56
+ "cannot describe the target marker environment."
57
+ )
58
+ short = ".".join(parts[:2])
59
+ return {
60
+ "implementation_name": "cpython",
61
+ "implementation_version": python_version,
62
+ "os_name": "posix",
63
+ "platform_machine": machine,
64
+ "platform_python_implementation": "CPython",
65
+ "platform_release": "",
66
+ "platform_system": "Android",
67
+ "platform_version": "",
68
+ "python_full_version": python_version,
69
+ "python_version": short,
70
+ "sys_platform": "android",
71
+ }