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,206 @@
1
+ """Parse ``pylock.android.toml`` back into an ``AndroidLockfile`` (android/02).
2
+
3
+ The neutral drift helpers (``LockError``, ``compute_pyproject_sha256``,
4
+ ``is_in_sync``) live in ``kivyforge.lock.reader``; this module adds the Android
5
+ ``pylock.android.toml`` parsing on top. The reader fails *cleanly* on corrupt,
6
+ hand-edited, or future-schema files — never a raw ``KeyError``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import tomllib
12
+ from pathlib import Path
13
+
14
+ from kivyforge.lock.pep751 import parse_package as _parse_package
15
+ from kivyforge.lock.reader import (
16
+ LockError,
17
+ compute_pyproject_sha256,
18
+ is_in_sync,
19
+ )
20
+
21
+ from .model import (
22
+ LOCK_VERSION,
23
+ TOOL_SCHEMA_VERSION,
24
+ AndroidLockfile,
25
+ GradleArtifact,
26
+ GradlePins,
27
+ GradleResolvedModule,
28
+ LockedAndroidLib,
29
+ LockedIncludeFile,
30
+ PythonAndroidRuntime,
31
+ )
32
+
33
+ __all__ = [
34
+ "LockError",
35
+ "compute_pyproject_sha256",
36
+ "is_in_sync",
37
+ "load",
38
+ "loads",
39
+ ]
40
+
41
+ SUPPORTED_LOCK_VERSION_MAJOR = int(LOCK_VERSION.split(".", 1)[0])
42
+ SUPPORTED_TOOL_SCHEMA_VERSION = TOOL_SCHEMA_VERSION
43
+
44
+
45
+ def loads(text: str) -> AndroidLockfile:
46
+ try:
47
+ raw = tomllib.loads(text)
48
+ except tomllib.TOMLDecodeError as exc:
49
+ raise LockError(f"pylock.android.toml is not valid TOML: {exc}") from exc
50
+ if not isinstance(raw, dict):
51
+ raise LockError("pylock.android.toml must be a TOML table.")
52
+ try:
53
+ return _from_raw(raw)
54
+ except LockError:
55
+ raise
56
+ except (KeyError, TypeError, AttributeError, ValueError) as exc:
57
+ raise LockError(f"pylock.android.toml is malformed: {exc!r}") from exc
58
+
59
+
60
+ def load(path: str | Path) -> AndroidLockfile:
61
+ return loads(Path(path).read_text(encoding="utf-8"))
62
+
63
+
64
+ def _from_raw(raw: dict) -> AndroidLockfile:
65
+ lock_version = _check_lock_version(raw)
66
+
67
+ tool_raw = raw.get("tool", {})
68
+ if not isinstance(tool_raw, dict):
69
+ raise LockError("pylock.android.toml [tool] must be a table.")
70
+ tool = tool_raw.get("kivyforge", {})
71
+ if not isinstance(tool, dict) or not tool:
72
+ raise LockError("pylock.android.toml is missing the [tool.kivyforge] table.")
73
+
74
+ schema_version = _check_tool_schema_version(tool)
75
+
76
+ runtimes = tuple(_parse_runtime(r) for r in _as_list(tool, "python_android"))
77
+ if not runtimes:
78
+ raise LockError(
79
+ "pylock.android.toml has no [[tool.kivyforge.python_android]] "
80
+ "entries; re-run `kivyforge lock -p android`."
81
+ )
82
+ packages = tuple(_parse_package(p) for p in _as_list(raw, "packages"))
83
+ android_libs = tuple(_parse_lib(x) for x in _as_list(tool, "android_libs"))
84
+ gradle = _parse_gradle(tool.get("gradle"))
85
+ include_files = tuple(
86
+ _parse_include_file(f) for f in _as_list(tool, "include_files")
87
+ )
88
+
89
+ kivy_generation = tool.get("kivy_generation", 2)
90
+ if kivy_generation not in (2, 3):
91
+ raise LockError(
92
+ f"pylock.android.toml [tool.kivyforge].kivy_generation "
93
+ f"{kivy_generation!r} invalid."
94
+ )
95
+
96
+ return AndroidLockfile(
97
+ requires_python=raw.get("requires-python", ">=3.14"),
98
+ packages=packages,
99
+ python_android=runtimes,
100
+ kivyforge_version=tool.get("kivyforge_version", ""),
101
+ generated_at=tool.get("generated_at", ""),
102
+ pyproject_sha256=tool.get("pyproject_sha256", ""),
103
+ tool_kivy_android_schema_version=tool.get(
104
+ "tool_kivy_android_schema_version", 1
105
+ ),
106
+ kivy_generation=kivy_generation,
107
+ android_libs=android_libs,
108
+ gradle=gradle,
109
+ include_files=include_files,
110
+ schema_version=schema_version,
111
+ lock_version=lock_version,
112
+ created_by=raw.get("created-by", "kivyforge"),
113
+ extras=tuple(raw.get("extras", [])),
114
+ dependency_groups=tuple(raw.get("dependency-groups", [])),
115
+ default_groups=tuple(raw.get("default-groups", [])),
116
+ )
117
+
118
+
119
+ def _parse_runtime(entry: dict) -> PythonAndroidRuntime:
120
+ return PythonAndroidRuntime(
121
+ version=entry["version"],
122
+ abi=entry["abi"],
123
+ url=entry.get("url"),
124
+ path=entry.get("path"),
125
+ sha256=entry["sha256"],
126
+ min_api=int(entry.get("min_api", 24)),
127
+ )
128
+
129
+
130
+ def _parse_lib(entry: dict) -> LockedAndroidLib:
131
+ return LockedAndroidLib(
132
+ name=entry["name"],
133
+ kind=entry["kind"],
134
+ version=entry["version"],
135
+ url=entry.get("url"),
136
+ path=entry.get("path"),
137
+ sha256=entry["sha256"],
138
+ )
139
+
140
+
141
+ def _parse_gradle(table: object) -> GradlePins:
142
+ if table is None:
143
+ return GradlePins()
144
+ if not isinstance(table, dict):
145
+ raise LockError("pylock.android.toml [tool.kivyforge.gradle] must be a table.")
146
+ resolved = tuple(
147
+ GradleResolvedModule(
148
+ coordinate=m["coordinate"],
149
+ artifacts=tuple(
150
+ GradleArtifact(name=a["name"], sha256=a["sha256"])
151
+ for a in m.get("artifacts", [])
152
+ ),
153
+ )
154
+ for m in table.get("resolved", [])
155
+ )
156
+ return GradlePins(
157
+ dependencies=tuple(table.get("dependencies", [])),
158
+ repositories=tuple(table.get("repositories", [])),
159
+ resolved=resolved,
160
+ )
161
+
162
+
163
+ def _parse_include_file(entry: dict) -> LockedIncludeFile:
164
+ return LockedIncludeFile(
165
+ source=entry["source"], dest=entry["dest"], sha256=entry["sha256"]
166
+ )
167
+
168
+
169
+ def _check_lock_version(raw: dict) -> str:
170
+ lock_version = raw.get("lock-version", LOCK_VERSION)
171
+ if not isinstance(lock_version, str):
172
+ raise LockError("pylock.android.toml lock-version must be a string.")
173
+ major = lock_version.split(".", 1)[0]
174
+ if not major.isdigit():
175
+ raise LockError(
176
+ f"pylock.android.toml lock-version {lock_version!r} is not a valid version."
177
+ )
178
+ if int(major) > SUPPORTED_LOCK_VERSION_MAJOR:
179
+ raise LockError(
180
+ f"pylock.android.toml lock-version {lock_version} is newer than "
181
+ f"this kivyforge understands (<= {SUPPORTED_LOCK_VERSION_MAJOR}.x); "
182
+ "upgrade kivyforge."
183
+ )
184
+ return lock_version
185
+
186
+
187
+ def _check_tool_schema_version(tool: dict) -> int:
188
+ schema_version = tool.get("schema_version", TOOL_SCHEMA_VERSION)
189
+ if not isinstance(schema_version, int) or isinstance(schema_version, bool):
190
+ raise LockError(
191
+ "pylock.android.toml [tool.kivyforge].schema_version must be an integer."
192
+ )
193
+ if schema_version > SUPPORTED_TOOL_SCHEMA_VERSION:
194
+ raise LockError(
195
+ f"pylock.android.toml [tool.kivyforge].schema_version "
196
+ f"{schema_version} is newer than this kivyforge understands "
197
+ f"(<= {SUPPORTED_TOOL_SCHEMA_VERSION}); upgrade kivyforge."
198
+ )
199
+ return schema_version
200
+
201
+
202
+ def _as_list(table: dict, key: str) -> list:
203
+ value = table.get(key, [])
204
+ if not isinstance(value, list):
205
+ raise LockError(f"pylock.android.toml {key!r} must be an array of tables.")
206
+ return value
@@ -0,0 +1,276 @@
1
+ """Android dependency resolver (android/02 §"Resolution semantics").
2
+
3
+ Turns ``[project].dependencies`` into fully-pinned per-ABI Android wheels,
4
+ **host-independent**: every compiled package is pinned for every ABI in
5
+ ``[tool.kivy.android].abis``, regardless of the host running ``lock``.
6
+
7
+ Host-independence covers *marker evaluation*, not just wheel tags. pip's
8
+ ``--platform``/``--abi`` select acceptable tags but leave ``sys_platform`` and
9
+ friends pointing at the machine running pip, which made the same lock succeed
10
+ on Linux and fail on Windows. Resolution therefore runs through
11
+ ``_pip_shim.py``, which retargets ``packaging``'s marker environment at Android
12
+ (see ``markers.py``) before handing off to pip.
13
+
14
+ Mirrors the iOS resolver (same pip ``--report`` backend, same fail-fast
15
+ semantics): a package missing any targeted ABI slice is rejected, and a
16
+ version skew across ABIs (one ABI's wheel published before another's) is
17
+ rejected rather than merged. The wheel-tag API level is a floor — pip's
18
+ PEP 738 tag expansion accepts an ``android_21_*`` wheel at ``min_sdk = 24``
19
+ (android/01 §find_links), so kivyforge never re-implements that rule.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+ import tempfile
29
+ from dataclasses import dataclass, field
30
+ from pathlib import Path
31
+ from typing import Protocol
32
+
33
+ from kivyforge.lock import _pip_shim
34
+ from kivyforge.lock._pip_shim import MARKER_ENV_VAR
35
+ from kivyforge.lock.model import canonical_name
36
+ from kivyforge.lock.resolver import (
37
+ MIN_PIP_VERSION,
38
+ abi_tags,
39
+ dep_names_for_environment,
40
+ pip_python_version,
41
+ pip_version,
42
+ version_str,
43
+ )
44
+
45
+ from .markers import android_marker_environment
46
+
47
+ # Run by path, not ``-m``: the shim imports nothing from kivyforge, so it also
48
+ # works when resolving with an interpreter that only has pip installed.
49
+ _SHIM = Path(_pip_shim.__file__)
50
+
51
+ # Android wheels: android_<apilevel>_<abi> (PEP 738). pip >= 25.1 understands
52
+ # them (including cross-download); enforce the same floor everywhere.
53
+ MIN_ANDROID_PIP_VERSION = (25, 1)
54
+
55
+
56
+ class ResolverError(Exception):
57
+ """A resolution failure (missing ABI, no matching wheel, backend error)."""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ResolvedWheel:
62
+ filename: str
63
+ url: str
64
+ sha256: str
65
+ upload_time: str | None = None
66
+ size: int | None = None
67
+
68
+
69
+ @dataclass
70
+ class ResolvedPackage:
71
+ name: str
72
+ version: str
73
+ wheels: list[ResolvedWheel] = field(default_factory=list)
74
+ requires_python: str | None = None
75
+ dependencies: list[str] = field(default_factory=list)
76
+ source_index: str | None = None
77
+
78
+
79
+ def abi_platform_tag(min_sdk: int, abi: str) -> str:
80
+ """The pip ``--platform`` tag for one targeted ABI (android/06 step 4)."""
81
+ return f"android_{min_sdk}_{abi}"
82
+
83
+
84
+ class Resolver(Protocol):
85
+ def resolve(
86
+ self,
87
+ requirements: list[str],
88
+ *,
89
+ python_version: str,
90
+ min_sdk: int,
91
+ abis: tuple[str, ...],
92
+ extra_index_urls: list[str],
93
+ find_links: list[str] | None = None,
94
+ offline: bool = False,
95
+ ) -> list[ResolvedPackage]:
96
+ """Resolve requirements to per-ABI Android wheels (all ABIs pinned)."""
97
+ ...
98
+
99
+
100
+ class PipResolver:
101
+ """Default backend: stock pip cross-resolution via ``pip install --report``.
102
+
103
+ One dry-run report per targeted ABI; results merge into one
104
+ ``ResolvedPackage`` per distribution (pure-Python wheels dedupe by
105
+ filename across ABIs).
106
+ """
107
+
108
+ def __init__(self, python_executable: str | None = None) -> None:
109
+ self._python = python_executable or sys.executable
110
+
111
+ def resolve(
112
+ self,
113
+ requirements: list[str],
114
+ *,
115
+ python_version: str,
116
+ min_sdk: int,
117
+ abis: tuple[str, ...],
118
+ extra_index_urls: list[str],
119
+ find_links: list[str] | None = None,
120
+ offline: bool = False,
121
+ ) -> list[ResolvedPackage]:
122
+ if not requirements:
123
+ return []
124
+ self._require_modern_pip()
125
+ cp_abis = abi_tags(python_version)
126
+ merged: dict[str, ResolvedPackage] = {}
127
+ seen_filenames: dict[str, set[str]] = {}
128
+
129
+ for abi in abis:
130
+ # Markers are evaluated for the *target*, per ABI: platform_machine
131
+ # differs between arm64_v8a and x86_64, and packages do gate on it.
132
+ environment = android_marker_environment(
133
+ python_version=python_version, abi=abi
134
+ )
135
+ report = self._run_report(
136
+ requirements,
137
+ python_version=python_version,
138
+ platform_tag=abi_platform_tag(min_sdk, abi),
139
+ abis=cp_abis,
140
+ extra_index_urls=extra_index_urls,
141
+ find_links=find_links or [],
142
+ offline=offline,
143
+ marker_environment=environment,
144
+ )
145
+ for item in report.get("install", []):
146
+ self._absorb(item, merged, seen_filenames, environment)
147
+
148
+ return list(merged.values())
149
+
150
+ def _require_modern_pip(self) -> None:
151
+ version = pip_version(self._python)
152
+ floor = max(MIN_PIP_VERSION, MIN_ANDROID_PIP_VERSION)
153
+ if version is not None and version < floor:
154
+ raise ResolverError(
155
+ f"kivyforge needs pip >= {version_str(floor)} to resolve "
156
+ f"Android wheels, but {self._python} has pip "
157
+ f"{version_str(version)}.\n"
158
+ " pip < 25.1 predates PEP 738 android platform tags, so "
159
+ "compatible wheels (e.g. android_24_*) are reported as missing.\n"
160
+ f" Upgrade it: {self._python} -m pip install --upgrade pip"
161
+ )
162
+
163
+ def _absorb(self, item, merged, seen_filenames, environment) -> None:
164
+ meta = item.get("metadata", {})
165
+ name = meta.get("name")
166
+ version = meta.get("version")
167
+ download = item.get("download_info", {})
168
+ url = download.get("url", "")
169
+ if not url.endswith(".whl"):
170
+ # sdist/vcs/directory are refused (android/02): wheels only — the
171
+ # backend has no on-host cross-compile step.
172
+ raise ResolverError(
173
+ f"{name} {version} resolved to a non-wheel source ({url}); "
174
+ "kivyforge installs only Android wheels (no from-source "
175
+ "pipeline — build an android_* wheel out-of-band, android/03)."
176
+ )
177
+ archive = download.get("archive_info", {})
178
+ hashes = archive.get("hashes", {})
179
+ sha256 = hashes.get("sha256", "")
180
+ filename = url.rsplit("/", 1)[-1]
181
+ key = canonical_name(name)
182
+ pkg = merged.get(key)
183
+ if pkg is None:
184
+ pkg = ResolvedPackage(
185
+ name=name,
186
+ version=version,
187
+ requires_python=meta.get("requires_python"),
188
+ dependencies=dep_names_for_environment(
189
+ meta.get("requires_dist", []), environment
190
+ ),
191
+ )
192
+ merged[key] = pkg
193
+ seen_filenames[key] = set()
194
+ elif version != pkg.version:
195
+ # Each ABI is a separate pip resolution; if upstream published the
196
+ # ABIs at different times they can disagree on version. Merging
197
+ # would produce a lock that installs mismatched binaries per ABI.
198
+ raise ResolverError(
199
+ f"{name} resolved to inconsistent versions across Android ABIs: "
200
+ f"{pkg.version!r} and {version!r}. Re-lock once upstream "
201
+ "publishes the lagging ABI (android/02 fail-fast)."
202
+ )
203
+ if filename not in seen_filenames[key]:
204
+ seen_filenames[key].add(filename)
205
+ pkg.wheels.append(ResolvedWheel(filename=filename, url=url, sha256=sha256))
206
+
207
+ def _run_report(
208
+ self,
209
+ requirements: list[str],
210
+ *,
211
+ python_version: str,
212
+ platform_tag: str,
213
+ abis: tuple[str, ...],
214
+ extra_index_urls: list[str],
215
+ find_links: list[str],
216
+ offline: bool,
217
+ marker_environment: dict[str, str],
218
+ ) -> dict:
219
+ with tempfile.TemporaryDirectory(prefix="kivy-lock-") as tmp:
220
+ report_path = Path(tmp) / "report.json"
221
+ # Not "-m pip": the shim retargets marker evaluation at the Android
222
+ # environment first, then runs pip unchanged (see _pip_shim.py).
223
+ cmd = [
224
+ self._python,
225
+ str(_SHIM),
226
+ "install",
227
+ "--dry-run",
228
+ "--ignore-installed",
229
+ "--only-binary=:all:",
230
+ "--python-version",
231
+ pip_python_version(python_version),
232
+ "--implementation",
233
+ "cp",
234
+ "--platform",
235
+ platform_tag,
236
+ "--target",
237
+ str(Path(tmp) / "target"),
238
+ "--report",
239
+ str(report_path),
240
+ ]
241
+ for abi in abis:
242
+ cmd += ["--abi", abi]
243
+ for index in extra_index_urls:
244
+ cmd += ["--extra-index-url", index]
245
+ for link in find_links:
246
+ cmd += ["--find-links", link]
247
+ if offline:
248
+ cmd += ["--no-index"]
249
+ cmd += list(requirements)
250
+
251
+ env = dict(os.environ)
252
+ env[MARKER_ENV_VAR] = json.dumps(marker_environment)
253
+ proc = subprocess.run(cmd, capture_output=True, text=True, env=env)
254
+ if proc.returncode != 0:
255
+ raise ResolverError(
256
+ f"pip could not resolve the Android ABI {platform_tag!r}.\n"
257
+ f" This usually means a dependency has no android_* wheel "
258
+ f"for that ABI (vendor one via find_links, android/03).\n"
259
+ f" pip said:\n{_indent(proc.stderr or proc.stdout)}"
260
+ )
261
+ try:
262
+ # pip writes the report as UTF-8; never trust the locale codec
263
+ # (cp1252 on Windows chokes on non-ASCII package metadata).
264
+ return json.loads(report_path.read_text(encoding="utf-8"))
265
+ except (OSError, json.JSONDecodeError) as exc:
266
+ raise ResolverError(f"could not read pip report: {exc}") from exc
267
+
268
+
269
+ def get_resolver(backend: str = "pip", **kwargs) -> Resolver:
270
+ if backend == "pip":
271
+ return PipResolver(**kwargs)
272
+ raise ResolverError(f"unknown resolver backend {backend!r} (expected pip)")
273
+
274
+
275
+ def _indent(text: str) -> str:
276
+ return "\n".join(f" {line}" for line in (text or "").splitlines())
@@ -0,0 +1,121 @@
1
+ """Serialize an ``AndroidLockfile`` to ``pylock.android.toml`` text (android/02).
2
+
3
+ Hand-rolled emitter like the iOS writer: exact PEP 751 layout via the shared
4
+ ``kivyforge.lock.pep751`` helpers, deterministic ordering everywhere (packages
5
+ by name/version, wheels by filename, runtimes by ABI, libs by name, resolved
6
+ Maven modules by coordinate) so re-lock diffs show only real changes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from kivyforge.lock.pep751 import arr as _arr
12
+ from kivyforge.lock.pep751 import emit_package as _emit_package
13
+ from kivyforge.lock.pep751 import s as _s
14
+
15
+ from .model import (
16
+ AndroidLockfile,
17
+ GradleResolvedModule,
18
+ LockedAndroidLib,
19
+ PythonAndroidRuntime,
20
+ )
21
+
22
+
23
+ def dumps(lock: AndroidLockfile) -> str:
24
+ lines: list[str] = []
25
+
26
+ # --- PEP 751 top-level scalars ---
27
+ lines.append(f"lock-version = {_s(lock.lock_version)}")
28
+ lines.append(f"created-by = {_s(lock.created_by)}")
29
+ lines.append(f"requires-python = {_s(lock.requires_python)}")
30
+ lines.append(f"extras = {_arr(lock.extras)}")
31
+ lines.append(f"dependency-groups = {_arr(lock.dependency_groups)}")
32
+ lines.append(f"default-groups = {_arr(lock.default_groups)}")
33
+ lines.append("")
34
+
35
+ # --- [[packages]] (deterministic order) ---
36
+ for pkg in sorted(lock.packages, key=lambda p: p.sort_key):
37
+ _emit_package(lines, pkg)
38
+
39
+ # --- [tool.kivyforge] extension ---
40
+ lines.append("[tool.kivyforge]")
41
+ lines.append(f"schema_version = {lock.schema_version}")
42
+ lines.append(f"kivyforge_version = {_s(lock.kivyforge_version)}")
43
+ lines.append(f"generated_at = {_s(lock.generated_at)}")
44
+ lines.append(f"pyproject_sha256 = {_s(lock.pyproject_sha256)}")
45
+ lines.append(
46
+ f"tool_kivy_android_schema_version = {lock.tool_kivy_android_schema_version}"
47
+ )
48
+ lines.append(f"kivy_generation = {lock.kivy_generation}")
49
+
50
+ # --- [[tool.kivyforge.python_android]] (one per ABI, sorted) ---
51
+ for runtime in sorted(lock.python_android, key=lambda r: r.abi):
52
+ lines.append("")
53
+ _emit_runtime(lines, runtime)
54
+
55
+ # --- [[tool.kivyforge.android_libs]] ---
56
+ for lib in sorted(lock.android_libs, key=lambda x: (x.name.lower(), x.version)):
57
+ lines.append("")
58
+ _emit_android_lib(lines, lib)
59
+
60
+ # --- [tool.kivyforge.gradle] (only when Maven deps are declared) ---
61
+ if lock.gradle.declared:
62
+ lines.append("")
63
+ lines.append("[tool.kivyforge.gradle]")
64
+ lines.append(f"dependencies = {_arr(lock.gradle.dependencies)}")
65
+ lines.append(f"repositories = {_arr(lock.gradle.repositories)}")
66
+ for module in sorted(lock.gradle.resolved, key=lambda m: m.coordinate):
67
+ lines.append("")
68
+ _emit_resolved_module(lines, module)
69
+
70
+ # --- [[tool.kivyforge.include_files]] ---
71
+ for pin in sorted(lock.include_files, key=lambda p: (p.dest, p.source)):
72
+ lines.append("")
73
+ lines.append("[[tool.kivyforge.include_files]]")
74
+ lines.append(f"source = {_s(pin.source)}")
75
+ lines.append(f"dest = {_s(pin.dest)}")
76
+ lines.append(f"sha256 = {_s(pin.sha256)}")
77
+
78
+ return "\n".join(lines) + "\n"
79
+
80
+
81
+ def _emit_runtime(lines: list[str], runtime: PythonAndroidRuntime) -> None:
82
+ lines.append("[[tool.kivyforge.python_android]]")
83
+ lines.append(f"version = {_s(runtime.version)}")
84
+ lines.append(f"abi = {_s(runtime.abi)}")
85
+ if runtime.url:
86
+ lines.append(f"url = {_s(runtime.url)}")
87
+ else:
88
+ path = runtime.path
89
+ assert path is not None
90
+ lines.append(f"path = {_s(path)}")
91
+ lines.append(f"sha256 = {_s(runtime.sha256)}")
92
+ lines.append(f"min_api = {runtime.min_api}")
93
+
94
+
95
+ def _emit_android_lib(lines: list[str], lib: LockedAndroidLib) -> None:
96
+ lines.append("[[tool.kivyforge.android_libs]]")
97
+ lines.append(f"name = {_s(lib.name)}")
98
+ lines.append(f"kind = {_s(lib.kind)}")
99
+ lines.append(f"version = {_s(lib.version)}")
100
+ if lib.url:
101
+ lines.append(f"url = {_s(lib.url)}")
102
+ else:
103
+ path = lib.path
104
+ assert path is not None
105
+ lines.append(f"path = {_s(path)}")
106
+ lines.append(f"sha256 = {_s(lib.sha256)}")
107
+
108
+
109
+ def _emit_resolved_module(lines: list[str], module: GradleResolvedModule) -> None:
110
+ lines.append("[[tool.kivyforge.gradle.resolved]]")
111
+ lines.append(f"coordinate = {_s(module.coordinate)}")
112
+ artifacts = sorted(module.artifacts, key=lambda a: a.name)
113
+ if not artifacts:
114
+ lines.append("artifacts = []")
115
+ return
116
+ lines.append("artifacts = [")
117
+ for artifact in artifacts:
118
+ lines.append(
119
+ f" {{ name = {_s(artifact.name)}, sha256 = {_s(artifact.sha256)} }},"
120
+ )
121
+ lines.append("]")