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,76 @@
1
+ package org.kivyforge.test;
2
+
3
+ import static org.junit.Assert.assertTrue;
4
+ import static org.junit.Assert.fail;
5
+
6
+ import android.content.Context;
7
+ import android.content.Intent;
8
+ import androidx.test.core.app.ApplicationProvider;
9
+ import androidx.test.ext.junit.runners.AndroidJUnit4;
10
+ import java.io.File;
11
+ import java.nio.charset.StandardCharsets;
12
+ import java.nio.file.Files;
13
+ import org.junit.Test;
14
+ import org.junit.runner.RunWith;
15
+
16
+ /**
17
+ * The kivyforge contract smoke test (android/06 --smoke).
18
+ *
19
+ * Launches PythonActivity, which runs the app in self-test mode (the
20
+ * KIVYFORGE_SELFTEST flag makes the bootstrap import a known stdlib + wheel
21
+ * extension through the sys.meta_path finder, fire an invoke0 proxy
22
+ * round-trip, and pull the Activity through Kivy's bootstrap contract), then
23
+ * asserts the on-device result markers. No app-specific test code — this
24
+ * validates the load-bearing runtime mechanisms (extension-module finder,
25
+ * invoke0 glue, and the Kivy 3 Activity contract) on a real device/emulator.
26
+ */
27
+ @RunWith(AndroidJUnit4.class)
28
+ public class KivyforgeContractTest {
29
+
30
+ private static final int TIMEOUT_MS = 90_000;
31
+ private static final String RESULT_FILE = "kivyforge_selftest.txt";
32
+
33
+ @Test
34
+ public void contractSelfTestPasses() throws Exception {
35
+ Context ctx = ApplicationProvider.getApplicationContext();
36
+ File result = new File(ctx.getFilesDir(), RESULT_FILE);
37
+ if (result.exists() && !result.delete()) {
38
+ fail("could not clear stale self-test result " + result);
39
+ }
40
+
41
+ Intent intent = new Intent(ctx, org.kivy.android.PythonActivity.class);
42
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
43
+ intent.putExtra("kivyforge_selftest", true);
44
+ ctx.startActivity(intent);
45
+
46
+ long deadline = System.currentTimeMillis() + TIMEOUT_MS;
47
+ String contents = "";
48
+ while (System.currentTimeMillis() < deadline) {
49
+ if (result.exists()) {
50
+ contents = new String(
51
+ Files.readAllBytes(result.toPath()), StandardCharsets.UTF_8);
52
+ if (contents.contains("SELFTEST_DONE")) {
53
+ break;
54
+ }
55
+ }
56
+ Thread.sleep(500);
57
+ }
58
+
59
+ assertTrue(
60
+ "self-test did not complete within " + TIMEOUT_MS + "ms; got: "
61
+ + contents,
62
+ contents.contains("SELFTEST_DONE"));
63
+ assertTrue(
64
+ "extension-module finder check failed: " + contents,
65
+ contents.contains("EXT_OK"));
66
+ assertTrue(
67
+ "pyjnius invoke0 proxy round-trip failed: " + contents,
68
+ contents.contains("PROXY_OK"));
69
+ assertTrue(
70
+ "Kivy's Android bootstrap contract failed: " + contents,
71
+ contents.contains("KIVY_CONTRACT_OK"));
72
+ assertTrue(
73
+ "self-test reported a failure: " + contents,
74
+ contents.contains("SELFTEST_ALL_OK"));
75
+ }
76
+ }
@@ -0,0 +1,50 @@
1
+ """kivyforge's implementation of Kivy's Android bootstrap contract.
2
+
3
+ Kivy 3 holds no Android bootstrap class name of its own: it imports this
4
+ module — the name is Kivy's, not kivyforge's — and asks it for the current
5
+ ``android.app.Activity``. That indirection is the whole point of the contract:
6
+ Kivy runs on a kivyforge-built app and a python-for-android-built one without
7
+ knowing, or caring, which produced it. kivyforge's obligation is just this
8
+ file; the activity class name below is kivyforge's business alone.
9
+
10
+ Kivy pulls on first use rather than kivyforge registering at startup, so nothing
11
+ here imports Kivy. That is deliberate: importing Kivy from the bootstrap would
12
+ fix Kivy's ``KIVY_*`` environment and configuration before the app's ``main.py``
13
+ had a chance to set it.
14
+
15
+ The Activity is read fresh on every call and never cached. Android recreates it
16
+ on rotation, on configuration change and after process death, so a stored
17
+ instance both goes stale and pins a JNI reference to a dead Activity.
18
+
19
+ Only ``get_activity()`` is implemented; the contract's optional members are
20
+ deliberately absent. ``get_context()`` would only repeat what Kivy already
21
+ derives from the Activity. ``remove_presplash()`` has nothing to do: kivyforge's
22
+ splash is the platform's own — the system splash window on API 31+, and the
23
+ theme's ``windowBackground`` below it — so it is dismissed by the framework when
24
+ the first frame draws rather than being a View anyone can tear down, unlike
25
+ python-for-android, which overlays a View and must remove it. Kivy treats an
26
+ absent hook as the no-op it is, so there is nothing to stub out.
27
+ """
28
+
29
+ from jnius import autoclass
30
+
31
+ # kivyforge's own activity, fixed at build time (generate/manifest.py names the
32
+ # same class as the launcher activity). Kivy never sees this string.
33
+ _ACTIVITY_CLASS = "org.kivy.android.PythonActivity"
34
+
35
+ _activity_class = None
36
+
37
+
38
+ def get_activity():
39
+ """Return the current ``android.app.Activity``, or ``None`` if there is none.
40
+
41
+ ``None`` is a legitimate answer, not a failure: code may run in a process
42
+ with no Activity, and Kivy treats it as such.
43
+ """
44
+ global _activity_class
45
+ if _activity_class is None:
46
+ # Resolved on first call rather than at import, so that a reflection
47
+ # failure surfaces from the call that needs the Activity instead of from
48
+ # Kivy's discovery import, where it would look like a missing module.
49
+ _activity_class = autoclass(_ACTIVITY_CLASS)
50
+ return _activity_class.mActivity
@@ -0,0 +1,54 @@
1
+ """Kivyforge extension-module finder (Phase-0 prototype).
2
+
3
+ Android exposes native libraries only from the flat nativeLibraryDir; Python
4
+ extension modules are imported by dotted name from nested paths. The build
5
+ flattens every extension .so into jniLibs/<abi>/ as ``libpy.<dotted>.so`` and
6
+ records the mapping in ``ext_manifest.json``; this finder resolves imports
7
+ through that manifest with a stock ExtensionFileLoader.
8
+
9
+ See docs/design/platforms/android/05-bootstrap-android.md, "Extension-module
10
+ finder".
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import sys
16
+ from importlib.machinery import ExtensionFileLoader, ModuleSpec
17
+
18
+
19
+ class KivyforgeExtensionFinder:
20
+ def __init__(self, mapping, native_dir):
21
+ self._mapping = mapping
22
+ self._native_dir = native_dir
23
+
24
+ def find_spec(self, fullname, path=None, target=None):
25
+ filename = self._mapping.get(fullname)
26
+ if filename is None:
27
+ return None
28
+ location = os.path.join(self._native_dir, filename)
29
+ if not os.path.exists(location):
30
+ raise ImportError(
31
+ f"kivyforge: manifest maps {fullname!r} to {filename!r}, "
32
+ f"but it is not present in {self._native_dir!r} — "
33
+ "the APK native-library extraction did not deliver it"
34
+ )
35
+ loader = ExtensionFileLoader(fullname, location)
36
+ spec = ModuleSpec(fullname, loader, origin=location)
37
+ # Submodule extensions (e.g. jnius.jnius) need is_package=False, which
38
+ # ModuleSpec already defaults to for ExtensionFileLoader.
39
+ return spec
40
+
41
+ def __repr__(self):
42
+ return (
43
+ f"<KivyforgeExtensionFinder {len(self._mapping)} modules "
44
+ f"at {self._native_dir!r}>"
45
+ )
46
+
47
+
48
+ def install(manifest_path, native_dir):
49
+ with open(manifest_path, encoding="utf-8") as f:
50
+ mapping = json.load(f)
51
+ finder = KivyforgeExtensionFinder(mapping, native_dir)
52
+ sys.meta_path.insert(0, finder)
53
+ print(f"kivyforge bootstrap: finder installed ({len(mapping)} extensions)")
54
+ return finder
@@ -0,0 +1,97 @@
1
+ """kivyforge contract self-test (android/05 §contract smoke-test hook).
2
+
3
+ Inert in a normal launch: the native launcher imports and runs this only when
4
+ KIVYFORGE_SELFTEST is set (the instrumented KivyforgeContractTest sets it via
5
+ an intent extra the Activity forwards to the environment). It exercises the two
6
+ novel, load-bearing runtime mechanisms on-device — the flattened-extension
7
+ sys.meta_path finder and the pyjnius invoke0 glue — and writes a result file
8
+ the instrumentation polls.
9
+
10
+ Adds no attack surface or startup cost to a shipped app: it never runs unless
11
+ the test flag is present.
12
+ """
13
+
14
+ import os
15
+ import traceback
16
+
17
+
18
+ def _result_path() -> str:
19
+ private = os.environ.get("ANDROID_PRIVATE", ".")
20
+ return os.path.join(private, "kivyforge_selftest.txt")
21
+
22
+
23
+ def run() -> bool:
24
+ lines: list[str] = []
25
+ ok = True
26
+
27
+ # 1. A stdlib lib-dynload extension AND a wheel extension, both resolved
28
+ # through the kivyforge finder (flattened .so + manifest).
29
+ try:
30
+ import _ssl # noqa: F401 (stdlib, DT_NEEDEDs libssl_python.so)
31
+
32
+ import jnius # noqa: F401 (wheel extension: jnius.jnius)
33
+
34
+ lines.append("EXT_OK")
35
+ except Exception:
36
+ ok = False
37
+ lines.append("EXT_FAIL")
38
+ lines.append(traceback.format_exc())
39
+
40
+ # 2. pyjnius invoke0 round-trip: a Python-implemented Java interface driven
41
+ # from Java (the NativeInvocationHandler matched pair).
42
+ try:
43
+ from jnius import PythonJavaClass, autoclass, java_method
44
+
45
+ class _Cmp(PythonJavaClass):
46
+ __javainterfaces__ = ["java/util/Comparator"]
47
+ __javacontext__ = "app"
48
+
49
+ @java_method("(Ljava/lang/Object;Ljava/lang/Object;)I")
50
+ def compare(self, a, b):
51
+ sa, sb = str(a), str(b)
52
+ return -1 if sa < sb else (1 if sa > sb else 0)
53
+
54
+ ArrayList = autoclass("java.util.ArrayList")
55
+ Collections = autoclass("java.util.Collections")
56
+ lst = ArrayList()
57
+ for item in ("cherry", "apple", "banana"):
58
+ lst.add(item)
59
+ Collections.sort(lst, _Cmp())
60
+ ordered = [lst.get(i) for i in range(lst.size())]
61
+ assert ordered == ["apple", "banana", "cherry"], ordered
62
+ lines.append("PROXY_OK")
63
+ except Exception:
64
+ ok = False
65
+ lines.append("PROXY_FAIL")
66
+ lines.append(traceback.format_exc())
67
+
68
+ # 3. Kivy 3's Android bootstrap contract: Kivy names no activity class of
69
+ # its own, it imports _kivy_bootstrap and pulls the Activity from there.
70
+ # Checked on-device because this is kivyforge's side of an interface
71
+ # another project relies on, and the failure it guards against — a
72
+ # bundle that starts fine and dies when Kivy first needs the Activity —
73
+ # is invisible to a unit test of the staging code.
74
+ try:
75
+ import _kivy_bootstrap
76
+
77
+ activity = _kivy_bootstrap.get_activity()
78
+ assert activity is not None, "no Activity while the Activity is running"
79
+ # Prove it is the live Activity rather than some retained husk.
80
+ assert activity.getPackageName(), "Activity did not answer as a Context"
81
+ lines.append("KIVY_CONTRACT_OK")
82
+ except Exception:
83
+ ok = False
84
+ lines.append("KIVY_CONTRACT_FAIL")
85
+ lines.append(traceback.format_exc())
86
+
87
+ if ok:
88
+ lines.append("SELFTEST_ALL_OK")
89
+ lines.append("SELFTEST_DONE")
90
+
91
+ try:
92
+ with open(_result_path(), "w", encoding="utf-8") as f:
93
+ f.write("\n".join(lines) + "\n")
94
+ except Exception:
95
+ traceback.print_exc()
96
+ print("kivyforge selftest:", " ".join(lines[:1] + ["…"]), flush=True)
97
+ return ok
@@ -0,0 +1,20 @@
1
+ cmake_minimum_required(VERSION 3.10)
2
+ project(kivyforge_launcher)
3
+
4
+ # The kivyforge native launcher: exports SDL_main, initializes CPython.
5
+ # Headers + libpython come from the python.org Android runtime prefix.
6
+ #
7
+ # Multi-ABI: AGP invokes CMake once per ABI; PYTHON_PREFIX_DIR arrives with a
8
+ # literal {{triplet}} placeholder that resolves to this ABI's target triplet
9
+ # (aarch64-linux-android / x86_64-linux-android) — the same mechanism the
10
+ # python.org testbed uses, so each ABI's launcher links its own libpython.
11
+ string(
12
+ REPLACE {{triplet}} ${CMAKE_LIBRARY_ARCHITECTURE}
13
+ PYTHON_PREFIX_DIR ${PYTHON_PREFIX_DIR}
14
+ )
15
+
16
+ include_directories(${PYTHON_PREFIX_DIR}/include/python${PYTHON_VERSION})
17
+ link_directories(${PYTHON_PREFIX_DIR}/lib)
18
+
19
+ add_library(main SHARED main.c)
20
+ target_link_libraries(main log python${PYTHON_VERSION})
@@ -0,0 +1,150 @@
1
+ /* Kivyforge native launcher (libmain.so), from the Phase-0 prototype.
2
+ *
3
+ * Two callers, one sequence:
4
+ * - SDL calls SDL_main() on its own thread after SDLActivity has loaded the
5
+ * SDL family + libpython + this library. By that point PythonActivity has
6
+ * unpacked the bundle and set the environment contract.
7
+ * - PythonService calls nativeStart() from its own background thread, in its
8
+ * own process, after doing the same unpack + environment setup. No SDL is
9
+ * loaded there; nothing below needs it.
10
+ *
11
+ * This implements the 05-bootstrap-android.md launch sequence steps 4-6:
12
+ * PyConfig with site_import deferred and explicit module search paths,
13
+ * finder installation from the bundle manifest, site + addsitedir, then
14
+ * the entry-point import. Everything is read from the environment, so these
15
+ * sources carry no project-specific values.
16
+ */
17
+ #include <android/log.h>
18
+ #include <jni.h>
19
+ #include <Python.h>
20
+ #include <stdio.h>
21
+ #include <stdlib.h>
22
+ #include <string.h>
23
+
24
+ #define TAG "kivyforge"
25
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
26
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
27
+
28
+ static int fail_status(const char *where, PyStatus status) {
29
+ LOGE("%s failed: %s", where, status.err_msg ? status.err_msg : "(no message)");
30
+ return 1;
31
+ }
32
+
33
+ /* Initialize CPython from the unpacked bundle and import the entry point.
34
+ * Returns 0 on success. Shared by SDL_main and the service entry. */
35
+ static int kf_start_python(void) {
36
+ const char *bundle = getenv("KF_BUNDLE");
37
+ const char *native_dir = getenv("KF_NATIVE_DIR");
38
+ if (!bundle || !native_dir) {
39
+ LOGE("KF_BUNDLE / KF_NATIVE_DIR not set");
40
+ return 1;
41
+ }
42
+ LOGI("launcher: bundle=%s native=%s", bundle, native_dir);
43
+
44
+ char stdlib_path[1024], bootstrap_path[1024], app_path[1024], sp_path[1024];
45
+ snprintf(stdlib_path, sizeof stdlib_path, "%s/stdlib", bundle);
46
+ snprintf(bootstrap_path, sizeof bootstrap_path, "%s/bootstrap", bundle);
47
+ snprintf(app_path, sizeof app_path, "%s/app", bundle);
48
+ snprintf(sp_path, sizeof sp_path, "%s/site-packages", bundle);
49
+
50
+ PyPreConfig pre;
51
+ PyPreConfig_InitPythonConfig(&pre);
52
+ pre.utf8_mode = 1;
53
+ PyStatus status = Py_PreInitialize(&pre);
54
+ if (PyStatus_Exception(status)) return fail_status("Py_PreInitialize", status);
55
+
56
+ PyConfig config;
57
+ PyConfig_InitPythonConfig(&config);
58
+ config.buffered_stdio = 0;
59
+ config.install_signal_handlers = 1;
60
+ config.site_import = 0; /* deferred: finder first, then site */
61
+ /* No use_system_logger field on the 3.14 Android build: CPython redirects
62
+ * the Python-level sys.stdout/stderr to logcat itself (per the runtime
63
+ * testbed). Finding for the docs: this is automatic on Android. */
64
+ /* Explicit search paths: bypass getpath landmark discovery entirely.
65
+ * The kivyforge bundle is its own layout, not a prefix. */
66
+ config.module_search_paths_set = 1;
67
+ wchar_t *wpaths[3];
68
+ const char *paths[3] = { stdlib_path, bootstrap_path, app_path };
69
+ for (int i = 0; i < 3; i++) {
70
+ wpaths[i] = Py_DecodeLocale(paths[i], NULL);
71
+ status = PyWideStringList_Append(&config.module_search_paths, wpaths[i]);
72
+ if (PyStatus_Exception(status))
73
+ return fail_status("module_search_paths", status);
74
+ }
75
+ status = PyConfig_SetBytesString(&config, &config.home, bundle);
76
+ if (PyStatus_Exception(status)) return fail_status("home", status);
77
+ status = PyConfig_SetBytesString(&config, &config.prefix, bundle);
78
+ if (PyStatus_Exception(status)) return fail_status("prefix", status);
79
+ status = PyConfig_SetBytesString(&config, &config.exec_prefix, bundle);
80
+ if (PyStatus_Exception(status)) return fail_status("exec_prefix", status);
81
+ status = PyConfig_SetBytesString(&config, &config.program_name, "kivyforge-app");
82
+ if (PyStatus_Exception(status)) return fail_status("program_name", status);
83
+
84
+ status = Py_InitializeFromConfig(&config);
85
+ PyConfig_Clear(&config);
86
+ if (PyStatus_Exception(status))
87
+ return fail_status("Py_InitializeFromConfig", status);
88
+ LOGI("launcher: interpreter up");
89
+
90
+ /* Step 5: finder, then site, then pip-deps site dir, then the entry point
91
+ * named by KF_ENTRY_POINT. When KIVYFORGE_SELFTEST is set (the instrumented
92
+ * contract test's intent extra, forwarded to the env by PythonActivity),
93
+ * run the inert self-test hook instead of the app entry point.
94
+ *
95
+ * KF_SERVICE_MARKER, when set, names a file the interpreter stamps once it
96
+ * is up and stamps again if the entry-point import raises — the signal the
97
+ * generated service contract test polls (a service entry point normally
98
+ * never returns, so there is nothing else to wait for). */
99
+ const char *selftest = getenv("KIVYFORGE_SELFTEST");
100
+ char code[4096];
101
+ snprintf(code, sizeof code,
102
+ "import _kivyforge_bootstrap\n"
103
+ "_kivyforge_bootstrap.install(%s'%s/ext_manifest.json', %s'%s')\n"
104
+ "import site\n"
105
+ "site.main()\n"
106
+ "site.addsitedir(%s'%s')\n"
107
+ "import importlib, os, traceback\n"
108
+ "_marker = os.environ.get('KF_SERVICE_MARKER')\n"
109
+ "if _marker:\n"
110
+ " with open(_marker, 'w') as _f:\n"
111
+ " _f.write('SERVICE_READY\\n')\n"
112
+ "try:\n"
113
+ " if %d:\n"
114
+ " import _kivyforge_selftest\n"
115
+ " _kivyforge_selftest.run()\n"
116
+ " else:\n"
117
+ " importlib.import_module(os.environ.get('KF_ENTRY_POINT') or 'main')\n"
118
+ "except Exception:\n"
119
+ " traceback.print_exc()\n"
120
+ " if _marker:\n"
121
+ " with open(_marker, 'a') as _f:\n"
122
+ " _f.write('SERVICE_ENTRY_FAILED\\n')\n"
123
+ " raise SystemExit(1)\n",
124
+ "r", bootstrap_path, "r", native_dir, "r", sp_path,
125
+ (selftest && selftest[0] == '1') ? 1 : 0);
126
+ int rc = PyRun_SimpleString(code);
127
+ LOGI("launcher: entry-point import returned %d", rc);
128
+
129
+ /* Keep the process alive briefly so instrumentation/logcat settle, then
130
+ * finalize. A real app never reaches here while Kivy runs its loop. */
131
+ Py_Finalize();
132
+ return rc == 0 ? 0 : 1;
133
+ }
134
+
135
+ /* SDL's Java glue dlopens libmain.so and dlsyms "SDL_main" — the symbol must
136
+ * be exported regardless of compiler visibility defaults. */
137
+ __attribute__((visibility("default")))
138
+ int SDL_main(int argc, char *argv[]) {
139
+ return kf_start_python();
140
+ }
141
+
142
+ /* org.kivy.android.PythonService.nativeStart(): the same sequence for a
143
+ * generated service's process. Resolved by JNI name, so it must be exported. */
144
+ __attribute__((visibility("default")))
145
+ JNIEXPORT jint JNICALL
146
+ Java_org_kivy_android_PythonService_nativeStart(JNIEnv *env, jobject thiz) {
147
+ (void)env;
148
+ (void)thiz;
149
+ return (jint)kf_start_python();
150
+ }
@@ -0,0 +1,48 @@
1
+ package org.jnius;
2
+ import java.lang.reflect.InvocationHandler;
3
+ import java.lang.reflect.Method;
4
+
5
+ // SPIKE item 1 / kivyforge bootstrap-template simulation.
6
+ //
7
+ // This file is delivered APP-SIDE (via `android.add_src` in buildozer.spec), NOT
8
+ // from the pyjnius wheel. It mirrors how kivyforge will ship
9
+ // NativeInvocationHandler.java as a generated bootstrap template alongside
10
+ // MainActivity.java / PythonActivity.java, compiled + dex'd by the app's Gradle.
11
+ //
12
+ // MATCHED-PAIR COUPLING: the `native Object invoke0(...)` signature below is bound
13
+ // at runtime (RegisterNatives) by the pyjnius wheel. This class and the wheel's
14
+ // invoke0 native-method contract MUST be updated together -- nothing enforces this
15
+ // now that the glue no longer ships in the wheel. Keep in sync with
16
+ // jnius/jnius_proxy.pxi in the pyjnius source.
17
+ public class NativeInvocationHandler implements InvocationHandler {
18
+ static boolean DEBUG = false;
19
+ private long ptr;
20
+
21
+ public NativeInvocationHandler(long ptr) {
22
+ this.ptr = ptr;
23
+ }
24
+
25
+ public Object invoke(Object proxy, Method method, Object[] args) {
26
+ if ( DEBUG ) {
27
+ String message = "+ java:invoke(<proxy>, " + method + ", " + args;
28
+ System.out.print(message);
29
+ System.out.println(")");
30
+ System.out.flush();
31
+ }
32
+
33
+ Object ret = invoke0(proxy, method, args);
34
+
35
+ if ( DEBUG ) {
36
+ System.out.print("+ java:invoke returned: ");
37
+ System.out.println(ret);
38
+ }
39
+
40
+ return ret;
41
+ }
42
+
43
+ public long getPythonObjectPointer() {
44
+ return ptr;
45
+ }
46
+
47
+ native Object invoke0(Object proxy, Method method, Object[] args);
48
+ }
@@ -0,0 +1,75 @@
1
+ package org.kivy.android;
2
+
3
+ import android.os.Bundle;
4
+ import android.system.Os;
5
+ import android.util.Log;
6
+
7
+ import org.libsdl.app.SDLActivity;
8
+
9
+ import java.io.File;
10
+
11
+ /**
12
+ * Kivyforge PythonActivity.
13
+ *
14
+ * Generation-agnostic: the SDL family named in getLibraries() below is
15
+ * substituted at render time (SDL2 for Kivy 2.x, SDL3 for Kivy 3.x), and the
16
+ * matching org/libsdl/app glue is vendored alongside it.
17
+ *
18
+ * Load model (docs/design/platforms/android/05-bootstrap-android.md):
19
+ * - SDLActivity static init loads the SDL family -> libpython -> libmain
20
+ * (getLibraries order below), so the SDL JNIEnv getter is resident before
21
+ * any Python runs.
22
+ * - The asset bundle (_python_bundle) is unpacked to app-private storage
23
+ * before super.onCreate(), version-stamped so a second launch skips it.
24
+ * - The environment contract is set before any native code runs.
25
+ * - libmain's SDL_main initializes CPython (site_import deferred), installs
26
+ * the extension-module finder from the bundle manifest, then imports the
27
+ * entry-point module named by KF_ENTRY_POINT.
28
+ */
29
+ public class PythonActivity extends SDLActivity {
30
+ private static final String TAG = "kivyforge";
31
+
32
+ // [tool.kivy].entry_point, substituted at render time. The launcher reads
33
+ // it from the environment, so the native sources stay project-independent.
34
+ private static final String ENTRY_POINT = "main";
35
+
36
+ // Kivy-compatibility: kivy.app / kivy.metrics reach the activity through
37
+ // autoclass('org.kivy.android.PythonActivity').mActivity, and
38
+ // org.renpy.android.Hardware.getDPI() reads it too. Preserving this static
39
+ // is what keeps unmodified Kivy-Android code working (android/05
40
+ // §namespace preservation).
41
+ public static PythonActivity mActivity = null;
42
+
43
+ @Override
44
+ protected String getMainFunction() {
45
+ return "SDL_main";
46
+ }
47
+
48
+ @Override
49
+ protected String[] getLibraries() {
50
+ return new String[] {
51
+ "SDL2", "SDL2_image", "SDL2_mixer", "SDL2_ttf",
52
+ "python3.14",
53
+ "main",
54
+ };
55
+ }
56
+
57
+ @Override
58
+ protected void onCreate(Bundle savedInstanceState) {
59
+ mActivity = this;
60
+ try {
61
+ // Contract smoke-test hook (android/05): the instrumented test
62
+ // passes this extra so the launcher runs the inert self-test.
63
+ if (getIntent() != null
64
+ && getIntent().getBooleanExtra("kivyforge_selftest", false)) {
65
+ Os.setenv("KIVYFORGE_SELFTEST", "1", true);
66
+ }
67
+ File bundleDir = PythonBundle.unpack(this);
68
+ PythonBundle.setEnvironment(this, bundleDir, ENTRY_POINT);
69
+ } catch (Exception e) {
70
+ Log.e(TAG, "bundle unpack failed", e);
71
+ throw new RuntimeException(e);
72
+ }
73
+ super.onCreate(savedInstanceState);
74
+ }
75
+ }