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,215 @@
1
+ """adb / emulator integration (android/06 §run).
2
+
3
+ Device selection: an explicit ``--serial`` wins; else the single connected
4
+ device; else boot the named (or only) AVD and wait for ``sys.boot_completed``.
5
+ All paths resolve tools from the detected SDK so no PATH setup is required.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ _EXE = ".exe" if os.name == "nt" else ""
16
+ BOOT_TIMEOUT_SEC = 300
17
+
18
+
19
+ def _sdk_root() -> Path | None:
20
+ from .doctor import RealAndroidProbe
21
+
22
+ return RealAndroidProbe().sdk_root()
23
+
24
+
25
+ class AdbError(Exception):
26
+ pass
27
+
28
+
29
+ def sdk_tool(name: str, *, subdir: str) -> str:
30
+ sdk = _sdk_root()
31
+ if sdk is not None:
32
+ candidate = sdk / subdir / f"{name}{_EXE}"
33
+ if candidate.exists():
34
+ return str(candidate)
35
+ import shutil as _shutil
36
+
37
+ tool = _shutil.which(name)
38
+ if tool is None:
39
+ raise AdbError(
40
+ f"{name} not found (looked in the SDK's {subdir}/ and PATH); "
41
+ f"run `kivyforge doctor -p android`."
42
+ )
43
+ return tool
44
+
45
+
46
+ def adb(*args: str, serial: str | None = None, check: bool = True) -> str:
47
+ cmd = [sdk_tool("adb", subdir="platform-tools")]
48
+ if serial:
49
+ cmd += ["-s", serial]
50
+ cmd += list(args)
51
+ proc = subprocess.run(cmd, capture_output=True, text=True)
52
+ if check and proc.returncode != 0:
53
+ raise AdbError(f"adb {' '.join(args)} failed:\n{proc.stderr or proc.stdout}")
54
+ return proc.stdout
55
+
56
+
57
+ def connected_devices() -> list[str]:
58
+ out = adb("devices")
59
+ devices = []
60
+ for line in out.splitlines()[1:]:
61
+ parts = line.split()
62
+ if len(parts) == 2 and parts[1] == "device":
63
+ devices.append(parts[0])
64
+ return devices
65
+
66
+
67
+ def available_avds() -> list[str]:
68
+ emulator = sdk_tool("emulator", subdir="emulator")
69
+ proc = subprocess.run([emulator, "-list-avds"], capture_output=True, text=True)
70
+ return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
71
+
72
+
73
+ def boot_emulator(avd: str) -> str:
74
+ """Boot ``avd`` headless-friendly; return the emulator serial once booted."""
75
+ before = set(connected_devices())
76
+ emulator = sdk_tool("emulator", subdir="emulator")
77
+ subprocess.Popen(
78
+ [emulator, "-avd", avd, "-no-boot-anim", "-no-audio"],
79
+ stdout=subprocess.DEVNULL,
80
+ stderr=subprocess.DEVNULL,
81
+ )
82
+ deadline = time.monotonic() + BOOT_TIMEOUT_SEC
83
+ serial: str | None = None
84
+ while time.monotonic() < deadline:
85
+ new = [d for d in connected_devices() if d not in before]
86
+ if new:
87
+ serial = new[0]
88
+ booted = adb(
89
+ "shell",
90
+ "getprop",
91
+ "sys.boot_completed",
92
+ serial=serial,
93
+ check=False,
94
+ ).strip()
95
+ if booted == "1":
96
+ return serial
97
+ time.sleep(2)
98
+ raise AdbError(
99
+ f"emulator {avd!r} did not finish booting within "
100
+ f"{BOOT_TIMEOUT_SEC}s (serial: {serial or 'never appeared'})."
101
+ )
102
+
103
+
104
+ def resolve_device(
105
+ *,
106
+ prefer_emulator: bool = False,
107
+ require_physical: bool = False,
108
+ serial: str | None = None,
109
+ avd: str | None = None,
110
+ ) -> str:
111
+ """android/06 selection rules -> a ready adb serial.
112
+
113
+ ``require_physical`` is ``run --device``: never fall back to booting an
114
+ emulator, because silently testing on an AVD is the opposite of what the
115
+ user asked for.
116
+ """
117
+ if serial:
118
+ if serial not in connected_devices():
119
+ raise AdbError(
120
+ f"--serial {serial!r} is not a connected device "
121
+ f"(adb devices: {', '.join(connected_devices()) or 'none'})."
122
+ )
123
+ return serial
124
+ devices = connected_devices()
125
+ if require_physical or not prefer_emulator:
126
+ physical = [d for d in devices if not d.startswith("emulator-")]
127
+ if len(physical) == 1:
128
+ return physical[0]
129
+ if len(physical) > 1:
130
+ raise AdbError(
131
+ f"multiple devices attached ({', '.join(physical)}); pass --serial."
132
+ )
133
+ if require_physical:
134
+ attached = ", ".join(devices) or "none"
135
+ raise AdbError(
136
+ "--device asks for a physical device, but adb sees none "
137
+ f"(attached: {attached}). Enable USB debugging and authorize "
138
+ "this host, or drop --device to use an emulator."
139
+ )
140
+ emulators = [d for d in devices if d.startswith("emulator-")]
141
+ if emulators:
142
+ return emulators[0]
143
+ avds = available_avds()
144
+ if avd is None:
145
+ if not avds:
146
+ raise AdbError(
147
+ "no device attached and no AVD exists; create one "
148
+ "(Android Studio Device Manager or avdmanager) or plug in a "
149
+ "device."
150
+ )
151
+ avd = avds[0]
152
+ elif avd not in avds:
153
+ raise AdbError(f"AVD {avd!r} not found (available: {', '.join(avds)}).")
154
+ return boot_emulator(avd)
155
+
156
+
157
+ # The two ABIs kivyforge builds for, keyed by the Android ABI names adb reports.
158
+ _ABI_FROM_ANDROID = {"arm64-v8a": "arm64_v8a", "x86_64": "x86_64"}
159
+
160
+
161
+ def device_abi(serial: str) -> str | None:
162
+ """The kivyforge ABI name for ``serial``'s preferred supported ABI.
163
+
164
+ Asking the target beats inferring it: an arm64 emulator on Apple Silicon and
165
+ an arm64 phone want the same ABI as an x86_64 AVD does not, and installing
166
+ the wrong one only fails later, as ``INSTALL_FAILED_NO_MATCHING_ABIS``.
167
+ Returns ``None`` when the device reports nothing kivyforge builds for (a
168
+ 32-bit-only device), leaving the caller to decide.
169
+ """
170
+ out = adb(
171
+ "shell", "getprop", "ro.product.cpu.abilist", serial=serial, check=False
172
+ ).strip()
173
+ if not out:
174
+ out = adb(
175
+ "shell", "getprop", "ro.product.cpu.abi", serial=serial, check=False
176
+ ).strip()
177
+ for reported in out.split(","):
178
+ mapped = _ABI_FROM_ANDROID.get(reported.strip())
179
+ if mapped:
180
+ return mapped
181
+ return None
182
+
183
+
184
+ def host_abi() -> str:
185
+ """The ABI an emulator runs at native speed on this host.
186
+
187
+ The fallback for :func:`device_abi`, and what android/06 documents for
188
+ ``run --emulator``: x86_64 system images on an x86_64 host, arm64 ones on an
189
+ arm64 host such as Apple Silicon.
190
+ """
191
+ import platform
192
+
193
+ return (
194
+ "arm64_v8a" if platform.machine().lower() in ("arm64", "aarch64") else "x86_64"
195
+ )
196
+
197
+
198
+ def install_apk(serial: str, apk: Path) -> None:
199
+ adb("install", "-r", str(apk), serial=serial)
200
+
201
+
202
+ def launch(serial: str, package: str, activity: str) -> None:
203
+ adb("shell", "am", "start", "-n", f"{package}/{activity}", serial=serial)
204
+
205
+
206
+ def force_stop(serial: str, package: str) -> None:
207
+ adb("shell", "am", "force-stop", package, serial=serial, check=False)
208
+
209
+
210
+ def logcat_dump(serial: str) -> str:
211
+ return adb("logcat", "-d", serial=serial)
212
+
213
+
214
+ def logcat_clear(serial: str) -> None:
215
+ adb("logcat", "-c", serial=serial, check=False)
@@ -0,0 +1,7 @@
1
+ """The kivyforge-owned Android bootstrap (android/05).
2
+
3
+ Templates extracted from the Phase-0 prototype that proved the load model
4
+ on-device (docs/design/dev/android-loadmodel-findings.md) — never edited
5
+ against an unproven contract. ``render`` parameterizes them per project;
6
+ ``contract`` enforces the pyjnius ``invoke0`` matched-pair gate.
7
+ """
@@ -0,0 +1,166 @@
1
+ """The bootstrap's matched-pair gates (android/05).
2
+
3
+ Two independent pairings are enforced here. Both are contracts between a
4
+ generated Java source and a binary that ships in a wheel, with nothing linking
5
+ them at compile time — so kivyforge checks them explicitly at build time rather
6
+ than discovering them on-device.
7
+
8
+ ``org.jnius.NativeInvocationHandler``'s ``invoke0`` native method and the
9
+ pyjnius wheel's implementation are an ABI contract that must move together.
10
+ The wheel ships no Java and nothing links them at compile time, so kivyforge
11
+ enforces the pairing explicitly: the bootstrap template carries a contract
12
+ version and the pyjnius version range it is compatible with, and
13
+ ``kivyforge build`` **fails** (doctor: **FAIL**) when the locked pyjnius falls
14
+ outside it. A mismatch is a guaranteed runtime crash — never something to ship.
15
+
16
+ Wheel-side status: the spike wheel carries no machine-readable marker yet
17
+ (see android/05 §"Implementation status"), so this range list IS the
18
+ enforcement until upstream pyjnius grows one. Widening the range is a
19
+ bootstrap-template change that must re-run the contract smoke test
20
+ (android/08 §revalidation).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from pathlib import Path
27
+
28
+ from packaging.specifiers import SpecifierSet
29
+ from packaging.version import InvalidVersion, Version
30
+
31
+ # The invoke0 contract version this template set implements (android/08 row
32
+ # "invoke0 contract v1": Object invoke0(Object proxy, Method method,
33
+ # Object[] args) via RegisterNatives — the shape proven by the spike's Step 5
34
+ # and the Phase-0 prototype).
35
+ INVOKE0_CONTRACT_VERSION = 1
36
+
37
+ # pyjnius releases whose invoke0 implementation matches contract v1. 1.7.x is
38
+ # the spike-validated series; anything newer must be revalidated (and this
39
+ # range widened) before it is accepted.
40
+ COMPATIBLE_PYJNIUS = SpecifierSet(">=1.7.0,<1.8")
41
+
42
+
43
+ class ContractError(Exception):
44
+ """The locked pyjnius is outside the bootstrap template's invoke0 range."""
45
+
46
+
47
+ def check_pyjnius_contract(locked_version: str) -> None:
48
+ """Hard build gate (android/05): raise unless the pyjnius version is in range."""
49
+ try:
50
+ version = Version(locked_version)
51
+ except InvalidVersion as exc:
52
+ raise ContractError(
53
+ f"locked pyjnius version {locked_version!r} is not a valid version; "
54
+ "cannot verify the NativeInvocationHandler.invoke0 contract."
55
+ ) from exc
56
+ if version not in COMPATIBLE_PYJNIUS:
57
+ raise ContractError(
58
+ f"locked pyjnius {locked_version} is outside the bootstrap "
59
+ f"template's compatible range ({COMPATIBLE_PYJNIUS}) for invoke0 "
60
+ f"contract v{INVOKE0_CONTRACT_VERSION}.\n"
61
+ " The invoke0 native-method signature is an ABI contract between "
62
+ "the pyjnius wheel and the generated "
63
+ "org.jnius.NativeInvocationHandler; a mismatch crashes at first "
64
+ "proxy use.\n"
65
+ " Fix: re-lock to a compatible pyjnius, or upgrade kivyforge for "
66
+ "a newer bootstrap template (android/05 §matched pair)."
67
+ )
68
+
69
+
70
+ # --- SDL glue <-> libSDL<N>.so ----------------------------------------------
71
+ # SDLActivity.onCreate compares its own compiled-in SDL_{MAJOR,MINOR,MICRO}
72
+ # _VERSION constants against nativeGetVersion() and, when they differ, sets
73
+ # mBrokenLibraries and *returns before creating the surface*. It logs nothing:
74
+ # the app shows a black screen, SDL_main is never called, and no Python ever
75
+ # runs — indistinguishable from a hung interpreter unless you know to look.
76
+ # The Java glue and the shipped libSDL<N>.so must therefore come from the same
77
+ # SDL release (docs/design/dev/android-wheel-build-recipe.md step 2). Both
78
+ # generations are checked: an SDL3 project whose wheel ships SDL2 fails just as
79
+ # opaquely, so neither is allowed to skip the gate.
80
+
81
+ _JAVA_VERSION_RE = re.compile(
82
+ r"SDL_MAJOR_VERSION\s*=\s*(\d+).*?"
83
+ r"SDL_MINOR_VERSION\s*=\s*(\d+).*?"
84
+ r"SDL_MICRO_VERSION\s*=\s*(\d+)",
85
+ re.DOTALL,
86
+ )
87
+ # SDL stamps its build with SDL_REVISION, e.g. "release-2.32.10-0-g5d2495703".
88
+ _SO_REVISION_RE = re.compile(rb"release-(\d+)\.(\d+)\.(\d+)[-\w.]*")
89
+ # The family core library, e.g. libSDL2.so / libSDL3.so (not libSDL2_image.so).
90
+ _SDL_CORE_RE = re.compile(r"^libSDL(\d+)\.so$")
91
+
92
+
93
+ def sdl_library_name(generation: int) -> str:
94
+ """The core SDL library the generation's Java glue will ``loadLibrary``."""
95
+ return f"libSDL{generation}.so"
96
+
97
+
98
+ def check_sdl_generation(*, generation: int, staged: list[str]) -> None:
99
+ """The staged jniLibs must carry the SDL family the rendered glue loads.
100
+
101
+ A wheel shipping the other generation's SDL is not a version mismatch the
102
+ glue can detect — ``SDLActivity`` dies in its static initializer on
103
+ ``loadLibrary("SDL3")`` before any of its own code runs, so the build has to
104
+ catch it.
105
+ """
106
+ expected = sdl_library_name(generation)
107
+ if expected in staged:
108
+ return
109
+ present = sorted(name for name in staged if _SDL_CORE_RE.match(name))
110
+ if not present:
111
+ return # nothing SDL staged (e.g. a Kivy-less project): nothing to check
112
+ raise ContractError(
113
+ f"kivy_generation = {generation} renders the SDL{generation} Java glue, "
114
+ f"which loads {expected} — but the staged native libraries provide "
115
+ f"{', '.join(present)}.\n"
116
+ " SDLActivity loads the SDL family in its static initializer, so the "
117
+ "app dies with an UnsatisfiedLinkError before any kivyforge or Python "
118
+ "code runs.\n"
119
+ f" Fix: set [tool.kivy.android].kivy_generation to match the Kivy "
120
+ f"wheel's SDL family, or lock a Kivy that ships {expected} "
121
+ "(android/05 §matched pair)."
122
+ )
123
+
124
+
125
+ def sdl_version_from_glue(java_source: str) -> str:
126
+ """The SDL version the Java glue was taken from, as ``major.minor.micro``."""
127
+ match = _JAVA_VERSION_RE.search(java_source)
128
+ if match is None:
129
+ raise ContractError(
130
+ "SDLActivity.java carries no SDL_MAJOR/MINOR/MICRO_VERSION "
131
+ "constants; the SDL Java glue template is not a stock SDL source "
132
+ "and its version cannot be verified against libSDL2.so."
133
+ )
134
+ return ".".join(match.groups())
135
+
136
+
137
+ def sdl_version_from_library(so_path: Path) -> str | None:
138
+ """The SDL version stamped into ``libSDL<N>.so``, or ``None`` if absent.
139
+
140
+ ``None`` is not a failure: a stripped or unusually-built SDL simply cannot
141
+ be checked, and refusing to build over it would be worse than the risk.
142
+ """
143
+ try:
144
+ blob = so_path.read_bytes()
145
+ except OSError:
146
+ return None
147
+ match = _SO_REVISION_RE.search(blob)
148
+ return ".".join(part.decode() for part in match.groups()) if match else None
149
+
150
+
151
+ def check_sdl_glue_contract(*, java_source: str, so_path: Path) -> None:
152
+ """Hard build gate: the SDL Java glue must match the shipped libSDL<N>.so."""
153
+ glue = sdl_version_from_glue(java_source)
154
+ native = sdl_version_from_library(so_path)
155
+ if native is None or glue == native:
156
+ return
157
+ raise ContractError(
158
+ f"SDL version mismatch: the bootstrap's Java glue is SDL {glue} but "
159
+ f"{so_path.name} is SDL {native}.\n"
160
+ " SDLActivity refuses to start on a mismatch and returns from "
161
+ "onCreate *silently* — the app would show a black screen with no "
162
+ "error in logcat and no Python ever running.\n"
163
+ " Fix: ship the org/libsdl/app/*.java glue from the same SDL release "
164
+ f"as the {so_path.name} in the Kivy wheel (android/05 §matched pair; "
165
+ "docs/design/dev/android-wheel-build-recipe.md step 2)."
166
+ )
@@ -0,0 +1,244 @@
1
+ """Render the bootstrap templates for one project (android/05).
2
+
3
+ The templates are the *proven* Phase-0 prototype sources; rendering is
4
+ deliberately minimal — targeted substitutions on known markers, never a
5
+ general templating language — so that rendering with the prototype's own
6
+ parameters (``sdl=2``, Python 3.14) reproduces the proven sources byte-for-
7
+ byte (the Phase-3 diff-clean gate).
8
+
9
+ Substitution points today:
10
+ - ``PythonActivity.java`` ``getLibraries()``: the SDL family for the selected
11
+ generation + the ``pythonX.Y`` soname stem.
12
+ - ``PythonActivity.java`` ``ENTRY_POINT``: ``[tool.kivy].entry_point``, which
13
+ the activity exports as ``KF_ENTRY_POINT`` for the launcher to import.
14
+ - ``PythonService.java`` ``PYTHON_LIB``: the same soname stem, because a service
15
+ process loads libpython itself.
16
+
17
+ The per-service subclasses named by the manifest are generated from config, not
18
+ from a template; see ``generate/services.py``.
19
+
20
+ ``main.c``/``CMakeLists.txt`` need no substitution (the launcher reads its
21
+ environment at runtime; CMake gets ``PYTHON_VERSION`` as a Gradle argument).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
31
+
32
+ # The SDL2 family the proven prototype loads, in load order (android/05).
33
+ SDL2_LIBRARIES = ("SDL2", "SDL2_image", "SDL2_mixer", "SDL2_ttf")
34
+ # SDL3 names for the sdl = 3 generation (schema-complete; on-device validation
35
+ # is Pending until a Kivy 3.0 host exists — android/08).
36
+ SDL3_LIBRARIES = ("SDL3", "SDL3_image", "SDL3_mixer", "SDL3_ttf")
37
+
38
+ # The exact getLibraries block in the proven PythonActivity.java; rendering
39
+ # replaces it wholesale so the template file itself stays byte-identical to
40
+ # the prototype.
41
+ _PROTO_LIBRARIES_BLOCK = (
42
+ ' "SDL2", "SDL2_image", "SDL2_mixer", "SDL2_ttf",\n'
43
+ ' "python3.14",\n'
44
+ )
45
+ _PROTO_PYTHON_STEM = "python3.14"
46
+
47
+ # PythonService loads libpython itself (no SDLActivity in a service process), so
48
+ # the stem is substituted there too.
49
+ _PROTO_SERVICE_PYTHON_LIB = 'private static final String PYTHON_LIB = "python3.14";'
50
+
51
+ # The ENTRY_POINT constant as the template carries it; rendering the default
52
+ # entry point back over it is a no-op (the diff-clean gate).
53
+ _PROTO_ENTRY_POINT_LINE = ' private static final String ENTRY_POINT = "main";'
54
+ _DOTTED_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$")
55
+
56
+
57
+ class RenderError(Exception):
58
+ pass
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class RenderedFile:
63
+ """One generated source file, relative to the app module root."""
64
+
65
+ relpath: str # POSIX, relative to app/src/main/
66
+ content: str
67
+
68
+
69
+ def python_stem(python_version: str) -> str:
70
+ """``3.14.6`` -> ``python3.14`` (the libpython soname stem)."""
71
+ parts = python_version.split(".")
72
+ if len(parts) < 2 or not all(p.split("rc")[0].isdigit() for p in parts[:2]):
73
+ raise RenderError(
74
+ f"cannot derive a libpython soname from python version {python_version!r}"
75
+ )
76
+ return f"python{parts[0]}.{parts[1]}"
77
+
78
+
79
+ def render_bootstrap(
80
+ *, sdl: int, python_version: str, entry_point: str = "main"
81
+ ) -> list[RenderedFile]:
82
+ """Render every bootstrap source for the generated project's app module.
83
+
84
+ Returns Java sources (kivyforge bootstrap + SDL glue), the native
85
+ launcher's C sources, and the finder module content is exposed separately
86
+ via :func:`finder_source` (it ships in the asset bundle, not ``src/main``).
87
+ """
88
+ if sdl == 2:
89
+ sdl_dir = TEMPLATES_DIR / "sdl2"
90
+ libraries = SDL2_LIBRARIES
91
+ elif sdl == 3:
92
+ sdl_dir = TEMPLATES_DIR / "sdl3"
93
+ libraries = SDL3_LIBRARIES
94
+ if not sdl_dir.is_dir():
95
+ raise RenderError(
96
+ "sdl = 3 is schema-complete but this kivyforge release ships "
97
+ "no SDL3 Java glue yet: the SDL3 path is Pending on-device "
98
+ "validation until a Kivy 3.0 host exists (android/08). "
99
+ "Use sdl = 2 (Kivy 2.3.1) for now."
100
+ )
101
+ else: # pragma: no cover - loader rule 9 rejects earlier
102
+ raise RenderError(f"unknown sdl generation {sdl!r}")
103
+
104
+ out: list[RenderedFile] = []
105
+
106
+ # 1. PythonActivity with the generation's library list + python stem.
107
+ activity = _read(TEMPLATES_DIR / "java/org/kivy/android/PythonActivity.java")
108
+ stem = python_stem(python_version)
109
+ # Same shape as the prototype block: the SDL family on one line, then the
110
+ # libpython stem — so rendering with the prototype's parameters is a
111
+ # byte-identical no-op (the diff-clean gate).
112
+ sdl_line = " " + ", ".join(f'"{lib}"' for lib in libraries) + ",\n"
113
+ lib_lines = sdl_line + f' "{stem}",\n'
114
+ if _PROTO_LIBRARIES_BLOCK not in activity:
115
+ raise RenderError(
116
+ "PythonActivity.java template drifted: the proven getLibraries "
117
+ "block was not found (re-extract from a proven prototype)."
118
+ )
119
+ activity = activity.replace(_PROTO_LIBRARIES_BLOCK, lib_lines)
120
+ activity = _substitute_entry_point(activity, entry_point)
121
+ out.append(RenderedFile("java/org/kivy/android/PythonActivity.java", activity))
122
+
123
+ # 1b. The unpack + environment helper both the activity and the generated
124
+ # services use, and the service base class they extend. The base loads
125
+ # libpython itself (a service process has no SDLActivity to do it), so the
126
+ # same stem is substituted there.
127
+ out.append(
128
+ RenderedFile(
129
+ "java/org/kivy/android/PythonBundle.java",
130
+ _read(TEMPLATES_DIR / "java/org/kivy/android/PythonBundle.java"),
131
+ )
132
+ )
133
+ out.append(
134
+ RenderedFile(
135
+ "java/org/kivy/android/PythonService.java",
136
+ _substitute_service_python_lib(
137
+ _read(TEMPLATES_DIR / "java/org/kivy/android/PythonService.java"), stem
138
+ ),
139
+ )
140
+ )
141
+
142
+ # 2. The pyjnius glue (matched pair; see contract.py).
143
+ out.append(
144
+ RenderedFile(
145
+ "java/org/jnius/NativeInvocationHandler.java",
146
+ _read(TEMPLATES_DIR / "java/org/jnius/NativeInvocationHandler.java"),
147
+ )
148
+ )
149
+
150
+ # 2b. Kivy-compatibility shims (org.renpy.android.*), verbatim. Kivy's own
151
+ # Python code autoclasses these (android/05 §namespace preservation).
152
+ for compat in sorted((TEMPLATES_DIR / "java/org/renpy/android").glob("*.java")):
153
+ out.append(RenderedFile(f"java/org/renpy/android/{compat.name}", _read(compat)))
154
+
155
+ # 3. Stock SDL Java glue for the generation, verbatim.
156
+ for java in sorted(sdl_dir.rglob("*.java")):
157
+ rel = java.relative_to(sdl_dir).as_posix()
158
+ out.append(RenderedFile(f"java/{rel}", _read(java)))
159
+
160
+ # 4. Native launcher sources (compiled by the NDK via externalNativeBuild).
161
+ out.append(RenderedFile("cpp/main.c", _read(TEMPLATES_DIR / "cpp/main.c")))
162
+ out.append(
163
+ RenderedFile("cpp/CMakeLists.txt", _read(TEMPLATES_DIR / "cpp/CMakeLists.txt"))
164
+ )
165
+ return out
166
+
167
+
168
+ def _substitute_service_python_lib(service: str, stem: str) -> str:
169
+ if _PROTO_SERVICE_PYTHON_LIB not in service:
170
+ raise RenderError(
171
+ "PythonService.java template drifted: the PYTHON_LIB constant was "
172
+ "not found (re-extract from a proven prototype)."
173
+ )
174
+ return service.replace(
175
+ _PROTO_SERVICE_PYTHON_LIB,
176
+ f'private static final String PYTHON_LIB = "{stem}";',
177
+ )
178
+
179
+
180
+ def _substitute_entry_point(activity: str, entry_point: str) -> str:
181
+ """Put ``[tool.kivy].entry_point`` into the activity's ENTRY_POINT constant.
182
+
183
+ The value lands inside a Java string literal, so it is re-validated here
184
+ even though the config loader already checked it: a value carrying a quote
185
+ or newline would otherwise be arbitrary Java in a generated source.
186
+ """
187
+ if not _DOTTED_IDENTIFIER.match(entry_point):
188
+ raise RenderError(
189
+ f"entry_point {entry_point!r} is not a valid dotted Python "
190
+ "identifier; cannot render the bootstrap for it."
191
+ )
192
+ if _PROTO_ENTRY_POINT_LINE not in activity:
193
+ raise RenderError(
194
+ "PythonActivity.java template drifted: the ENTRY_POINT constant "
195
+ "was not found (re-extract from a proven prototype)."
196
+ )
197
+ return activity.replace(
198
+ _PROTO_ENTRY_POINT_LINE,
199
+ f' private static final String ENTRY_POINT = "{entry_point}";',
200
+ )
201
+
202
+
203
+ def finder_source() -> str:
204
+ """The extension-module finder shipped in the asset bundle's bootstrap/."""
205
+ return _read(TEMPLATES_DIR / "bootstrap_py/_kivyforge_bootstrap.py")
206
+
207
+
208
+ def kivy_bootstrap_source() -> str:
209
+ """kivyforge's side of Kivy 3's Android bootstrap contract.
210
+
211
+ Named by Kivy, not by us: Kivy imports ``_kivy_bootstrap`` to reach the
212
+ current Activity, so this is what makes an unmodified Kivy run on a
213
+ kivyforge-built app. Ships in the bundle's ``bootstrap/``, which is on
214
+ ``sys.path`` before the app's entry module.
215
+ """
216
+ return _read(TEMPLATES_DIR / "bootstrap_py/_kivy_bootstrap.py")
217
+
218
+
219
+ def selftest_source() -> str:
220
+ """The inert contract self-test, shipped in the asset bundle's bootstrap/."""
221
+ return _read(TEMPLATES_DIR / "bootstrap_py/_kivyforge_selftest.py")
222
+
223
+
224
+ def androidtest_files() -> list[RenderedFile]:
225
+ """The generated instrumented contract test, relative to app/src/."""
226
+ out: list[RenderedFile] = []
227
+ root = TEMPLATES_DIR / "androidtest"
228
+ for java in sorted(root.glob("*.java")):
229
+ out.append(
230
+ RenderedFile(
231
+ f"androidTest/java/org/kivyforge/test/{java.name}", _read(java)
232
+ )
233
+ )
234
+ return out
235
+
236
+
237
+ def _read(path: Path) -> str:
238
+ try:
239
+ return path.read_text(encoding="utf-8")
240
+ except OSError as exc:
241
+ raise RenderError(
242
+ f"bootstrap template missing or unreadable: {path} ({exc}); "
243
+ "the kivyforge installation is incomplete."
244
+ ) from exc