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,269 @@
1
+ """macOS dispatch for the shared ``build`` / ``run`` / ``package`` verbs.
2
+
3
+ Keeps the macOS ``.app`` flow out of the (iOS/Xcode-centric) verb modules: each
4
+ verb resolves the target platform and, when it is macOS, calls in here. Loads the
5
+ macOS config + ``pylock.macos.toml``, runs the drift check, and drives the
6
+ ``.app`` bundler.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ import click
16
+
17
+ from kivyforge.cli._common import ToolchainError, lockfile_path_for
18
+ from kivyforge.config import ConfigError, load_config
19
+ from kivyforge.lock.reader import LockError, is_in_sync
20
+
21
+ from .. import HostCapabilityError, get_platform
22
+ from . import AppBundleError
23
+ from .bundle import build_app_bundle
24
+ from .lock import MacosLockfile
25
+ from .lock import load as load_macos_lock
26
+ from .notarize import notarize_and_staple
27
+ from .signing import sign_bundle_developer_id
28
+
29
+
30
+ def macos_build(
31
+ project_root: Path,
32
+ *,
33
+ arch: str | None,
34
+ no_verify_lock: bool,
35
+ no_cache: bool,
36
+ sign: bool = True,
37
+ release: bool = False,
38
+ ) -> Path:
39
+ """Assemble (and ad-hoc sign) the macOS ``.app``; return its path."""
40
+ _require_macos_host()
41
+ config = _load_config(project_root)
42
+ lock = _load_lock(project_root)
43
+
44
+ pyproject = project_root / "pyproject.toml"
45
+ if not no_verify_lock and not is_in_sync(lock, pyproject.read_text("utf-8")):
46
+ raise ToolchainError(
47
+ f"{lockfile_path_for('macos').name} is out of date with "
48
+ "pyproject.toml.\n"
49
+ " Run `kivyforge lock -p macos` to regenerate it (or pass "
50
+ "--no-verify-lock to build against the stale lock anyway)."
51
+ )
52
+
53
+ try:
54
+ app = build_app_bundle(
55
+ config,
56
+ lock,
57
+ project_root,
58
+ arch=arch,
59
+ sign=sign,
60
+ no_cache=no_cache,
61
+ release=release,
62
+ )
63
+ except AppBundleError as exc:
64
+ raise ToolchainError(str(exc)) from exc
65
+
66
+ click.echo(f"Built {app.relative_to(project_root)}")
67
+ return app
68
+
69
+
70
+ def macos_run(
71
+ project_root: Path,
72
+ *,
73
+ arch: str | None,
74
+ no_build: bool,
75
+ ) -> None:
76
+ """Build (unless --no-build) and launch the ``.app`` in the foreground."""
77
+ if no_build:
78
+ _require_macos_host()
79
+ config = _load_config(project_root)
80
+ app = project_root / "build" / "macos" / f"{config.display_name}.app"
81
+ if not app.exists():
82
+ raise ToolchainError(
83
+ f"no built app at {app.relative_to(project_root)}; run without "
84
+ "--no-build first."
85
+ )
86
+ else:
87
+ app = macos_build(project_root, arch=arch, no_verify_lock=False, no_cache=False)
88
+
89
+ executable = app / "Contents" / "MacOS" / _executable_name(app)
90
+ click.echo(f"Launching {app.name} ...")
91
+ # Foreground exec (not `open`) so the dev sees stdout/stderr + tracebacks,
92
+ # the desktop analog of the simulator console.
93
+ proc = subprocess.run([str(executable)])
94
+ if proc.returncode != 0:
95
+ raise ToolchainError(f"{app.name} exited with status {proc.returncode}.")
96
+
97
+
98
+ def macos_package(
99
+ project_root: Path,
100
+ *,
101
+ arch: str | None,
102
+ no_verify_lock: bool,
103
+ no_cache: bool,
104
+ signing_identity: str | None = None,
105
+ notarize: bool | None = None,
106
+ notary_profile: str | None = None,
107
+ ) -> Path:
108
+ """Produce the finished, signed ``.app`` distributable.
109
+
110
+ Signing tier is config-driven: with ``[tool.kivy.macos.signing].identity``
111
+ set (or ``--signing-identity``), the bundle is Developer-ID deep-signed
112
+ (Hardened Runtime + timestamp) and — when a notary profile is configured —
113
+ notarized and stapled. Without an identity, the ad-hoc floor applies.
114
+ """
115
+ _require_macos_host()
116
+ config = _load_config(project_root)
117
+ signing = config.macos_required.signing
118
+ identity = signing_identity or signing.identity
119
+ profile = notary_profile or signing.notary_profile
120
+ # None = config-driven: notarize whenever a profile is configured.
121
+ do_notarize = notarize if notarize is not None else bool(profile)
122
+
123
+ if not identity:
124
+ if notarize:
125
+ raise ToolchainError(
126
+ "--notarize requires Developer ID signing, but no identity is "
127
+ "configured.\n"
128
+ " Set [tool.kivy.macos.signing].identity to your 'Developer ID "
129
+ "Application: ...' certificate (or pass --signing-identity)."
130
+ )
131
+ app = macos_build(
132
+ project_root,
133
+ arch=arch,
134
+ no_verify_lock=no_verify_lock,
135
+ no_cache=no_cache,
136
+ sign=True,
137
+ release=True,
138
+ )
139
+ click.echo(
140
+ f"Packaged {app.relative_to(project_root)} (ad-hoc signed).\n"
141
+ " Distribute the .app directly, or wrap it in a .dmg with an "
142
+ "external tool (see docs). For Gatekeeper-trusted distribution, "
143
+ "configure [tool.kivy.macos.signing]."
144
+ )
145
+ return app
146
+
147
+ if do_notarize and not profile:
148
+ raise ToolchainError(
149
+ "notarization needs a notary keychain profile.\n"
150
+ " Create one: `xcrun notarytool store-credentials <name> "
151
+ "--apple-id <id> --team-id <team>` and set "
152
+ '[tool.kivy.macos.signing].notary_profile = "<name>" '
153
+ "(or pass --notary-profile)."
154
+ )
155
+
156
+ # Assemble unsigned; the Developer ID deep-sign below seals every Mach-O.
157
+ app = macos_build(
158
+ project_root,
159
+ arch=arch,
160
+ no_verify_lock=no_verify_lock,
161
+ no_cache=no_cache,
162
+ sign=False,
163
+ release=True,
164
+ )
165
+ try:
166
+ click.echo(f"Developer ID signing with {identity!r} ...")
167
+ count = sign_bundle_developer_id(
168
+ app,
169
+ identity,
170
+ extra_entitlements=config.macos_required.entitlements,
171
+ )
172
+ click.echo(f" signed {count} Mach-O binaries + the bundle")
173
+ if do_notarize:
174
+ notarize_and_staple(app, profile=profile)
175
+ except AppBundleError as exc:
176
+ raise ToolchainError(str(exc)) from exc
177
+
178
+ trust = "notarized + stapled" if do_notarize else "signed (not notarized)"
179
+ click.echo(
180
+ f"Packaged {app.relative_to(project_root)} (Developer ID {trust}).\n"
181
+ " Distribute the .app directly, or wrap it in a .dmg with an external "
182
+ "tool (see docs)."
183
+ )
184
+ return app
185
+
186
+
187
+ def macos_status(project_root: Path) -> None:
188
+ """Show app identity, Python version, lock sync, and build state."""
189
+ config = _load_config(project_root)
190
+ macos = config.macos_required
191
+ click.echo(f"App: {config.display_name} ({macos.bundle_id})")
192
+ click.echo(f"Python: {macos.python_version or '(unset)'}")
193
+ click.echo(f"Lock: {_lock_state(project_root)}")
194
+
195
+ app = project_root / "build" / "macos" / f"{config.display_name}.app"
196
+ click.echo(f"Build: {_build_state(app)}")
197
+
198
+
199
+ def _lock_state(project_root: Path) -> str:
200
+ path = lockfile_path_for("macos", project_root)
201
+ if not path.is_file():
202
+ return "missing (run `kivyforge lock -p macos`)"
203
+ try:
204
+ lock = load_macos_lock(path)
205
+ except LockError:
206
+ return "unreadable (run `kivyforge lock -p macos`)"
207
+ pyproject = project_root / "pyproject.toml"
208
+ if is_in_sync(lock, pyproject.read_text("utf-8")):
209
+ return "in sync"
210
+ return "out of date (run `kivyforge lock -p macos`)"
211
+
212
+
213
+ def _build_state(app: Path) -> str:
214
+ if not app.exists():
215
+ return "not built"
216
+ age = time.time() - app.stat().st_mtime
217
+ return f"last built {_humanize(age)}"
218
+
219
+
220
+ def _humanize(seconds: float) -> str:
221
+ seconds = int(seconds)
222
+ if seconds < 60:
223
+ return "just now"
224
+ if seconds < 3600:
225
+ m = seconds // 60
226
+ return f"{m} minute{'s' if m != 1 else ''} ago"
227
+ if seconds < 86400:
228
+ h = seconds // 3600
229
+ return f"{h} hour{'s' if h != 1 else ''} ago"
230
+ d = seconds // 86400
231
+ return f"{d} day{'s' if d != 1 else ''} ago"
232
+
233
+
234
+ def _executable_name(app: Path) -> str:
235
+ macos_dir = app / "Contents" / "MacOS"
236
+ entries = (
237
+ [p for p in macos_dir.iterdir() if p.is_file()] if macos_dir.is_dir() else []
238
+ )
239
+ if not entries:
240
+ raise ToolchainError(f"{app.name} has no launcher in Contents/MacOS.")
241
+ return entries[0].name
242
+
243
+
244
+ def _require_macos_host() -> None:
245
+ try:
246
+ get_platform("macos").check_host_capability()
247
+ except HostCapabilityError as exc:
248
+ raise ToolchainError(str(exc)) from exc
249
+
250
+
251
+ def _load_config(project_root: Path):
252
+ try:
253
+ return load_config(
254
+ project_root / "pyproject.toml", require_ios=False, require_macos=True
255
+ )
256
+ except ConfigError as exc:
257
+ raise ToolchainError(exc.format()) from exc
258
+
259
+
260
+ def _load_lock(project_root: Path) -> MacosLockfile:
261
+ path = lockfile_path_for("macos", project_root)
262
+ if not path.is_file():
263
+ raise ToolchainError(
264
+ f"no {path.name} found. Run `kivyforge lock -p macos` first."
265
+ )
266
+ try:
267
+ return load_macos_lock(path)
268
+ except LockError as exc:
269
+ raise ToolchainError(str(exc)) from exc