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,235 @@
1
+ """Assemble the Linux AppDir (linux-spec).
2
+
3
+ Orchestrates the stages: pick the assembly arch from the lock (+ ``--arch``),
4
+ stage the runtime + wheels, copy the app sources, render the icons + ``AppRun``
5
+ launcher + ``.desktop`` entry. Produces the layout documented in linux-spec:
6
+
7
+ build/linux/<Name>.AppDir/
8
+ ├── AppRun
9
+ ├── <app_id>.desktop
10
+ ├── <app_id>.png
11
+ └── usr/{app, lib, python, share/icons/hicolor/...}
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import platform
17
+ import shutil
18
+ import tempfile
19
+ from pathlib import Path
20
+
21
+ import click
22
+
23
+ from kivyforge.artifacts.cache import ArtifactCache
24
+ from kivyforge.bundle.pycompile import PycompileError, byte_compile, select_compiler
25
+ from kivyforge.config.model import Config
26
+
27
+ from . import AppDirError
28
+ from .desktop import validate_desktop_file, write_desktop_entry
29
+ from .icons import stage_icons
30
+ from .launcher import build_apprun, entry_point_rel_path
31
+ from .lock import LinuxLockfile
32
+ from .native_stage import stage_native_binaries
33
+ from .runtime_stage import stage_runtime
34
+ from .wheels_stage import stage_wheels
35
+
36
+ _IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc")
37
+
38
+
39
+ def resolve_assembly_arch(locked: tuple[str, ...], arch: str | None) -> str:
40
+ """The single arch to assemble for an ``--arch`` request.
41
+
42
+ Linux ships one arch per AppImage, so this always resolves to exactly one.
43
+ ``None`` -> the lock's arch (there is exactly one this phase). An explicit
44
+ arch not covered by the lock is an error.
45
+ """
46
+ if not locked:
47
+ raise AppDirError("the lock covers no architectures; re-run `kivyforge lock`.")
48
+ if arch is None:
49
+ return locked[0]
50
+ if arch not in locked:
51
+ raise AppDirError(
52
+ f"--arch {arch} is not in the lock (covers {', '.join(locked)}).\n"
53
+ f" Add {arch!r} to [tool.kivy.linux].archs and re-lock, or pick a "
54
+ "locked arch."
55
+ )
56
+ return arch
57
+
58
+
59
+ def _setting_applies(value: bool | str, *, release: bool) -> bool:
60
+ """A ``build_settings`` tri-state resolved for the build being produced."""
61
+ if isinstance(value, bool):
62
+ return value
63
+ return release # DESKTOP_RELEASE_ONLY
64
+
65
+
66
+ def _resolve_byte_compile(
67
+ config: Config,
68
+ *,
69
+ staged_interpreter: Path,
70
+ target_arch: str,
71
+ python_version: str,
72
+ release: bool,
73
+ ) -> tuple[tuple[str, ...] | None, bool]:
74
+ """Decide whether to byte-compile, and with which interpreter.
75
+
76
+ Returns ``(compiler_argv_or_None, strip_source)``; ``None`` means do not
77
+ byte-compile. ``byte_compile = true`` is read as "I insist", so a missing
78
+ interpreter is an error; the default ``"release"`` degrades to shipping
79
+ source with a warning rather than breaking a build the user never
80
+ configured.
81
+ """
82
+ settings = config.linux_required.build_settings
83
+ if not _setting_applies(settings.byte_compile, release=release):
84
+ return None, False
85
+ native = platform.machine().lower() == target_arch
86
+ compiler = select_compiler(
87
+ staged_interpreter=staged_interpreter,
88
+ native=native,
89
+ python_version=python_version,
90
+ )
91
+ if compiler is None:
92
+ message = (
93
+ f"this project ships CPython {python_version}, and no CPython of "
94
+ "that minor could be found to byte-compile with (a .pyc is only "
95
+ "loadable by the exact CPython minor that wrote it)"
96
+ )
97
+ if settings.byte_compile is True:
98
+ raise AppDirError(
99
+ "[tool.kivy.linux.build_settings].byte_compile = true but "
100
+ f"{message}.\n"
101
+ " Install a matching CPython, or set byte_compile = false."
102
+ )
103
+ click.echo(f"[stage] not byte-compiling: {message}.")
104
+ return None, False
105
+ strip = _setting_applies(settings.strip_source, release=release)
106
+ return compiler, strip
107
+
108
+
109
+ def build_appdir(
110
+ config: Config,
111
+ lock: LinuxLockfile,
112
+ project_root: Path,
113
+ *,
114
+ arch: str | None = None,
115
+ staging_dir: Path | None = None,
116
+ no_cache: bool = False,
117
+ cache: ArtifactCache | None = None,
118
+ release: bool = False,
119
+ echo=click.echo,
120
+ ) -> Path:
121
+ """Build the AppDir tree and return its path."""
122
+ target_arch = resolve_assembly_arch(lock.archs, arch)
123
+ cache = cache or ArtifactCache()
124
+ staging_dir = staging_dir or (project_root / "build" / "linux")
125
+ appdir = staging_dir / f"{config.display_name}.AppDir"
126
+
127
+ # Assemble into a temp tree and swap it in only on success, so a failure
128
+ # mid-assembly (e.g. a transient runtime/wheel fetch error) leaves the
129
+ # previous, working AppDir untouched instead of a half-written one.
130
+ staging_dir.mkdir(parents=True, exist_ok=True)
131
+ work = Path(tempfile.mkdtemp(dir=staging_dir, prefix=f".{appdir.name}.tmp-"))
132
+ try:
133
+ (work / "usr").mkdir(parents=True)
134
+
135
+ echo(
136
+ f"Staging CPython {lock.python_runtime.version} runtime ({target_arch}) ..."
137
+ )
138
+ stage_runtime(
139
+ lock.python_runtime,
140
+ target_arch,
141
+ work / "usr" / "python",
142
+ project_root=project_root,
143
+ cache=cache,
144
+ no_cache=no_cache,
145
+ )
146
+
147
+ echo(f"Installing {len(lock.packages)} locked packages ...")
148
+ stage_wheels(
149
+ lock.packages,
150
+ target_arch,
151
+ work / "usr" / "lib",
152
+ project_root=project_root,
153
+ cache=cache,
154
+ no_cache=no_cache,
155
+ )
156
+
157
+ if lock.native_binaries:
158
+ echo(f"Staging {len(lock.native_binaries)} native binaries ...")
159
+ stage_native_binaries(
160
+ lock,
161
+ work / "usr",
162
+ project_root=project_root,
163
+ cache=cache,
164
+ no_cache=no_cache,
165
+ )
166
+
167
+ _copy_app_sources(config, project_root, work / "usr" / "app")
168
+
169
+ compiler, strip_source = _resolve_byte_compile(
170
+ config,
171
+ staged_interpreter=work / "usr" / "python" / "bin" / "python3",
172
+ target_arch=target_arch,
173
+ python_version=lock.python_runtime.version,
174
+ release=release,
175
+ )
176
+ if compiler is not None:
177
+ with_what = " ".join(compiler) if compiler else "this interpreter"
178
+ echo(
179
+ f"[stage] byte-compiling the Python payload with {with_what}"
180
+ + (" (.pyc only)" if strip_source else "")
181
+ )
182
+ try:
183
+ byte_compile(
184
+ [work / "usr" / "app", work / "usr" / "lib"],
185
+ compiler=compiler,
186
+ strip_source=strip_source,
187
+ stripdir=work,
188
+ )
189
+ except PycompileError as exc:
190
+ raise AppDirError(
191
+ f"{exc}\n Fix it, or set "
192
+ "[tool.kivy.linux.build_settings].byte_compile = false."
193
+ ) from exc
194
+
195
+ stage_icons(config, project_root, work)
196
+
197
+ build_apprun(
198
+ work / "AppRun",
199
+ entry_point=config.kivy.entry_point,
200
+ app_id=config.linux_required.app_id,
201
+ has_native_binaries=bool(lock.native_binaries),
202
+ )
203
+ desktop_path = work / f"{config.linux_required.app_id}.desktop"
204
+ write_desktop_entry(config, desktop_path)
205
+ validate_desktop_file(desktop_path)
206
+ except BaseException:
207
+ shutil.rmtree(work, ignore_errors=True)
208
+ raise
209
+
210
+ # Swap the freshly built tree in for the previous one. The rename is atomic
211
+ # within staging_dir; the brief unlink→rename window only exists after a
212
+ # fully successful build.
213
+ if appdir.exists():
214
+ shutil.rmtree(appdir)
215
+ work.replace(appdir)
216
+ return appdir
217
+
218
+
219
+ def _copy_app_sources(config: Config, project_root: Path, dest: Path) -> None:
220
+ src = (project_root / config.kivy.app_dir).resolve()
221
+ if not src.is_dir():
222
+ raise AppDirError(
223
+ f"[tool.kivy].app_dir points to {config.kivy.app_dir!r}, which is not "
224
+ f"a directory under {project_root}."
225
+ )
226
+ # A dotted entry_point (e.g. "pkg.start") maps to a nested source file
227
+ # (src/pkg/start.py), matching the shared app_dir/entry_point layout.
228
+ entry_rel = f"{entry_point_rel_path(config.kivy.entry_point)}.py"
229
+ entry = src / entry_rel
230
+ if not entry.is_file():
231
+ raise AppDirError(
232
+ f"entry point {entry_rel} not found in "
233
+ f"{config.kivy.app_dir}/ (set [tool.kivy].entry_point)."
234
+ )
235
+ shutil.copytree(src, dest, ignore=_IGNORE)
@@ -0,0 +1,232 @@
1
+ """Linux dispatch for the shared ``build`` / ``run`` / ``package`` verbs.
2
+
3
+ Keeps the Linux AppDir flow out of the (iOS/Xcode-centric) verb modules: each
4
+ verb resolves the target platform and, when it is Linux, calls in here. Loads the
5
+ Linux config + ``pylock.linux.toml``, runs the drift check, and drives the
6
+ AppDir bundler.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ import click
16
+
17
+ from kivyforge.cli._common import ToolchainError, lockfile_path_for
18
+ from kivyforge.config import ConfigError, load_config
19
+ from kivyforge.lock.reader import LockError, is_in_sync
20
+
21
+ from .. import HostCapabilityError, get_platform
22
+ from . import AppDirError
23
+ from .appimage import build_appimage
24
+ from .bundle import build_appdir, resolve_assembly_arch
25
+ from .lock import LinuxLockfile
26
+ from .lock import load as load_linux_lock
27
+
28
+
29
+ def linux_build(
30
+ project_root: Path,
31
+ *,
32
+ arch: str | None,
33
+ no_verify_lock: bool,
34
+ no_cache: bool,
35
+ ) -> Path:
36
+ """Assemble the Linux AppDir; return its path."""
37
+ _require_linux_host()
38
+ config, lock = _load_and_verify(project_root, no_verify_lock)
39
+ appdir = _assemble(
40
+ config, lock, project_root, arch=arch, no_cache=no_cache, release=False
41
+ )
42
+ click.echo(f"Built {appdir.relative_to(project_root)}")
43
+ return appdir
44
+
45
+
46
+ def linux_package(
47
+ project_root: Path,
48
+ *,
49
+ fmt: str,
50
+ arch: str | None,
51
+ no_verify_lock: bool,
52
+ no_cache: bool,
53
+ ) -> Path:
54
+ """Produce the distributable artifact: an ``.AppImage`` (default) or the AppDir."""
55
+ _require_linux_host()
56
+ config, lock = _load_and_verify(project_root, no_verify_lock)
57
+ target_arch = _resolve_arch(lock, arch)
58
+ appdir = _assemble(
59
+ config, lock, project_root, arch=target_arch, no_cache=no_cache, release=True
60
+ )
61
+
62
+ if fmt == "folder":
63
+ click.echo(
64
+ f"Packaged {appdir.relative_to(project_root)} (AppDir folder).\n"
65
+ " Run it with ./AppRun, or `kivyforge package -f appimage` for a "
66
+ "single-file distributable."
67
+ )
68
+ return appdir
69
+
70
+ output = (
71
+ project_root
72
+ / "dist"
73
+ / "linux"
74
+ / f"{config.app_slug}-{config.project.version}-{target_arch}.AppImage"
75
+ )
76
+ try:
77
+ result = build_appimage(
78
+ appdir,
79
+ output,
80
+ target_arch,
81
+ project_root=project_root,
82
+ no_cache=no_cache,
83
+ echo=click.echo,
84
+ )
85
+ except AppDirError as exc:
86
+ raise ToolchainError(str(exc)) from exc
87
+
88
+ click.echo(
89
+ f"Packaged {result.relative_to(project_root)}.\n"
90
+ " Distribute the .AppImage directly (chmod +x, then run). The host needs "
91
+ "glibc ≥ the effective floor, libGL/libEGL, and an X11/Wayland session.\n"
92
+ " No libfuse2 package is required (static-FUSE runtime embedded). If the "
93
+ "host lacks kernel FUSE (/dev/fuse) — e.g. some containers/CI — run it "
94
+ "with --appimage-extract-and-run (or APPIMAGE_EXTRACT_AND_RUN=1)."
95
+ )
96
+ return result
97
+
98
+
99
+ def _assemble(config, lock, project_root, *, arch, no_cache, release) -> Path:
100
+ try:
101
+ return build_appdir(
102
+ config, lock, project_root, arch=arch, no_cache=no_cache, release=release
103
+ )
104
+ except AppDirError as exc:
105
+ raise ToolchainError(str(exc)) from exc
106
+
107
+
108
+ def _resolve_arch(lock: LinuxLockfile, arch: str | None) -> str:
109
+ try:
110
+ return resolve_assembly_arch(lock.archs, arch)
111
+ except AppDirError as exc:
112
+ raise ToolchainError(str(exc)) from exc
113
+
114
+
115
+ def _load_and_verify(
116
+ project_root: Path, no_verify_lock: bool
117
+ ) -> tuple[object, LinuxLockfile]:
118
+ config = _load_config(project_root)
119
+ lock = _load_lock(project_root)
120
+ pyproject = project_root / "pyproject.toml"
121
+ if not no_verify_lock and not is_in_sync(lock, pyproject.read_text("utf-8")):
122
+ raise ToolchainError(
123
+ f"{lockfile_path_for('linux').name} is out of date with "
124
+ "pyproject.toml.\n"
125
+ " Run `kivyforge lock -p linux` to regenerate it (or pass "
126
+ "--no-verify-lock to build against the stale lock anyway)."
127
+ )
128
+ return config, lock
129
+
130
+
131
+ def linux_run(
132
+ project_root: Path,
133
+ *,
134
+ arch: str | None,
135
+ no_build: bool,
136
+ ) -> None:
137
+ """Build (unless --no-build) and launch the app via ``AppRun`` in foreground."""
138
+ if no_build:
139
+ _require_linux_host()
140
+ config = _load_config(project_root)
141
+ appdir = project_root / "build" / "linux" / f"{config.display_name}.AppDir"
142
+ if not appdir.exists():
143
+ raise ToolchainError(
144
+ f"no built AppDir at {appdir.relative_to(project_root)}; run "
145
+ "without --no-build first."
146
+ )
147
+ else:
148
+ appdir = linux_build(
149
+ project_root, arch=arch, no_verify_lock=False, no_cache=False
150
+ )
151
+
152
+ apprun = appdir / "AppRun"
153
+ click.echo(f"Launching {appdir.name} ...")
154
+ # Foreground exec so the dev sees stdout/stderr + tracebacks.
155
+ proc = subprocess.run([str(apprun)])
156
+ if proc.returncode != 0:
157
+ raise ToolchainError(f"{appdir.name} exited with status {proc.returncode}.")
158
+
159
+
160
+ def linux_status(project_root: Path) -> None:
161
+ """Show app identity, Python version, lock sync, and build state."""
162
+ config = _load_config(project_root)
163
+ linux = config.linux_required
164
+ click.echo(f"App: {config.display_name} ({linux.app_id})")
165
+ click.echo(f"Python: {linux.python_version or '(unset)'}")
166
+ click.echo(f"Lock: {_lock_state(project_root)}")
167
+
168
+ appdir = project_root / "build" / "linux" / f"{config.display_name}.AppDir"
169
+ click.echo(f"Build: {_build_state(appdir)}")
170
+
171
+
172
+ def _lock_state(project_root: Path) -> str:
173
+ path = lockfile_path_for("linux", project_root)
174
+ if not path.is_file():
175
+ return "missing (run `kivyforge lock -p linux`)"
176
+ try:
177
+ lock = load_linux_lock(path)
178
+ except LockError:
179
+ return "unreadable (run `kivyforge lock -p linux`)"
180
+ pyproject = project_root / "pyproject.toml"
181
+ if is_in_sync(lock, pyproject.read_text("utf-8")):
182
+ return "in sync"
183
+ return "out of date (run `kivyforge lock -p linux`)"
184
+
185
+
186
+ def _build_state(appdir: Path) -> str:
187
+ if not appdir.exists():
188
+ return "not built"
189
+ age = time.time() - appdir.stat().st_mtime
190
+ return f"last built {_humanize(age)}"
191
+
192
+
193
+ def _humanize(seconds: float) -> str:
194
+ seconds = int(seconds)
195
+ if seconds < 60:
196
+ return "just now"
197
+ if seconds < 3600:
198
+ m = seconds // 60
199
+ return f"{m} minute{'s' if m != 1 else ''} ago"
200
+ if seconds < 86400:
201
+ h = seconds // 3600
202
+ return f"{h} hour{'s' if h != 1 else ''} ago"
203
+ d = seconds // 86400
204
+ return f"{d} day{'s' if d != 1 else ''} ago"
205
+
206
+
207
+ def _require_linux_host() -> None:
208
+ try:
209
+ get_platform("linux").check_host_capability()
210
+ except HostCapabilityError as exc:
211
+ raise ToolchainError(str(exc)) from exc
212
+
213
+
214
+ def _load_config(project_root: Path):
215
+ try:
216
+ return load_config(
217
+ project_root / "pyproject.toml", require_ios=False, require_linux=True
218
+ )
219
+ except ConfigError as exc:
220
+ raise ToolchainError(exc.format()) from exc
221
+
222
+
223
+ def _load_lock(project_root: Path) -> LinuxLockfile:
224
+ path = lockfile_path_for("linux", project_root)
225
+ if not path.is_file():
226
+ raise ToolchainError(
227
+ f"no {path.name} found. Run `kivyforge lock -p linux` first."
228
+ )
229
+ try:
230
+ return load_linux_lock(path)
231
+ except LockError as exc:
232
+ raise ToolchainError(str(exc)) from exc
@@ -0,0 +1,71 @@
1
+ """Generate the freedesktop ``.desktop`` entry for the AppDir (linux-spec).
2
+
3
+ The Linux analog of the macOS ``Info.plist``: kivyforge owns the entry, built
4
+ from ``[project]`` + ``[tool.kivy]`` + ``[tool.kivy.linux]``. ``Exec=AppRun %f``
5
+ and ``StartupWMClass=<app_id>`` (matched by the ``AppRun`` launcher's SDL
6
+ WM_CLASS env) so the running window groups under the app's own icon. The
7
+ generated file must pass ``desktop-file-validate`` (also a ``doctor`` check).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import subprocess
14
+ from pathlib import Path
15
+
16
+ from kivyforge.config.model import Config
17
+
18
+ from . import AppDirError
19
+
20
+
21
+ def render_desktop_entry(config: Config) -> str:
22
+ """The ``.desktop`` file contents for *config* (pure/testable)."""
23
+ linux = config.linux_required
24
+ categories = "".join(f"{c};" for c in linux.desktop.categories)
25
+ lines = [
26
+ "[Desktop Entry]",
27
+ "Type=Application",
28
+ f"Name={config.display_name}",
29
+ ]
30
+ if config.project.description:
31
+ lines.append(f"Comment={config.project.description}")
32
+ lines += [
33
+ "Exec=AppRun %f",
34
+ f"Icon={linux.app_id}",
35
+ f"Categories={categories}",
36
+ f"StartupWMClass={linux.app_id}",
37
+ "Terminal=false",
38
+ ]
39
+ return "\n".join(lines) + "\n"
40
+
41
+
42
+ def write_desktop_entry(config: Config, dest: Path) -> None:
43
+ """Write ``<app_id>.desktop`` for *config* at *dest*."""
44
+ dest.parent.mkdir(parents=True, exist_ok=True)
45
+ dest.write_text(render_desktop_entry(config), encoding="utf-8")
46
+
47
+
48
+ def validate_desktop_file(dest: Path) -> None:
49
+ """Run ``desktop-file-validate`` on *dest*; hard-fail on validation errors.
50
+
51
+ The generated ``.desktop`` must pass ``desktop-file-validate`` (linux-spec):
52
+ an invalid entry produces an AppImage that launches but fails desktop
53
+ integration (menu/search/icon association). ``desktop-file-validate`` (from
54
+ ``desktop-file-utils``) is optional on the build host — when it isn't
55
+ installed we can't run it, so we skip (config-time category validation still
56
+ guards the common typo). When it *is* installed, any reported error is a hard
57
+ failure.
58
+ """
59
+ tool = shutil.which("desktop-file-validate")
60
+ if tool is None:
61
+ return
62
+ try:
63
+ proc = subprocess.run([tool, str(dest)], capture_output=True, text=True)
64
+ except (OSError, ValueError):
65
+ return
66
+ if proc.returncode != 0:
67
+ detail = (proc.stdout + proc.stderr).strip() or "validation failed"
68
+ raise AppDirError(
69
+ f"generated {dest.name} failed desktop-file-validate:\n{detail}\n"
70
+ " Fix [tool.kivy.linux.desktop] / [project] metadata and rebuild."
71
+ )