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,148 @@
1
+ """Hermetic ELF reader: LOAD-segment alignment + DT_NEEDED (android/04 §16 KB).
2
+
3
+ Pure-Python parsing of the parts kivyforge's doctor needs — no NDK/readelf
4
+ dependency, so the 16 KB alignment check runs on any host. Android 15/16 devices
5
+ with 16 KB memory pages refuse to load 4 KB-aligned ``.so``s; the check scans
6
+ every staged library's LOAD-segment ``p_align`` and flags anything below 16 KB.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import struct
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ PAGE_16K = 0x4000
16
+
17
+ _PT_LOAD = 1
18
+ _DT_NEEDED = 1
19
+ _DT_STRTAB = 5
20
+ _DT_NULL = 0
21
+
22
+
23
+ class ElfError(Exception):
24
+ pass
25
+
26
+
27
+ # e_machine values for the two ABIs the python.org runtime ships.
28
+ EM_X86_64 = 0x3E
29
+ EM_AARCH64 = 0xB7
30
+ _MACHINE_NAMES = {
31
+ EM_X86_64: "x86_64",
32
+ EM_AARCH64: "aarch64",
33
+ 0x28: "armv7 (32-bit)",
34
+ 0x03: "i386 (32-bit)",
35
+ }
36
+
37
+
38
+ def machine_name(e_machine: int) -> str:
39
+ return _MACHINE_NAMES.get(e_machine, f"e_machine 0x{e_machine:x}")
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ElfInfo:
44
+ is_64: bool
45
+ max_load_align: int
46
+ needed: tuple[str, ...]
47
+ machine: int = 0
48
+
49
+ @property
50
+ def is_16k_aligned(self) -> bool:
51
+ return self.max_load_align >= PAGE_16K
52
+
53
+
54
+ def read_elf(path: Path) -> ElfInfo:
55
+ """Parse an ELF's LOAD alignments + DT_NEEDED; raise ``ElfError`` if not ELF."""
56
+ data = path.read_bytes()
57
+ if len(data) < 64 or data[:4] != b"\x7fELF":
58
+ raise ElfError(f"{path} is not an ELF file")
59
+ ei_class = data[4]
60
+ little = data[5] == 1
61
+ is_64 = ei_class == 2
62
+ end = "<" if little else ">"
63
+ e_machine = struct.unpack_from(end + "H", data, 0x12)[0]
64
+
65
+ if is_64:
66
+ e_phoff = struct.unpack_from(end + "Q", data, 0x20)[0]
67
+ e_phentsize = struct.unpack_from(end + "H", data, 0x36)[0]
68
+ e_phnum = struct.unpack_from(end + "H", data, 0x38)[0]
69
+ else:
70
+ e_phoff = struct.unpack_from(end + "I", data, 0x1C)[0]
71
+ e_phentsize = struct.unpack_from(end + "H", data, 0x2A)[0]
72
+ e_phnum = struct.unpack_from(end + "H", data, 0x2C)[0]
73
+
74
+ max_align = 0
75
+ dynamic_off = dynamic_size = 0
76
+ load_segments = []
77
+ for i in range(e_phnum):
78
+ off = e_phoff + i * e_phentsize
79
+ p_type = struct.unpack_from(end + "I", data, off)[0]
80
+ if is_64:
81
+ p_offset = struct.unpack_from(end + "Q", data, off + 8)[0]
82
+ p_filesz = struct.unpack_from(end + "Q", data, off + 0x20)[0]
83
+ p_align = struct.unpack_from(end + "Q", data, off + 0x30)[0]
84
+ p_vaddr = struct.unpack_from(end + "Q", data, off + 0x10)[0]
85
+ else:
86
+ p_offset = struct.unpack_from(end + "I", data, off + 4)[0]
87
+ p_filesz = struct.unpack_from(end + "I", data, off + 0x10)[0]
88
+ p_align = struct.unpack_from(end + "I", data, off + 0x1C)[0]
89
+ p_vaddr = struct.unpack_from(end + "I", data, off + 8)[0]
90
+ if p_type == _PT_LOAD:
91
+ max_align = max(max_align, p_align)
92
+ load_segments.append((p_offset, p_vaddr, p_filesz))
93
+ elif p_type == 2: # PT_DYNAMIC
94
+ dynamic_off, dynamic_size = p_offset, p_filesz
95
+
96
+ needed = _read_needed(data, end, is_64, dynamic_off, dynamic_size, load_segments)
97
+ return ElfInfo(
98
+ is_64=is_64, max_load_align=max_align, needed=needed, machine=e_machine
99
+ )
100
+
101
+
102
+ def _read_needed(data, end, is_64, dyn_off, dyn_size, load_segments) -> tuple[str, ...]:
103
+ if not dyn_off:
104
+ return ()
105
+ entry = 16 if is_64 else 8
106
+ fmt = end + ("qQ" if is_64 else "iI")
107
+ strtab_vaddr = 0
108
+ needed_offsets: list[int] = []
109
+ for i in range(dyn_size // entry):
110
+ tag, val = struct.unpack_from(fmt, data, dyn_off + i * entry)
111
+ if tag == _DT_NULL:
112
+ break
113
+ if tag == _DT_STRTAB:
114
+ strtab_vaddr = val
115
+ elif tag == _DT_NEEDED:
116
+ needed_offsets.append(val)
117
+ if not strtab_vaddr:
118
+ return ()
119
+ strtab_file = _vaddr_to_offset(strtab_vaddr, load_segments)
120
+ if strtab_file is None:
121
+ return ()
122
+ names: list[str] = []
123
+ for str_off in needed_offsets:
124
+ start = strtab_file + str_off
125
+ end_idx = data.find(b"\x00", start)
126
+ if end_idx > start:
127
+ names.append(data[start:end_idx].decode("utf-8", "replace"))
128
+ return tuple(names)
129
+
130
+
131
+ def _vaddr_to_offset(vaddr, load_segments) -> int | None:
132
+ for p_offset, p_vaddr, p_filesz in load_segments:
133
+ if p_vaddr <= vaddr < p_vaddr + p_filesz:
134
+ return p_offset + (vaddr - p_vaddr)
135
+ return None
136
+
137
+
138
+ def scan_alignment(jnilibs_dir: Path) -> list[tuple[Path, int]]:
139
+ """Return ``(path, align)`` for every ``.so`` below 16 KB LOAD alignment."""
140
+ bad: list[tuple[Path, int]] = []
141
+ for so in sorted(jnilibs_dir.rglob("*.so")):
142
+ try:
143
+ info = read_elf(so)
144
+ except (ElfError, OSError, struct.error):
145
+ continue
146
+ if not info.is_16k_aligned:
147
+ bad.append((so, info.max_load_align))
148
+ return bad
@@ -0,0 +1 @@
1
+ """Gradle-project generation: manifest, build.gradle files, resources, pins."""
@@ -0,0 +1,261 @@
1
+ """Generate ``AndroidManifest.xml`` (android/04 §manifest generation).
2
+
3
+ Deterministic emission from ``AndroidConfig``: permissions (bare names
4
+ auto-prefixed; nothing force-added beyond a foreground service's
5
+ ``FOREGROUND_SERVICE*`` pair), implied non-required ``<uses-feature>``
6
+ synthesis (android/01 §auto_features), services/activities/intent filters,
7
+ attribute passthrough, and substituted raw-XML fragments (well-formedness was
8
+ already enforced at config time; fragments are re-parsed here as a belt).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from xml.sax.saxutils import escape, quoteattr
14
+
15
+ from kivyforge.config.model import AndroidConfig, AndroidService
16
+
17
+ ANDROID_NS = "http://schemas.android.com/apk/res/android"
18
+ MAIN_ACTIVITY = "org.kivy.android.PythonActivity"
19
+ GENERATED_THEME = "@style/Theme.Kivyforge"
20
+
21
+ # Google's permission -> implied-hardware-feature table (android/01
22
+ # §implied features), including the two documented deliberate extras
23
+ # (USE_BIOMETRIC mirroring USE_FINGERPRINT; the API-31 Bluetooth pair).
24
+ IMPLIED_FEATURES: dict[str, tuple[str, ...]] = {
25
+ "android.permission.CAMERA": (
26
+ "android.hardware.camera",
27
+ "android.hardware.camera.autofocus",
28
+ ),
29
+ "android.permission.RECORD_AUDIO": ("android.hardware.microphone",),
30
+ "android.permission.ACCESS_FINE_LOCATION": (
31
+ "android.hardware.location",
32
+ "android.hardware.location.gps",
33
+ ),
34
+ "android.permission.ACCESS_COARSE_LOCATION": (
35
+ "android.hardware.location",
36
+ "android.hardware.location.network",
37
+ ),
38
+ "android.permission.BLUETOOTH": ("android.hardware.bluetooth",),
39
+ "android.permission.BLUETOOTH_CONNECT": ("android.hardware.bluetooth",),
40
+ "android.permission.BLUETOOTH_SCAN": (
41
+ "android.hardware.bluetooth",
42
+ "android.hardware.bluetooth_le",
43
+ ),
44
+ "android.permission.NFC": ("android.hardware.nfc",),
45
+ "android.permission.USE_BIOMETRIC": ("android.hardware.fingerprint",),
46
+ "android.permission.USE_FINGERPRINT": ("android.hardware.fingerprint",),
47
+ }
48
+
49
+ # [tool.kivy].orientation -> android:screenOrientation. A single value maps
50
+ # directly; the portrait pair / landscape pair map to their sensor variants;
51
+ # any wider mix means "let the sensor decide".
52
+ _ORIENTATION_MAP = {
53
+ ("portrait",): "portrait",
54
+ ("portrait-upside-down",): "reversePortrait",
55
+ ("landscape-left",): "reverseLandscape",
56
+ ("landscape-right",): "landscape",
57
+ }
58
+
59
+
60
+ class ManifestError(Exception):
61
+ pass
62
+
63
+
64
+ def qualified_permissions(uses: tuple[str, ...]) -> list[str]:
65
+ """Bare names get the ``android.permission.`` prefix; dotted pass verbatim."""
66
+ return [p if "." in p else f"android.permission.{p}" for p in uses]
67
+
68
+
69
+ def implied_features(permissions: list[str]) -> list[str]:
70
+ out: list[str] = []
71
+ for permission in permissions:
72
+ for feature in IMPLIED_FEATURES.get(permission, ()):
73
+ if feature not in out:
74
+ out.append(feature)
75
+ return sorted(out)
76
+
77
+
78
+ def screen_orientation(orientation: tuple[str, ...]) -> str:
79
+ key = tuple(sorted(orientation))
80
+ if len(orientation) == 1:
81
+ return _ORIENTATION_MAP[tuple(orientation)]
82
+ if key == ("landscape-left", "landscape-right"):
83
+ return "sensorLandscape"
84
+ if key == ("portrait", "portrait-upside-down"):
85
+ return "sensorPortrait"
86
+ return "fullSensor"
87
+
88
+
89
+ def service_class_name(service: AndroidService) -> str:
90
+ """The generated PythonService subclass FQCN (namespace preserved)."""
91
+ return f"org.kivy.android.Service{service.name}"
92
+
93
+
94
+ def generate_manifest(android: AndroidConfig, *, orientation: tuple[str, ...]) -> str:
95
+ permissions = qualified_permissions(android.permissions.uses)
96
+ # A declared foreground service auto-adds FOREGROUND_SERVICE + the typed
97
+ # permission — the ONLY implicit permissions (android/01 §managed keys).
98
+ for service in android.services:
99
+ if service.foreground:
100
+ _add_unique(permissions, "android.permission.FOREGROUND_SERVICE")
101
+ fstype = service.foreground_service_type or ""
102
+ typed = "android.permission.FOREGROUND_SERVICE_" + _snake_upper(fstype)
103
+ _add_unique(permissions, typed)
104
+
105
+ lines: list[str] = []
106
+ lines.append('<?xml version="1.0" encoding="utf-8"?>')
107
+ lines.append(f'<manifest xmlns:android="{ANDROID_NS}">')
108
+
109
+ for permission in permissions:
110
+ lines.append(f" <uses-permission android:name={quoteattr(permission)} />")
111
+
112
+ # Explicit features override the synthesized entry for the same name.
113
+ explicit = {f.name: f.required for f in android.permissions.features}
114
+ synthesized = (
115
+ [f for f in implied_features(permissions) if f not in explicit]
116
+ if android.permissions.auto_features
117
+ else []
118
+ )
119
+ for name in synthesized:
120
+ lines.append(
121
+ f" <uses-feature android:name={quoteattr(name)} "
122
+ 'android:required="false" />'
123
+ )
124
+ for feature in android.permissions.features:
125
+ required = "true" if feature.required else "false"
126
+ lines.append(
127
+ f" <uses-feature android:name={quoteattr(feature.name)} "
128
+ f'android:required="{required}" />'
129
+ )
130
+
131
+ if android.manifest.extra_manifest_xml.strip():
132
+ lines.append(_substitute(android.manifest.extra_manifest_xml, android))
133
+
134
+ app_attrs: dict[str, str] = {
135
+ "android:label": "@string/app_name",
136
+ "android:icon": "@mipmap/ic_launcher",
137
+ "android:theme": GENERATED_THEME,
138
+ # roundIcon only exists in the generated resources when an icon source
139
+ # is configured; naming it otherwise would fail AAPT.
140
+ **(
141
+ {"android:roundIcon": "@mipmap/ic_launcher_round"}
142
+ if android.icons.source
143
+ else {}
144
+ ),
145
+ # extractNativeLibs is controlled via app/build.gradle's
146
+ # packaging.jniLibs.useLegacyPackaging (AGP warns if set in the
147
+ # manifest); the gradle setting is the toolchain-managed source.
148
+ }
149
+ for key, value in sorted(android.manifest.application.items()):
150
+ app_attrs[key] = _attr_str(value)
151
+ lines.append(f" <application {_attrs(app_attrs)}>")
152
+
153
+ activity_attrs: dict[str, str] = {
154
+ "android:name": MAIN_ACTIVITY,
155
+ "android:exported": "true",
156
+ "android:configChanges": "keyboardHidden|orientation|screenSize",
157
+ "android:screenOrientation": screen_orientation(orientation),
158
+ "android:theme": GENERATED_THEME,
159
+ }
160
+ for key, value in sorted(android.manifest.activity.items()):
161
+ activity_attrs[key] = _attr_str(value)
162
+ lines.append(f" <activity {_attrs(activity_attrs)}>")
163
+ lines.append(" <intent-filter>")
164
+ lines.append(' <action android:name="android.intent.action.MAIN" />')
165
+ lines.append(
166
+ ' <category android:name="android.intent.category.LAUNCHER" />'
167
+ )
168
+ lines.append(" </intent-filter>")
169
+
170
+ for entry in android.intent_filters:
171
+ lines.append(" <intent-filter>")
172
+ lines.append(
173
+ f" <action android:name={quoteattr(entry.action)} />"
174
+ )
175
+ for category in entry.categories:
176
+ lines.append(
177
+ f" <category android:name={quoteattr(category)} />"
178
+ )
179
+ for data in entry.data:
180
+ attrs = " ".join(
181
+ f"android:{k}={quoteattr(v)}" for k, v in sorted(data.items())
182
+ )
183
+ lines.append(f" <data {attrs} />")
184
+ lines.append(" </intent-filter>")
185
+
186
+ if android.manifest.extra_activity_xml.strip():
187
+ lines.append(_substitute(android.manifest.extra_activity_xml, android))
188
+ lines.append(" </activity>")
189
+
190
+ for extra in android.activities:
191
+ lines.append(
192
+ f" <activity android:name={quoteattr(extra.name)} "
193
+ f'android:exported="{_b(extra.exported)}" />'
194
+ )
195
+
196
+ for service in android.services:
197
+ attrs = {
198
+ "android:name": service_class_name(service),
199
+ "android:process": f":service_{service.name.lower()}",
200
+ "android:exported": _b(service.exported),
201
+ }
202
+ if service.foreground:
203
+ attrs["android:foregroundServiceType"] = (
204
+ service.foreground_service_type or ""
205
+ )
206
+ lines.append(f" <service {_attrs(attrs)} />")
207
+
208
+ if android.manifest.extra_application_xml.strip():
209
+ lines.append(_substitute(android.manifest.extra_application_xml, android))
210
+
211
+ lines.append(" </application>")
212
+ lines.append("</manifest>")
213
+ text = "\n".join(lines) + "\n"
214
+ _assert_well_formed(text)
215
+ return text
216
+
217
+
218
+ def _substitute(fragment: str, android: AndroidConfig) -> str:
219
+ out = fragment.replace("${applicationId}", android.package)
220
+ for key, value in android.manifest.placeholders.items():
221
+ out = out.replace("${" + key + "}", value)
222
+ return "\n".join(f" {line}" for line in out.strip().splitlines())
223
+
224
+
225
+ def _assert_well_formed(text: str) -> None:
226
+ import xml.etree.ElementTree as ET
227
+
228
+ try:
229
+ ET.fromstring(text)
230
+ except ET.ParseError as exc: # pragma: no cover - config validation upstream
231
+ raise ManifestError(
232
+ f"generated AndroidManifest.xml is not well-formed: {exc}"
233
+ ) from exc
234
+
235
+
236
+ def _attrs(attrs: dict[str, str]) -> str:
237
+ return " ".join(f"{k}={quoteattr(v)}" for k, v in attrs.items())
238
+
239
+
240
+ def _attr_str(value: object) -> str:
241
+ if isinstance(value, bool):
242
+ return "true" if value else "false"
243
+ return escape(str(value))
244
+
245
+
246
+ def _b(value: bool) -> str:
247
+ return "true" if value else "false"
248
+
249
+
250
+ def _snake_upper(camel: str) -> str:
251
+ out: list[str] = []
252
+ for ch in camel:
253
+ if ch.isupper():
254
+ out.append("_")
255
+ out.append(ch.upper())
256
+ return "".join(out)
257
+
258
+
259
+ def _add_unique(items: list[str], value: str) -> None:
260
+ if value not in items:
261
+ items.append(value)