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,316 @@
1
+ """Process execution + simctl/devicectl command builders (spec 05 run/open).
2
+
3
+ ``run_command`` is the single execution funnel (injectable for tests). The
4
+ simctl/devicectl builders are pure argv factories so the run sequence is
5
+ unit-testable without a simulator or device.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import subprocess
12
+ import tempfile
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ class CommandError(Exception):
18
+ """A spawned tool exited non-zero."""
19
+
20
+ def __init__(self, argv: list[str], returncode: int, output: str = "") -> None:
21
+ self.argv = argv
22
+ self.returncode = returncode
23
+ self.output = output
24
+ super().__init__(
25
+ f"command failed ({returncode}): {' '.join(argv)}"
26
+ + (f"\n{output}" if output else "")
27
+ )
28
+
29
+
30
+ def run_command(argv: list[str], *, runner=subprocess.run, check: bool = True):
31
+ proc = runner(argv, capture_output=True, text=True)
32
+ if check and proc.returncode != 0:
33
+ # xcodebuild routes destination-matching notes/warnings to stderr but
34
+ # the actual `error:` diagnostics (signing, compile failures, ...) to
35
+ # stdout — picking only one stream (whichever is non-empty) can hide
36
+ # the real failure reason behind a harmless warning. Keep both.
37
+ output = "\n".join(part for part in (proc.stdout, proc.stderr) if part)
38
+ raise CommandError(argv, proc.returncode, output)
39
+ return proc
40
+
41
+
42
+ def open_command(xcodeproj: str | Path) -> list[str]:
43
+ return ["open", str(xcodeproj)]
44
+
45
+
46
+ # ---- simulator (simctl) -------------------------------------------------
47
+
48
+
49
+ def simctl_boot(destination: str) -> list[str]:
50
+ return ["xcrun", "simctl", "boot", destination]
51
+
52
+
53
+ def simctl_install(destination: str, app_path: str | Path) -> list[str]:
54
+ return ["xcrun", "simctl", "install", destination, str(app_path)]
55
+
56
+
57
+ def simctl_launch(
58
+ destination: str, bundle_id: str, *, console: bool = True
59
+ ) -> list[str]:
60
+ cmd = ["xcrun", "simctl", "launch"]
61
+ if console:
62
+ cmd.append("--console-pty")
63
+ cmd += [destination, bundle_id]
64
+ return cmd
65
+
66
+
67
+ def simctl_list() -> list[str]:
68
+ return ["xcrun", "simctl", "list", "devices", "available"]
69
+
70
+
71
+ def simctl_list_json(*, state: str = "available") -> list[str]:
72
+ return ["xcrun", "simctl", "list", "devices", state, "-j"]
73
+
74
+
75
+ def open_simulator_app() -> list[str]:
76
+ return ["open", "-a", "Simulator"]
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class SimulatorDevice:
81
+ udid: str
82
+ name: str
83
+ state: str
84
+ runtime: str
85
+
86
+
87
+ def parse_simctl_devices(payload: dict) -> list[SimulatorDevice]:
88
+ """Flatten ``simctl list devices … -j`` output into a single device list."""
89
+ devices: list[SimulatorDevice] = []
90
+ for runtime_key, entries in payload.get("devices", {}).items():
91
+ runtime = _runtime_label(runtime_key)
92
+ for entry in entries:
93
+ if not entry.get("isAvailable", True):
94
+ continue
95
+ devices.append(
96
+ SimulatorDevice(
97
+ udid=entry["udid"],
98
+ name=entry["name"],
99
+ state=entry.get("state", "Shutdown"),
100
+ runtime=runtime,
101
+ )
102
+ )
103
+ return devices
104
+
105
+
106
+ def pick_simulator(
107
+ devices: list[SimulatorDevice], destination: str | None = None
108
+ ) -> SimulatorDevice:
109
+ """Choose a simulator by explicit destination, booted state, or best default."""
110
+ if not devices:
111
+ raise CommandError(
112
+ ["simctl"],
113
+ 1,
114
+ "no iOS simulators available (install one in Xcode > Settings > Components)",
115
+ )
116
+ if destination:
117
+ return _match_simulator(devices, destination)
118
+ booted = [d for d in devices if d.state == "Booted"]
119
+ if booted:
120
+ return booted[0]
121
+ iphones = [d for d in devices if "iphone" in d.name.lower()]
122
+ return max(iphones or devices, key=_default_simulator_sort_key)
123
+
124
+
125
+ def resolve_simulator_destination(
126
+ destination: str | None,
127
+ *,
128
+ runner=subprocess.run,
129
+ ) -> SimulatorDevice:
130
+ """Return the simulator to target, booting a default when none is running."""
131
+ proc = run_command(simctl_list_json(), runner=runner)
132
+ devices = parse_simctl_devices(json.loads(proc.stdout))
133
+ device = pick_simulator(devices, destination)
134
+ if device.state != "Booted":
135
+ run_command(simctl_boot(device.udid), runner=runner, check=False)
136
+ run_command(open_simulator_app(), runner=runner, check=False)
137
+ return device
138
+
139
+
140
+ def _match_simulator(
141
+ devices: list[SimulatorDevice], destination: str
142
+ ) -> SimulatorDevice:
143
+ dest = destination.lower()
144
+ for device in devices:
145
+ if device.udid.lower() == dest:
146
+ return device
147
+ for device in devices:
148
+ if device.name.lower() == dest:
149
+ return device
150
+ for device in devices:
151
+ if dest in device.name.lower():
152
+ return device
153
+ raise CommandError(
154
+ ["simctl"],
155
+ 1,
156
+ f"no simulator matches {destination!r} (run `kivyforge run --list-devices`)",
157
+ )
158
+
159
+
160
+ def _runtime_label(runtime_key: str) -> str:
161
+ # com.apple.CoreSimulator.SimRuntime.iOS-26-5 -> 26.5
162
+ tail = runtime_key.rsplit(".", 1)[-1]
163
+ if tail.startswith("iOS-"):
164
+ return tail[4:].replace("-", ".")
165
+ return tail
166
+
167
+
168
+ def _default_simulator_sort_key(device: SimulatorDevice) -> tuple:
169
+ parts: list[int] = []
170
+ for piece in device.runtime.split("."):
171
+ try:
172
+ parts.append(int(piece))
173
+ except ValueError:
174
+ parts.append(0)
175
+ return (tuple(parts), device.name)
176
+
177
+
178
+ # ---- device (devicectl) -------------------------------------------------
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class Device:
183
+ identifier: str
184
+ name: str
185
+ platform: str
186
+ tunnel_state: str
187
+ pairing_state: str
188
+
189
+
190
+ def parse_devicectl_devices(payload: dict) -> list[Device]:
191
+ """Flatten ``devicectl list devices -j`` output into a device list."""
192
+ devices: list[Device] = []
193
+ for entry in payload.get("result", {}).get("devices", []):
194
+ props = entry.get("deviceProperties", {})
195
+ hardware = entry.get("hardwareProperties", {})
196
+ connection = entry.get("connectionProperties", {})
197
+ devices.append(
198
+ Device(
199
+ identifier=entry["identifier"],
200
+ name=props.get("name", entry["identifier"]),
201
+ platform=hardware.get("platform", "unknown"),
202
+ tunnel_state=connection.get("tunnelState", "disconnected"),
203
+ pairing_state=connection.get("pairingState", "unpaired"),
204
+ )
205
+ )
206
+ return devices
207
+
208
+
209
+ def pick_device(devices: list[Device], destination: str | None = None) -> Device:
210
+ """Choose a device by explicit destination, or the sole paired iPhone/iPad.
211
+
212
+ ``tunnelState`` only reports ``"connected"`` while some CoreDevice client
213
+ (normally Xcode) already has a tunnel open to the device — it stays
214
+ ``"disconnected"`` for a device that is plugged in and paired but idle,
215
+ e.g. when Xcode isn't running. ``devicectl install``/``launch`` establish
216
+ the tunnel themselves on demand, so a paired device is a valid target
217
+ regardless of ``tunnelState``.
218
+ """
219
+ if destination:
220
+ if not devices:
221
+ raise CommandError(
222
+ ["devicectl"],
223
+ 1,
224
+ "no iOS devices found (connect one via USB/Wi-Fi and trust "
225
+ "this Mac, then run `kivyforge run --list-devices`)",
226
+ )
227
+ return _match_device(devices, destination)
228
+ # No explicit destination: only consider paired iOS devices — unpaired
229
+ # accessories or devices that are merely discoverable don't qualify.
230
+ paired = [d for d in devices if d.platform == "iOS" and d.pairing_state == "paired"]
231
+ if len(paired) == 1:
232
+ return paired[0]
233
+ if not paired:
234
+ raise CommandError(
235
+ ["devicectl"],
236
+ 1,
237
+ "no paired iOS device found (connect it via USB/Wi-Fi and trust "
238
+ "this Mac, then run `kivyforge run --list-devices`)",
239
+ )
240
+ names = ", ".join(f"{d.name!r}" for d in paired)
241
+ raise CommandError(
242
+ ["devicectl"],
243
+ 1,
244
+ f"multiple paired iOS devices found ({names}) — pass one "
245
+ "explicitly, e.g. `kivyforge run --device --destination 'NAME'`",
246
+ )
247
+
248
+
249
+ def _match_device(devices: list[Device], destination: str) -> Device:
250
+ dest = destination.lower()
251
+ for device in devices:
252
+ if device.identifier.lower() == dest:
253
+ return device
254
+ for device in devices:
255
+ if device.name.lower() == dest:
256
+ return device
257
+ for device in devices:
258
+ if dest in device.name.lower():
259
+ return device
260
+ raise CommandError(
261
+ ["devicectl"],
262
+ 1,
263
+ f"no device matches {destination!r} (run `kivyforge run --list-devices`)",
264
+ )
265
+
266
+
267
+ def devicectl_list_json(output_path: str | Path) -> list[str]:
268
+ return ["xcrun", "devicectl", "list", "devices", "-j", str(output_path)]
269
+
270
+
271
+ def resolve_device_destination(
272
+ destination: str | None,
273
+ *,
274
+ runner=subprocess.run,
275
+ ) -> Device:
276
+ """Return the physical device to target for ``--device`` install/launch."""
277
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as fh:
278
+ output_path = Path(fh.name)
279
+ try:
280
+ run_command(devicectl_list_json(output_path), runner=runner)
281
+ # devicectl writes JSON as UTF-8 (device names may be non-ASCII).
282
+ payload = json.loads(output_path.read_text(encoding="utf-8"))
283
+ finally:
284
+ output_path.unlink(missing_ok=True)
285
+ devices = parse_devicectl_devices(payload)
286
+ return pick_device(devices, destination)
287
+
288
+
289
+ def devicectl_install(destination: str, app_path: str | Path) -> list[str]:
290
+ return [
291
+ "xcrun",
292
+ "devicectl",
293
+ "device",
294
+ "install",
295
+ "app",
296
+ "--device",
297
+ destination,
298
+ str(app_path),
299
+ ]
300
+
301
+
302
+ def devicectl_launch(destination: str, bundle_id: str) -> list[str]:
303
+ return [
304
+ "xcrun",
305
+ "devicectl",
306
+ "device",
307
+ "process",
308
+ "launch",
309
+ "--device",
310
+ destination,
311
+ bundle_id,
312
+ ]
313
+
314
+
315
+ def devicectl_list() -> list[str]:
316
+ return ["xcrun", "devicectl", "list", "devices"]
@@ -0,0 +1,120 @@
1
+ """Linux platform backend.
2
+
3
+ Produces a self-contained AppDir and, by default, an AppImage (linux-spec).
4
+ Linux is the host default on Linux, so a Linux box with ``[tool.kivy.linux]``
5
+ configured resolves to this backend without ``-p``. It requires a Linux host:
6
+ the bundled python-build-standalone runtime is a gnu/glibc ELF build.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import platform as _platform
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ from ..base import HostCapabilityError, Platform
16
+
17
+ if TYPE_CHECKING:
18
+ from kivyforge.doctor.result import CheckResult
19
+
20
+
21
+ class AppDirError(Exception):
22
+ """A Linux AppDir bundling failure surfaced with an actionable message."""
23
+
24
+
25
+ class LinuxPlatform(Platform):
26
+ name = "linux"
27
+ # Linux is a desktop target built on its own OS, so Linux maps to it as the
28
+ # host default (resolution-chain step 3).
29
+ host_system = "Linux"
30
+ # AppImage is the primary distributable; the AppDir folder is the substrate.
31
+ package_formats = ("appimage", "folder")
32
+
33
+ def check_host_capability(self, *, host_system: str | None = None) -> None:
34
+ host = host_system if host_system is not None else _platform.system()
35
+ if host != "Linux":
36
+ raise HostCapabilityError(
37
+ "building for Linux requires a Linux host; "
38
+ f"this host is {host!r}.\n"
39
+ " The AppDir bundles a python-build-standalone gnu/glibc runtime "
40
+ "and runs the app against the host's libGL/X11/Wayland.\n"
41
+ " Run `kivyforge doctor -p linux` on a Linux box to check the "
42
+ "setup."
43
+ )
44
+
45
+ def build(
46
+ self,
47
+ project_root: Path,
48
+ *,
49
+ target: str | None,
50
+ arch: str | None,
51
+ no_verify_lock: bool,
52
+ no_cache: bool,
53
+ team_id: str | None,
54
+ signing_identity: str | None,
55
+ export_method: str,
56
+ debug: bool = False,
57
+ fmt: str | None = None,
58
+ abi: str | None = None,
59
+ ) -> None:
60
+ self.reject_ios_only_target(target)
61
+ from .cli import linux_build
62
+
63
+ linux_build(
64
+ project_root,
65
+ arch=arch,
66
+ no_verify_lock=no_verify_lock,
67
+ no_cache=no_cache,
68
+ )
69
+
70
+ def run(
71
+ self,
72
+ project_root: Path,
73
+ *,
74
+ target: str,
75
+ arch: str | None,
76
+ destination: str | None,
77
+ no_build: bool,
78
+ ) -> None:
79
+ from .cli import linux_run
80
+
81
+ linux_run(project_root, arch=arch, no_build=no_build)
82
+
83
+ def package(
84
+ self,
85
+ project_root: Path,
86
+ *,
87
+ fmt: str,
88
+ arch: str | None,
89
+ team_id: str | None,
90
+ signing_identity: str | None,
91
+ export_method: str,
92
+ notarize: bool | None,
93
+ notary_profile: str | None,
94
+ no_verify_lock: bool,
95
+ no_cache: bool,
96
+ abi: str | None = None,
97
+ keystore: str | None = None,
98
+ key_alias: str | None = None,
99
+ ) -> None:
100
+ from .cli import linux_package
101
+
102
+ linux_package(
103
+ project_root,
104
+ fmt=fmt,
105
+ arch=arch,
106
+ no_verify_lock=no_verify_lock,
107
+ no_cache=no_cache,
108
+ )
109
+
110
+ def status(self, project_root: Path) -> None:
111
+ from .cli import linux_status
112
+
113
+ linux_status(project_root)
114
+
115
+ def doctor(
116
+ self, cwd: Path, *, kivyforge_version: str, offline: bool
117
+ ) -> list[CheckResult]:
118
+ from .doctor import linux_doctor
119
+
120
+ return linux_doctor(cwd, kivyforge_version=kivyforge_version, offline=offline)
@@ -0,0 +1,169 @@
1
+ """Wrap an AppDir into a single ``.AppImage`` (linux-spec, Step 5).
2
+
3
+ Acquires two **pinned build tools** through the shared ``kivyforge/artifacts``
4
+ download / cache / SHA-256-verify machinery — a static ``appimagetool`` and a
5
+ type2 **static-FUSE** runtime file — then runs ``appimagetool --runtime-file``
6
+ to produce the output. Embedding a static-FUSE runtime means the *shipped*
7
+ AppImage needs no host ``libfuse2`` *package* (it does still use the kernel's
8
+ ``/dev/fuse`` to self-mount unless run with ``--appimage-extract-and-run``);
9
+ invoking appimagetool with ``APPIMAGE_EXTRACT_AND_RUN=1`` means the *build host*
10
+ needs no FUSE at all (WSL2 / containers / CI just work).
11
+
12
+ The pins are constants here (versioned with kivyforge releases): they are build
13
+ tools, not app dependencies, so they never enter the app's lockfile.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ from pathlib import Path
22
+
23
+ from kivyforge.artifacts.cache import ArtifactCache
24
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
25
+
26
+ from . import AppDirError
27
+
28
+ # Pinned appimagetool (AppImage/appimagetool) and type2 static-FUSE runtime
29
+ # (AppImage/type2-runtime). Per-arch so aarch64 is purely additive later.
30
+ APPIMAGETOOL_VERSION = "1.9.1"
31
+ TYPE2_RUNTIME_VERSION = "20251108"
32
+
33
+ _APPIMAGETOOL = {
34
+ "x86_64": (
35
+ "https://github.com/AppImage/appimagetool/releases/download/"
36
+ f"{APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage",
37
+ "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0",
38
+ ),
39
+ }
40
+
41
+ _TYPE2_RUNTIME = {
42
+ "x86_64": (
43
+ "https://github.com/AppImage/type2-runtime/releases/download/"
44
+ f"{TYPE2_RUNTIME_VERSION}/runtime-x86_64",
45
+ "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d",
46
+ ),
47
+ }
48
+
49
+
50
+ def appimagetool_asset(arch: str) -> tuple[str, str]:
51
+ """The (url, sha256) for the pinned appimagetool for *arch*."""
52
+ try:
53
+ return _APPIMAGETOOL[arch]
54
+ except KeyError:
55
+ raise AppDirError(
56
+ f"no pinned appimagetool for arch {arch!r} "
57
+ f"(have: {', '.join(sorted(_APPIMAGETOOL))})."
58
+ ) from None
59
+
60
+
61
+ def type2_runtime_asset(arch: str) -> tuple[str, str]:
62
+ """The (url, sha256) for the pinned type2 static-FUSE runtime for *arch*."""
63
+ try:
64
+ return _TYPE2_RUNTIME[arch]
65
+ except KeyError:
66
+ raise AppDirError(
67
+ f"no pinned type2 runtime for arch {arch!r} "
68
+ f"(have: {', '.join(sorted(_TYPE2_RUNTIME))})."
69
+ ) from None
70
+
71
+
72
+ def build_appimage(
73
+ appdir: Path,
74
+ output: Path,
75
+ arch: str,
76
+ *,
77
+ project_root: Path,
78
+ cache: ArtifactCache | None = None,
79
+ no_cache: bool = False,
80
+ echo=lambda *a: None,
81
+ ) -> Path:
82
+ """Package *appdir* into an ``.AppImage`` at *output*; return *output*."""
83
+ cache = cache or ArtifactCache()
84
+ tool = _acquire(
85
+ appimagetool_asset(arch),
86
+ f"appimagetool-{APPIMAGETOOL_VERSION}-{arch}.AppImage",
87
+ "appimagetool",
88
+ project_root,
89
+ cache,
90
+ no_cache,
91
+ )
92
+ runtime = _acquire(
93
+ type2_runtime_asset(arch),
94
+ f"appimage-runtime-{TYPE2_RUNTIME_VERSION}-{arch}",
95
+ "type2 runtime",
96
+ project_root,
97
+ cache,
98
+ no_cache,
99
+ )
100
+ # appimagetool must be executable; run it from a copy so the
101
+ # content-addressed cache entry is never mutated.
102
+ tool = _executable_copy(tool, cache)
103
+
104
+ output.parent.mkdir(parents=True, exist_ok=True)
105
+ # Emit to a temp file and swap it in only on success, so a tool failure (or
106
+ # crash) leaves any previous .AppImage intact rather than deleting it up
107
+ # front — the write-in-place guarantee the other platforms also uphold.
108
+ tmp_out = output.parent / f".{output.name}.tmp-{os.getpid()}"
109
+ tmp_out.unlink(missing_ok=True)
110
+
111
+ echo(f"Packaging {output.name} with appimagetool {APPIMAGETOOL_VERSION} ...")
112
+ env = {
113
+ **os.environ,
114
+ "ARCH": arch,
115
+ # No FUSE on the build host: appimagetool extracts + runs itself.
116
+ "APPIMAGE_EXTRACT_AND_RUN": "1",
117
+ }
118
+ cmd = [
119
+ str(tool),
120
+ "--runtime-file",
121
+ str(runtime),
122
+ str(appdir),
123
+ str(tmp_out),
124
+ ]
125
+ try:
126
+ proc = subprocess.run(cmd, capture_output=True, text=True, env=env)
127
+ except OSError as exc:
128
+ tmp_out.unlink(missing_ok=True)
129
+ raise AppDirError(f"failed to run appimagetool: {exc}") from exc
130
+ if proc.returncode != 0:
131
+ tmp_out.unlink(missing_ok=True)
132
+ raise AppDirError(
133
+ "appimagetool failed to build the AppImage.\n"
134
+ f" {(proc.stderr or proc.stdout).strip()}"
135
+ )
136
+ tmp_out.chmod(0o755)
137
+ os.replace(tmp_out, output)
138
+ return output
139
+
140
+
141
+ def _acquire(
142
+ asset: tuple[str, str],
143
+ filename: str,
144
+ label: str,
145
+ project_root: Path,
146
+ cache: ArtifactCache,
147
+ no_cache: bool,
148
+ ) -> Path:
149
+ url, sha256 = asset
150
+ try:
151
+ return fetch_artifact(
152
+ name=label,
153
+ sha256=sha256,
154
+ filename=filename,
155
+ url=url,
156
+ project_root=project_root,
157
+ cache=cache,
158
+ no_cache=no_cache,
159
+ )
160
+ except DownloadError as exc:
161
+ raise AppDirError(str(exc)) from exc
162
+
163
+
164
+ def _executable_copy(tool: Path, cache: ArtifactCache) -> Path:
165
+ exec_copy = cache.root.parent / "bin" / tool.name
166
+ exec_copy.parent.mkdir(parents=True, exist_ok=True)
167
+ shutil.copy2(tool, exec_copy)
168
+ exec_copy.chmod(0o755)
169
+ return exec_copy