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.
- kivyforge/__init__.py +9 -0
- kivyforge/artifacts/__init__.py +41 -0
- kivyforge/artifacts/cache.py +81 -0
- kivyforge/artifacts/collect.py +354 -0
- kivyforge/artifacts/download.py +125 -0
- kivyforge/artifacts/frameworks.py +240 -0
- kivyforge/artifacts/native_stage_util.py +346 -0
- kivyforge/artifacts/runtime.py +65 -0
- kivyforge/artifacts/verify.py +45 -0
- kivyforge/artifacts/wheels.py +89 -0
- kivyforge/bundle/__init__.py +1 -0
- kivyforge/bundle/pycompile.py +161 -0
- kivyforge/cli/__init__.py +52 -0
- kivyforge/cli/_common.py +62 -0
- kivyforge/cli/_platform.py +84 -0
- kivyforge/cli/build.py +111 -0
- kivyforge/cli/clean.py +135 -0
- kivyforge/cli/doctor.py +52 -0
- kivyforge/cli/init.py +612 -0
- kivyforge/cli/init_writer.py +716 -0
- kivyforge/cli/lock.py +261 -0
- kivyforge/cli/open_cmd.py +15 -0
- kivyforge/cli/package.py +134 -0
- kivyforge/cli/run.py +162 -0
- kivyforge/cli/status.py +20 -0
- kivyforge/cli/upgrade.py +314 -0
- kivyforge/config/__init__.py +46 -0
- kivyforge/config/errors.py +43 -0
- kivyforge/config/icons.py +60 -0
- kivyforge/config/loader.py +3079 -0
- kivyforge/config/model.py +817 -0
- kivyforge/doctor/__init__.py +19 -0
- kivyforge/doctor/checks_common.py +95 -0
- kivyforge/doctor/probe.py +357 -0
- kivyforge/doctor/result.py +41 -0
- kivyforge/lock/__init__.py +42 -0
- kivyforge/lock/_pip_shim.py +63 -0
- kivyforge/lock/find_links.py +182 -0
- kivyforge/lock/model.py +69 -0
- kivyforge/lock/pep751.py +111 -0
- kivyforge/lock/reader.py +30 -0
- kivyforge/lock/resolver.py +124 -0
- kivyforge/lock/wheelruntime/__init__.py +70 -0
- kivyforge/lock/wheelruntime/builder.py +280 -0
- kivyforge/lock/wheelruntime/model.py +94 -0
- kivyforge/lock/wheelruntime/native_binaries.py +127 -0
- kivyforge/lock/wheelruntime/pbs_github.py +133 -0
- kivyforge/lock/wheelruntime/profile.py +104 -0
- kivyforge/lock/wheelruntime/resolver.py +248 -0
- kivyforge/lock/wheelruntime/runtime.py +161 -0
- kivyforge/lock/wheelruntime/serialize.py +245 -0
- kivyforge/platforms/__init__.py +114 -0
- kivyforge/platforms/android/__init__.py +140 -0
- kivyforge/platforms/android/adb.py +215 -0
- kivyforge/platforms/android/bootstrap/__init__.py +7 -0
- kivyforge/platforms/android/bootstrap/contract.py +166 -0
- kivyforge/platforms/android/bootstrap/render.py +244 -0
- kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +76 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +50 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
- kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +97 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
- kivyforge/platforms/android/bootstrap/templates/cpp/main.c +150 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +75 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +132 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +189 -0
- kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +276 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2120 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
- kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
- kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
- kivyforge/platforms/android/cli.py +1023 -0
- kivyforge/platforms/android/doctor.py +899 -0
- kivyforge/platforms/android/elf.py +148 -0
- kivyforge/platforms/android/generate/__init__.py +1 -0
- kivyforge/platforms/android/generate/manifest.py +261 -0
- kivyforge/platforms/android/generate/project.py +462 -0
- kivyforge/platforms/android/generate/services.py +187 -0
- kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
- kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
- kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
- kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
- kivyforge/platforms/android/gradlew.py +60 -0
- kivyforge/platforms/android/icons.py +237 -0
- kivyforge/platforms/android/lock/__init__.py +1 -0
- kivyforge/platforms/android/lock/archives.py +109 -0
- kivyforge/platforms/android/lock/builder.py +387 -0
- kivyforge/platforms/android/lock/markers.py +71 -0
- kivyforge/platforms/android/lock/maven.py +252 -0
- kivyforge/platforms/android/lock/model.py +113 -0
- kivyforge/platforms/android/lock/python_meta.py +199 -0
- kivyforge/platforms/android/lock/reader.py +206 -0
- kivyforge/platforms/android/lock/resolver.py +276 -0
- kivyforge/platforms/android/lock/writer.py +121 -0
- kivyforge/platforms/android/policy.py +214 -0
- kivyforge/platforms/android/signing.py +145 -0
- kivyforge/platforms/android/smoke.py +43 -0
- kivyforge/platforms/android/splash.py +293 -0
- kivyforge/platforms/android/stage/__init__.py +1 -0
- kivyforge/platforms/android/stage/bundle.py +295 -0
- kivyforge/platforms/android/stage/jnilibs.py +186 -0
- kivyforge/platforms/android/stage/runtime.py +61 -0
- kivyforge/platforms/android/stage/wheels.py +106 -0
- kivyforge/platforms/android/toolchain.py +29 -0
- kivyforge/platforms/base.py +157 -0
- kivyforge/platforms/ios/__init__.py +129 -0
- kivyforge/platforms/ios/assets.py +193 -0
- kivyforge/platforms/ios/buildsettings.py +128 -0
- kivyforge/platforms/ios/cli.py +543 -0
- kivyforge/platforms/ios/doctor.py +410 -0
- kivyforge/platforms/ios/entitlements.py +34 -0
- kivyforge/platforms/ios/generator.py +318 -0
- kivyforge/platforms/ios/lock/__init__.py +71 -0
- kivyforge/platforms/ios/lock/builder.py +393 -0
- kivyforge/platforms/ios/lock/markers.py +69 -0
- kivyforge/platforms/ios/lock/model.py +95 -0
- kivyforge/platforms/ios/lock/python_meta.py +187 -0
- kivyforge/platforms/ios/lock/reader.py +191 -0
- kivyforge/platforms/ios/lock/resolver.py +304 -0
- kivyforge/platforms/ios/lock/spm.py +217 -0
- kivyforge/platforms/ios/lock/writer.py +114 -0
- kivyforge/platforms/ios/lock/xcframework.py +188 -0
- kivyforge/platforms/ios/materialize.py +59 -0
- kivyforge/platforms/ios/plist.py +91 -0
- kivyforge/platforms/ios/privacy.py +39 -0
- kivyforge/platforms/ios/skeleton.py +165 -0
- kivyforge/platforms/ios/sources.py +63 -0
- kivyforge/platforms/ios/staging.py +225 -0
- kivyforge/platforms/ios/swift_packages.py +324 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +34 -0
- kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +146 -0
- kivyforge/platforms/ios/templates/main.m +15 -0
- kivyforge/platforms/ios/templates/main_config.h.in +12 -0
- kivyforge/platforms/ios/xcframeworks.py +194 -0
- kivyforge/platforms/ios/xcode/__init__.py +76 -0
- kivyforge/platforms/ios/xcode/commands.py +296 -0
- kivyforge/platforms/ios/xcode/runner.py +316 -0
- kivyforge/platforms/linux/__init__.py +120 -0
- kivyforge/platforms/linux/appimage.py +169 -0
- kivyforge/platforms/linux/bundle.py +235 -0
- kivyforge/platforms/linux/cli.py +232 -0
- kivyforge/platforms/linux/desktop.py +71 -0
- kivyforge/platforms/linux/doctor.py +438 -0
- kivyforge/platforms/linux/elftools.py +95 -0
- kivyforge/platforms/linux/icons.py +129 -0
- kivyforge/platforms/linux/launcher.py +107 -0
- kivyforge/platforms/linux/lock/__init__.py +126 -0
- kivyforge/platforms/linux/lock/profile.py +254 -0
- kivyforge/platforms/linux/lock/runtime.py +75 -0
- kivyforge/platforms/linux/native_stage.py +47 -0
- kivyforge/platforms/linux/runtime_stage.py +107 -0
- kivyforge/platforms/linux/wheels_stage.py +103 -0
- kivyforge/platforms/macos/__init__.py +121 -0
- kivyforge/platforms/macos/bundle.py +255 -0
- kivyforge/platforms/macos/cli.py +269 -0
- kivyforge/platforms/macos/doctor.py +480 -0
- kivyforge/platforms/macos/icns.py +75 -0
- kivyforge/platforms/macos/launcher.py +118 -0
- kivyforge/platforms/macos/lock/__init__.py +112 -0
- kivyforge/platforms/macos/lock/profile.py +119 -0
- kivyforge/platforms/macos/lock/runtime.py +58 -0
- kivyforge/platforms/macos/machotools.py +138 -0
- kivyforge/platforms/macos/native_stage.py +47 -0
- kivyforge/platforms/macos/notarize.py +143 -0
- kivyforge/platforms/macos/plist.py +37 -0
- kivyforge/platforms/macos/runtime_stage.py +110 -0
- kivyforge/platforms/macos/signing.py +78 -0
- kivyforge/platforms/macos/wheels_stage.py +108 -0
- kivyforge/platforms/windows/__init__.py +133 -0
- kivyforge/platforms/windows/assets.py +76 -0
- kivyforge/platforms/windows/bundle.py +298 -0
- kivyforge/platforms/windows/cli.py +314 -0
- kivyforge/platforms/windows/doctor.py +563 -0
- kivyforge/platforms/windows/fsswap.py +87 -0
- kivyforge/platforms/windows/icons.py +64 -0
- kivyforge/platforms/windows/launcher/__init__.py +193 -0
- kivyforge/platforms/windows/launcher/build_launcher.py +507 -0
- kivyforge/platforms/windows/launcher/launcher.c +379 -0
- kivyforge/platforms/windows/lock/__init__.py +112 -0
- kivyforge/platforms/windows/lock/profile.py +101 -0
- kivyforge/platforms/windows/lock/runtime.py +64 -0
- kivyforge/platforms/windows/naming.py +52 -0
- kivyforge/platforms/windows/native_stage.py +66 -0
- kivyforge/platforms/windows/petools.py +98 -0
- kivyforge/platforms/windows/rcedit.py +116 -0
- kivyforge/platforms/windows/runtime_stage.py +221 -0
- kivyforge/platforms/windows/signing.py +168 -0
- kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
- kivyforge/platforms/windows/vendor/NOTICE +17 -0
- kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
- kivyforge/platforms/windows/vendor/TOOLSET.txt +4 -0
- kivyforge/platforms/windows/vendor/__init__.py +12 -0
- kivyforge/platforms/windows/vendor/fetch_rcedit.py +97 -0
- kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
- kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
- kivyforge/platforms/windows/wheels_stage.py +139 -0
- kivyforge-3.0.0.dev0.dist-info/METADATA +609 -0
- kivyforge-3.0.0.dev0.dist-info/RECORD +225 -0
- kivyforge-3.0.0.dev0.dist-info/WHEEL +5 -0
- kivyforge-3.0.0.dev0.dist-info/entry_points.txt +3 -0
- kivyforge-3.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- 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
|