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,462 @@
1
+ """Generate the Gradle/AGP project files (android/04).
2
+
3
+ Everything here is regenerated on every build (``<app>-android/`` is a
4
+ managed artifact); emission is deterministic — sorted dependency and property
5
+ lines, stable attribute order — so re-runs are byte-identical.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import stat
12
+ from pathlib import Path
13
+
14
+ from kivyforge.config.model import AndroidConfig, Config
15
+
16
+ from .. import policy, toolchain
17
+ from ..lock.model import AndroidLockfile
18
+
19
+ # Wheel-tag ABI spelling -> Android's dashed directory/filter names.
20
+ ABI_TO_ANDROID = {"arm64_v8a": "arm64-v8a", "x86_64": "x86_64"}
21
+
22
+ # Wheel-tag ABI spelling -> the NDK target triplet (CMAKE_LIBRARY_ARCHITECTURE);
23
+ # the runtime staging area is keyed by triplet so CMake's {{triplet}}
24
+ # substitution (see the CMakeLists template) finds each ABI's own prefix.
25
+ ABI_TO_TRIPLET = {
26
+ "arm64_v8a": "aarch64-linux-android",
27
+ "x86_64": "x86_64-linux-android",
28
+ }
29
+
30
+ _WRAPPER_DIR = Path(__file__).parent.parent / "gradle_wrapper"
31
+
32
+ # The generated task that copies AGP's merged release manifest somewhere stable,
33
+ # and where it lands (relative to the generated project root). `kivyforge
34
+ # package` runs the task, then lints the copy.
35
+ MERGED_MANIFEST_TASK = "exportKivyforgeReleaseManifest"
36
+ MERGED_MANIFEST_RELPATH = Path(
37
+ "app", "build", "kivyforge", "AndroidManifest-merged-release.xml"
38
+ )
39
+
40
+
41
+ class ProjectGenError(Exception):
42
+ pass
43
+
44
+
45
+ def android_abi(abi: str) -> str:
46
+ return ABI_TO_ANDROID[abi]
47
+
48
+
49
+ def write_settings_gradle(dest: Path, android: AndroidConfig | None = None) -> None:
50
+ """Emit ``settings.gradle``, including any declared extra Maven repos.
51
+
52
+ ``[tool.kivy.android.gradle].repositories`` has to be honored here as well
53
+ as at lock time: the lock resolves the graph in a scratch project, so a
54
+ coordinate that only exists in a private repo would lock fine and then fail
55
+ to resolve in the generated build (android/01 §gradle).
56
+ """
57
+ extra = tuple(android.gradle.repositories) if android is not None else ()
58
+ repos = [" google()", " mavenCentral()"]
59
+ repos += [f" maven {{ url = uri({_groovy_str(url)}) }}" for url in extra]
60
+ _write(
61
+ dest / "settings.gradle",
62
+ "pluginManagement {\n"
63
+ " repositories { google(); mavenCentral(); gradlePluginPortal() }\n"
64
+ "}\n"
65
+ "dependencyResolutionManagement {\n"
66
+ " repositories {\n" + "\n".join(repos) + "\n }\n"
67
+ "}\n"
68
+ 'rootProject.name = "kivyforge-app"\n'
69
+ 'include ":app"\n',
70
+ )
71
+
72
+
73
+ def _groovy_str(value: str) -> str:
74
+ """A single-quoted Groovy string literal."""
75
+ return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
76
+
77
+
78
+ def write_root_build_gradle(dest: Path, android: AndroidConfig) -> None:
79
+ lines = [
80
+ "plugins {",
81
+ f" id 'com.android.application' version '{toolchain.AGP_VERSION}' "
82
+ "apply false",
83
+ ]
84
+ if android.src.kotlin:
85
+ lines.append(
86
+ f" id 'org.jetbrains.kotlin.android' version "
87
+ f"'{toolchain.KOTLIN_VERSION}' apply false"
88
+ )
89
+ lines.append("}")
90
+ _write(dest / "build.gradle", "\n".join(lines) + "\n")
91
+
92
+
93
+ def write_gradle_properties(dest: Path, android: AndroidConfig) -> None:
94
+ managed = {
95
+ "android.useAndroidX": "true",
96
+ "org.gradle.jvmargs": "-Xmx2048m -Dfile.encoding=UTF-8",
97
+ }
98
+ merged = dict(managed)
99
+ for key, value in android.gradle_properties.items():
100
+ # Reserved keys were rejected at config time (rule 15).
101
+ merged[str(key)] = _prop_str(value)
102
+ lines = [f"{k}={v}" for k, v in sorted(merged.items())]
103
+ _write(dest / "gradle.properties", "\n".join(lines) + "\n")
104
+
105
+
106
+ def stage_gradle_wrapper(dest: Path) -> None:
107
+ """Stage the vendored wrapper + the pinned distribution properties."""
108
+ wrapper_dir = dest / "gradle" / "wrapper"
109
+ wrapper_dir.mkdir(parents=True, exist_ok=True)
110
+ jar = _WRAPPER_DIR / "gradle-wrapper.jar"
111
+ if not jar.is_file():
112
+ raise ProjectGenError(
113
+ f"vendored gradle-wrapper.jar missing at {jar}; the kivyforge "
114
+ "installation is incomplete."
115
+ )
116
+ shutil.copy2(jar, wrapper_dir / "gradle-wrapper.jar")
117
+ _write(
118
+ wrapper_dir / "gradle-wrapper.properties",
119
+ "distributionBase=GRADLE_USER_HOME\n"
120
+ "distributionPath=wrapper/dists\n"
121
+ f"distributionUrl={toolchain.GRADLE_DISTRIBUTION_URL}\n"
122
+ "zipStoreBase=GRADLE_USER_HOME\n"
123
+ "zipStorePath=wrapper/dists\n",
124
+ )
125
+ for script in ("gradlew", "gradlew.bat"):
126
+ source = _WRAPPER_DIR / script
127
+ target = dest / script
128
+ shutil.copy2(source, target)
129
+ if script == "gradlew":
130
+ target.chmod(target.stat().st_mode | stat.S_IEXEC)
131
+
132
+
133
+ def write_app_build_gradle(
134
+ dest: Path,
135
+ config: Config,
136
+ android: AndroidConfig,
137
+ *,
138
+ python_version: str,
139
+ runtime_root: Path,
140
+ staged_libs: list[str],
141
+ abis: tuple[str, ...] | None = None,
142
+ signing_config_block: str = "",
143
+ release_signing_config: str | None = None,
144
+ test_build_type: str | None = None,
145
+ ) -> None:
146
+ """Emit ``app/build.gradle``.
147
+
148
+ ``abis`` is the *effective* ABI set for this build (a ``--abi`` restriction
149
+ must narrow the Gradle abiFilters too, or AGP builds ABIs whose runtime was
150
+ never staged); defaults to the full configured set.
151
+
152
+ ``release_signing_config`` names the signing config the release build type
153
+ uses (default: ``release`` when a ``signing_config_block`` was supplied,
154
+ otherwise unsigned). ``test_build_type`` selects which variant the
155
+ instrumented tests run against — AGP only generates
156
+ ``connected<Variant>AndroidTest`` for that one variant, so the release smoke
157
+ test needs it set to ``release``.
158
+
159
+ ``runtime_root`` is the staging directory holding one extracted runtime
160
+ per target triplet (``<runtime_root>/<triplet>/prefix``). AGP invokes
161
+ CMake once per ABI; the ``{{triplet}}`` placeholder in ``PYTHON_PREFIX_DIR``
162
+ is resolved *inside* CMake via ``CMAKE_LIBRARY_ARCHITECTURE`` (the
163
+ python.org testbed's mechanism), so each ABI's launcher links its own
164
+ libpython — a Gradle-side lookup cannot do this for plain ``gradlew``
165
+ builds (``android.injected.build.abi`` is Studio-only).
166
+ """
167
+ effective = abis if abis is not None else tuple(android.abis)
168
+ abis_filter = ", ".join(f"'{android_abi(a)}'" for a in effective)
169
+ stem = "python" + ".".join(python_version.split(".")[:2])
170
+ prefix_arg = _gradle_path(runtime_root) + "/{{triplet}}/prefix"
171
+
172
+ deps: list[str] = [
173
+ " implementation 'com.google.android.material:material:1.11.0'",
174
+ # The generated contract smoke test (android/06 --smoke).
175
+ " androidTestImplementation 'androidx.test.ext:junit:1.1.5'",
176
+ " androidTestImplementation 'androidx.test:core:1.5.0'",
177
+ " androidTestImplementation 'androidx.test:runner:1.5.2'",
178
+ ]
179
+ for lib in sorted(staged_libs):
180
+ deps.append(f" implementation files('libs/{lib}')")
181
+ for coordinate in sorted(android.gradle.dependencies):
182
+ deps.append(f" implementation '{coordinate}'")
183
+
184
+ plugins = [" id 'com.android.application'"]
185
+ if android.src.kotlin:
186
+ plugins.append(" id 'org.jetbrains.kotlin.android'")
187
+
188
+ src_dirs = "".join(
189
+ f" java.srcDirs += '{_gradle_path(Path(p))}'\n"
190
+ for p in [*android.src.java, *android.src.kotlin]
191
+ )
192
+
193
+ bs = android.build_settings
194
+ strip = _release_flag(bs.strip_native_libs)
195
+ minify = "true" if bs.minify else "false"
196
+ shrink = "true" if (bs.shrink_resources and bs.minify) else "false"
197
+ # Keep .so debug symbols unless a stripped release is configured (AGP does
198
+ # the strip, tracking the toolchain + preserving 16 KB alignment).
199
+ keep_symbols_line = "" if strip else " keepDebugSymbols += ['**/*.so']\n"
200
+ debug_symbol_level = {
201
+ "symbol_table": "SYMBOL_TABLE",
202
+ "full": "FULL",
203
+ "none": "NONE",
204
+ }[bs.debug_symbols]
205
+ # At minSdk 21+ the platform loads multiple dex files natively, so this is
206
+ # only about whether AGP is *allowed* to split: left on, the bundled .aars
207
+ # plus the bootstrap can exceed the 64K method limit without failing the
208
+ # build; turned off, an over-limit project fails at dexing.
209
+ multidex = "true" if bs.multidex else "false"
210
+ signing_name = release_signing_config or ("release" if signing_config_block else "")
211
+ release_signing = (
212
+ f" signingConfig signingConfigs.{signing_name}\n"
213
+ if signing_name
214
+ else ""
215
+ )
216
+ test_build_type_line = (
217
+ f" testBuildType '{test_build_type}'\n" if test_build_type else ""
218
+ )
219
+
220
+ lint_checks = ", ".join(f"'{check}'" for check in policy.LINT_CHECKS)
221
+ merged_manifest_name = MERGED_MANIFEST_RELPATH.name
222
+
223
+ text = f"""\
224
+ import com.android.build.api.artifact.SingleArtifact
225
+
226
+ plugins {{
227
+ {chr(10).join(plugins)}
228
+ }}
229
+
230
+ android {{
231
+ namespace '{android.package}'
232
+ compileSdk {android.compile_sdk}
233
+ ndkVersion '{toolchain.NDK_VERSION}'
234
+ {test_build_type_line}
235
+ defaultConfig {{
236
+ applicationId '{android.package}'
237
+ minSdk {android.min_sdk}
238
+ targetSdk {android.target_sdk}
239
+ versionCode {android.version_code}
240
+ versionName '{config.project.version}'
241
+ multiDexEnabled {multidex}
242
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
243
+ ndk {{ abiFilters {abis_filter} }}
244
+ externalNativeBuild {{
245
+ cmake {{
246
+ arguments '-DPYTHON_VERSION={stem[6:]}',
247
+ '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON',
248
+ '-DPYTHON_PREFIX_DIR={prefix_arg}'
249
+ }}
250
+ }}
251
+ }}
252
+
253
+ externalNativeBuild {{
254
+ cmake {{ path 'src/main/cpp/CMakeLists.txt' }}
255
+ }}
256
+
257
+ // AAPT's default ignore pattern drops asset dirs starting with '_', which
258
+ // would silently exclude _python_bundle (loadmodel findings #4).
259
+ aaptOptions.ignoreAssetsPattern = 'kivyforge-dont-ignore-anything'
260
+
261
+ packagingOptions {{
262
+ jniLibs {{
263
+ // Extract native libs so dlopen-by-path works for the flattened
264
+ // extension modules (android/04 §load model).
265
+ useLegacyPackaging = true
266
+ {keep_symbols_line} }}
267
+ }}
268
+
269
+ {signing_config_block}
270
+ buildTypes {{
271
+ release {{
272
+ minifyEnabled {minify}
273
+ shrinkResources {shrink}
274
+ {release_signing} ndk {{ debugSymbolLevel '{debug_symbol_level}' }}
275
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), \
276
+ 'proguard-rules.pro'
277
+ }}
278
+ }}
279
+
280
+ sourceSets {{
281
+ main {{
282
+ {src_dirs} }}
283
+ }}
284
+
285
+ compileOptions {{
286
+ sourceCompatibility JavaVersion.VERSION_1_8
287
+ targetCompatibility JavaVersion.VERSION_1_8
288
+ }}
289
+
290
+ // The release policy's delegated half (android/06 §package). checkOnly
291
+ // narrows the run to the curated subset and `fatal` raises exactly those to
292
+ // build-breaking, so a future Lint's new checks can never spuriously block a
293
+ // release (and a stale id stays a warning instead of failing every build).
294
+ // `kivyforge package` runs lintRelease before it assembles, so a finding
295
+ // blocks before signing.
296
+ lint {{
297
+ checkOnly.addAll([{lint_checks}])
298
+ fatal.addAll([{lint_checks}])
299
+ abortOnError = true
300
+ checkReleaseBuilds = true
301
+ htmlReport = true
302
+ xmlReport = true
303
+ }}
304
+ }}
305
+
306
+ // kivyforge's own half of the policy lints the *merged* release manifest, so
307
+ // export it to a stable path (AGP's intermediates layout is not a contract).
308
+ androidComponents {{
309
+ onVariants(selector().withName('release')) {{ variant ->
310
+ tasks.register('{MERGED_MANIFEST_TASK}', Copy) {{
311
+ from(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST.INSTANCE))
312
+ into(layout.buildDirectory.dir('kivyforge'))
313
+ rename {{ '{merged_manifest_name}' }}
314
+ }}
315
+ }}
316
+ }}
317
+
318
+ dependencies {{
319
+ {chr(10).join(deps)}
320
+ }}
321
+ """
322
+ _write(dest / "app" / "build.gradle", text)
323
+ _write(
324
+ dest / "app" / "proguard-rules.pro",
325
+ "# kivyforge keep-rules: pyjnius reflects into these at runtime.\n"
326
+ "-keep class org.jnius.** {{ *; }}\n".replace("{{", "{").replace("}}", "}")
327
+ + "-keep class org.kivy.android.** { *; }\n"
328
+ "-keep class org.renpy.android.** { *; }\n"
329
+ "-keep class org.libsdl.app.** { *; }\n",
330
+ )
331
+
332
+
333
+ def _release_flag(value: bool | str) -> bool:
334
+ """A build_settings tri-state -> whether it applies to a release build."""
335
+ from kivyforge.config.model import ANDROID_RELEASE_ONLY
336
+
337
+ return value is True or value == ANDROID_RELEASE_ONLY
338
+
339
+
340
+ def write_resources(
341
+ dest: Path, config: Config, android: AndroidConfig, *, project_root: Path
342
+ ) -> None:
343
+ res = dest / "app" / "src" / "main" / "res"
344
+ values = res / "values"
345
+ values.mkdir(parents=True, exist_ok=True)
346
+ _write(
347
+ values / "strings.xml",
348
+ '<resources><string name="app_name">'
349
+ f"{_xml_escape(config.display_name)}</string></resources>\n",
350
+ )
351
+ # The generated manifest names @mipmap/ic_launcher unconditionally, so the
352
+ # icon set is written on every build — the default one when no source is
353
+ # configured, the full adaptive set when there is.
354
+ from ..icons import IconError, write_icons
355
+ from ..splash import SplashError, write_splash
356
+
357
+ try:
358
+ write_icons(res, config, android, project_root)
359
+ except IconError as exc:
360
+ raise ProjectGenError(str(exc)) from exc
361
+ try:
362
+ splash = write_splash(res, android, project_root)
363
+ except SplashError as exc:
364
+ raise ProjectGenError(str(exc)) from exc
365
+ _write(values / "styles.xml", _styles_xml(android, splash, v31=False))
366
+ if splash.configured:
367
+ # A values-v31 resource replaces the same-named one wholesale, so the
368
+ # override repeats the whole style rather than adding to it.
369
+ v31 = res / "values-v31"
370
+ v31.mkdir(parents=True, exist_ok=True)
371
+ _write(v31 / "styles.xml", _styles_xml(android, splash, v31=True))
372
+
373
+
374
+ def _styles_xml(android: AndroidConfig, splash, *, v31: bool) -> str:
375
+ """The app theme, with the splash items the given API level understands."""
376
+ items: list[tuple[str, str]] = []
377
+ if splash.window_background is not None:
378
+ items.append(("android:windowBackground", splash.window_background))
379
+ if v31:
380
+ items += list(splash.v31_items)
381
+ if not items:
382
+ return (
383
+ "<resources>\n"
384
+ f' <style name="Theme.Kivyforge" parent="{android.base_theme}" />\n'
385
+ "</resources>\n"
386
+ )
387
+ body = "".join(
388
+ f' <item name="{name}">{value}</item>\n' for name, value in items
389
+ )
390
+ return (
391
+ "<resources>\n"
392
+ f' <style name="Theme.Kivyforge" parent="{android.base_theme}">\n'
393
+ f"{body}"
394
+ " </style>\n"
395
+ "</resources>\n"
396
+ )
397
+
398
+
399
+ def write_gradle_pins(dest: Path, lock: AndroidLockfile) -> None:
400
+ """Materialize the committed Maven-graph record into the project (android/02).
401
+
402
+ Only when Maven coordinates are declared. The app's declared coordinates are
403
+ version-pinned in ``app/build.gradle`` (``implementation 'g:a:v'``), and the
404
+ **full resolved transitive graph with a per-artifact SHA-256 is the committed
405
+ audit record in ``pylock.android.toml``** (``[[tool.kivyforge.gradle.resolved]]``);
406
+ `write_gradle_pins` mirrors that record into ``app/gradle.lockfile`` as
407
+ comments, for reference. Neither Gradle dependency locking nor Gradle artifact
408
+ verification is enabled, so this channel's hashes are audited, not enforced —
409
+ the docs say so in those words (android/02 §Gradle pins).
410
+
411
+ > **Scoped in v1.** Gradle's ``verification-metadata.xml`` enforces SHA-256
412
+ > over the *entire* build classpath (AGP, androidx, transforms), not just the
413
+ > app's Maven deps — which the lock does not resolve. Emitting a partial
414
+ > metadata file makes Gradle reject every unlisted build artifact, so v1
415
+ > records the app-dep hashes in the lock (the auditable source of truth) and
416
+ > defers Gradle-*enforced* verification to a future full-classpath resolve.
417
+ > See docs/design/platforms/android/02 §Gradle pins (realized-vs-designed).
418
+ """
419
+ # Remove any stale strict metadata a prior kivyforge emitted (it would make
420
+ # Gradle verify the entire build classpath and fail) — the generated project
421
+ # is regenerated every build, so leftovers must be swept.
422
+ stale = dest / "gradle" / "verification-metadata.xml"
423
+ if stale.exists():
424
+ stale.unlink()
425
+ if not lock.gradle.declared:
426
+ return
427
+ lock_lines = [
428
+ "# Generated by kivyforge from pylock.android.toml — the resolved Maven",
429
+ "# graph (per-artifact SHA-256) is committed in pylock.android.toml.",
430
+ "#",
431
+ "# INFORMATIONAL. Gradle dependency locking is not enabled in this project,",
432
+ "# so this file gates nothing: it mirrors the lock's audit record at the",
433
+ "# path a Gradle lock would live. Declared coordinates are version-pinned",
434
+ "# in app/build.gradle. See docs/design/platforms/android/02 §Gradle pins.",
435
+ ]
436
+ for module in sorted(lock.gradle.resolved, key=lambda m: m.coordinate):
437
+ for artifact in module.artifacts:
438
+ lock_lines.append(
439
+ f"# {module.coordinate} {artifact.name} sha256:{artifact.sha256}"
440
+ )
441
+ _write(dest / "app" / "gradle.lockfile", "\n".join(lock_lines) + "\n")
442
+
443
+
444
+ def _write(path: Path, text: str) -> None:
445
+ path.parent.mkdir(parents=True, exist_ok=True)
446
+ path.write_text(text, encoding="utf-8", newline="\n")
447
+
448
+
449
+ def _gradle_path(path: Path) -> str:
450
+ return str(path).replace("\\", "/")
451
+
452
+
453
+ def _prop_str(value: object) -> str:
454
+ if isinstance(value, bool):
455
+ return "true" if value else "false"
456
+ return str(value)
457
+
458
+
459
+ def _xml_escape(text: str) -> str:
460
+ from xml.sax.saxutils import escape
461
+
462
+ return escape(text)
@@ -0,0 +1,187 @@
1
+ """Generate the ``PythonService`` subclasses for ``[[services]]`` (android/05).
2
+
3
+ The manifest declares ``org.kivy.android.Service<Name>`` for every configured
4
+ service; a manifest entry with no class behind it compiles and links fine and
5
+ then throws ``ClassNotFoundException`` the first time anything starts the
6
+ service. These are the classes behind those names: each one is a few overrides
7
+ naming its Python entry point and, for a foreground service, the notification
8
+ the OS requires.
9
+
10
+ The instrumented service contract test is generated alongside them (and only
11
+ when a service exists — there is nothing to test otherwise). It starts the first
12
+ declared service and waits for the ready marker the interpreter stamps, which is
13
+ what proves the whole chain: the manifest name resolves, the service process
14
+ starts, the bundle unpacks in a second process, and the interpreter comes up.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from kivyforge.config.model import AndroidService
20
+
21
+ from ..bootstrap.render import RenderedFile
22
+
23
+ SERVICE_PACKAGE = "org.kivy.android"
24
+
25
+
26
+ def service_class_simple_name(service: AndroidService) -> str:
27
+ """``Downloader`` -> ``ServiceDownloader`` (matches the manifest emitter)."""
28
+ return f"Service{service.name}"
29
+
30
+
31
+ def render_service_classes(services: tuple[AndroidService, ...]) -> list[RenderedFile]:
32
+ """One ``PythonService`` subclass per declared service."""
33
+ out: list[RenderedFile] = []
34
+ for service in services:
35
+ name = service_class_simple_name(service)
36
+ out.append(
37
+ RenderedFile(
38
+ f"java/org/kivy/android/{name}.java",
39
+ _service_source(service, class_name=name),
40
+ )
41
+ )
42
+ return out
43
+
44
+
45
+ def _service_source(service: AndroidService, *, class_name: str) -> str:
46
+ overrides = [
47
+ _override("String", "getServiceName", _java_string(service.name)),
48
+ _override("String", "getEntryPoint", _java_string(service.entry_point)),
49
+ ]
50
+ if service.foreground:
51
+ overrides.append(_override("boolean", "isForeground", "true"))
52
+ note = service.notification
53
+ if note is not None:
54
+ overrides += [
55
+ _override("String", "getChannelId", _java_string(note.channel_id)),
56
+ _override("String", "getChannelName", _java_string(note.channel_name)),
57
+ _override("String", "getNotificationTitle", _java_string(note.title)),
58
+ _override("String", "getNotificationText", _java_string(note.text)),
59
+ _override(
60
+ "String",
61
+ "getNotificationIcon",
62
+ _java_string(note.icon) if note.icon else "null",
63
+ ),
64
+ ]
65
+ return (
66
+ f"package {SERVICE_PACKAGE};\n"
67
+ "\n"
68
+ "/**\n"
69
+ f" * Generated from [[tool.kivy.android.services]] entry {service.name!r}\n"
70
+ " * (android/05 §services). Regenerated on every build — edit\n"
71
+ " * pyproject.toml, not this file.\n"
72
+ " */\n"
73
+ f"public class {class_name} extends PythonService {{\n"
74
+ + "\n".join(overrides)
75
+ + "}\n"
76
+ )
77
+
78
+
79
+ def _override(return_type: str, method: str, value: str) -> str:
80
+ return (
81
+ " @Override\n"
82
+ f" protected {return_type} {method}() {{\n"
83
+ f" return {value};\n"
84
+ " }\n"
85
+ )
86
+
87
+
88
+ def _java_string(value: str) -> str:
89
+ escaped = (
90
+ value.replace("\\", "\\\\")
91
+ .replace('"', '\\"')
92
+ .replace("\n", "\\n")
93
+ .replace("\r", "\\r")
94
+ )
95
+ return f'"{escaped}"'
96
+
97
+
98
+ def render_service_contract_test(
99
+ services: tuple[AndroidService, ...],
100
+ ) -> list[RenderedFile]:
101
+ """The instrumented service probe, or nothing when no service is declared."""
102
+ if not services:
103
+ return []
104
+ service = services[0]
105
+ class_name = service_class_simple_name(service)
106
+ source = f"""\
107
+ package org.kivyforge.test;
108
+
109
+ import static org.junit.Assert.assertFalse;
110
+ import static org.junit.Assert.assertTrue;
111
+
112
+ import android.content.Context;
113
+ import android.content.Intent;
114
+ import androidx.test.core.app.ApplicationProvider;
115
+ import androidx.test.ext.junit.runners.AndroidJUnit4;
116
+ import java.io.File;
117
+ import java.nio.charset.StandardCharsets;
118
+ import java.nio.file.Files;
119
+ import org.junit.Test;
120
+ import org.junit.runner.RunWith;
121
+
122
+ /**
123
+ * The kivyforge service contract test, generated for [[services]] entry
124
+ * {service.name!r} (android/05 §services, android/06 --smoke).
125
+ *
126
+ * A declared service is only real if its manifest name resolves to a class, its
127
+ * process starts, and an interpreter comes up there. The service's Python
128
+ * normally never returns, so the probe waits for the marker the launcher stamps
129
+ * once the interpreter is up and the finder installed.
130
+ */
131
+ @RunWith(AndroidJUnit4.class)
132
+ public class KivyforgeServiceContractTest {{
133
+
134
+ private static final int TIMEOUT_MS = 90_000;
135
+ private static final String MARKER = "kivyforge_service_{service.name}.txt";
136
+
137
+ @Test
138
+ public void declaredServiceStartsAnInterpreter() throws Exception {{
139
+ Context ctx = ApplicationProvider.getApplicationContext();
140
+ File marker = new File(ctx.getFilesDir(), MARKER);
141
+ marker.delete();
142
+
143
+ Intent intent = new Intent(ctx, org.kivy.android.{class_name}.class);
144
+ ctx.startService(intent);
145
+
146
+ long deadline = System.currentTimeMillis() + TIMEOUT_MS;
147
+ String contents = "";
148
+ while (System.currentTimeMillis() < deadline) {{
149
+ if (marker.exists()) {{
150
+ contents = new String(
151
+ Files.readAllBytes(marker.toPath()), StandardCharsets.UTF_8);
152
+ if (contents.contains("SERVICE_READY")) {{
153
+ break;
154
+ }}
155
+ }}
156
+ Thread.sleep(500);
157
+ }}
158
+ assertTrue(
159
+ "service {service.name} did not start an interpreter within "
160
+ + TIMEOUT_MS + "ms; got: " + contents,
161
+ contents.contains("SERVICE_READY"));
162
+
163
+ // The entry-point import happens right after the marker, so give it a
164
+ // moment and confirm it did not blow up.
165
+ Thread.sleep(2000);
166
+ if (marker.exists()) {{
167
+ contents = new String(
168
+ Files.readAllBytes(marker.toPath()), StandardCharsets.UTF_8);
169
+ }}
170
+ assertFalse(
171
+ "service entry point {service.entry_point} raised on import: "
172
+ + contents,
173
+ contents.contains("SERVICE_ENTRY_FAILED"));
174
+ assertFalse(
175
+ "service native start failed: " + contents,
176
+ contents.contains("SERVICE_START_FAILED"));
177
+
178
+ ctx.stopService(intent);
179
+ }}
180
+ }}
181
+ """
182
+ return [
183
+ RenderedFile(
184
+ "androidTest/java/org/kivyforge/test/KivyforgeServiceContractTest.java",
185
+ source,
186
+ )
187
+ ]
@@ -0,0 +1,3 @@
1
+ The vendored gradle-wrapper.jar and gradlew launcher scripts are part
2
+ of Gradle (https://gradle.org), licensed under the Apache License 2.0.
3
+ https://www.apache.org/licenses/LICENSE-2.0
@@ -0,0 +1 @@
1
+ 5e27c39c2336c25748f279d8b105162d14b1a39eb7839d0b658432282d0ce79f gradle-wrapper.jar