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 @@
1
+ """Stage-step helpers: runtime extract, per-ABI wheel install, jniLibs, bundle."""
@@ -0,0 +1,295 @@
1
+ """Assemble the ABI-independent Python asset bundle (android/04 §asset bundle).
2
+
3
+ Layout under ``app/src/main/assets/_python_bundle/``:
4
+
5
+ - ``stdlib/`` — the runtime's pure-Python stdlib (no ``lib-dynload``,
6
+ no ``__pycache__``, no ``*.gz`` — AGP auto-decompresses ``.gz`` assets,
7
+ loadmodel findings §corrections #5; ``test``/``idlelib``/``turtledemo``
8
+ excluded for size, matching the proven prototype).
9
+ - ``site-packages/`` — pure-Python content of the installed wheels (``.so``s
10
+ and ``.libs/`` were hoisted into ``jniLibs``).
11
+ - ``app/`` — the user's ``app_dir`` payload.
12
+ - ``bootstrap/`` — the finder module, Kivy's ``_kivy_bootstrap`` contract
13
+ module, and ``ext_manifest.json``.
14
+ - ``VERSION`` — content stamp; a changed bundle re-extracts on-device.
15
+
16
+ ``byte_compile``/``strip_source`` (android/01 §build_settings) act on the
17
+ assembled tree just before the stamp is taken, so the stamp always describes
18
+ what actually ships.
19
+
20
+ **ABI-independent by construction, and verified so**: the bundle is assembled
21
+ from the canonical ABI (first in ``abis``); every other ABI's non-``.so``
22
+ site-packages payload must be byte-for-byte identical or the build fails
23
+ naming the path and both hashes (version skew is already rejected at lock
24
+ time; this catches content skew).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import hashlib
30
+ import shutil
31
+ from pathlib import Path
32
+
33
+ from kivyforge.artifacts.verify import sha256_file
34
+
35
+ # Imported as a module, not by name: ``assemble_bundle`` takes a *parameter*
36
+ # called ``byte_compile`` (the compiler argv), so a bare import would shadow.
37
+ from kivyforge.bundle import pycompile
38
+
39
+ BUNDLE_DIRNAME = "_python_bundle"
40
+
41
+ _STDLIB_EXCLUDED_DIRS = {
42
+ "__pycache__",
43
+ "lib-dynload",
44
+ "site-packages",
45
+ "test",
46
+ "idlelib",
47
+ "turtledemo",
48
+ }
49
+
50
+
51
+ class BundleError(Exception):
52
+ pass
53
+
54
+
55
+ def assemble_bundle(
56
+ bundle_dir: Path,
57
+ *,
58
+ stdlib_src: Path,
59
+ site_packages_by_abi: dict[str, Path],
60
+ canonical_abi: str,
61
+ app_src: Path,
62
+ finder_source: str,
63
+ kivy_bootstrap_source: str,
64
+ ext_manifest_json: str,
65
+ selftest_source: str = "",
66
+ entry_point: str = "main",
67
+ service_entry_points: dict[str, str] | None = None,
68
+ byte_compile: tuple[str, ...] | None = None,
69
+ strip_source: bool = False,
70
+ ) -> str:
71
+ """Assemble the bundle; returns the content stamp written to ``VERSION``."""
72
+ if bundle_dir.exists():
73
+ shutil.rmtree(bundle_dir)
74
+ bundle_dir.mkdir(parents=True)
75
+
76
+ _copy_stdlib(stdlib_src, bundle_dir / "stdlib")
77
+ canonical_sp = site_packages_by_abi[canonical_abi]
78
+ _copy_pure(canonical_sp, bundle_dir / "site-packages")
79
+ _assert_abi_identity(site_packages_by_abi, canonical_abi)
80
+ _copy_app(
81
+ app_src,
82
+ bundle_dir / "app",
83
+ entry_point=entry_point,
84
+ service_entry_points=service_entry_points or {},
85
+ )
86
+
87
+ bootstrap = bundle_dir / "bootstrap"
88
+ bootstrap.mkdir()
89
+ (bootstrap / "_kivyforge_bootstrap.py").write_text(finder_source, encoding="utf-8")
90
+ # Required, not optional: Kivy resolves the Activity by importing this
91
+ # module, so an app built without it would start and then fail the moment
92
+ # Kivy needed the Activity.
93
+ (bootstrap / "_kivy_bootstrap.py").write_text(
94
+ kivy_bootstrap_source, encoding="utf-8"
95
+ )
96
+ if selftest_source:
97
+ (bootstrap / "_kivyforge_selftest.py").write_text(
98
+ selftest_source, encoding="utf-8"
99
+ )
100
+ (bootstrap / "ext_manifest.json").write_text(ext_manifest_json, encoding="utf-8")
101
+
102
+ if byte_compile is not None:
103
+ _byte_compile(bundle_dir, compiler=byte_compile, strip_source=strip_source)
104
+
105
+ stamp = _content_stamp(bundle_dir)
106
+ (bundle_dir / "VERSION").write_text(stamp, encoding="utf-8")
107
+ return stamp
108
+
109
+
110
+ def _byte_compile(
111
+ bundle_dir: Path, *, compiler: tuple[str, ...], strip_source: bool
112
+ ) -> None:
113
+ """Compile the bundle's Python payload to ``.pyc`` in place (android/01).
114
+
115
+ The mechanics live in :mod:`kivyforge.bundle.pycompile` (shared with the
116
+ desktop backends); this only names the payload areas and translates the
117
+ failure into a ``BundleError`` carrying the Android config key.
118
+
119
+ Android compiles **all four** areas, stdlib included — unlike desktop,
120
+ which leaves the stdlib alone. The payload is unpacked to app-private
121
+ storage on first launch, so stripping stdlib source is what keeps the APK
122
+ (and that unpack) small.
123
+
124
+ Paths are stripped to be bundle-relative for the same reason ``_copy_pure``
125
+ drops ``direct_url.json``: a ``.pyc`` records the path it was compiled from,
126
+ so the default would both leak local host paths into the shipped APK and
127
+ make the content stamp differ per machine.
128
+ """
129
+ trees = [
130
+ bundle_dir / name for name in ("stdlib", "site-packages", "app", "bootstrap")
131
+ ]
132
+ try:
133
+ pycompile.byte_compile(
134
+ trees,
135
+ compiler=compiler,
136
+ strip_source=strip_source,
137
+ stripdir=bundle_dir,
138
+ )
139
+ except pycompile.PycompileError as exc:
140
+ raise BundleError(
141
+ f"{exc}\n"
142
+ " Fix it, or set [tool.kivy.android.build_settings].byte_compile "
143
+ "= false."
144
+ ) from exc
145
+
146
+
147
+ def _copy_stdlib(src: Path, dest: Path) -> None:
148
+ def ignore(directory: str, names: list[str]) -> list[str]:
149
+ return [n for n in names if n in _STDLIB_EXCLUDED_DIRS or n.endswith(".gz")]
150
+
151
+ shutil.copytree(src, dest, ignore=ignore)
152
+
153
+
154
+ def _copy_pure(src: Path, dest: Path) -> None:
155
+ """Copy an installed site-packages tree minus native payload.
156
+
157
+ ``dist-info/direct_url.json`` is dropped entirely: pip writes the install
158
+ source into it (a per-ABI ``file:///`` URL for vendored wheels), which
159
+ both breaks ABI identity by design and would leak local host paths into
160
+ the shipped APK.
161
+
162
+ ``bin/`` — pip's console-script launchers — is dropped for the same class
163
+ of reason, and it is worth being explicit because the symptom is
164
+ confusing. Nothing on Android can invoke a console script, and pip
165
+ generates the launchers for the *host*: installing from Windows yields
166
+ Windows ``.exe`` wrappers (``filetype.exe``, ``idna.exe``, …) inside an
167
+ Android APK. They also embed the per-ABI target path, so they differ
168
+ between slices and trip the ABI-identity assertion — which is how this was
169
+ found: ``kivyforge build -p android`` across both ABIs failed with
170
+ "ABI content skew in the pure-Python payload: 'bin/filetype.exe'".
171
+ """
172
+
173
+ def ignore(directory: str, names: list[str]) -> list[str]:
174
+ out = [n for n in names if n == "__pycache__" or n.endswith(".so")]
175
+ if Path(directory).name.endswith(".dist-info"):
176
+ out += [n for n in names if n == "direct_url.json"]
177
+ if Path(directory).resolve() == src.resolve():
178
+ # top-level flat .libs/ was hoisted into jniLibs
179
+ out += [n for n in names if n == ".libs"]
180
+ # host-generated console scripts; see the docstring
181
+ out += [n for n in names if n == "bin"]
182
+ return out
183
+
184
+ if src.exists():
185
+ shutil.copytree(src, dest, ignore=ignore)
186
+ else:
187
+ dest.mkdir(parents=True)
188
+
189
+
190
+ def _require_module(src: Path, entry_point: str, hint: str) -> None:
191
+ """A dotted entry point must resolve to a module or package in ``app_dir``.
192
+
193
+ ``pkg.start`` maps to a nested module; a package entry point is imported
194
+ through its ``__init__.py``. Catching an unresolvable one here beats a black
195
+ screen and an ImportError in logcat.
196
+ """
197
+ rel = "/".join(entry_point.split("."))
198
+ if not (src / f"{rel}.py").is_file() and not (src / rel / "__init__.py").is_file():
199
+ raise BundleError(f"entry point {rel}.py not found in {src.name}/ ({hint}).")
200
+
201
+
202
+ def _copy_app(
203
+ src: Path,
204
+ dest: Path,
205
+ *,
206
+ entry_point: str = "main",
207
+ service_entry_points: dict[str, str],
208
+ ) -> None:
209
+ if not src.is_dir():
210
+ raise BundleError(
211
+ f"[tool.kivy].app_dir does not exist or is not a directory: {src}"
212
+ )
213
+ _require_module(src, entry_point, "set [tool.kivy].entry_point")
214
+ # A service whose entry point does not exist starts, comes up, and then dies
215
+ # on the import in a separate process — where nothing is watching.
216
+ for name, service_entry in sorted(service_entry_points.items()):
217
+ _require_module(
218
+ src,
219
+ service_entry,
220
+ f"set entry_point for [[tool.kivy.android.services]] {name!r}",
221
+ )
222
+
223
+ def ignore(directory: str, names: list[str]) -> list[str]:
224
+ return [n for n in names if n == "__pycache__"]
225
+
226
+ shutil.copytree(src, dest, ignore=ignore)
227
+
228
+
229
+ def _assert_abi_identity(
230
+ site_packages_by_abi: dict[str, Path], canonical_abi: str
231
+ ) -> None:
232
+ """Every ABI's non-``.so`` payload must match the canonical ABI's exactly."""
233
+ canonical = _pure_hashes(site_packages_by_abi[canonical_abi])
234
+ for abi, sp in site_packages_by_abi.items():
235
+ if abi == canonical_abi:
236
+ continue
237
+ other = _pure_hashes(sp)
238
+ if other == canonical:
239
+ continue
240
+ for rel in sorted(set(canonical) | set(other)):
241
+ a, b = canonical.get(rel), other.get(rel)
242
+ if a != b:
243
+ raise BundleError(
244
+ f"ABI content skew in the pure-Python payload: {rel!r} is "
245
+ f"{'missing' if a is None else a[:12] + '…'} for "
246
+ f"{canonical_abi} but "
247
+ f"{'missing' if b is None else b[:12] + '…'} for {abi}.\n"
248
+ f" An ABI slice shipping different Python source/data at "
249
+ f"the same version cannot back a single shared bundle "
250
+ f"(android/04 §asset bundle)."
251
+ )
252
+ raise BundleError( # pragma: no cover - loop above always raises
253
+ f"ABI content skew between {canonical_abi} and {abi}"
254
+ )
255
+
256
+
257
+ def _pure_hashes(root: Path) -> dict[str, str]:
258
+ out: dict[str, str] = {}
259
+ if not root.exists():
260
+ return out
261
+ for path in sorted(root.rglob("*")):
262
+ if not path.is_file():
263
+ continue
264
+ rel = path.relative_to(root)
265
+ parts = rel.parts
266
+ # Mirrors _copy_pure's exclusions: anything not shipped must not be
267
+ # compared either, or the assertion fails on files the bundle drops.
268
+ if "__pycache__" in parts or parts[0] in (".libs", "bin"):
269
+ continue
270
+ if path.suffix == ".so":
271
+ continue
272
+ # dist-info RECORD/WHEEL are per-ABI BY DESIGN (they list the wheel's
273
+ # own files/tags, which include the ABI-specific .so names), so they
274
+ # are exempt from the identity assertion; the canonical ABI's copies
275
+ # ship in the bundle. Everything else in dist-info stays checked.
276
+ if (
277
+ len(parts) >= 2
278
+ and parts[-2].endswith(".dist-info")
279
+ and parts[-1] in ("RECORD", "WHEEL", "direct_url.json")
280
+ ):
281
+ continue
282
+ out[rel.as_posix()] = sha256_file(path)
283
+ return out
284
+
285
+
286
+ def _content_stamp(bundle_dir: Path) -> str:
287
+ """A deterministic stamp over every (relpath, sha256) pair in the bundle."""
288
+ digest = hashlib.sha256()
289
+ for path in sorted(bundle_dir.rglob("*")):
290
+ if not path.is_file():
291
+ continue
292
+ rel = path.relative_to(bundle_dir).as_posix()
293
+ digest.update(rel.encode("utf-8"))
294
+ digest.update(sha256_file(path).encode("ascii"))
295
+ return digest.hexdigest()[:16]
@@ -0,0 +1,186 @@
1
+ """Populate ``jniLibs/<abi>/`` (android/04 §"Native libraries and the .so load model").
2
+
3
+ Two kinds of ``.so``, found two different ways at runtime:
4
+
5
+ - **Shared libraries** resolved by soname (``System.loadLibrary`` /
6
+ ``DT_NEEDED``): ``libpython3.X.so``, the runtime's ``lib*_python.so``, the
7
+ SDL family from a wheel's ``.libs/``. Copied verbatim, names unchanged.
8
+ - **Extension modules** imported by dotted name: flattened to
9
+ ``libpy.<dotted>.so`` (the platform's native-lib extraction requires the
10
+ ``lib*.so`` shape — proven on-device, loadmodel findings §confirmed #1) and
11
+ recorded in the ``ext_manifest.json`` the bootstrap finder consumes.
12
+
13
+ Wheel ``.libs/`` is **flat-only**: a nested subdirectory is a malformed wheel
14
+ and fails the build (android/03/04 — the wheel tag is the sole ABI truth).
15
+ The duplicate policy is the iOS rule adapted: identical content dedupes
16
+ silently; same basename with different bytes aborts naming both providers.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import shutil
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+
26
+ from kivyforge.artifacts.verify import sha256_file
27
+
28
+ EXT_MANIFEST_NAME = "ext_manifest.json"
29
+
30
+ # Suffix pattern of CPython extension modules: everything from the first "."
31
+ # after the module path is the ABI suffix (".cpython-314-....so" or just ".so").
32
+ _SO_SUFFIX = ".so"
33
+
34
+
35
+ class JniLibsError(Exception):
36
+ pass
37
+
38
+
39
+ @dataclass
40
+ class JniLibsStager:
41
+ """Accumulates ``.so`` files for one ABI, enforcing the duplicate policy.
42
+
43
+ ``provider`` strings name where a file came from (e.g. ``"runtime"``,
44
+ ``"wheel kivy"``) so a conflict diagnostic can name both sides.
45
+ """
46
+
47
+ dest: Path
48
+ # basename -> (sha256, provider)
49
+ _seen: dict[str, tuple[str, str]] = field(default_factory=dict)
50
+ # dotted module -> flattened filename
51
+ _manifest: dict[str, str] = field(default_factory=dict)
52
+
53
+ def add_shared_library(self, source: Path, *, provider: str) -> None:
54
+ """Copy a soname-resolved library verbatim (name unchanged)."""
55
+ self._add(source, source.name, provider=provider)
56
+
57
+ def add_extension_module(self, source: Path, *, dotted: str, provider: str) -> None:
58
+ """Flatten one extension module and record it in the finder manifest."""
59
+ flattened = f"libpy.{dotted}.so"
60
+ existing = self._manifest.get(dotted)
61
+ if existing is not None and existing != flattened:
62
+ raise JniLibsError(
63
+ f"extension module {dotted!r} maps to both {existing!r} and "
64
+ f"{flattened!r}"
65
+ )
66
+ self._add(source, flattened, provider=provider)
67
+ self._manifest[dotted] = flattened
68
+
69
+ def manifest(self) -> dict[str, str]:
70
+ return dict(sorted(self._manifest.items()))
71
+
72
+ def write_manifest(self, bootstrap_dir: Path) -> Path:
73
+ bootstrap_dir.mkdir(parents=True, exist_ok=True)
74
+ out = bootstrap_dir / EXT_MANIFEST_NAME
75
+ out.write_text(
76
+ json.dumps(self.manifest(), indent=1, sort_keys=True) + "\n",
77
+ encoding="utf-8",
78
+ )
79
+ return out
80
+
81
+ def _add(self, source: Path, basename: str, *, provider: str) -> None:
82
+ digest = sha256_file(source)
83
+ prior = self._seen.get(basename)
84
+ if prior is not None:
85
+ prior_sha, prior_provider = prior
86
+ if prior_sha == digest:
87
+ return # identical -> silent dedupe
88
+ raise JniLibsError(
89
+ f"conflicting native library {basename!r} for {self.dest.name}: "
90
+ f"{prior_provider} and {provider} supply different contents "
91
+ f"(sha256 {prior_sha[:12]}… vs {digest[:12]}…).\n"
92
+ f" kivyforge never picks a winner — a version-mismatched .so "
93
+ f"can link and then crash at runtime (android/04 §duplicate "
94
+ f".so policy)."
95
+ )
96
+ self.dest.mkdir(parents=True, exist_ok=True)
97
+ shutil.copy2(source, self.dest / basename)
98
+ self._seen[basename] = (digest, provider)
99
+
100
+
101
+ def dotted_module_name(so_path: Path, site_packages_root: Path) -> str:
102
+ """``site-packages/jnius/jnius.cpython-314-….so`` -> ``jnius.jnius``."""
103
+ rel = so_path.relative_to(site_packages_root)
104
+ stem = rel.name.split(".", 1)[0]
105
+ parts = [*rel.parts[:-1], stem]
106
+ return ".".join(parts)
107
+
108
+
109
+ def stage_wheel_libs_dir(
110
+ stager: JniLibsStager, libs_dir: Path, *, wheel_name: str
111
+ ) -> int:
112
+ """Ingest one wheel's flat ``.libs/`` (SDL family etc.). Returns the count.
113
+
114
+ Flat-only: any subdirectory is a malformed wheel (android/03). The ABI is
115
+ the wheel tag's business — the caller already routed this stager to the
116
+ right ABI.
117
+ """
118
+ count = 0
119
+ for entry in sorted(libs_dir.iterdir()):
120
+ if entry.is_dir():
121
+ raise JniLibsError(
122
+ f"wheel {wheel_name!r} has a nested .libs/ subdirectory "
123
+ f"({entry.name!r}); .libs/ must be flat — the wheel platform "
124
+ f"tag is the sole source of truth for the ABI (android/03 "
125
+ f"§flat only)."
126
+ )
127
+ if entry.suffix == _SO_SUFFIX:
128
+ stager.add_shared_library(entry, provider=f"wheel {wheel_name}")
129
+ count += 1
130
+ return count
131
+
132
+
133
+ def stage_site_packages_extensions(
134
+ stager: JniLibsStager, site_packages: Path, *, wheel_name: str
135
+ ) -> int:
136
+ """Flatten every extension ``.so`` under an installed wheel's tree."""
137
+ count = 0
138
+ for so in sorted(site_packages.rglob(f"*{_SO_SUFFIX}")):
139
+ if ".libs" in so.relative_to(site_packages).parts[:1]:
140
+ continue # the flat .libs/ dir is handled by stage_wheel_libs_dir
141
+ stager.add_extension_module(
142
+ so,
143
+ dotted=dotted_module_name(so, site_packages),
144
+ provider=f"wheel {wheel_name}",
145
+ )
146
+ count += 1
147
+ return count
148
+
149
+
150
+ def stage_runtime_libs(
151
+ stager: JniLibsStager, prefix_lib: Path, *, python_stem: str
152
+ ) -> tuple[int, int]:
153
+ """Stage the extracted runtime's ``prefix/lib`` for one ABI.
154
+
155
+ Returns ``(shared_count, extension_count)``. Follows the python.org
156
+ app-integration split, extended for multi-ABI (android/03 channel 2):
157
+
158
+ - ``lib<python_stem>.so`` + ``lib*_python.so`` -> verbatim (soname libs).
159
+ The runtime's plain ``lib{ssl,crypto,sqlite3}.so`` / ``engines-3`` /
160
+ ``ossl-modules`` are NOT shipped — the ``_python``-soname copies are the
161
+ ones the extensions link against (proven on-device; loadmodel findings
162
+ §runtime-package facts).
163
+ - ``lib/<python_stem>/lib-dynload/*.so`` -> flattened extension modules
164
+ (all top-level stdlib names).
165
+ """
166
+ shared = 0
167
+ libpython = prefix_lib / f"lib{python_stem}.so"
168
+ if not libpython.is_file():
169
+ raise JniLibsError(
170
+ f"runtime prefix has no lib{python_stem}.so under {prefix_lib}"
171
+ )
172
+ stager.add_shared_library(libpython, provider="runtime")
173
+ shared += 1
174
+ for candidate in sorted(prefix_lib.glob("lib*_python.so")):
175
+ stager.add_shared_library(candidate, provider="runtime")
176
+ shared += 1
177
+
178
+ extensions = 0
179
+ dynload = prefix_lib / python_stem / "lib-dynload"
180
+ if not dynload.is_dir():
181
+ raise JniLibsError(f"runtime prefix has no lib-dynload directory at {dynload}")
182
+ for so in sorted(dynload.glob(f"*{_SO_SUFFIX}")):
183
+ dotted = so.name.split(".", 1)[0]
184
+ stager.add_extension_module(so, dotted=dotted, provider="runtime")
185
+ extensions += 1
186
+ return shared, extensions
@@ -0,0 +1,61 @@
1
+ """Extract the python.org Android runtime per ABI (android/03 channel 2).
2
+
3
+ The tarball layout (loadmodel findings §runtime-package facts):
4
+ ``README.md``, ``android-env.sh``, ``android.py``, ``prefix/``, ``testbed/``.
5
+ Only ``prefix/`` matters to the build: ``prefix/lib`` feeds ``jniLibs`` (via
6
+ ``stage.jnilibs.stage_runtime_libs``) and ``prefix/lib/pythonX.Y`` (minus
7
+ ``lib-dynload``) feeds the asset bundle.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import tarfile
14
+ from pathlib import Path
15
+
16
+
17
+ class RuntimeStageError(Exception):
18
+ pass
19
+
20
+
21
+ def extract_runtime(tarball: Path, dest: Path) -> Path:
22
+ """Extract ``tarball`` into ``dest`` and return the ``prefix`` directory.
23
+
24
+ Idempotent per destination: an existing extraction is replaced (the
25
+ tarball's SHA-256 was verified by the collect step before this runs).
26
+ """
27
+ if dest.exists():
28
+ shutil.rmtree(dest)
29
+ dest.mkdir(parents=True)
30
+ try:
31
+ with tarfile.open(tarball, "r:gz") as tf:
32
+ tf.extractall(dest, filter="data")
33
+ except (tarfile.TarError, OSError) as exc:
34
+ raise RuntimeStageError(
35
+ f"could not extract the python.org runtime {tarball.name}: {exc}"
36
+ ) from exc
37
+ prefix = dest / "prefix"
38
+ if not prefix.is_dir():
39
+ # Tolerate a single wrapping directory around the documented layout.
40
+ candidates = [p / "prefix" for p in dest.iterdir() if p.is_dir()]
41
+ matches = [c for c in candidates if c.is_dir()]
42
+ if len(matches) == 1:
43
+ prefix = matches[0]
44
+ else:
45
+ raise RuntimeStageError(
46
+ f"{tarball.name} does not contain the documented prefix/ "
47
+ f"layout (android/03 channel 2); found: "
48
+ f"{[p.name for p in dest.iterdir()]}"
49
+ )
50
+ return prefix
51
+
52
+
53
+ def stdlib_dir(prefix: Path, python_stem: str) -> Path:
54
+ """``prefix`` -> the pure-Python stdlib tree (``lib/pythonX.Y``)."""
55
+ lib = prefix / "lib" / python_stem
56
+ if not lib.is_dir():
57
+ raise RuntimeStageError(
58
+ f"runtime prefix has no stdlib at {lib}; the embeddable package "
59
+ f"layout changed — re-lock and report if this persists."
60
+ )
61
+ return lib
@@ -0,0 +1,106 @@
1
+ """Install locked wheels per ABI (android/06 build step 4).
2
+
3
+ The lock holds the full transitive set with URLs/hashes, so installation is
4
+ ``pip install --no-deps --target`` on the **verified local wheel files** — pip
5
+ never re-resolves and never touches an index. Wheel selection per ABI applies
6
+ the tag-floor rule (an ``android_<api>_<abi>`` wheel with ``api <= min_sdk``;
7
+ highest compatible api wins) plus the shared ``py3-none-any`` entries.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from kivyforge.lock.model import LockedPackage, LockedWheel
17
+
18
+
19
+ class WheelStageError(Exception):
20
+ pass
21
+
22
+
23
+ def select_wheel(package: LockedPackage, *, abi: str, min_sdk: int) -> LockedWheel:
24
+ """Pick the wheel slice this ABI installs (android/01 §tag floor)."""
25
+ best: tuple[int, LockedWheel] | None = None
26
+ for wheel in package.wheels:
27
+ if wheel.is_pure_python:
28
+ if best is None:
29
+ best = (-1, wheel)
30
+ continue
31
+ tag = wheel.platform_tag
32
+ parts = tag.split("_", 2)
33
+ if parts[0] != "android" or len(parts) != 3 or parts[2] != abi:
34
+ continue
35
+ try:
36
+ api = int(parts[1])
37
+ except ValueError:
38
+ continue
39
+ if api <= min_sdk and (best is None or api > best[0]):
40
+ best = (api, wheel)
41
+ if best is None:
42
+ raise WheelStageError(
43
+ f"{package.name} {package.version} has no wheel for ABI {abi!r} "
44
+ f"(min_sdk {min_sdk}) in the lock — re-run `kivyforge lock`."
45
+ )
46
+ return best[1]
47
+
48
+
49
+ def install_wheels(
50
+ wheel_files: list[Path],
51
+ target: Path,
52
+ *,
53
+ python_version: str,
54
+ python_executable: str | None = None,
55
+ ) -> None:
56
+ """``pip install --no-deps --target`` the verified wheel files.
57
+
58
+ Cross-install: the host interpreter rarely matches the target's version or
59
+ platform, so the target environment is described explicitly — the platform
60
+ tags are taken from the selected wheel files themselves (exact match by
61
+ construction), the version/ABI from the locked target Python.
62
+ """
63
+ if not wheel_files:
64
+ target.mkdir(parents=True, exist_ok=True)
65
+ return
66
+ from kivyforge.lock.resolver import abi_tags, pip_python_version
67
+
68
+ platforms: list[str] = []
69
+ for wheel in wheel_files:
70
+ stem = wheel.name[:-4] if wheel.name.endswith(".whl") else wheel.name
71
+ tag = stem.rsplit("-", 1)[-1]
72
+ if tag != "any" and tag not in platforms:
73
+ platforms.append(tag)
74
+
75
+ cmd = [
76
+ python_executable or sys.executable,
77
+ "-m",
78
+ "pip",
79
+ "install",
80
+ "--no-deps",
81
+ "--no-index",
82
+ "--only-binary=:all:",
83
+ "--python-version",
84
+ pip_python_version(python_version),
85
+ "--implementation",
86
+ "cp",
87
+ "--target",
88
+ str(target),
89
+ "--quiet",
90
+ ]
91
+ for platform_tag in platforms:
92
+ cmd += ["--platform", platform_tag]
93
+ for abi in abi_tags(python_version):
94
+ cmd += ["--abi", abi]
95
+ cmd += [str(w) for w in wheel_files]
96
+
97
+ proc = subprocess.run(cmd, capture_output=True, text=True)
98
+ if proc.returncode != 0:
99
+ raise WheelStageError(
100
+ "pip could not install the locked wheels into the staging "
101
+ f"directory.\n pip said:\n{_indent(proc.stderr or proc.stdout)}"
102
+ )
103
+
104
+
105
+ def _indent(text: str) -> str:
106
+ return "\n".join(f" {line}" for line in (text or "").splitlines())
@@ -0,0 +1,29 @@
1
+ """kivyforge-emitted Android toolchain pins (android/06 §versioning, layer 2).
2
+
3
+ These move with a kivyforge release and are validated together — the exact
4
+ combination the Phase-0 prototype proved on-device
5
+ (docs/design/dev/android-loadmodel-findings.md §runtime-package facts). The
6
+ generated project is regenerated on every build, so bumping kivyforge and
7
+ rebuilding is how projects move across these pins.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ # Gradle wrapper distribution the generated project pins (the python.org
13
+ # testbed's own combo, proven with AGP below).
14
+ GRADLE_VERSION = "8.11.1"
15
+ GRADLE_DISTRIBUTION_URL = (
16
+ f"https\\://services.gradle.org/distributions/gradle-{GRADLE_VERSION}-bin.zip"
17
+ )
18
+
19
+ # Android Gradle Plugin version emitted into the root build.gradle.
20
+ AGP_VERSION = "8.10.0"
21
+
22
+ # Kotlin plugin version (applied only when [tool.kivy.android.src].kotlin is
23
+ # non-empty).
24
+ KOTLIN_VERSION = "1.9.22"
25
+
26
+ # ndkVersion emitted into app/build.gradle: the launcher compile is
27
+ # deterministic across hosts (android/04 §native launcher). r27.3 is the
28
+ # proven prototype pin; cibuildwheel's wheels pin the same family.
29
+ NDK_VERSION = "27.3.13750724"