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,438 @@
1
+ """Linux-specific doctor checks (linux-spec).
2
+
3
+ Reuses the platform-neutral checks (kivyforge version, app source dir) from
4
+ ``checks`` and adds the Linux environment (host libc, GL libraries, display
5
+ session) and project checks (glibc floor, arch coverage, icon, generated
6
+ ``.desktop`` validity, find_links, reachable hosts) over the
7
+ ``pylock.linux.toml`` model. The reachable-hosts check also covers the pinned
8
+ appimagetool / type2-runtime asset hosts, since packaging fetches them.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import tempfile
14
+ from pathlib import Path
15
+ from urllib.parse import urlparse
16
+
17
+ from kivyforge.config import ConfigError, load_config
18
+ from kivyforge.config.icons import APP_ICON_SIZE, icon_source_problem
19
+ from kivyforge.config.model import Config
20
+ from kivyforge.doctor import checks_common as C
21
+ from kivyforge.doctor.checks_common import _ver_tuple
22
+ from kivyforge.doctor.probe import Probe, RealProbe
23
+ from kivyforge.doctor.result import CheckResult, Status
24
+ from kivyforge.lock.find_links import find_links_doctor_detail
25
+ from kivyforge.lock.reader import LockError
26
+
27
+ from . import AppDirError
28
+ from .appimage import appimagetool_asset, type2_runtime_asset
29
+ from .desktop import render_desktop_entry
30
+ from .elftools import ARCH_ELF, describe, elf_machine, is_elf
31
+ from .lock import (
32
+ LinuxLockfile,
33
+ effective_glibc_floor,
34
+ linux_wheel_coverage,
35
+ load,
36
+ )
37
+ from .lock.runtime import DEFAULT_GLIBC_FLOOR
38
+
39
+ _GL_LIBS = ("libGL.so.1", "libEGL.so.1")
40
+
41
+
42
+ # ---- environment checks -------------------------------------------------
43
+
44
+
45
+ def check_linux_host(probe: Probe) -> CheckResult:
46
+ host = probe.host_system()
47
+ if host != "Linux":
48
+ return CheckResult(
49
+ "Host is Linux",
50
+ Status.FAIL,
51
+ f"host is {host!r}",
52
+ hint="the AppImage/AppDir backend bundles a gnu/glibc "
53
+ "python-build-standalone runtime and requires a Linux host.",
54
+ )
55
+ if probe.linux_libc() == "musl":
56
+ return CheckResult(
57
+ "Host is Linux",
58
+ Status.WARN,
59
+ "musl libc host",
60
+ hint="the bundled python-build-standalone runtime is a gnu/glibc "
61
+ "build and will not run on musl (Alpine); build on a glibc host.",
62
+ )
63
+ return CheckResult("Host is Linux", Status.PASS, "Linux (glibc)")
64
+
65
+
66
+ def check_gl_libraries(probe: Probe) -> CheckResult:
67
+ libs = probe.shared_libraries()
68
+ missing = [lib for lib in _GL_LIBS if lib not in libs]
69
+ if missing:
70
+ return CheckResult(
71
+ "GL libraries",
72
+ Status.WARN,
73
+ f"{', '.join(missing)} not found by ldconfig",
74
+ hint="libGL/libEGL come from the host, never the bundle. Install "
75
+ "your distro's OpenGL runtime (Debian/Ubuntu: libgl1 libegl1; "
76
+ "Fedora: mesa-libGL mesa-libEGL).",
77
+ )
78
+ return CheckResult("GL libraries", Status.PASS, ", ".join(_GL_LIBS))
79
+
80
+
81
+ def check_display_session(probe: Probe) -> CheckResult:
82
+ sessions = probe.display_session()
83
+ if not sessions:
84
+ return CheckResult(
85
+ "Display session",
86
+ Status.WARN,
87
+ "no DISPLAY or WAYLAND_DISPLAY",
88
+ hint="the app needs an X11 or Wayland session to open a window; "
89
+ "for headless CI run it under `xvfb-run` with software GL.",
90
+ )
91
+ return CheckResult("Display session", Status.PASS, ", ".join(sorted(sessions)))
92
+
93
+
94
+ # ---- project checks -----------------------------------------------------
95
+
96
+
97
+ def check_linux_glibc_floor(config: Config, lock: LinuxLockfile | None) -> CheckResult:
98
+ declared = config.linux_required.glibc_floor
99
+ runtime_floor = (lock.python_runtime.floor if lock else None) or DEFAULT_GLIBC_FLOOR
100
+ if declared and _ver_tuple(declared) < _ver_tuple(runtime_floor):
101
+ return CheckResult(
102
+ "glibc floor",
103
+ Status.FAIL,
104
+ f"glibc_floor {declared} < runtime floor {runtime_floor}",
105
+ hint=f"raise [tool.kivy.linux].glibc_floor to >= {runtime_floor}.",
106
+ )
107
+ if lock is None:
108
+ return CheckResult("glibc floor", Status.SKIP, "no pylock.linux.toml")
109
+ effective = effective_glibc_floor(lock)
110
+ return CheckResult(
111
+ "glibc floor", Status.PASS, f"effective host floor glibc {effective}"
112
+ )
113
+
114
+
115
+ def check_linux_arch_coverage(
116
+ config: Config, lock: LinuxLockfile | None
117
+ ) -> CheckResult:
118
+ archs = config.linux_required.archs
119
+ if lock is None:
120
+ return CheckResult("Architecture coverage", Status.SKIP, "no pylock.linux.toml")
121
+
122
+ runtime_archs = {a.arch for a in lock.python_runtime.artifacts}
123
+ missing_runtime = [a for a in archs if a not in runtime_archs]
124
+ if missing_runtime:
125
+ return CheckResult(
126
+ "Architecture coverage",
127
+ Status.FAIL,
128
+ f"runtime missing arch(es): {', '.join(missing_runtime)}",
129
+ hint="re-run `kivyforge lock -p linux` after setting archs.",
130
+ )
131
+
132
+ plain_linux_warnings: list[str] = []
133
+ for pkg in lock.packages:
134
+ if any(w.is_pure_python for w in pkg.wheels):
135
+ continue
136
+ # Source-aware coverage: a plain linux_* wheel counts only when vendored
137
+ # via find_links, and yields the mandated no-glibc-promise warning.
138
+ covered: set[str] = set()
139
+ for wheel in pkg.wheels:
140
+ wheel_archs, warning = linux_wheel_coverage(wheel, archs)
141
+ covered.update(wheel_archs)
142
+ if warning:
143
+ plain_linux_warnings.append(f"{pkg.name}: {warning}")
144
+ missing = [a for a in archs if a not in covered]
145
+ if missing:
146
+ return CheckResult(
147
+ "Architecture coverage",
148
+ Status.FAIL,
149
+ f"{pkg.name} missing manylinux wheel(s) for: {', '.join(missing)}",
150
+ hint="a compiled dep needs a manylinux wheel per arch; raise "
151
+ "[tool.kivy.linux].glibc_floor or supply it via "
152
+ "extra_index_urls/find_links, then re-lock.",
153
+ )
154
+ if plain_linux_warnings:
155
+ return CheckResult(
156
+ "Architecture coverage",
157
+ Status.WARN,
158
+ f"covers {', '.join(archs)} but relies on vendored plain linux_* wheel(s)",
159
+ hint="; ".join(plain_linux_warnings),
160
+ )
161
+ return CheckResult(
162
+ "Architecture coverage", Status.PASS, f"all deps cover {', '.join(archs)}"
163
+ )
164
+
165
+
166
+ def check_linux_app_icon(config: Config, project_root: Path) -> CheckResult:
167
+ source = config.linux_required.icons.source
168
+ if not source:
169
+ return CheckResult("App icon", Status.SKIP, "not configured")
170
+ problem = icon_source_problem((project_root / source).resolve())
171
+ if problem:
172
+ return CheckResult("App icon", Status.FAIL, f"{source} invalid", hint=problem)
173
+ return CheckResult("App icon", Status.PASS, f"{APP_ICON_SIZE}x{APP_ICON_SIZE} PNG")
174
+
175
+
176
+ def check_linux_desktop_entry(probe: Probe, config: Config) -> CheckResult:
177
+ if not probe.has_desktop_file_validate():
178
+ return CheckResult(
179
+ "Desktop entry valid",
180
+ Status.SKIP,
181
+ "desktop-file-validate not installed",
182
+ )
183
+ app_id = config.linux_required.app_id
184
+ with tempfile.TemporaryDirectory() as tmp:
185
+ path = Path(tmp) / f"{app_id}.desktop"
186
+ path.write_text(render_desktop_entry(config), encoding="utf-8")
187
+ errors = probe.desktop_file_errors(path)
188
+ if errors:
189
+ return CheckResult(
190
+ "Desktop entry valid",
191
+ Status.FAIL,
192
+ "generated .desktop failed validation",
193
+ hint=errors,
194
+ )
195
+ return CheckResult("Desktop entry valid", Status.PASS, f"{app_id}.desktop")
196
+
197
+
198
+ def check_linux_find_links(config: Config, project_root: Path) -> CheckResult:
199
+ entries = config.linux_required.find_links
200
+ if not entries:
201
+ return CheckResult("find_links directories", Status.SKIP, "not configured")
202
+ root = project_root.resolve()
203
+ problems: list[tuple[str, str | None]] = []
204
+ warnings: list[tuple[str, str | None]] = []
205
+ ok: list[str] = []
206
+ for entry in entries:
207
+ path = (root / entry).resolve()
208
+ detail, hint = find_links_doctor_detail(root, entry, path, platform="linux")
209
+ if not path.is_dir():
210
+ problems.append((detail, hint))
211
+ elif not any(path.glob("*.whl")):
212
+ warnings.append((detail, hint))
213
+ else:
214
+ ok.append(detail)
215
+ if problems:
216
+ return CheckResult(
217
+ "find_links directories",
218
+ Status.FAIL,
219
+ "; ".join(d for d, _ in problems),
220
+ hint=next((h for _, h in problems if h), ""),
221
+ )
222
+ if warnings:
223
+ return CheckResult(
224
+ "find_links directories",
225
+ Status.WARN,
226
+ "; ".join(d for d, _ in warnings),
227
+ hint=next((h for _, h in warnings if h), ""),
228
+ )
229
+ return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
230
+
231
+
232
+ def check_linux_native_binaries(config: Config, project_root: Path) -> CheckResult:
233
+ """Vendored native-binary sources exist and (once built) match the arch.
234
+
235
+ SKIPs when no ``[tool.kivy.linux.native.binaries]`` are declared. Vendored
236
+ (repo-relative) sources must be present on disk; URL sources are re-checked
237
+ for reachability by ``check_linux_hosts_reachable``. No two single-file
238
+ (non-archive) sources may share a staged basename (they would overwrite each
239
+ other in ``usr/bin``; archive-member collisions are caught at build time).
240
+ When the AppDir has been built, every ELF staged under ``usr/bin`` must match
241
+ each configured arch's class + machine (a 32-bit / aarch64 ``.so`` in an
242
+ x86_64 AppImage would only fail at ``dlopen`` time).
243
+ """
244
+ declared = config.linux_required.binaries
245
+ if not declared:
246
+ return CheckResult("Native binaries", Status.SKIP, "not configured")
247
+
248
+ root = project_root.resolve()
249
+ missing = [
250
+ dep.source
251
+ for dep in declared
252
+ if not dep.source.startswith(("http://", "https://"))
253
+ and not (root / dep.source).is_file()
254
+ ]
255
+ if missing:
256
+ return CheckResult(
257
+ "Native binaries",
258
+ Status.FAIL,
259
+ f"vendored source(s) not found: {', '.join(missing)}",
260
+ hint="build/vendor the artifact under the project and re-run "
261
+ "`kivyforge lock -p linux`.",
262
+ )
263
+
264
+ # Statically-detectable staging collisions: two single-file (non-archive)
265
+ # sources whose basenames match land on the same usr/bin/<name> and the
266
+ # second silently clobbers the first. Archive-member collisions need
267
+ # extraction and are caught (loudly) at build time.
268
+ staged: dict[str, str] = {}
269
+ for dep in declared:
270
+ if dep.source.lower().endswith((".zip", ".tar.gz", ".tgz")):
271
+ continue
272
+ base = dep.source.rsplit("/", 1)[-1] or dep.name
273
+ prev = staged.get(base)
274
+ if prev is not None:
275
+ return CheckResult(
276
+ "Native binaries",
277
+ Status.FAIL,
278
+ f"{prev!r} and {dep.name!r} both stage usr/bin/{base}",
279
+ hint="two native binaries would overwrite each other in "
280
+ "usr/bin; rename one artifact/source so each stages to a "
281
+ "unique name.",
282
+ )
283
+ staged[base] = dep.name
284
+
285
+ archs = config.linux_required.archs
286
+ expected = {ARCH_ELF[a] for a in archs if a in ARCH_ELF}
287
+ bin_dir = root / "build" / "linux" / f"{config.display_name}.AppDir" / "usr" / "bin"
288
+ if expected and bin_dir.is_dir():
289
+ problems: list[str] = []
290
+ elves = 0
291
+ for p in sorted(bin_dir.rglob("*")):
292
+ if not is_elf(p):
293
+ continue
294
+ elves += 1
295
+ found = elf_machine(p)
296
+ if found not in expected:
297
+ want = ", ".join(sorted(describe(c, m) for c, m in expected))
298
+ problems.append(f"{p.name} is {describe(*found)}, need {want}")
299
+ if problems:
300
+ return CheckResult(
301
+ "Native binaries",
302
+ Status.FAIL,
303
+ "; ".join(problems),
304
+ hint="rebuild the native binaries for the configured arch "
305
+ f"({', '.join(archs)}), then re-lock.",
306
+ )
307
+ return CheckResult(
308
+ "Native binaries",
309
+ Status.PASS,
310
+ f"{elves} ELF match {', '.join(archs)}",
311
+ )
312
+ return CheckResult(
313
+ "Native binaries",
314
+ Status.PASS,
315
+ f"{len(declared)} declared; sources present (build to verify arch match)",
316
+ )
317
+
318
+
319
+ def check_linux_hosts_reachable(
320
+ probe: Probe, config: Config, lock: LinuxLockfile | None
321
+ ) -> CheckResult:
322
+ hosts: set[str] = set()
323
+ if lock is not None:
324
+ for pkg in lock.packages:
325
+ for wheel in pkg.wheels:
326
+ if wheel.url:
327
+ _add_host(hosts, wheel.url)
328
+ for art in lock.python_runtime.artifacts:
329
+ if art.url:
330
+ _add_host(hosts, art.url)
331
+ for nb in lock.native_binaries:
332
+ if nb.url:
333
+ _add_host(hosts, nb.url)
334
+ # Packaging fetches the pinned build tools too, so their hosts matter.
335
+ for arch in config.linux_required.archs:
336
+ try:
337
+ _add_host(hosts, appimagetool_asset(arch)[0])
338
+ _add_host(hosts, type2_runtime_asset(arch)[0])
339
+ except AppDirError:
340
+ continue
341
+ if not hosts:
342
+ return CheckResult(
343
+ "Required hosts reachable", Status.PASS, "all artifacts vendored"
344
+ )
345
+ unreachable = [h for h in sorted(hosts) if not probe.tcp_reachable(h, 443)]
346
+ if unreachable:
347
+ return CheckResult(
348
+ "Required hosts reachable",
349
+ Status.FAIL,
350
+ f"unreachable: {', '.join(unreachable)}",
351
+ hint="check your network/proxy; these hosts serve pinned wheels, the "
352
+ "runtime, and the appimagetool/type2-runtime build tools.",
353
+ )
354
+ return CheckResult(
355
+ "Required hosts reachable", Status.PASS, ", ".join(sorted(hosts))
356
+ )
357
+
358
+
359
+ def _add_host(hosts: set[str], url: str) -> None:
360
+ netloc = urlparse(url).hostname
361
+ if netloc:
362
+ hosts.add(netloc)
363
+
364
+
365
+ def run_linux_checks(
366
+ probe: Probe,
367
+ *,
368
+ kivyforge_version: str,
369
+ config: Config | None = None,
370
+ project_root: Path | None = None,
371
+ lock: LinuxLockfile | None = None,
372
+ offline: bool = False,
373
+ ) -> list[CheckResult]:
374
+ """Run Linux doctor checks (linux-spec). Project checks SKIP without config."""
375
+ project_root = project_root or Path.cwd()
376
+ results = [
377
+ check_linux_host(probe),
378
+ check_gl_libraries(probe),
379
+ check_display_session(probe),
380
+ C.check_kivyforge_version(probe, kivyforge_version, offline=offline),
381
+ ]
382
+
383
+ if config is None:
384
+ for name in (
385
+ "App source directory",
386
+ "glibc floor",
387
+ "Architecture coverage",
388
+ "App icon",
389
+ "Desktop entry valid",
390
+ "find_links directories",
391
+ "Native binaries",
392
+ "Required hosts reachable",
393
+ ):
394
+ results.append(CheckResult(name, Status.SKIP, C.SKIP_NOTE))
395
+ return results
396
+
397
+ results += [
398
+ C.check_app_dir(config, project_root),
399
+ check_linux_glibc_floor(config, lock),
400
+ check_linux_arch_coverage(config, lock),
401
+ check_linux_app_icon(config, project_root),
402
+ check_linux_desktop_entry(probe, config),
403
+ check_linux_find_links(config, project_root),
404
+ check_linux_native_binaries(config, project_root),
405
+ check_linux_hosts_reachable(probe, config, lock),
406
+ ]
407
+ return results
408
+
409
+
410
+ def linux_doctor(
411
+ cwd: Path, *, kivyforge_version: str, offline: bool
412
+ ) -> list[CheckResult]:
413
+ """Load the Linux project + lock (if present) and run the Linux check set."""
414
+ pyproject = cwd / "pyproject.toml"
415
+ config = None
416
+ lock = None
417
+ parse_results: list[CheckResult] = []
418
+ if pyproject.is_file():
419
+ try:
420
+ config = load_config(pyproject, require_ios=False, require_linux=True)
421
+ except ConfigError as exc:
422
+ parse_results.append(
423
+ CheckResult("pyproject.toml", Status.FAIL, exc.format())
424
+ )
425
+ lockfile = cwd / "pylock.linux.toml"
426
+ if lockfile.is_file():
427
+ try:
428
+ lock = load(lockfile)
429
+ except LockError as exc:
430
+ parse_results.append(C.lock_parse_fail("linux", exc))
431
+ return parse_results + run_linux_checks(
432
+ RealProbe(),
433
+ kivyforge_version=kivyforge_version,
434
+ config=config,
435
+ project_root=cwd,
436
+ lock=lock,
437
+ offline=offline,
438
+ )
@@ -0,0 +1,95 @@
1
+ """A tiny, hermetic ELF header reader (the ELF analog of ``machotools``).
2
+
3
+ ``doctor`` uses this to confirm a staged native binary matches the AppImage's
4
+ target arch: a 32-bit or aarch64 ``.so`` in an x86_64 AppImage only fails at
5
+ ``dlopen`` time with a cryptic message, so it is caught here from the header.
6
+
7
+ Pure-Python, no ``readelf`` dependency, so the check works on any host and in
8
+ unit tests. The endianness matters: ``e_machine`` is a 16-bit field whose byte
9
+ order is given by ``EI_DATA`` (``e_ident[5]``), so hard-coding little-endian
10
+ would misread a big-endian (mis-supplied) artifact — exactly the case this
11
+ check exists to catch.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import struct
17
+ from pathlib import Path
18
+
19
+ ELF_MAGIC = b"\x7fELF"
20
+
21
+ # EI_CLASS (e_ident[4]).
22
+ ELFCLASS32 = 1
23
+ ELFCLASS64 = 2
24
+
25
+ # EI_DATA (e_ident[5]).
26
+ ELFDATA2LSB = 1
27
+ ELFDATA2MSB = 2
28
+
29
+ # e_machine values kivyforge maps its arches to.
30
+ EM_386 = 3
31
+ EM_X86_64 = 62
32
+ EM_AARCH64 = 183
33
+
34
+ # kivyforge arch -> (EI_CLASS, e_machine). aarch64 is deferred at the backend
35
+ # level but listed so the arch check is purely additive when it lands.
36
+ ARCH_ELF: dict[str, tuple[int, int]] = {
37
+ "x86_64": (ELFCLASS64, EM_X86_64),
38
+ "aarch64": (ELFCLASS64, EM_AARCH64),
39
+ }
40
+
41
+ _CLASS_NAMES = {ELFCLASS32: "ELF32", ELFCLASS64: "ELF64"}
42
+ _MACHINE_NAMES = {EM_386: "i386", EM_X86_64: "x86_64", EM_AARCH64: "aarch64"}
43
+
44
+
45
+ class ElfError(Exception):
46
+ """The file is not a readable ELF header."""
47
+
48
+
49
+ def is_elf(path: Path) -> bool:
50
+ """True if *path* is a regular (non-symlink) file starting with ``\\x7fELF``.
51
+
52
+ Mirrors ``is_macho``: a non-ELF helper (e.g. a ``#!/bin/sh`` script) is
53
+ skipped by the arch check rather than failed for "not being x86_64".
54
+ """
55
+ if not path.is_file() or path.is_symlink():
56
+ return False
57
+ try:
58
+ with path.open("rb") as fh:
59
+ head = fh.read(4)
60
+ except OSError:
61
+ return False
62
+ return head == ELF_MAGIC
63
+
64
+
65
+ def elf_machine(path: Path) -> tuple[int, int]:
66
+ """Return ``(EI_CLASS, e_machine)`` for the ELF at *path*.
67
+
68
+ Decodes the 16-bit ``e_machine`` (offset 18) using the endianness named by
69
+ ``EI_DATA`` (offset 5). Raises :class:`ElfError` if *path* is not a readable
70
+ ELF header.
71
+ """
72
+ try:
73
+ with path.open("rb") as fh:
74
+ header = fh.read(20)
75
+ except OSError as exc:
76
+ raise ElfError(f"cannot read {path}: {exc}") from exc
77
+ if len(header) < 20 or header[:4] != ELF_MAGIC:
78
+ raise ElfError(f"{path} is not an ELF file")
79
+ ei_class = header[4]
80
+ ei_data = header[5]
81
+ if ei_data == ELFDATA2LSB:
82
+ endian = "<"
83
+ elif ei_data == ELFDATA2MSB:
84
+ endian = ">"
85
+ else:
86
+ raise ElfError(f"{path} has an invalid EI_DATA byte {ei_data!r}")
87
+ (e_machine,) = struct.unpack(f"{endian}H", header[18:20])
88
+ return ei_class, e_machine
89
+
90
+
91
+ def describe(elf_class: int, e_machine: int) -> str:
92
+ """A short human label for an ``(EI_CLASS, e_machine)`` pair."""
93
+ cls = _CLASS_NAMES.get(elf_class, f"class{elf_class}")
94
+ mach = _MACHINE_NAMES.get(e_machine, f"machine{e_machine}")
95
+ return f"{mach} ({cls})"
@@ -0,0 +1,129 @@
1
+ """Generate the AppDir icon set (linux-spec).
2
+
3
+ Linux has no guaranteed host icon tool (the macOS ``sips``/``iconutil`` analog),
4
+ so when the project supplies ``[tool.kivy.linux.icons].source`` (a 1024×1024
5
+ PNG, validated like iOS/macOS) kivyforge resizes it with
6
+ [Pillow](https://python-pillow.org/) — the optional ``kivyforge[linux]`` extra —
7
+ into the freedesktop **hicolor** size set plus the root ``<app_id>.png``.
8
+
9
+ When no source is configured, a plain generated default icon is written instead
10
+ (using only the standard library) so ``appimagetool`` — which requires the icon
11
+ its ``.desktop`` ``Icon=`` key names — always has one to embed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import shutil
17
+ import struct
18
+ import zlib
19
+ from pathlib import Path
20
+
21
+ from kivyforge.config.icons import validate_icon_source
22
+ from kivyforge.config.model import Config
23
+
24
+ from . import AppDirError
25
+
26
+ # freedesktop hicolor sizes to emit. The root AppDir icon is the 256px render.
27
+ HICOLOR_SIZES = (16, 32, 48, 64, 128, 256, 512)
28
+ ROOT_ICON_SIZE = 256
29
+
30
+ # The default icon's fill (a neutral slate), used when no source is configured.
31
+ _DEFAULT_RGBA = (45, 52, 64, 255)
32
+
33
+
34
+ def stage_icons(config: Config, project_root: Path, appdir: Path) -> bool:
35
+ """Write the icon set into *appdir*; return ``True`` if a *user* icon was used.
36
+
37
+ With no configured source a generated default icon is written (returns
38
+ ``False``) so the AppDir always carries the root ``<app_id>.png`` that
39
+ ``appimagetool`` requires.
40
+ """
41
+ app_id = config.linux_required.app_id
42
+ source = config.linux_required.icons.source
43
+ if not source:
44
+ _write_default(appdir, app_id)
45
+ return False
46
+
47
+ src = (project_root / source).resolve()
48
+ validate_icon_source(src)
49
+ image = _open(src)
50
+ apps_root = appdir / "usr" / "share" / "icons" / "hicolor"
51
+ for size in HICOLOR_SIZES:
52
+ dest = apps_root / f"{size}x{size}" / "apps" / f"{app_id}.png"
53
+ dest.parent.mkdir(parents=True, exist_ok=True)
54
+ _resize(image, size).save(dest, format="PNG")
55
+
56
+ root_icon = appdir / f"{app_id}.png"
57
+ _resize(image, ROOT_ICON_SIZE).save(root_icon, format="PNG")
58
+ _write_diricon(appdir, root_icon)
59
+ return True
60
+
61
+
62
+ def _write_default(appdir: Path, app_id: str) -> None:
63
+ """Emit a plain solid-colour root + 256px hicolor icon (no Pillow needed)."""
64
+ png = _solid_png(ROOT_ICON_SIZE, _DEFAULT_RGBA)
65
+ root_icon = appdir / f"{app_id}.png"
66
+ root_icon.write_bytes(png)
67
+ hicolor = appdir / "usr" / "share" / "icons" / "hicolor" / "256x256" / "apps"
68
+ hicolor.mkdir(parents=True, exist_ok=True)
69
+ (hicolor / f"{app_id}.png").write_bytes(png)
70
+ _write_diricon(appdir, root_icon)
71
+
72
+
73
+ def _write_diricon(appdir: Path, root_icon: Path) -> None:
74
+ """Write ``.DirIcon`` (a copy of the root icon) at the AppDir root.
75
+
76
+ ``.DirIcon`` is the freedesktop AppDir convention file managers and AppImage
77
+ consumers read to show the directory/app icon. ``appimagetool`` generates it
78
+ from ``Icon=`` at package time, but the ``-f folder`` artifact is consumed
79
+ directly, so kivyforge writes it too for a consistent icon across both. A
80
+ plain copy (not a symlink) keeps the folder artifact self-contained and
81
+ relocatable.
82
+ """
83
+ shutil.copyfile(root_icon, appdir / ".DirIcon")
84
+
85
+
86
+ def _solid_png(size: int, rgba: tuple[int, int, int, int]) -> bytes:
87
+ """A minimal valid 8-bit RGBA PNG filled with a single colour."""
88
+ pixel = bytes(rgba)
89
+ row = b"\x00" + pixel * size # per-scanline filter byte (0) + pixels
90
+ idat = zlib.compress(row * size, 9)
91
+
92
+ def chunk(tag: bytes, data: bytes) -> bytes:
93
+ body = tag + data
94
+ crc = zlib.crc32(body) & 0xFFFFFFFF
95
+ return struct.pack(">I", len(data)) + body + struct.pack(">I", crc)
96
+
97
+ ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
98
+ return (
99
+ b"\x89PNG\r\n\x1a\n"
100
+ + chunk(b"IHDR", ihdr)
101
+ + chunk(b"IDAT", idat)
102
+ + chunk(b"IEND", b"")
103
+ )
104
+
105
+
106
+ def _open(src: Path):
107
+ image_module = _pillow()
108
+ try:
109
+ return image_module.open(src).convert("RGBA")
110
+ except OSError as exc:
111
+ raise AppDirError(f"cannot read app icon {src}: {exc}") from exc
112
+
113
+
114
+ def _resize(image, size: int):
115
+ from PIL import Image
116
+
117
+ return image.resize((size, size), Image.LANCZOS)
118
+
119
+
120
+ def _pillow():
121
+ try:
122
+ from PIL import Image
123
+ except ModuleNotFoundError as exc:
124
+ raise AppDirError(
125
+ "generating Linux icons needs Pillow, which is not installed.\n"
126
+ " Install the Linux extra: pip install 'kivyforge[linux]'\n"
127
+ " (or unset [tool.kivy.linux.icons].source to build without an icon)."
128
+ ) from exc
129
+ return Image