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,214 @@
1
+ """Release manifest-policy preflight (android/04 §release policy, android/06).
2
+
3
+ Raw XML + attribute passthrough make it possible to ship a footgun (an
4
+ accidentally-exported component, a debuggable/cleartext release, the
5
+ ``org.example.*`` placeholder). ``kivyforge package`` lints the *generated*
6
+ manifest and **fails before signing** on a violation.
7
+
8
+ Scope note (android/06): a full release lint delegates the well-covered checks
9
+ to a curated Android ``lintRelease`` subset (see ``LINT_CHECKS``, emitted into
10
+ ``app/build.gradle`` and run by ``kivyforge package``). This module hand-rolls
11
+ the checks Lint misses or that are kivyforge-specific.
12
+
13
+ It runs **twice**: first over the manifest kivyforge generated, to fail before
14
+ any Gradle work, and then over AGP's *merged* release manifest — the one that is
15
+ actually packaged, including whatever ``.aar``/Maven library manifests
16
+ contribute. Because a library's component cannot be edited out of pyproject.toml,
17
+ ``[tool.kivy.android.manifest].allow_exported`` is the escape hatch for an
18
+ exported component a dependency insists on. Findings are (severity, message);
19
+ any FAIL aborts.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import xml.etree.ElementTree as ET
25
+ from collections.abc import Iterable
26
+ from dataclasses import dataclass
27
+
28
+ ANDROID_NS = "http://schemas.android.com/apk/res/android"
29
+ _NS = f"{{{ANDROID_NS}}}"
30
+
31
+ # Exported components kivyforge itself ships in *every* generated project, not
32
+ # something a user's pyproject.toml controls, so they are baked into the
33
+ # allowlist rather than making every user rediscover and add them via
34
+ # allow_exported:
35
+ # - org.kivy.android.PythonActivity: the app's own launcher.
36
+ # - androidx.profileinstaller.ProfileInstallReceiver: pulled in transitively
37
+ # by the hardcoded `com.google.android.material` dependency (generate/
38
+ # project.py). It is exported by AndroidX's own design so ADB/Play/
39
+ # Macrobenchmark tooling can trigger baseline-profile installation, and is
40
+ # itself gated by the system-only android.permission.DUMP, so no
41
+ # third-party app can reach it.
42
+ BOOTSTRAP_EXPORTED = {
43
+ "org.kivy.android.PythonActivity",
44
+ "androidx.profileinstaller.ProfileInstallReceiver",
45
+ }
46
+
47
+ # The curated `lintRelease` subset the generated project runs as errors. These
48
+ # are the checks Lint already does better than a hand-rolled XML walk: exported
49
+ # components reachable without a permission, the backup posture, and cleartext
50
+ # HTTP (both the manifest flag and a network-security-config that re-enables it).
51
+ # Kept deliberately small: `checkOnly` means everything else stays advisory, so
52
+ # a Lint release with new checks can't spuriously block someone's release.
53
+ LINT_CHECKS = (
54
+ "AllowBackup",
55
+ "ExportedActivity",
56
+ "ExportedContentProvider",
57
+ "ExportedPreferenceActivity",
58
+ "ExportedReceiver",
59
+ "ExportedService",
60
+ "InsecureBaseConfiguration",
61
+ "IntentFilterExportedReceiver",
62
+ )
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class PolicyFinding:
67
+ severity: str # "FAIL" | "INFO"
68
+ message: str
69
+
70
+
71
+ class ManifestPolicyError(Exception):
72
+ def __init__(self, fails: list[PolicyFinding]):
73
+ self.fails = fails
74
+ joined = "\n".join(f" • {f.message}" for f in fails)
75
+ super().__init__(
76
+ "release manifest policy check failed (blocked before signing):\n"
77
+ f"{joined}\n"
78
+ " Fix these through the manifest escape hatches "
79
+ "(passthrough / raw XML), never by editing the generated file "
80
+ "(android/04 §release policy)."
81
+ )
82
+
83
+
84
+ # Dangerous runtime permissions worth an INFO reminder (need a runtime request).
85
+ _DANGEROUS = {
86
+ "android.permission.CAMERA",
87
+ "android.permission.RECORD_AUDIO",
88
+ "android.permission.ACCESS_FINE_LOCATION",
89
+ "android.permission.ACCESS_COARSE_LOCATION",
90
+ "android.permission.READ_CONTACTS",
91
+ "android.permission.READ_EXTERNAL_STORAGE",
92
+ "android.permission.WRITE_EXTERNAL_STORAGE",
93
+ "android.permission.READ_PHONE_STATE",
94
+ }
95
+
96
+
97
+ def check_release_manifest(
98
+ manifest_xml: str, *, package: str, allow_exported: Iterable[str] = ()
99
+ ) -> list[PolicyFinding]:
100
+ """Return all findings; the caller raises on any FAIL. INFO is advisory."""
101
+ findings: list[PolicyFinding] = []
102
+ allowed = BOOTSTRAP_EXPORTED | set(allow_exported)
103
+ root = ET.fromstring(manifest_xml)
104
+ app = root.find("application")
105
+
106
+ # 1. applicationId must not be the init placeholder.
107
+ if package.startswith("org.example."):
108
+ findings.append(
109
+ PolicyFinding(
110
+ "FAIL",
111
+ f"applicationId {package!r} is the init placeholder "
112
+ "(org.example.*); set a real package before release.",
113
+ )
114
+ )
115
+
116
+ # 2. debuggable must not be forced true.
117
+ if app is not None and app.get(f"{_NS}debuggable") == "true":
118
+ findings.append(
119
+ PolicyFinding(
120
+ "FAIL", "android:debuggable is forced true in a release build."
121
+ )
122
+ )
123
+
124
+ # 3. Exactly one LAUNCHER activity, and it is the bootstrap's.
125
+ launchers = []
126
+ for activity in root.iter("activity"):
127
+ for category in activity.iter("category"):
128
+ if category.get(f"{_NS}name") == "android.intent.category.LAUNCHER":
129
+ launchers.append(activity.get(f"{_NS}name"))
130
+ if len(launchers) != 1:
131
+ findings.append(
132
+ PolicyFinding(
133
+ "FAIL",
134
+ f"expected exactly one LAUNCHER activity, found {len(launchers)}: "
135
+ f"{launchers}.",
136
+ )
137
+ )
138
+
139
+ # 4. Every intent-filtered component sets exported explicitly, and only the
140
+ # bootstrap components are exported.
141
+ for kind in ("activity", "service", "receiver", "provider"):
142
+ for comp in root.iter(kind):
143
+ name = comp.get(f"{_NS}name")
144
+ has_filter = comp.find("intent-filter") is not None
145
+ exported = comp.get(f"{_NS}exported")
146
+ if has_filter and exported is None:
147
+ findings.append(
148
+ PolicyFinding(
149
+ "FAIL",
150
+ f"{kind} {name!r} has an intent-filter but no explicit "
151
+ "android:exported.",
152
+ )
153
+ )
154
+ if exported == "true" and name not in allowed:
155
+ findings.append(
156
+ PolicyFinding(
157
+ "FAIL",
158
+ f"{kind} {name!r} is exported; only "
159
+ f"{sorted(allowed)} may be exported. If a dependency "
160
+ "requires this component to be exported, add it to "
161
+ "[tool.kivy.android.manifest].allow_exported.",
162
+ )
163
+ )
164
+
165
+ # 5. No conflicting deep-link <data> (same scheme+host on two filters).
166
+ seen_data: set[tuple[str, str]] = set()
167
+ for data in root.iter("data"):
168
+ key = (data.get(f"{_NS}scheme", ""), data.get(f"{_NS}host", ""))
169
+ if key != ("", "") and key in seen_data:
170
+ findings.append(
171
+ PolicyFinding(
172
+ "FAIL",
173
+ f"conflicting deep-link <data> scheme/host {key} appears on "
174
+ "more than one intent-filter.",
175
+ )
176
+ )
177
+ seen_data.add(key)
178
+
179
+ # 6. Cleartext posture (INFO if explicitly enabled).
180
+ if app is not None and app.get(f"{_NS}usesCleartextTraffic") == "true":
181
+ findings.append(
182
+ PolicyFinding(
183
+ "INFO",
184
+ "android:usesCleartextTraffic is true — the release permits "
185
+ "cleartext HTTP.",
186
+ )
187
+ )
188
+
189
+ # 7. Dangerous runtime permissions (INFO reminder).
190
+ for perm in root.iter("uses-permission"):
191
+ name = perm.get(f"{_NS}name")
192
+ if name in _DANGEROUS:
193
+ findings.append(
194
+ PolicyFinding(
195
+ "INFO",
196
+ f"{name} is a dangerous runtime permission; it needs a "
197
+ "runtime request (not just the manifest entry).",
198
+ )
199
+ )
200
+
201
+ return findings
202
+
203
+
204
+ def enforce_release_manifest(
205
+ manifest_xml: str, *, package: str, allow_exported: Iterable[str] = ()
206
+ ) -> list[PolicyFinding]:
207
+ """Raise ``ManifestPolicyError`` on any FAIL; return the INFO findings."""
208
+ findings = check_release_manifest(
209
+ manifest_xml, package=package, allow_exported=allow_exported
210
+ )
211
+ fails = [f for f in findings if f.severity == "FAIL"]
212
+ if fails:
213
+ raise ManifestPolicyError(fails)
214
+ return [f for f in findings if f.severity == "INFO"]
@@ -0,0 +1,145 @@
1
+ """Release signing preflight + Gradle signingConfig values (android/06, 07).
2
+
3
+ Android signing is **mandatory** and kivyforge owns it via Gradle's signing
4
+ config. The preflight resolves the effective keystore/alias (overlay or CLI
5
+ override), confirms the passwords are present in the configured env vars, and
6
+ verifies the alias exists — failing fast **before** Gradle, with the android/06
7
+ remediation text. Passwords are read by Gradle at build time from the env;
8
+ kivyforge never writes them to disk.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ from kivyforge.config.model import AndroidSigningConfig
20
+
21
+
22
+ class SigningError(Exception):
23
+ pass
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ResolvedSigning:
28
+ """The effective, verified signing identity for a release build."""
29
+
30
+ keystore: Path
31
+ key_alias: str
32
+ store_password_env: str
33
+ key_password_env: str
34
+ v1_signing: bool
35
+ v2_signing: bool
36
+ v3_signing: bool
37
+ v4_signing: bool
38
+
39
+
40
+ def resolve_signing(
41
+ config: AndroidSigningConfig,
42
+ *,
43
+ project_root: Path,
44
+ keystore_override: str | None = None,
45
+ key_alias_override: str | None = None,
46
+ ) -> ResolvedSigning:
47
+ """Resolve + verify the signing identity; raise ``SigningError`` if unusable."""
48
+ keystore = keystore_override or config.keystore
49
+ key_alias = key_alias_override or config.key_alias
50
+ if not keystore or not key_alias:
51
+ raise SigningError(
52
+ "code signing required to package a release, but no keystore is "
53
+ "resolved.\n"
54
+ " Set it one of these ways:\n"
55
+ " • [tool.kivy.android.signing] keystore/key_alias in "
56
+ "pyproject.toml, then re-lock\n"
57
+ " • kivyforge package --keystore release.keystore "
58
+ "--key-alias upload\n"
59
+ " And export the passwords:\n"
60
+ " • export KIVYFORGE_KEYSTORE_PASSWORD=... "
61
+ "(and KIVYFORGE_KEY_PASSWORD if different)"
62
+ )
63
+
64
+ keystore_path = Path(keystore)
65
+ if not keystore_path.is_absolute():
66
+ keystore_path = (project_root / keystore_path).resolve()
67
+ if not keystore_path.is_file():
68
+ raise SigningError(
69
+ f"signing keystore not found: {keystore_path}\n"
70
+ " Provide a valid keystore path (repo-relative or absolute)."
71
+ )
72
+
73
+ store_pw = os.environ.get(config.store_password_env)
74
+ if not store_pw:
75
+ raise SigningError(
76
+ f"the keystore password env var ${config.store_password_env} is not "
77
+ "set.\n export "
78
+ f"{config.store_password_env}=... before packaging a release."
79
+ )
80
+ # The key password is optional: if its env var is unset, Gradle's signing
81
+ # config falls back to the store password (keytool convention, android/01).
82
+
83
+ _verify_alias(keystore_path, key_alias, store_pw)
84
+
85
+ return ResolvedSigning(
86
+ keystore=keystore_path,
87
+ key_alias=key_alias,
88
+ store_password_env=config.store_password_env,
89
+ key_password_env=config.key_password_env,
90
+ v1_signing=config.v1_signing,
91
+ v2_signing=config.v2_signing,
92
+ v3_signing=config.v3_signing,
93
+ v4_signing=config.v4_signing,
94
+ )
95
+
96
+
97
+ def _verify_alias(keystore: Path, alias: str, store_password: str) -> None:
98
+ keytool = shutil.which("keytool")
99
+ if keytool is None:
100
+ # Non-fatal: a JDK is a doctor prerequisite; Gradle will surface a
101
+ # clear error if the alias is truly wrong. Don't block on tool absence.
102
+ return
103
+ proc = subprocess.run(
104
+ [
105
+ keytool,
106
+ "-list",
107
+ "-keystore",
108
+ str(keystore),
109
+ "-alias",
110
+ alias,
111
+ "-storepass",
112
+ store_password,
113
+ ],
114
+ capture_output=True,
115
+ text=True,
116
+ )
117
+ if proc.returncode != 0:
118
+ raise SigningError(
119
+ f"key alias {alias!r} not found in {keystore.name} (or the keystore "
120
+ "password is wrong).\n"
121
+ f" keytool said:\n {(proc.stderr or proc.stdout).strip()}"
122
+ )
123
+
124
+
125
+ def signing_config_gradle(signing: ResolvedSigning) -> str:
126
+ """The ``signingConfigs { release { … } }`` block for app/build.gradle.
127
+
128
+ Reads passwords from the env at Gradle time via ``System.getenv`` — never
129
+ written to disk. The v1–v4 scheme toggles apply to ``.apk`` outputs.
130
+ """
131
+ keystore = str(signing.keystore).replace("\\", "/")
132
+ return f"""\
133
+ signingConfigs {{
134
+ release {{
135
+ storeFile file('{keystore}')
136
+ storePassword System.getenv('{signing.store_password_env}')
137
+ keyAlias '{signing.key_alias}'
138
+ keyPassword System.getenv('{signing.key_password_env}') \
139
+ ?: System.getenv('{signing.store_password_env}')
140
+ enableV1Signing {str(signing.v1_signing).lower()}
141
+ enableV2Signing {str(signing.v2_signing).lower()}
142
+ enableV3Signing {str(signing.v3_signing).lower()}
143
+ enableV4Signing {str(signing.v4_signing).lower()}
144
+ }}
145
+ }}"""
@@ -0,0 +1,43 @@
1
+ """``kivyforge run --smoke`` — the contract smoke test (android/06 §--smoke).
2
+
3
+ Drives the generated instrumented test via Gradle's own connected-test path
4
+ (``connectedDebugAndroidTest`` / ``connectedReleaseAndroidTest``), which
5
+ validates the load-bearing runtime mechanisms — the extension-module finder, the
6
+ pyjnius ``invoke0`` glue, and, for a project that declares services, that a
7
+ declared service really starts an interpreter in its own process — on a real
8
+ device/emulator with no app-specific test code. Non-zero exit on any failure; it
9
+ is the mechanism that promotes a compatibility-matrix row to Validated
10
+ (android/08).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ from .gradlew import GradleError, run_gradle
18
+
19
+
20
+ class SmokeError(Exception):
21
+ pass
22
+
23
+
24
+ def run_smoke(project_dir: Path, *, release: bool = False) -> None:
25
+ """Run the generated contract test; raise ``SmokeError`` on failure.
26
+
27
+ A device/emulator must already be attached (Gradle's connected-test task
28
+ selects it). ``release`` targets the release variant so the probe also
29
+ exercises byte-compilation, stripping, and R8 under a test signing config.
30
+ """
31
+ task = "connectedReleaseAndroidTest" if release else "connectedDebugAndroidTest"
32
+ try:
33
+ run_gradle(project_dir, [task])
34
+ except GradleError as exc:
35
+ raise SmokeError(
36
+ f"the contract smoke test failed ({task}).\n"
37
+ " This means a load-bearing runtime mechanism broke on-device — "
38
+ "the extension-module finder, the pyjnius invoke0 glue, or a "
39
+ "declared service's interpreter.\n"
40
+ f" Gradle output is above; the test report is under "
41
+ f"{project_dir / 'app' / 'build' / 'reports' / 'androidTests'}.\n"
42
+ f" {exc}"
43
+ ) from exc
@@ -0,0 +1,293 @@
1
+ """Generate the splash-screen resources (android/01 §splash, android/04).
2
+
3
+ The splash model is the platform's: *a centered icon on a background*, driven
4
+ entirely by ``windowSplashScreen*`` theme attributes with no runtime dependency
5
+ and no code in the activity. Two layers of resources come out of one config:
6
+
7
+ - ``values-v31/`` carries the real attributes. On Android 12+ the OS draws the
8
+ system splash from them, so an ``AnimatedVectorDrawable`` source animates and
9
+ ``animation_duration``/``branding`` mean what they say.
10
+ - The base theme's ``android:windowBackground`` gets a generated layer-list of
11
+ the same background + icon. That is what fills the gap below API 31 — where no
12
+ system splash exists at all — and, on every API level, what the window shows
13
+ between the splash handing off and Kivy drawing its first frame. Without it
14
+ that handoff cuts to a blank window, which for a cold start (unpack the bundle,
15
+ boot CPython) is seconds of apparent hang.
16
+
17
+ **Icon sizing** follows the platform spec: the icon drawable is 288dp with the
18
+ inner two thirds guaranteed visible, or 240dp/160dp when ``icon_background``
19
+ puts a colored disc behind it. A raster source is scaled into that inner box on
20
+ a transparent canvas, so a plain square PNG is not clipped by the circular mask
21
+ the system splash applies.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+
29
+ from kivyforge.config.model import AndroidConfig
30
+
31
+ # The dp buckets a generated raster drawable is emitted for.
32
+ DENSITIES = {"mdpi": 1, "hdpi": 1.5, "xhdpi": 2, "xxhdpi": 3, "xxxhdpi": 4}
33
+
34
+ # Platform spec: canvas dp -> guaranteed-visible inner dp.
35
+ ICON_CANVAS_DP = 288
36
+ ICON_INNER_DP = 192
37
+ # With a colored disc behind the icon the canvas shrinks.
38
+ ICON_CANVAS_WITH_BACKGROUND_DP = 240
39
+ ICON_INNER_WITH_BACKGROUND_DP = 160
40
+ # The platform's maximum branding-image box.
41
+ BRANDING_MAX_DP = (200, 80)
42
+
43
+ # windowSplashScreen* are API 31 attributes; AAPT resolves attribute names
44
+ # against the compile SDK regardless of which values-* folder they sit in.
45
+ SPLASH_MIN_COMPILE_SDK = 31
46
+
47
+ DEFAULT_BACKGROUND = "#ffffff"
48
+
49
+ ICON_DRAWABLE = "kf_splash_icon"
50
+ BRANDING_DRAWABLE = "kf_splash_branding"
51
+ WINDOW_DRAWABLE = "kf_splash"
52
+ BACKGROUND_COLOR = "kf_splash_background"
53
+ ICON_BACKGROUND_COLOR = "kf_splash_icon_background"
54
+
55
+
56
+ class SplashError(Exception):
57
+ """The configured splash resources could not be generated."""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class SplashTheme:
62
+ """What the generated app theme has to say about the splash.
63
+
64
+ Empty when no splash is configured, in which case the theme is emitted
65
+ exactly as it was before splash support existed.
66
+ """
67
+
68
+ window_background: str | None = None
69
+ v31_items: tuple[tuple[str, str], ...] = ()
70
+
71
+ @property
72
+ def configured(self) -> bool:
73
+ return self.window_background is not None
74
+
75
+
76
+ def write_splash(res: Path, android: AndroidConfig, project_root: Path) -> SplashTheme:
77
+ """Write the splash resources under *res*; return the theme contribution."""
78
+ splash = android.splash
79
+ if not splash.source:
80
+ return SplashTheme()
81
+ if android.compile_sdk < SPLASH_MIN_COMPILE_SDK:
82
+ raise SplashError(
83
+ "[tool.kivy.android.splash] needs compile_sdk >= "
84
+ f"{SPLASH_MIN_COMPILE_SDK} (windowSplashScreen* are API "
85
+ f"{SPLASH_MIN_COMPILE_SDK} attributes); this project compiles "
86
+ f"against android-{android.compile_sdk}.\n"
87
+ " Raise [tool.kivy.android].compile_sdk, or drop the "
88
+ "[tool.kivy.android.splash] table."
89
+ )
90
+
91
+ background = splash.background or DEFAULT_BACKGROUND
92
+ icon_background = splash.icon_background
93
+ canvas_dp, inner_dp = (
94
+ (ICON_CANVAS_WITH_BACKGROUND_DP, ICON_INNER_WITH_BACKGROUND_DP)
95
+ if icon_background
96
+ else (ICON_CANVAS_DP, ICON_INNER_DP)
97
+ )
98
+
99
+ _write_colors(res, background=background, icon_background=icon_background)
100
+ animated = _write_icon(
101
+ res,
102
+ project_root / splash.source,
103
+ canvas_dp=canvas_dp,
104
+ inner_dp=inner_dp,
105
+ )
106
+ if splash.branding:
107
+ _write_branding(res, project_root / splash.branding)
108
+ _write_window_drawable(res)
109
+
110
+ items = [
111
+ ("android:windowSplashScreenBackground", f"@color/{BACKGROUND_COLOR}"),
112
+ ("android:windowSplashScreenAnimatedIcon", f"@drawable/{ICON_DRAWABLE}"),
113
+ ]
114
+ if icon_background:
115
+ items.append(
116
+ (
117
+ "android:windowSplashScreenIconBackgroundColor",
118
+ f"@color/{ICON_BACKGROUND_COLOR}",
119
+ )
120
+ )
121
+ # Duration only drives an AnimatedVectorDrawable; emitting it for a static
122
+ # PNG would delay every launch for nothing.
123
+ if splash.animation_duration is not None and animated:
124
+ items.append(
125
+ (
126
+ "android:windowSplashScreenAnimationDuration",
127
+ str(splash.animation_duration),
128
+ )
129
+ )
130
+ if splash.branding:
131
+ items.append(
132
+ (
133
+ "android:windowSplashScreenBrandingImage",
134
+ f"@drawable/{BRANDING_DRAWABLE}",
135
+ )
136
+ )
137
+ return SplashTheme(
138
+ window_background=f"@drawable/{WINDOW_DRAWABLE}", v31_items=tuple(items)
139
+ )
140
+
141
+
142
+ def window_drawable_xml() -> str:
143
+ """The ``windowBackground`` layer-list: background color + centered icon."""
144
+ return (
145
+ '<?xml version="1.0" encoding="utf-8"?>\n'
146
+ '<layer-list xmlns:android="http://schemas.android.com/apk/res/android">\n'
147
+ f' <item android:drawable="@color/{BACKGROUND_COLOR}" />\n'
148
+ ' <item android:gravity="center">\n'
149
+ f' <bitmap android:src="@drawable/{ICON_DRAWABLE}" '
150
+ 'android:gravity="center" />\n'
151
+ " </item>\n"
152
+ "</layer-list>\n"
153
+ )
154
+
155
+
156
+ def vector_window_drawable_xml() -> str:
157
+ """The same layer-list for a vector icon (``<bitmap>`` needs a raster)."""
158
+ return (
159
+ '<?xml version="1.0" encoding="utf-8"?>\n'
160
+ '<layer-list xmlns:android="http://schemas.android.com/apk/res/android">\n'
161
+ f' <item android:drawable="@color/{BACKGROUND_COLOR}" />\n'
162
+ f' <item android:drawable="@drawable/{ICON_DRAWABLE}" '
163
+ 'android:gravity="center" />\n'
164
+ "</layer-list>\n"
165
+ )
166
+
167
+
168
+ def _write_window_drawable(res: Path) -> None:
169
+ drawable = res / "drawable"
170
+ drawable.mkdir(parents=True, exist_ok=True)
171
+ vector = (drawable / f"{ICON_DRAWABLE}.xml").is_file()
172
+ xml = vector_window_drawable_xml() if vector else window_drawable_xml()
173
+ _write(drawable / f"{WINDOW_DRAWABLE}.xml", xml)
174
+
175
+
176
+ def _write_colors(res: Path, *, background: str, icon_background: str | None) -> None:
177
+ lines = [f' <color name="{BACKGROUND_COLOR}">{background}</color>']
178
+ if icon_background:
179
+ lines.append(
180
+ f' <color name="{ICON_BACKGROUND_COLOR}">{icon_background}</color>'
181
+ )
182
+ values = res / "values"
183
+ values.mkdir(parents=True, exist_ok=True)
184
+ _write(
185
+ values / "kf_splash.xml",
186
+ "<resources>\n" + "\n".join(lines) + "\n</resources>\n",
187
+ )
188
+
189
+
190
+ def _write_icon(res: Path, source: Path, *, canvas_dp: int, inner_dp: int) -> bool:
191
+ """Emit the splash icon; return True when it is an animatable vector."""
192
+ if not source.is_file():
193
+ raise SplashError(
194
+ f"[tool.kivy.android.splash].source not found: {source}\n"
195
+ " Expected a PNG, or an AnimatedVectorDrawable XML for an "
196
+ "animated splash."
197
+ )
198
+ if source.suffix.lower() == ".xml":
199
+ # A vector/AnimatedVectorDrawable is density-independent and the
200
+ # platform animates it as-is; passing it through Pillow would destroy
201
+ # exactly the property that makes it worth using.
202
+ drawable = res / "drawable"
203
+ drawable.mkdir(parents=True, exist_ok=True)
204
+ _write(drawable / f"{ICON_DRAWABLE}.xml", _read_vector(source))
205
+ return True
206
+
207
+ image = _open(source)
208
+ for bucket, scale in DENSITIES.items():
209
+ target = res / f"drawable-{bucket}"
210
+ target.mkdir(parents=True, exist_ok=True)
211
+ canvas = round(canvas_dp * scale)
212
+ inner = round(inner_dp * scale)
213
+ _centered(image, canvas=canvas, inner=inner).save(
214
+ target / f"{ICON_DRAWABLE}.png", format="PNG"
215
+ )
216
+ return False
217
+
218
+
219
+ def _write_branding(res: Path, source: Path) -> None:
220
+ if not source.is_file():
221
+ raise SplashError(f"[tool.kivy.android.splash].branding not found: {source}")
222
+ image = _open(source)
223
+ max_w, max_h = BRANDING_MAX_DP
224
+ for bucket, scale in DENSITIES.items():
225
+ target = res / f"drawable-{bucket}"
226
+ target.mkdir(parents=True, exist_ok=True)
227
+ _contain(image, round(max_w * scale), round(max_h * scale)).save(
228
+ target / f"{BRANDING_DRAWABLE}.png", format="PNG"
229
+ )
230
+
231
+
232
+ def _read_vector(source: Path) -> str:
233
+ try:
234
+ text = source.read_text(encoding="utf-8")
235
+ except OSError as exc:
236
+ raise SplashError(f"cannot read splash source {source}: {exc}") from exc
237
+ if "<animated-vector" not in text and "<vector" not in text:
238
+ raise SplashError(
239
+ f"[tool.kivy.android.splash].source {source.name} is XML but is "
240
+ "neither a <vector> nor an <animated-vector> drawable; AAPT would "
241
+ "reject it.\n"
242
+ " Use a PNG for a static splash, or an AnimatedVectorDrawable "
243
+ "for an animated one."
244
+ )
245
+ return text
246
+
247
+
248
+ def _centered(image, *, canvas: int, inner: int):
249
+ """*image* scaled to fit *inner*, centered on a transparent *canvas* square."""
250
+ from PIL import Image
251
+
252
+ layer = Image.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
253
+ fitted = _contain(image, inner, inner)
254
+ layer.paste(
255
+ fitted,
256
+ ((canvas - fitted.width) // 2, (canvas - fitted.height) // 2),
257
+ )
258
+ return layer
259
+
260
+
261
+ def _contain(image, max_w: int, max_h: int):
262
+ """*image* scaled to fit inside the box, preserving its aspect ratio."""
263
+ from PIL import Image
264
+
265
+ scale = min(max_w / image.width, max_h / image.height)
266
+ size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
267
+ return image.resize(size, Image.LANCZOS)
268
+
269
+
270
+ def _open(path: Path):
271
+ image_module = _pillow()
272
+ try:
273
+ return image_module.open(path).convert("RGBA")
274
+ except OSError as exc:
275
+ raise SplashError(f"cannot read splash image {path}: {exc}") from exc
276
+
277
+
278
+ def _write(path: Path, text: str) -> None:
279
+ path.write_text(text, encoding="utf-8", newline="\n")
280
+
281
+
282
+ def _pillow():
283
+ try:
284
+ from PIL import Image
285
+ except ModuleNotFoundError as exc:
286
+ raise SplashError(
287
+ "generating the splash resources needs Pillow, which is not "
288
+ "installed.\n"
289
+ " Install the Android extra: pip install 'kivyforge[android]'\n"
290
+ " (or use an AnimatedVectorDrawable XML source, which needs no "
291
+ "image processing, or drop [tool.kivy.android.splash])."
292
+ ) from exc
293
+ return Image