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,19 @@
1
+ """Health checks for ``kivyforge doctor`` (spec 05).
2
+
3
+ Checks are pure functions over an injectable :class:`Probe`, so every
4
+ PASS/WARN/FAIL/SKIP path is unit-testable without Xcode, a network, or a
5
+ keychain. ``cli/doctor.py`` wires the real probe and renders the report.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .probe import Probe, RealProbe
11
+ from .result import CheckResult, Status, worst_status
12
+
13
+ __all__ = [
14
+ "Probe",
15
+ "RealProbe",
16
+ "CheckResult",
17
+ "Status",
18
+ "worst_status",
19
+ ]
@@ -0,0 +1,95 @@
1
+ """Platform-neutral doctor checks shared by every backend (spec 05).
2
+
3
+ These checks are pure functions over an injectable :class:`Probe` and make no
4
+ assumptions about the target platform, so iOS, macOS, and Linux all reuse them.
5
+ Platform-specific checks live in ``platforms/<os>/doctor.py``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from ..config.model import Config
13
+ from ..lock.resolver import MIN_PIP_VERSION, version_str
14
+ from .probe import Probe
15
+ from .result import CheckResult, Status
16
+
17
+ SKIP_NOTE = "no pyproject.toml found in current directory"
18
+
19
+
20
+ def lock_parse_fail(platform: str, exc: Exception) -> CheckResult:
21
+ """A FAIL result for an unreadable ``pylock.<platform>.toml``."""
22
+ return CheckResult(
23
+ f"pylock.{platform}.toml",
24
+ Status.FAIL,
25
+ f"failed to parse: {exc}",
26
+ hint=f"Regenerate it with `kivyforge lock -p {platform}`.",
27
+ )
28
+
29
+
30
+ def _ver_tuple(v: str) -> tuple[int, ...]:
31
+ parts = []
32
+ for chunk in v.split("."):
33
+ digits = "".join(c for c in chunk if c.isdigit())
34
+ parts.append(int(digits) if digits else 0)
35
+ return tuple(parts)
36
+
37
+
38
+ def check_pip_version(probe: Probe) -> CheckResult:
39
+ """pip >= 24.3 is required to match PEP 730 iOS platform tags at lock time."""
40
+ minimum = version_str(MIN_PIP_VERSION)
41
+ version = probe.pip_version()
42
+ if version is None:
43
+ return CheckResult(
44
+ "pip version",
45
+ Status.WARN,
46
+ "could not determine pip version",
47
+ hint="ensure pip is available: `python -m ensurepip --upgrade`.",
48
+ )
49
+ if _ver_tuple(version) < MIN_PIP_VERSION:
50
+ return CheckResult(
51
+ "pip version",
52
+ Status.FAIL,
53
+ f"{version} (need >= {minimum})",
54
+ hint=(
55
+ "pip < 24.3 cannot match iOS platform tags (PEP 730); "
56
+ "upgrade with `python -m pip install -U pip`."
57
+ ),
58
+ )
59
+ return CheckResult("pip version", Status.PASS, version)
60
+
61
+
62
+ def check_kivyforge_version(
63
+ probe: Probe, current: str, *, offline: bool
64
+ ) -> CheckResult:
65
+ if offline:
66
+ return CheckResult("kivyforge version", Status.PASS, f"{current} (offline)")
67
+ latest = probe.latest_kivyforge_version()
68
+ if latest and _ver_tuple(latest) > _ver_tuple(current):
69
+ return CheckResult(
70
+ "kivyforge version",
71
+ Status.WARN,
72
+ f"{current} (latest {latest})",
73
+ hint="upgrade with `pip install -U kivyforge`.",
74
+ )
75
+ return CheckResult("kivyforge version", Status.PASS, current)
76
+
77
+
78
+ def check_app_dir(config: Config, project_root: Path) -> CheckResult:
79
+ """``app_dir`` must resolve to an existing directory — it carries the app's
80
+ Python source into the bundle. Config validation only checks the *string*
81
+ (relative, non-empty, etc.), not that it exists on disk; a missing one would
82
+ otherwise build an app with no code (caught at build time, but reported here
83
+ first).
84
+ """
85
+ app_dir = config.kivy.app_dir
86
+ path = project_root / app_dir
87
+ if not path.is_dir():
88
+ return CheckResult(
89
+ "App source directory",
90
+ Status.FAIL,
91
+ f"app_dir {app_dir!r} not found",
92
+ hint="create the directory or fix [tool.kivy].app_dir; it must "
93
+ "point at your app's Python source.",
94
+ )
95
+ return CheckResult("App source directory", Status.PASS, app_dir)
@@ -0,0 +1,357 @@
1
+ """Environment probes for doctor (the only place that touches the host).
2
+
3
+ ``Probe`` is the protocol the checks depend on; ``RealProbe`` is the macOS
4
+ implementation (subprocess/socket/Mach-O parsing). Unit tests supply a fake
5
+ object with the same methods, so the check logic never shells out.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import platform as _platform
12
+ import shutil
13
+ import socket
14
+ import struct
15
+ import subprocess
16
+ import sys
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Protocol
20
+
21
+ # How long to keep retrying to restore the build output after the lock probe
22
+ # moved it aside. It was just renamed there on the same volume with no open
23
+ # handles, so this normally succeeds on the first try; the retry only rides out
24
+ # a transient antivirus touch.
25
+ _RESTORE_TIMEOUT_S = 10.0
26
+
27
+ # Mach-O platform constants (LC_BUILD_VERSION `platform`).
28
+ PLATFORM_NAMES = {
29
+ 1: "macos",
30
+ 2: "ios",
31
+ 3: "tvos",
32
+ 4: "watchos",
33
+ 7: "ios-simulator",
34
+ 8: "tvos-simulator",
35
+ 9: "watchos-simulator",
36
+ }
37
+ LC_BUILD_VERSION = 0x32
38
+ _MACHO_MAGICS = {0xFEEDFACE, 0xFEEDFACF, 0xCEFAEDFE, 0xCFFAEDFE}
39
+
40
+
41
+ class Probe(Protocol):
42
+ def xcode_version(self) -> str | None: ...
43
+ def pip_version(self) -> str | None: ...
44
+ def xcode_select_path(self) -> str | None: ...
45
+ def has_xcrun_clang(self) -> bool: ...
46
+ def has_swift_toolchain(self) -> bool: ...
47
+ def simulator_runtimes(self) -> list[str]: ...
48
+ def latest_kivyforge_version(self) -> str | None: ...
49
+ def keychain_identities(self) -> list[str]: ...
50
+ def login_keychain_identities(self) -> list[str]: ...
51
+ def tcp_reachable(self, host: str, port: int) -> bool: ...
52
+ def binary_platforms(self, path: Path) -> set[str]: ...
53
+ def host_system(self) -> str: ...
54
+ def has_codesign(self) -> bool: ...
55
+ def has_notarytool(self) -> bool: ...
56
+ def is_root(self) -> bool: ...
57
+ def linux_libc(self) -> str: ...
58
+ def shared_libraries(self) -> frozenset[str]: ...
59
+ def display_session(self) -> set[str]: ...
60
+ def has_desktop_file_validate(self) -> bool: ...
61
+ def desktop_file_errors(self, path: Path) -> str | None: ...
62
+ def long_paths_enabled(self) -> bool | None: ...
63
+ def has_signtool(self) -> bool: ...
64
+ def code_signing_thumbprints(self, store_scope: str) -> list[str]: ...
65
+ def build_output_locked(self, path: Path) -> bool | None: ...
66
+ def filesystem_type(self, path: Path) -> str | None: ...
67
+
68
+
69
+ class RealProbe:
70
+ def host_system(self) -> str:
71
+ return _platform.system()
72
+
73
+ def has_codesign(self) -> bool:
74
+ return shutil.which("codesign") is not None
75
+
76
+ def has_notarytool(self) -> bool:
77
+ return bool(_capture(["xcrun", "--find", "notarytool"]))
78
+
79
+ def is_root(self) -> bool:
80
+ return hasattr(os, "geteuid") and os.geteuid() == 0
81
+
82
+ def xcode_version(self) -> str | None:
83
+ out = _capture(["xcodebuild", "-version"])
84
+ if not out:
85
+ return None
86
+ first = out.splitlines()[0]
87
+ return first.replace("Xcode", "").strip() or None
88
+
89
+ def pip_version(self) -> str | None:
90
+ # "pip 24.3.1 from /path/site-packages/pip (python 3.15)".
91
+ out = _capture([sys.executable, "-m", "pip", "--version"])
92
+ tokens = out.split()
93
+ if len(tokens) >= 2 and tokens[0] == "pip":
94
+ return tokens[1]
95
+ return None
96
+
97
+ def xcode_select_path(self) -> str | None:
98
+ return _capture(["xcode-select", "-p"]) or None
99
+
100
+ def has_xcrun_clang(self) -> bool:
101
+ return bool(_capture(["xcrun", "--find", "clang"]))
102
+
103
+ def has_swift_toolchain(self) -> bool:
104
+ return bool(_capture(["xcrun", "--find", "swift"]))
105
+
106
+ def simulator_runtimes(self) -> list[str]:
107
+ out = _capture(["xcrun", "simctl", "list", "runtimes", "iOS"])
108
+ versions = []
109
+ for line in out.splitlines():
110
+ # "iOS 18.0 (...) - com.apple.CoreSimulator.SimRuntime.iOS-18-0"
111
+ line = line.strip()
112
+ if line.startswith("iOS "):
113
+ parts = line.split()
114
+ if len(parts) >= 2:
115
+ versions.append(parts[1])
116
+ return versions
117
+
118
+ def latest_kivyforge_version(self) -> str | None:
119
+ # Best-effort PyPI lookup; failures are non-fatal.
120
+ try:
121
+ import json
122
+ import urllib.request
123
+
124
+ url = "https://pypi.org/pypi/kivyforge/json"
125
+ with urllib.request.urlopen(url, timeout=3) as resp: # noqa: S310
126
+ data = json.load(resp)
127
+ return data.get("info", {}).get("version")
128
+ except Exception:
129
+ return None
130
+
131
+ def keychain_identities(self) -> list[str]:
132
+ out = _capture(["security", "find-identity", "-v", "-p", "codesigning"])
133
+ return [line.strip() for line in out.splitlines() if line.strip()]
134
+
135
+ def login_keychain_identities(self) -> list[str]:
136
+ """Identities scoped to ``login.keychain-db`` (Apple recommends keeping
137
+ Developer ID identities out of it; see the ``Signing identity`` check).
138
+ """
139
+ login_keychain = str(
140
+ Path.home() / "Library" / "Keychains" / "login.keychain-db"
141
+ )
142
+ out = _capture(
143
+ ["security", "find-identity", "-v", "-p", "codesigning", login_keychain]
144
+ )
145
+ return [line.strip() for line in out.splitlines() if line.strip()]
146
+
147
+ def tcp_reachable(self, host: str, port: int) -> bool:
148
+ try:
149
+ with socket.create_connection((host, port), timeout=5):
150
+ return True
151
+ except OSError:
152
+ return False
153
+
154
+ def binary_platforms(self, path: Path) -> set[str]:
155
+ """Return the set of OS platforms a Mach-O (or fat) binary targets."""
156
+ try:
157
+ data = path.read_bytes()
158
+ except OSError:
159
+ return set()
160
+ return _macho_platforms(data)
161
+
162
+ def linux_libc(self) -> str:
163
+ """The host C library family: ``"glibc"``, ``"musl"``, or ``""``.
164
+
165
+ The bundled python-build-standalone runtime is a gnu/glibc ELF build,
166
+ so a musl host (Alpine) can't run it — doctor warns on that.
167
+ """
168
+ name = _platform.libc_ver()[0].lower()
169
+ if "glibc" in name or name == "libc":
170
+ return "glibc"
171
+ if any(Path("/lib").glob("ld-musl-*.so.1")):
172
+ return "musl"
173
+ return name
174
+
175
+ def shared_libraries(self) -> frozenset[str]:
176
+ """The sonames the dynamic linker's cache knows about (``ldconfig -p``)."""
177
+ out = _capture(["ldconfig", "-p"]) or _capture(["/sbin/ldconfig", "-p"])
178
+ libs: set[str] = set()
179
+ for line in out.splitlines():
180
+ token = line.strip().split(" ", 1)[0]
181
+ if token and token.startswith("lib"):
182
+ libs.add(token)
183
+ return frozenset(libs)
184
+
185
+ def display_session(self) -> set[str]:
186
+ """The display servers this session exposes via the environment."""
187
+ sessions: set[str] = set()
188
+ if os.environ.get("WAYLAND_DISPLAY"):
189
+ sessions.add("wayland")
190
+ if os.environ.get("DISPLAY"):
191
+ sessions.add("x11")
192
+ return sessions
193
+
194
+ def has_desktop_file_validate(self) -> bool:
195
+ return shutil.which("desktop-file-validate") is not None
196
+
197
+ def desktop_file_errors(self, path: Path) -> str | None:
198
+ try:
199
+ proc = subprocess.run(
200
+ ["desktop-file-validate", str(path)], capture_output=True, text=True
201
+ )
202
+ except (OSError, ValueError):
203
+ return None
204
+ if proc.returncode == 0:
205
+ return None
206
+ return (proc.stdout + proc.stderr).strip() or "validation failed"
207
+
208
+ def long_paths_enabled(self) -> bool | None:
209
+ """Whether ``LongPathsEnabled`` is on (``None`` when it can't be read).
210
+
211
+ Reads ``HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem`` — the
212
+ per-machine switch that lifts the legacy 260-char ``MAX_PATH`` limit that
213
+ deep bundle trees (a full PBS prefix under ``build/windows``) can hit.
214
+ """
215
+ try:
216
+ import winreg
217
+ except ImportError:
218
+ return None
219
+ try:
220
+ with winreg.OpenKey(
221
+ winreg.HKEY_LOCAL_MACHINE,
222
+ r"SYSTEM\CurrentControlSet\Control\FileSystem",
223
+ ) as key:
224
+ value, _ = winreg.QueryValueEx(key, "LongPathsEnabled")
225
+ return bool(value)
226
+ except OSError:
227
+ return None
228
+
229
+ def has_signtool(self) -> bool:
230
+ return shutil.which("signtool") is not None
231
+
232
+ def code_signing_thumbprints(self, store_scope: str) -> list[str]:
233
+ """Thumbprints of code-signing certs in the store *signtool* will use.
234
+
235
+ ``machine`` -> ``Cert:\\LocalMachine\\My`` (signtool's ``/sm``);
236
+ anything else -> ``Cert:\\CurrentUser\\My``. Thumbprints are normalized
237
+ to upper-case with no spaces to match the configured value.
238
+ """
239
+ store = "LocalMachine" if store_scope == "machine" else "CurrentUser"
240
+ script = (
241
+ f"Get-ChildItem Cert:\\{store}\\My -CodeSigningCert "
242
+ "| ForEach-Object { $_.Thumbprint }"
243
+ )
244
+ out = _capture(["powershell", "-NoProfile", "-Command", script])
245
+ return [
246
+ line.strip().replace(" ", "").upper()
247
+ for line in out.splitlines()
248
+ if line.strip()
249
+ ]
250
+
251
+ def build_output_locked(self, path: Path) -> bool | None:
252
+ """Whether a built onedir *path* is currently held open by a process.
253
+
254
+ ``None`` when *path* does not exist (nothing built). Otherwise performs
255
+ the exact metadata rename ``build``/``package`` will do — move the tree
256
+ aside — and immediately restores it, returning ``True`` when a sharing/
257
+ access lock blocks it (a running instance of the app, or an Explorer/
258
+ terminal window sitting in the folder), ``False`` when it is free.
259
+
260
+ A diagnostic must never lose the user's build: if the aside succeeds the
261
+ restore is retried and, in the vanishingly unlikely event it still fails,
262
+ raises ``OSError`` naming where the tree now sits — rather than silently
263
+ leaving the live build displaced under a hidden name.
264
+ """
265
+ if not path.exists():
266
+ return None
267
+ aside = path.with_name(f".{path.name}.doctorlock-{os.getpid()}")
268
+ try:
269
+ os.replace(path, aside)
270
+ except OSError:
271
+ return True
272
+ deadline = time.monotonic() + _RESTORE_TIMEOUT_S
273
+ delay = 0.1
274
+ while True:
275
+ try:
276
+ os.replace(aside, path)
277
+ return False
278
+ except OSError as exc:
279
+ if time.monotonic() >= deadline:
280
+ raise OSError(
281
+ f"doctor moved {path.name!r} aside to test its build "
282
+ f"lock but could not restore it; the build output is now "
283
+ f"at {aside}. Move it back to {path} manually."
284
+ ) from exc
285
+ time.sleep(delay)
286
+ delay = min(delay * 1.5, 1.0)
287
+
288
+ def filesystem_type(self, path: Path) -> str | None:
289
+ """The filesystem of the volume holding *path* (e.g. ``"NTFS"`` /
290
+ ``"ReFS"``), or ``None`` when it can't be read. A trusted ReFS Dev Drive
291
+ is where Defender runs asynchronous 'performance mode' scans.
292
+ """
293
+ import ctypes
294
+
295
+ if not hasattr(ctypes, "windll"):
296
+ return None
297
+ drive = os.path.splitdrive(os.path.abspath(str(path)))[0]
298
+ if not drive:
299
+ return None
300
+ buf = ctypes.create_unicode_buffer(261)
301
+ try:
302
+ ok = ctypes.windll.kernel32.GetVolumeInformationW( # type: ignore[attr-defined]
303
+ drive + "\\", None, 0, None, None, None, buf, len(buf)
304
+ )
305
+ except OSError:
306
+ return None
307
+ return buf.value if ok else None
308
+
309
+
310
+ def _capture(argv: list[str]) -> str:
311
+ try:
312
+ proc = subprocess.run(argv, capture_output=True, text=True)
313
+ except (OSError, ValueError):
314
+ return ""
315
+ return proc.stdout if proc.returncode == 0 else ""
316
+
317
+
318
+ def _macho_platforms(data: bytes) -> set[str]:
319
+ if len(data) < 4:
320
+ return set()
321
+ magic = struct.unpack(">I", data[:4])[0]
322
+ # Fat binary: parse each arch slice.
323
+ if magic in (0xCAFEBABE, 0xBEBAFECA):
324
+ platforms: set[str] = set()
325
+ nfat = struct.unpack(">I", data[4:8])[0]
326
+ for i in range(nfat):
327
+ off = 8 + i * 20
328
+ (offset,) = struct.unpack(">I", data[off + 8 : off + 12])
329
+ platforms |= _thin_platforms(data[offset:])
330
+ return platforms
331
+ return _thin_platforms(data)
332
+
333
+
334
+ def _thin_platforms(data: bytes) -> set[str]:
335
+ if len(data) < 4:
336
+ return set()
337
+ magic = struct.unpack(">I", data[:4])[0]
338
+ if magic not in _MACHO_MAGICS:
339
+ return set()
340
+ little = magic in (0xCEFAEDFE, 0xCFFAEDFE)
341
+ is64 = magic in (0xFEEDFACF, 0xCFFAEDFE)
342
+ end = "<" if little else ">"
343
+ header = 32 if is64 else 28
344
+ ncmds = struct.unpack(end + "I", data[16:20])[0]
345
+ offset = header
346
+ platforms: set[str] = set()
347
+ for _ in range(ncmds):
348
+ if offset + 8 > len(data):
349
+ break
350
+ cmd, cmdsize = struct.unpack(end + "II", data[offset : offset + 8])
351
+ if cmd == LC_BUILD_VERSION and offset + 16 <= len(data):
352
+ (plat,) = struct.unpack(end + "I", data[offset + 8 : offset + 12])
353
+ platforms.add(PLATFORM_NAMES.get(plat, f"platform-{plat}"))
354
+ if cmdsize == 0:
355
+ break
356
+ offset += cmdsize
357
+ return platforms
@@ -0,0 +1,41 @@
1
+ """Check result types for doctor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+
9
+ class Status(Enum):
10
+ PASS = "PASS"
11
+ WARN = "WARN"
12
+ FAIL = "FAIL"
13
+ SKIP = "SKIP"
14
+
15
+
16
+ # Severity ordering for computing the overall worst status.
17
+ _ORDER = {Status.PASS: 0, Status.SKIP: 0, Status.WARN: 1, Status.FAIL: 2}
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CheckResult:
22
+ name: str
23
+ status: Status
24
+ detail: str = ""
25
+ hint: str = ""
26
+
27
+ def render(self) -> str:
28
+ line = f"[{self.status.value}] {self.name}"
29
+ if self.detail:
30
+ line += f": {self.detail}"
31
+ if self.hint and self.status in (Status.WARN, Status.FAIL):
32
+ line += f"\n hint: {self.hint}"
33
+ return line
34
+
35
+
36
+ def worst_status(results: list[CheckResult]) -> Status:
37
+ worst = Status.PASS
38
+ for r in results:
39
+ if _ORDER[r.status] > _ORDER[worst]:
40
+ worst = r.status
41
+ return worst
@@ -0,0 +1,42 @@
1
+ """kivyforge shared lock core (spec 02).
2
+
3
+ Platform-neutral lock building blocks shared by every backend: the PEP 751
4
+ ``[[packages]]`` model, the drift check, the neutral pip helpers, and the
5
+ generic wheel+runtime engine (``kivyforge.lock.wheelruntime``). The iOS lock
6
+ surface lives in ``kivyforge.platforms.ios.lock``; the macOS/Linux surfaces in
7
+ ``kivyforge.platforms.<os>.lock``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .model import (
13
+ LOCK_VERSION,
14
+ LockedPackage,
15
+ LockedWheel,
16
+ PackageDep,
17
+ canonical_name,
18
+ )
19
+ from .reader import LockError, compute_pyproject_sha256, is_in_sync
20
+ from .resolver import (
21
+ MIN_PIP_VERSION,
22
+ abi_tags,
23
+ pip_python_version,
24
+ pip_version,
25
+ version_str,
26
+ )
27
+
28
+ __all__ = [
29
+ "LOCK_VERSION",
30
+ "LockedPackage",
31
+ "LockedWheel",
32
+ "PackageDep",
33
+ "canonical_name",
34
+ "LockError",
35
+ "compute_pyproject_sha256",
36
+ "is_in_sync",
37
+ "MIN_PIP_VERSION",
38
+ "abi_tags",
39
+ "pip_python_version",
40
+ "pip_version",
41
+ "version_str",
42
+ ]
@@ -0,0 +1,63 @@
1
+ """Run pip with the *target's* PEP 508 marker environment.
2
+
3
+ kivyforge invokes this instead of ``-m pip`` when cross-resolving. It replaces
4
+ ``packaging.markers.default_environment`` — the single function every
5
+ ``Marker.evaluate()`` consults — with the target environment supplied as JSON in
6
+ ``KIVYFORGE_TARGET_MARKER_ENV``, then hands control to pip unchanged.
7
+
8
+ pip still performs the whole resolution, including backtracking and platform
9
+ wheel-tag expansion; only its answer to "what platform am I resolving for?"
10
+ changes. kivyforge deliberately does not reimplement any of that.
11
+
12
+ Platform-neutral by design: each backend describes its own target
13
+ (``platforms/android/lock/markers.py``, ``platforms/ios/lock/markers.py``).
14
+
15
+ This is a documented dependency on a pip internal, so it fails loudly rather
16
+ than silently falling back to host markers: a quiet fallback would reintroduce
17
+ exactly the host-dependent lock this exists to prevent.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+
26
+ MARKER_ENV_VAR = "KIVYFORGE_TARGET_MARKER_ENV"
27
+
28
+
29
+ def main(argv: list[str]) -> int:
30
+ raw = os.environ.get(MARKER_ENV_VAR)
31
+ if not raw:
32
+ sys.stderr.write(
33
+ f"{MARKER_ENV_VAR} is not set; refusing to resolve with the "
34
+ "host's marker environment.\n"
35
+ )
36
+ return 2
37
+ environment = json.loads(raw)
38
+
39
+ try:
40
+ from pip._vendor.packaging import markers
41
+ except ImportError:
42
+ sys.stderr.write(
43
+ "this pip does not vendor packaging.markers where kivyforge "
44
+ "expects it, so environment markers cannot be evaluated for the "
45
+ "target platform. Please report the pip version.\n"
46
+ )
47
+ return 3
48
+ if not hasattr(markers, "default_environment"):
49
+ sys.stderr.write(
50
+ "pip's vendored packaging has no default_environment(); kivyforge "
51
+ "cannot retarget marker evaluation. Please report the pip version.\n"
52
+ )
53
+ return 3
54
+
55
+ markers.default_environment = lambda: dict(environment)
56
+
57
+ from pip._internal.cli.main import main as pip_main
58
+
59
+ return pip_main(argv)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ raise SystemExit(main(sys.argv[1:]))