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,252 @@
1
+ """Gradle/Maven channel resolution for the lock (android/02, channel 4).
2
+
3
+ ``kivyforge lock`` emits the declared coordinates into a **scratch** Gradle
4
+ project, runs Gradle with dependency verification metadata generation
5
+ (``--write-verification-metadata sha256``) against a task that force-resolves
6
+ the runtime classpath, and parses the resulting
7
+ ``gradle/verification-metadata.xml`` into ``[[tool.kivyforge.gradle.resolved]]``
8
+ pins. The values live **in the lock**; ``kivyforge build`` later materializes
9
+ ``app/gradle.lockfile`` + ``gradle/verification-metadata.xml`` into the
10
+ generated project from these pins (android/02 §gradle pins).
11
+
12
+ The Gradle invocation is behind the ``GradleMavenResolver`` protocol so unit
13
+ tests inject a fake and stay hermetic; the metadata parser is pure and tested
14
+ against recorded XML. A JDK is required only when Maven dependencies are
15
+ actually declared (android/06 §lock).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import shutil
21
+ import subprocess
22
+ import tempfile
23
+ import xml.etree.ElementTree as ET
24
+ from pathlib import Path
25
+ from typing import Protocol
26
+
27
+ from kivyforge.config.model import AndroidGradleConfig
28
+
29
+ from .model import GradleArtifact, GradlePins, GradleResolvedModule
30
+
31
+ _VERIFICATION_NS = "{https://schema.gradle.org/dependency-verification}"
32
+
33
+
34
+ class MavenResolverError(Exception):
35
+ pass
36
+
37
+
38
+ class GradleMavenResolver(Protocol):
39
+ def resolve(
40
+ self, gradle: AndroidGradleConfig, *, offline: bool = False
41
+ ) -> tuple[GradleResolvedModule, ...]: ...
42
+
43
+
44
+ def resolve_gradle_pins(
45
+ gradle: AndroidGradleConfig,
46
+ *,
47
+ resolver: GradleMavenResolver | None = None,
48
+ offline: bool = False,
49
+ ) -> GradlePins:
50
+ """Resolve the declared coordinates into hash-pinned modules.
51
+
52
+ Skipped entirely (empty pins) when no dependencies are declared — the
53
+ common Kivy case never needs a JDK at lock time.
54
+ """
55
+ if not gradle.dependencies:
56
+ return GradlePins()
57
+ backend = resolver or ScratchProjectResolver()
58
+ resolved = backend.resolve(gradle, offline=offline)
59
+ return GradlePins(
60
+ dependencies=tuple(gradle.dependencies),
61
+ repositories=tuple(gradle.repositories),
62
+ resolved=resolved,
63
+ )
64
+
65
+
66
+ def parse_verification_metadata(xml_text: str) -> tuple[GradleResolvedModule, ...]:
67
+ """Parse Gradle's ``verification-metadata.xml`` into resolved-module pins.
68
+
69
+ The document shape (Gradle dependency-verification schema):
70
+ ``<verification-metadata><components><component group= name= version=>
71
+ <artifact name=...><sha256 value=.../></artifact>...`` — one component per
72
+ resolved module, one artifact per downloaded file.
73
+ """
74
+ try:
75
+ root = ET.fromstring(xml_text)
76
+ except ET.ParseError as exc:
77
+ raise MavenResolverError(
78
+ f"could not parse Gradle verification-metadata.xml: {exc}"
79
+ ) from exc
80
+ modules: list[GradleResolvedModule] = []
81
+ for component in root.iter(f"{_VERIFICATION_NS}component"):
82
+ group = component.get("group", "")
83
+ name = component.get("name", "")
84
+ version = component.get("version", "")
85
+ artifacts: list[GradleArtifact] = []
86
+ for artifact in component.findall(f"{_VERIFICATION_NS}artifact"):
87
+ sha = artifact.find(f"{_VERIFICATION_NS}sha256")
88
+ value = sha.get("value", "") if sha is not None else ""
89
+ if value:
90
+ artifacts.append(
91
+ GradleArtifact(name=artifact.get("name", ""), sha256=value)
92
+ )
93
+ modules.append(
94
+ GradleResolvedModule(
95
+ coordinate=f"{group}:{name}:{version}",
96
+ artifacts=tuple(sorted(artifacts, key=lambda a: a.name)),
97
+ )
98
+ )
99
+ modules.sort(key=lambda m: m.coordinate)
100
+ return tuple(modules)
101
+
102
+
103
+ # --------------------------------------------------------------------------- #
104
+ # Default backend: a scratch Gradle project.
105
+ # --------------------------------------------------------------------------- #
106
+
107
+ _SETTINGS_GRADLE = """\
108
+ dependencyResolutionManagement {
109
+ repositories {
110
+ google()
111
+ mavenCentral()
112
+ %(extra_repos)s
113
+ }
114
+ }
115
+ rootProject.name = "kivyforge-maven-lock"
116
+ """
117
+
118
+ # A plain JVM project with an Android-attribute-tolerant configuration: the
119
+ # lenient artifact view lets .aar-packaged AndroidX modules resolve without
120
+ # AGP, since the lock only needs the *graph + bytes*, not a compiled classpath.
121
+ _BUILD_GRADLE = """\
122
+ plugins { id 'java-library' }
123
+
124
+ configurations {
125
+ kivyforgeLock {
126
+ canBeConsumed = false
127
+ canBeResolved = true
128
+ attributes {
129
+ attribute(Attribute.of('artifactType', String), 'jar')
130
+ }
131
+ }
132
+ }
133
+
134
+ dependencies {
135
+ %(dependencies)s
136
+ }
137
+
138
+ // Force full artifact download so verification metadata covers every file.
139
+ tasks.register('kivyforgeResolveAll') {
140
+ doLast {
141
+ def seen = configurations.kivyforgeLock.incoming.artifactView {
142
+ lenient = true
143
+ }.files.files
144
+ println "kivyforge: resolved ${seen.size()} artifact files"
145
+ }
146
+ }
147
+ """
148
+
149
+
150
+ class ScratchProjectResolver:
151
+ """Run Gradle in a temp project and harvest verification metadata.
152
+
153
+ Uses a standalone ``gradle`` if one is on PATH; otherwise stages kivyforge's
154
+ vendored Gradle wrapper into the scratch project and runs it (the same
155
+ wrapper `build` uses), so a project only needs a JDK — not a separate Gradle
156
+ install — for the Maven lock.
157
+ """
158
+
159
+ _WRAPPER_DIR = Path(__file__).parent.parent / "gradle_wrapper"
160
+
161
+ def __init__(self, gradle_executable: str | None = None) -> None:
162
+ self._gradle = gradle_executable
163
+
164
+ def resolve(
165
+ self, gradle: AndroidGradleConfig, *, offline: bool = False
166
+ ) -> tuple[GradleResolvedModule, ...]:
167
+ with tempfile.TemporaryDirectory(prefix="kivyforge-maven-") as tmp:
168
+ root = Path(tmp)
169
+ extra = "\n".join(
170
+ f" maven {{ url = uri({_groovy_str(u)}) }}"
171
+ for u in gradle.repositories
172
+ )
173
+ deps = "\n".join(
174
+ f" kivyforgeLock {_groovy_str(c)}" for c in gradle.dependencies
175
+ )
176
+ (root / "settings.gradle").write_text(
177
+ _SETTINGS_GRADLE % {"extra_repos": extra}, encoding="utf-8"
178
+ )
179
+ (root / "build.gradle").write_text(
180
+ _BUILD_GRADLE % {"dependencies": deps}, encoding="utf-8"
181
+ )
182
+ exe = self._gradle_command(root)
183
+ cmd = [
184
+ *exe,
185
+ "--no-daemon",
186
+ "--console=plain",
187
+ "--write-verification-metadata",
188
+ "sha256",
189
+ "kivyforgeResolveAll",
190
+ ]
191
+ if offline:
192
+ cmd.append("--offline")
193
+ proc = subprocess.run(cmd, cwd=root, capture_output=True, text=True)
194
+ if proc.returncode != 0:
195
+ raise MavenResolverError(
196
+ "Gradle could not resolve the declared Maven coordinates.\n"
197
+ f" Gradle said:\n{_indent(proc.stderr or proc.stdout)}"
198
+ )
199
+ metadata = root / "gradle" / "verification-metadata.xml"
200
+ if not metadata.is_file():
201
+ raise MavenResolverError(
202
+ "Gradle wrote no verification-metadata.xml; cannot pin the "
203
+ "resolved Maven graph."
204
+ )
205
+ return parse_verification_metadata(metadata.read_text(encoding="utf-8"))
206
+
207
+ def _gradle_command(self, root: Path) -> list[str]:
208
+ """A standalone gradle if present, else the vendored wrapper in ``root``."""
209
+ if self._gradle:
210
+ return [self._gradle]
211
+ standalone = shutil.which("gradle")
212
+ if standalone:
213
+ return [standalone]
214
+ jar = self._WRAPPER_DIR / "gradle-wrapper.jar"
215
+ if not jar.is_file():
216
+ raise MavenResolverError(
217
+ "[tool.kivy.android.gradle].dependencies is declared, but "
218
+ "neither a standalone `gradle` nor the vendored wrapper is "
219
+ "available (a JDK is still required for this channel; "
220
+ "android/06 §lock)."
221
+ )
222
+ import os as _os
223
+
224
+ wrapper = root / "gradle" / "wrapper"
225
+ wrapper.mkdir(parents=True, exist_ok=True)
226
+ shutil.copy2(jar, wrapper / "gradle-wrapper.jar")
227
+ (wrapper / "gradle-wrapper.properties").write_text(
228
+ "distributionBase=GRADLE_USER_HOME\n"
229
+ "distributionPath=wrapper/dists\n"
230
+ "distributionUrl=https\\://services.gradle.org/distributions/"
231
+ "gradle-8.11.1-bin.zip\n"
232
+ "zipStoreBase=GRADLE_USER_HOME\n"
233
+ "zipStorePath=wrapper/dists\n",
234
+ encoding="utf-8",
235
+ )
236
+ for script in ("gradlew", "gradlew.bat"):
237
+ src = self._WRAPPER_DIR / script
238
+ if src.is_file():
239
+ dst = root / script
240
+ shutil.copy2(src, dst)
241
+ if script == "gradlew":
242
+ dst.chmod(dst.stat().st_mode | 0o111)
243
+ return [str(root / ("gradlew.bat" if _os.name == "nt" else "gradlew"))]
244
+
245
+
246
+ def _groovy_str(value: str) -> str:
247
+ escaped = value.replace("\\", "\\\\").replace("'", "\\'")
248
+ return f"'{escaped}'"
249
+
250
+
251
+ def _indent(text: str) -> str:
252
+ return "\n".join(f" {line}" for line in (text or "").splitlines())
@@ -0,0 +1,113 @@
1
+ """In-memory model of ``pylock.android.toml`` (android/02).
2
+
3
+ PEP 751-shaped ``[[packages]]`` (shared ``LockedPackage``/``LockedWheel``) plus
4
+ the Android ``[tool.kivyforge]`` extension: the per-ABI python.org runtime,
5
+ ``.aar``/``.jar`` pins, the Gradle/Maven resolved graph, and the
6
+ ``include_files`` drift pins.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from kivyforge.lock.model import LOCK_VERSION, LockedPackage
14
+
15
+ CREATED_BY = "kivyforge"
16
+ TOOL_SCHEMA_VERSION = 1
17
+ # Android's CPython floor (Tier 3; android/02 requires-python fallback).
18
+ DEFAULT_REQUIRES_PYTHON = ">=3.14"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class PythonAndroidRuntime:
23
+ """One ``[[tool.kivyforge.python_android]]`` entry (per ABI)."""
24
+
25
+ version: str
26
+ abi: str
27
+ sha256: str
28
+ min_api: int
29
+ url: str | None = None
30
+ path: str | None = None
31
+
32
+ def __post_init__(self) -> None:
33
+ if bool(self.url) == bool(self.path):
34
+ raise ValueError(
35
+ f"python_android {self.abi!r} must have exactly one of url/path"
36
+ )
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class LockedAndroidLib:
41
+ """One ``[[tool.kivyforge.android_libs]]`` entry (.aar/.jar, channel 3)."""
42
+
43
+ name: str
44
+ kind: str # "aar" | "jar"
45
+ version: str
46
+ sha256: str
47
+ url: str | None = None
48
+ path: str | None = None
49
+
50
+ def __post_init__(self) -> None:
51
+ if bool(self.url) == bool(self.path):
52
+ raise ValueError(
53
+ f"android lib {self.name!r} must have exactly one of url/path"
54
+ )
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class GradleArtifact:
59
+ """One downloaded artifact of a resolved Maven module."""
60
+
61
+ name: str
62
+ sha256: str
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class GradleResolvedModule:
67
+ """One ``[[tool.kivyforge.gradle.resolved]]`` entry."""
68
+
69
+ coordinate: str
70
+ artifacts: tuple[GradleArtifact, ...]
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class GradlePins:
75
+ """``[tool.kivyforge.gradle]`` — declared coordinates + resolved graph."""
76
+
77
+ dependencies: tuple[str, ...] = ()
78
+ repositories: tuple[str, ...] = ()
79
+ resolved: tuple[GradleResolvedModule, ...] = ()
80
+
81
+ @property
82
+ def declared(self) -> bool:
83
+ return bool(self.dependencies)
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class LockedIncludeFile:
88
+ """One ``[[tool.kivyforge.include_files]]`` drift pin."""
89
+
90
+ source: str
91
+ dest: str
92
+ sha256: str
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class AndroidLockfile:
97
+ requires_python: str
98
+ packages: tuple[LockedPackage, ...]
99
+ python_android: tuple[PythonAndroidRuntime, ...]
100
+ kivyforge_version: str
101
+ generated_at: str
102
+ pyproject_sha256: str
103
+ tool_kivy_android_schema_version: int
104
+ kivy_generation: int
105
+ android_libs: tuple[LockedAndroidLib, ...] = ()
106
+ gradle: GradlePins = GradlePins()
107
+ include_files: tuple[LockedIncludeFile, ...] = ()
108
+ schema_version: int = TOOL_SCHEMA_VERSION
109
+ lock_version: str = LOCK_VERSION
110
+ created_by: str = CREATED_BY
111
+ extras: tuple[str, ...] = ()
112
+ dependency_groups: tuple[str, ...] = ()
113
+ default_groups: tuple[str, ...] = ()
@@ -0,0 +1,199 @@
1
+ """Resolve python.org Android runtime metadata for the lock (android/02).
2
+
3
+ ``kivyforge lock`` pins, per targeted ABI, the Android embeddable package's
4
+ URL + SHA-256 and its minimum-API floor into ``[[tool.kivyforge.python_android]]``.
5
+ Modeled on the iOS ``PythonOrgProvider`` — same cache/offline/retry posture —
6
+ but per-ABI (the runtime ships one artifact per ABI, not a fat xcframework).
7
+
8
+ Artifact naming is **discovered from the release's file listing**, not
9
+ hardcoded: the observed 3.14.6 names are
10
+ ``python-3.14.6-{aarch64,x86_64}-linux-android.tar.gz`` (see
11
+ docs/design/dev/android-loadmodel-findings.md §runtime-package facts, which
12
+ corrected the illustrative URL in android/02). The known pattern serves as a
13
+ fallback when the listing is unreachable but the artifact itself is.
14
+
15
+ The min-API floor is read from ``android-env.sh`` inside the tarball
16
+ (``ANDROID_API_LEVEL:=24``) — the same source python.org's own testbed Gradle
17
+ project reads it from, so there is no per-version table to maintain.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import re
24
+ import tarfile
25
+ import tempfile
26
+ import time
27
+ import urllib.request
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import Protocol
31
+
32
+ from kivyforge.artifacts.cache import ArtifactCache
33
+ from kivyforge.artifacts.verify import sha256_file
34
+
35
+ RELEASE_LISTING_URL = "https://www.python.org/ftp/python/{version}/"
36
+
37
+ # ABI (wheel-tag spelling) -> the arch component in python.org's filenames.
38
+ ABI_TO_PYTHON_ORG_ARCH = {"arm64_v8a": "aarch64", "x86_64": "x86_64"}
39
+
40
+ _FALLBACK_NAME = "python-{version}-{arch}-linux-android.tar.gz"
41
+ _ANDROID_TARBALL_RE = re.compile(
42
+ r'href="(python-(?P<version>[^"]+?)-(?P<arch>aarch64|x86_64)'
43
+ r'-linux-android\.tar\.gz)"'
44
+ )
45
+ _API_LEVEL_RE = re.compile(r"ANDROID_API_LEVEL:?=\s*(\d+)")
46
+
47
+ _DOWNLOAD_ATTEMPTS = 4
48
+ _RETRY_BACKOFF_SEC = (1.0, 2.0, 4.0)
49
+ # Returned when android-env.sh is absent or unparseable; matches the runtime's
50
+ # documented floor (android/01 min_sdk).
51
+ _MIN_API_FALLBACK = 24
52
+
53
+
54
+ class PythonAndroidError(Exception):
55
+ pass
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class PythonAndroidInfo:
60
+ """One per-ABI runtime pin for ``[[tool.kivyforge.python_android]]``."""
61
+
62
+ version: str
63
+ abi: str
64
+ url: str
65
+ sha256: str
66
+ min_api: int
67
+
68
+
69
+ class PythonAndroidProvider(Protocol):
70
+ def get(
71
+ self, version: str, abi: str, *, offline: bool = False
72
+ ) -> PythonAndroidInfo: ...
73
+
74
+
75
+ def read_min_api(archive_path: str | Path) -> int:
76
+ """Read ``ANDROID_API_LEVEL`` from ``android-env.sh`` inside the tarball."""
77
+ try:
78
+ with open(archive_path, "rb") as f:
79
+ return _read_min_api_from_fileobj(f)
80
+ except OSError:
81
+ return _MIN_API_FALLBACK
82
+
83
+
84
+ def _read_min_api_from_fileobj(fileobj) -> int:
85
+ """Same as :func:`read_min_api`, but reads an already-open, seekable file.
86
+
87
+ ``_download`` reads it via its own live ``NamedTemporaryFile`` handle
88
+ rather than reopening the path — reopening a still-open ``NamedTemporaryFile``
89
+ by name is a documented POSIX-only guarantee (it silently fails on Windows).
90
+ """
91
+ try:
92
+ with tarfile.open(fileobj=fileobj, mode="r:gz") as tf:
93
+ for member in tf.getmembers():
94
+ if not member.name.endswith("android-env.sh"):
95
+ continue
96
+ f = tf.extractfile(member)
97
+ if f is None:
98
+ continue
99
+ match = _API_LEVEL_RE.search(f.read().decode("utf-8", "replace"))
100
+ if match:
101
+ return int(match.group(1))
102
+ except Exception: # noqa: BLE001 — malformed archive, test fixture, etc.
103
+ pass
104
+ return _MIN_API_FALLBACK
105
+
106
+
107
+ class PythonOrgAndroidProvider:
108
+ """Default provider: discover the artifact from the release file listing."""
109
+
110
+ def __init__(self, *, cache: ArtifactCache | None = None) -> None:
111
+ self._cache = cache or ArtifactCache()
112
+ # version -> {arch -> filename}, from one listing fetch per version.
113
+ self._listing: dict[str, dict[str, str]] = {}
114
+
115
+ def get(
116
+ self, version: str, abi: str, *, offline: bool = False
117
+ ) -> PythonAndroidInfo:
118
+ arch = ABI_TO_PYTHON_ORG_ARCH.get(abi)
119
+ if arch is None:
120
+ raise PythonAndroidError(
121
+ f"unknown Android ABI {abi!r}; expected one of "
122
+ f"{sorted(ABI_TO_PYTHON_ORG_ARCH)}"
123
+ )
124
+ filename = self._filename(version, arch, offline=offline)
125
+ url = RELEASE_LISTING_URL.format(version=version) + filename
126
+
127
+ cached = self._cache.find_by_filename(filename)
128
+ if cached is not None:
129
+ return PythonAndroidInfo(
130
+ version=version,
131
+ abi=abi,
132
+ url=url,
133
+ sha256=sha256_file(cached),
134
+ min_api=read_min_api(cached),
135
+ )
136
+ if offline:
137
+ raise PythonAndroidError(
138
+ f"python.org Android runtime {version} ({abi}) is not in the "
139
+ f"artifact cache ({filename}).\n"
140
+ f" Run `kivyforge lock` online once, then retry with --offline."
141
+ )
142
+ sha256, min_api = self._download(url, version, abi)
143
+ return PythonAndroidInfo(
144
+ version=version, abi=abi, url=url, sha256=sha256, min_api=min_api
145
+ )
146
+
147
+ # ----------------------------------------------------------------- #
148
+ def _filename(self, version: str, arch: str, *, offline: bool) -> str:
149
+ fallback = _FALLBACK_NAME.format(version=version, arch=arch)
150
+ if offline:
151
+ return fallback
152
+ listing = self._listing.get(version)
153
+ if listing is None:
154
+ listing = self._fetch_listing(version)
155
+ self._listing[version] = listing
156
+ return listing.get(arch, fallback)
157
+
158
+ def _fetch_listing(self, version: str) -> dict[str, str]:
159
+ url = RELEASE_LISTING_URL.format(version=version)
160
+ try:
161
+ with urllib.request.urlopen(url) as resp: # noqa: S310
162
+ html = resp.read().decode("utf-8", "replace")
163
+ except OSError:
164
+ # The artifact fetch itself will surface a clear error if the
165
+ # fallback name is also wrong.
166
+ return {}
167
+ found: dict[str, str] = {}
168
+ for match in _ANDROID_TARBALL_RE.finditer(html):
169
+ found[match.group("arch")] = match.group(1)
170
+ return found
171
+
172
+ def _download(self, url: str, version: str, abi: str) -> tuple[str, int]:
173
+ last_exc: OSError | None = None
174
+ for attempt in range(_DOWNLOAD_ATTEMPTS):
175
+ try:
176
+ digest = hashlib.sha256()
177
+ with tempfile.NamedTemporaryFile(suffix=".tar.gz") as tmp:
178
+ with urllib.request.urlopen(url) as resp: # noqa: S310
179
+ for chunk in iter(lambda: resp.read(1 << 20), b""):
180
+ digest.update(chunk)
181
+ tmp.write(chunk)
182
+ tmp.flush()
183
+ tmp.seek(0)
184
+ min_api = _read_min_api_from_fileobj(tmp)
185
+ return digest.hexdigest(), min_api
186
+ except OSError as exc:
187
+ last_exc = exc
188
+ if attempt < _DOWNLOAD_ATTEMPTS - 1:
189
+ time.sleep(
190
+ _RETRY_BACKOFF_SEC[min(attempt, len(_RETRY_BACKOFF_SEC) - 1)]
191
+ )
192
+ assert last_exc is not None
193
+ raise PythonAndroidError(
194
+ f"could not fetch the python.org Android runtime {version} ({abi}) "
195
+ f"from {url}: {last_exc}\n"
196
+ f" Check the version exists at python.org/downloads/android/ and "
197
+ f"that you are online. Transient errors are retried "
198
+ f"{_DOWNLOAD_ATTEMPTS} times."
199
+ ) from last_exc