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,480 @@
1
+ """macOS-specific doctor checks (macos-spec).
2
+
3
+ Reuses the platform-neutral checks (pip, toolchain version, app source dir) from
4
+ ``checks`` and adds the macOS environment (host + codesign) and project checks
5
+ (arch coverage, runtime floor, icon, find_links, reachable hosts) over the
6
+ ``pylock.macos.toml`` model.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from urllib.parse import urlparse
13
+
14
+ from kivyforge.config import ConfigError, load_config
15
+ from kivyforge.config.icons import APP_ICON_SIZE, icon_source_problem
16
+ from kivyforge.config.model import Config
17
+ from kivyforge.doctor import checks_common as C
18
+ from kivyforge.doctor.checks_common import _ver_tuple
19
+ from kivyforge.doctor.probe import Probe, RealProbe
20
+ from kivyforge.doctor.result import CheckResult, Status
21
+ from kivyforge.lock.find_links import find_links_doctor_detail
22
+ from kivyforge.lock.reader import LockError
23
+
24
+ from .lock import MacosLockfile, load, wheel_arch
25
+ from .machotools import is_macho, macho_arches
26
+
27
+
28
+ def check_macos_host(probe: Probe) -> CheckResult:
29
+ host = probe.host_system()
30
+ if host != "Darwin":
31
+ return CheckResult(
32
+ "Host is macOS",
33
+ Status.FAIL,
34
+ f"host is {host!r}",
35
+ hint="the macOS .app backend (codesign + lipo) requires a macOS host.",
36
+ )
37
+ return CheckResult("Host is macOS", Status.PASS, "Darwin")
38
+
39
+
40
+ def check_codesign(probe: Probe) -> CheckResult:
41
+ if not probe.has_codesign():
42
+ return CheckResult(
43
+ "Codesign available",
44
+ Status.FAIL,
45
+ "codesign not found",
46
+ hint="install the Xcode command-line tools: `xcode-select --install`.",
47
+ )
48
+ return CheckResult("Codesign available", Status.PASS, "codesign present")
49
+
50
+
51
+ def check_macos_not_root(probe: Probe) -> CheckResult:
52
+ """Apple DTS: signing as root/sudo mixes execution contexts and is a common
53
+ source of keychain errors (e.g. ``errSecInternalComponent``) — see
54
+ "Resolving errSecInternalComponent errors during code signing".
55
+ """
56
+ if probe.is_root():
57
+ return CheckResult(
58
+ "Not running as root",
59
+ Status.WARN,
60
+ "running as root/sudo",
61
+ hint="codesign relies on the keychain, which is tied to a login "
62
+ "security context; running as root (or via sudo) often breaks "
63
+ "that context. Run `kivyforge package` as your normal user.",
64
+ )
65
+ return CheckResult("Not running as root", Status.PASS, "normal user")
66
+
67
+
68
+ def check_macos_app_icon(config: Config, project_root: Path) -> CheckResult:
69
+ source = config.macos_required.icons.source
70
+ if not source:
71
+ return CheckResult("App icon", Status.SKIP, "not configured")
72
+ problem = icon_source_problem((project_root / source).resolve())
73
+ if problem:
74
+ return CheckResult("App icon", Status.FAIL, f"{source} invalid", hint=problem)
75
+ return CheckResult("App icon", Status.PASS, f"{APP_ICON_SIZE}x{APP_ICON_SIZE} PNG")
76
+
77
+
78
+ def check_macos_find_links(config: Config, project_root: Path) -> CheckResult:
79
+ entries = config.macos_required.find_links
80
+ if not entries:
81
+ return CheckResult("find_links directories", Status.SKIP, "not configured")
82
+ root = project_root.resolve()
83
+ problems: list[tuple[str, str | None]] = []
84
+ warnings: list[tuple[str, str | None]] = []
85
+ ok: list[str] = []
86
+ for entry in entries:
87
+ path = (root / entry).resolve()
88
+ detail, hint = find_links_doctor_detail(root, entry, path, platform="macos")
89
+ if not path.is_dir():
90
+ problems.append((detail, hint))
91
+ elif not any(path.glob("*.whl")):
92
+ warnings.append((detail, hint))
93
+ else:
94
+ ok.append(detail)
95
+ if problems:
96
+ return CheckResult(
97
+ "find_links directories",
98
+ Status.FAIL,
99
+ "; ".join(d for d, _ in problems),
100
+ hint=next((h for _, h in problems if h), ""),
101
+ )
102
+ if warnings:
103
+ return CheckResult(
104
+ "find_links directories",
105
+ Status.WARN,
106
+ "; ".join(d for d, _ in warnings),
107
+ hint=next((h for _, h in warnings if h), ""),
108
+ )
109
+ return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
110
+
111
+
112
+ def check_macos_arch_coverage(
113
+ config: Config, lock: MacosLockfile | None
114
+ ) -> CheckResult:
115
+ if lock is None:
116
+ return CheckResult("Architecture coverage", Status.SKIP, "no pylock.macos.toml")
117
+ # macOS is arm64-only, so this is a single-arch check: every compiled
118
+ # dependency needs an arm64 or universal2 wheel (universal2 contains arm64).
119
+ target = config.macos_required.archs[0]
120
+
121
+ if target not in {a.arch for a in lock.python_runtime.artifacts}:
122
+ return CheckResult(
123
+ "Architecture coverage",
124
+ Status.FAIL,
125
+ f"runtime missing arch: {target}",
126
+ hint="re-run `kivyforge lock -p macos`.",
127
+ )
128
+
129
+ for pkg in lock.packages:
130
+ if any(w.is_pure_python for w in pkg.wheels):
131
+ continue
132
+ if not any(
133
+ wheel_arch(w.platform_tag) in ("universal2", target) for w in pkg.wheels
134
+ ):
135
+ return CheckResult(
136
+ "Architecture coverage",
137
+ Status.FAIL,
138
+ f"{pkg.name} has no {target} wheel",
139
+ hint=f"a compiled dep needs a {target} or universal2 wheel; re-lock.",
140
+ )
141
+ return CheckResult("Architecture coverage", Status.PASS, f"all deps cover {target}")
142
+
143
+
144
+ def check_macos_runtime_floor(
145
+ config: Config, lock: MacosLockfile | None
146
+ ) -> CheckResult:
147
+ declared = config.macos_required.minimum_system_version
148
+ floor = lock.python_runtime.floor if lock else None
149
+ if not declared or not floor:
150
+ return CheckResult(
151
+ "Runtime floor", Status.SKIP, "no explicit minimum_system_version/floor"
152
+ )
153
+ if _ver_tuple(declared) < _ver_tuple(floor):
154
+ return CheckResult(
155
+ "Runtime floor",
156
+ Status.FAIL,
157
+ f"minimum_system_version {declared} < runtime floor {floor}",
158
+ hint=f"raise [tool.kivy.macos].minimum_system_version to >= {floor}.",
159
+ )
160
+ return CheckResult("Runtime floor", Status.PASS, f"{declared} >= {floor}")
161
+
162
+
163
+ def check_macos_signing_identity(probe: Probe, config: Config) -> CheckResult:
164
+ """When Developer ID signing is configured, the identity is in the keychain.
165
+
166
+ Also nudges towards Apple's "Care and Feeding of Developer ID" best practice
167
+ of keeping the identity out of ``login.keychain-db``: a dedicated keychain
168
+ isolates the private key from unrelated login-keychain churn (password
169
+ resets, iCloud Keychain sync, etc.) that can corrupt its ACL and produce a
170
+ hard-to-diagnose ``errSecInternalComponent`` from ``codesign`` (see FAQ).
171
+ """
172
+ signing = config.macos_required.signing
173
+ if not signing.configured:
174
+ return CheckResult(
175
+ "Signing identity",
176
+ Status.SKIP,
177
+ "not configured (ad-hoc floor applies)",
178
+ )
179
+ identities = probe.keychain_identities()
180
+ matches = [line for line in identities if signing.identity in line]
181
+ if not matches:
182
+ return CheckResult(
183
+ "Signing identity",
184
+ Status.FAIL,
185
+ f"identity {signing.identity!r} not in keychain",
186
+ hint="import your 'Developer ID Application' certificate (Xcode -> "
187
+ "Settings -> Accounts -> Manage Certificates), or fix "
188
+ "[tool.kivy.macos.signing].identity.",
189
+ )
190
+ if len(matches) > 1:
191
+ return CheckResult(
192
+ "Signing identity",
193
+ Status.FAIL,
194
+ f"{len(matches)} certificates match {signing.identity!r} across your "
195
+ "keychains",
196
+ hint="codesign refuses to pick between identically-named "
197
+ "certificates in different keychains ('ambiguous'). Delete the "
198
+ "stale/duplicate copy (Keychain Access -> Certificates tab) so "
199
+ "only one remains.",
200
+ )
201
+ login_identities = probe.login_keychain_identities()
202
+ if any(signing.identity in line for line in login_identities):
203
+ return CheckResult(
204
+ "Signing identity",
205
+ Status.WARN,
206
+ f"{signing.identity} is in your login keychain",
207
+ hint="Apple recommends keeping Developer ID identities out of "
208
+ "login.keychain-db in a dedicated keychain, so unrelated login-"
209
+ "keychain issues can't corrupt the signing key. See FAQ.md "
210
+ "('macOS Developer ID signing fails with errSecInternalComponent') "
211
+ "for the migration steps.",
212
+ )
213
+ return CheckResult("Signing identity", Status.PASS, signing.identity)
214
+
215
+
216
+ def check_macos_signing_identity_type(config: Config) -> CheckResult:
217
+ """Notarization only accepts a 'Developer ID Application' identity (or
218
+ 'Developer ID Installer' for pkgs); other identity types (e.g. 'Apple
219
+ Development', used for day-to-day debugging) fail with "The binary is not
220
+ signed with a valid Developer ID certificate." — see Apple's "Resolving
221
+ common notarization issues".
222
+ """
223
+ signing = config.macos_required.signing
224
+ if not signing.configured:
225
+ return CheckResult(
226
+ "Signing identity type",
227
+ Status.SKIP,
228
+ "not configured (ad-hoc floor applies)",
229
+ )
230
+ if (
231
+ "Developer ID Application" in signing.identity
232
+ or "Developer ID Installer" in signing.identity
233
+ ):
234
+ return CheckResult("Signing identity type", Status.PASS, "Developer ID")
235
+ return CheckResult(
236
+ "Signing identity type",
237
+ Status.WARN,
238
+ f"{signing.identity!r} doesn't look like a Developer ID identity",
239
+ hint="notarization requires a 'Developer ID Application' (or "
240
+ "'Developer ID Installer') certificate — not 'Apple Development' or "
241
+ "another type, which is for local debugging only.",
242
+ )
243
+
244
+
245
+ def check_macos_notary_setup(probe: Probe, config: Config) -> CheckResult:
246
+ """When a notary profile is configured, notarytool must be available.
247
+
248
+ Profile *validity* needs a network round-trip to Apple, so it is verified
249
+ at submit time; doctor only validates the local tooling.
250
+ """
251
+ signing = config.macos_required.signing
252
+ if not signing.notary_profile:
253
+ return CheckResult("Notary setup", Status.SKIP, "no notary_profile")
254
+ if not probe.has_notarytool():
255
+ return CheckResult(
256
+ "Notary setup",
257
+ Status.FAIL,
258
+ "xcrun notarytool not found",
259
+ hint="notarytool ships with recent Xcode command-line tools; run "
260
+ "`xcode-select --install` / update the CLT.",
261
+ )
262
+ return CheckResult(
263
+ "Notary setup",
264
+ Status.PASS,
265
+ f"profile {signing.notary_profile!r}; notarytool present "
266
+ "(profile validity is checked at submit time)",
267
+ )
268
+
269
+
270
+ def check_macos_native_binaries(config: Config, project_root: Path) -> CheckResult:
271
+ """Vendored native-binary sources exist and (once built) cover the archs.
272
+
273
+ SKIPs when no ``[tool.kivy.macos.native.binaries]`` are declared. Vendored
274
+ (repo-relative) sources must be present on disk; URL sources are re-checked
275
+ for reachability by ``check_macos_hosts_reachable``. No two single-file
276
+ (non-zip) sources may share a staged basename (they would overwrite each
277
+ other in ``bin/``; zip-member collisions are caught at build time). When the
278
+ bundle has been built, each Mach-O staged under ``Resources/bin`` must carry
279
+ the target arch.
280
+ """
281
+ declared = config.macos_required.binaries
282
+ if not declared:
283
+ return CheckResult("Native binaries", Status.SKIP, "not configured")
284
+
285
+ root = project_root.resolve()
286
+ missing = [
287
+ dep.source
288
+ for dep in declared
289
+ if not dep.source.startswith(("http://", "https://"))
290
+ and not (root / dep.source).is_file()
291
+ ]
292
+ if missing:
293
+ return CheckResult(
294
+ "Native binaries",
295
+ Status.FAIL,
296
+ f"vendored source(s) not found: {', '.join(missing)}",
297
+ hint="build/vendor the artifact under the project and re-run "
298
+ "`kivyforge lock -p macos`.",
299
+ )
300
+
301
+ # Statically-detectable staging collisions: two single-file (non-zip)
302
+ # sources whose basenames match land on the same bin/<name> and the second
303
+ # silently clobbers the first. Zip-member collisions need extraction and are
304
+ # caught (loudly) at build time.
305
+ staged: dict[str, str] = {}
306
+ for dep in declared:
307
+ if dep.source.lower().endswith(".zip"):
308
+ continue
309
+ base = dep.source.rsplit("/", 1)[-1] or dep.name
310
+ prev = staged.get(base)
311
+ if prev is not None:
312
+ return CheckResult(
313
+ "Native binaries",
314
+ Status.FAIL,
315
+ f"{prev!r} and {dep.name!r} both stage bin/{base}",
316
+ hint="two native binaries would overwrite each other in "
317
+ "Contents/Resources/bin; rename one artifact/source so each "
318
+ "stages to a unique name.",
319
+ )
320
+ staged[base] = dep.name
321
+
322
+ target = config.macos_required.archs[0]
323
+ bin_dir = (
324
+ root
325
+ / "build"
326
+ / "macos"
327
+ / f"{config.display_name}.app"
328
+ / "Contents"
329
+ / "Resources"
330
+ / "bin"
331
+ )
332
+ if bin_dir.is_dir():
333
+ problems: list[str] = []
334
+ machos = 0
335
+ for p in sorted(bin_dir.rglob("*")):
336
+ if not is_macho(p):
337
+ continue
338
+ machos += 1
339
+ if target not in set(macho_arches(p)):
340
+ problems.append(f"{p.name} missing {target}")
341
+ if problems:
342
+ return CheckResult(
343
+ "Native binaries",
344
+ Status.FAIL,
345
+ "; ".join(problems),
346
+ hint=f"rebuild the native binaries for {target} "
347
+ f"(e.g. clang -arch {target}), then re-lock.",
348
+ )
349
+ return CheckResult(
350
+ "Native binaries",
351
+ Status.PASS,
352
+ f"{machos} Mach-O cover {target}",
353
+ )
354
+ return CheckResult(
355
+ "Native binaries",
356
+ Status.PASS,
357
+ f"{len(declared)} declared; sources present (build to verify arch coverage)",
358
+ )
359
+
360
+
361
+ def check_macos_hosts_reachable(
362
+ probe: Probe, lock: MacosLockfile | None
363
+ ) -> CheckResult:
364
+ if lock is None:
365
+ return CheckResult(
366
+ "Required hosts reachable", Status.SKIP, "no pylock.macos.toml"
367
+ )
368
+ hosts: set[str] = set()
369
+ for pkg in lock.packages:
370
+ for wheel in pkg.wheels:
371
+ if wheel.url:
372
+ _add_host(hosts, wheel.url)
373
+ for art in lock.python_runtime.artifacts:
374
+ if art.url:
375
+ _add_host(hosts, art.url)
376
+ for nb in lock.native_binaries:
377
+ if nb.url:
378
+ _add_host(hosts, nb.url)
379
+ if not hosts:
380
+ return CheckResult(
381
+ "Required hosts reachable", Status.PASS, "all artifacts vendored"
382
+ )
383
+ unreachable = [h for h in sorted(hosts) if not probe.tcp_reachable(h, 443)]
384
+ if unreachable:
385
+ return CheckResult(
386
+ "Required hosts reachable",
387
+ Status.FAIL,
388
+ f"unreachable: {', '.join(unreachable)}",
389
+ hint="check your network/proxy; these hosts serve pinned artifacts.",
390
+ )
391
+ return CheckResult(
392
+ "Required hosts reachable", Status.PASS, ", ".join(sorted(hosts))
393
+ )
394
+
395
+
396
+ def _add_host(hosts: set[str], url: str) -> None:
397
+ netloc = urlparse(url).hostname
398
+ if netloc:
399
+ hosts.add(netloc)
400
+
401
+
402
+ def run_macos_checks(
403
+ probe: Probe,
404
+ *,
405
+ kivyforge_version: str,
406
+ config: Config | None = None,
407
+ project_root: Path | None = None,
408
+ lock: MacosLockfile | None = None,
409
+ offline: bool = False,
410
+ ) -> list[CheckResult]:
411
+ """Run macOS doctor checks (macos-spec). Project checks SKIP without config."""
412
+ project_root = project_root or Path.cwd()
413
+ results = [
414
+ check_macos_host(probe),
415
+ check_codesign(probe),
416
+ check_macos_not_root(probe),
417
+ C.check_pip_version(probe),
418
+ C.check_kivyforge_version(probe, kivyforge_version, offline=offline),
419
+ ]
420
+
421
+ if config is None:
422
+ for name in (
423
+ "App source directory",
424
+ "Architecture coverage",
425
+ "Runtime floor",
426
+ "App icon",
427
+ "find_links directories",
428
+ "Native binaries",
429
+ "Signing identity",
430
+ "Signing identity type",
431
+ "Notary setup",
432
+ "Required hosts reachable",
433
+ ):
434
+ results.append(CheckResult(name, Status.SKIP, C.SKIP_NOTE))
435
+ return results
436
+
437
+ results += [
438
+ C.check_app_dir(config, project_root),
439
+ check_macos_arch_coverage(config, lock),
440
+ check_macos_runtime_floor(config, lock),
441
+ check_macos_app_icon(config, project_root),
442
+ check_macos_find_links(config, project_root),
443
+ check_macos_native_binaries(config, project_root),
444
+ check_macos_signing_identity(probe, config),
445
+ check_macos_signing_identity_type(config),
446
+ check_macos_notary_setup(probe, config),
447
+ check_macos_hosts_reachable(probe, lock),
448
+ ]
449
+ return results
450
+
451
+
452
+ def macos_doctor(
453
+ cwd: Path, *, kivyforge_version: str, offline: bool
454
+ ) -> list[CheckResult]:
455
+ """Load the macOS project + lock (if present) and run the macOS check set."""
456
+ pyproject = cwd / "pyproject.toml"
457
+ config = None
458
+ lock = None
459
+ parse_results: list[CheckResult] = []
460
+ if pyproject.is_file():
461
+ try:
462
+ config = load_config(pyproject, require_ios=False, require_macos=True)
463
+ except ConfigError as exc:
464
+ parse_results.append(
465
+ CheckResult("pyproject.toml", Status.FAIL, exc.format())
466
+ )
467
+ lockfile = cwd / "pylock.macos.toml"
468
+ if lockfile.is_file():
469
+ try:
470
+ lock = load(lockfile)
471
+ except LockError as exc:
472
+ parse_results.append(C.lock_parse_fail("macos", exc))
473
+ return parse_results + run_macos_checks(
474
+ RealProbe(),
475
+ kivyforge_version=kivyforge_version,
476
+ config=config,
477
+ project_root=cwd,
478
+ lock=lock,
479
+ offline=offline,
480
+ )
@@ -0,0 +1,75 @@
1
+ """Generate a macOS ``.icns`` from a 1024×1024 PNG source (macos-spec).
2
+
3
+ Uses the system ``sips`` (resize) + ``iconutil`` (iconset → icns), so no image
4
+ libraries are needed. The source is validated as a 1024×1024 PNG first, matching
5
+ the iOS icon discipline.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import subprocess
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ from kivyforge.config.icons import IconSourceError, validate_icon_source
16
+
17
+ from . import AppBundleError
18
+
19
+ # (pixel size, iconset filename) pairs Apple's iconutil expects. Retina (@2x)
20
+ # variants are rendered at 2× the point size from the single 1024² source.
21
+ _ICONSET = [
22
+ (16, "icon_16x16.png"),
23
+ (32, "icon_16x16@2x.png"),
24
+ (32, "icon_32x32.png"),
25
+ (64, "icon_32x32@2x.png"),
26
+ (128, "icon_128x128.png"),
27
+ (256, "icon_128x128@2x.png"),
28
+ (256, "icon_256x256.png"),
29
+ (512, "icon_256x256@2x.png"),
30
+ (512, "icon_512x512.png"),
31
+ (1024, "icon_512x512@2x.png"),
32
+ ]
33
+
34
+
35
+ def generate_icns(source: Path, dest: Path) -> None:
36
+ """Render *source* (1024×1024 PNG) into an ``.icns`` at *dest*."""
37
+ validate_icon_source(source)
38
+ with tempfile.TemporaryDirectory(prefix="kivy-icns-") as tmp:
39
+ iconset = Path(tmp) / "icon.iconset"
40
+ iconset.mkdir()
41
+ for size, name in _ICONSET:
42
+ _sips_resize(source, iconset / name, size)
43
+ _iconutil(iconset, dest)
44
+
45
+
46
+ def _sips_resize(source: Path, out: Path, size: int) -> None:
47
+ _run(
48
+ [
49
+ "sips",
50
+ "-z",
51
+ str(size),
52
+ str(size),
53
+ str(source),
54
+ "--out",
55
+ str(out),
56
+ ]
57
+ )
58
+
59
+
60
+ def _iconutil(iconset: Path, dest: Path) -> None:
61
+ dest.parent.mkdir(parents=True, exist_ok=True)
62
+ _run(["iconutil", "-c", "icns", str(iconset), "-o", str(dest)])
63
+
64
+
65
+ def _run(cmd: list[str]) -> None:
66
+ if shutil.which(cmd[0]) is None:
67
+ raise AppBundleError(
68
+ f"required macOS tool {cmd[0]!r} not found (ships with macOS)."
69
+ )
70
+ proc = subprocess.run(cmd, capture_output=True, text=True)
71
+ if proc.returncode != 0:
72
+ raise AppBundleError(f"{cmd[0]} failed: {(proc.stderr or proc.stdout).strip()}")
73
+
74
+
75
+ __all__ = ["AppBundleError", "IconSourceError", "generate_icns"]
@@ -0,0 +1,118 @@
1
+ """The ``.app`` launcher — a compiled Mach-O stub (the iOS ``main.m`` analog).
2
+
3
+ The bundle's ``CFBundleExecutable`` must be a real Mach-O binary, not a shell
4
+ script: Finder/LaunchServices treats a text-script main executable as a document
5
+ and spawns a Terminal/console window for it, and a script also can't be
6
+ ad-hoc-signed as the app's main image. So kivyforge compiles a tiny C launcher
7
+ (with ``clang`` from the Xcode command-line tools, which are already required for
8
+ ``codesign``). At runtime it locates the bundle relative to itself — so the
9
+ ``.app`` stays fully relocatable — points the bundled CPython at its own home +
10
+ the app's ``lib``/``app`` directories, and ``execv``s the entry-point module.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+ from . import AppBundleError
20
+
21
+ # The launcher is intentionally minimal: resolve its own path, derive the bundle
22
+ # layout, set the Python environment, and exec the interpreter on the entry
23
+ # script. ``{entry}`` is filled with the (identifier-only) entry-point name.
24
+ _SOURCE = r"""
25
+ #include <stdio.h>
26
+ #include <stdlib.h>
27
+ #include <string.h>
28
+ #include <limits.h>
29
+ #include <libgen.h>
30
+ #include <unistd.h>
31
+ #include <mach-o/dyld.h>
32
+
33
+ static const char *ENTRY = "{entry}";
34
+
35
+ int main(int argc, char *argv[]) {{
36
+ char raw[PATH_MAX];
37
+ uint32_t size = sizeof(raw);
38
+ if (_NSGetExecutablePath(raw, &size) != 0) return 71;
39
+ char exe[PATH_MAX];
40
+ if (realpath(raw, exe) == NULL) return 71; /* .../Contents/MacOS/<name> */
41
+
42
+ char a[PATH_MAX];
43
+ strncpy(a, exe, sizeof(a)); a[sizeof(a) - 1] = 0;
44
+ char *macos = dirname(a); /* .../Contents/MacOS */
45
+ char b[PATH_MAX];
46
+ strncpy(b, macos, sizeof(b)); b[sizeof(b) - 1] = 0;
47
+ char *contents = dirname(b); /* .../Contents */
48
+
49
+ char home[PATH_MAX], app[PATH_MAX], lib[PATH_MAX], bin[PATH_MAX];
50
+ char py[PATH_MAX], script[PATH_MAX], pypath[2 * PATH_MAX];
51
+ snprintf(home, sizeof(home), "%s/Resources/python", contents);
52
+ snprintf(app, sizeof(app), "%s/Resources/app", contents);
53
+ snprintf(lib, sizeof(lib), "%s/Resources/lib", contents);
54
+ snprintf(bin, sizeof(bin), "%s/Resources/bin", contents);
55
+ snprintf(py, sizeof(py), "%s/bin/python3", home);
56
+ snprintf(script, sizeof(script), "%s/%s.py", app, ENTRY);
57
+ snprintf(pypath, sizeof(pypath), "%s:%s", app, lib);
58
+
59
+ setenv("PYTHONHOME", home, 1);
60
+ setenv("PYTHONPATH", pypath, 1);
61
+ /* Isolate from ~/.local and user site config; the bundle is self-contained. */
62
+ setenv("PYTHONNOUSERSITE", "1", 1);
63
+ /* Prepend Resources/bin so user-declared native helper executables resolve by
64
+ name (subprocess/PATH lookups). Harmless when the directory is absent. */
65
+ const char *old_path = getenv("PATH");
66
+ char newpath[3 * PATH_MAX];
67
+ if (old_path != NULL && old_path[0] != 0) {{
68
+ snprintf(newpath, sizeof(newpath), "%s:%s", bin, old_path);
69
+ }} else {{
70
+ snprintf(newpath, sizeof(newpath), "%s", bin);
71
+ }}
72
+ setenv("PATH", newpath, 1);
73
+ chdir(app);
74
+
75
+ char **child = (char **)malloc(sizeof(char *) * (argc + 2));
76
+ if (child == NULL) return 71;
77
+ child[0] = py;
78
+ child[1] = script;
79
+ for (int i = 1; i < argc; i++) child[i + 1] = argv[i];
80
+ child[argc + 1] = NULL;
81
+ execv(py, child);
82
+ perror("kivyforge launcher: execv");
83
+ return 71;
84
+ }}
85
+ """
86
+
87
+
88
+ def render_launcher_source(entry_point: str) -> str:
89
+ """The C source for a launcher that execs *entry_point*.py (pure/testable)."""
90
+ if not entry_point.isidentifier():
91
+ raise AppBundleError(f"entry_point {entry_point!r} is not a valid module name.")
92
+ return _SOURCE.format(entry=entry_point)
93
+
94
+
95
+ def build_launcher(dest: Path, *, entry_point: str, arch: str) -> None:
96
+ """Compile the launcher for *arch* to *dest*."""
97
+ dest.parent.mkdir(parents=True, exist_ok=True)
98
+ source = render_launcher_source(entry_point)
99
+ with tempfile.TemporaryDirectory(prefix="kivy-launcher-") as tmp:
100
+ src = Path(tmp) / "launcher.c"
101
+ src.write_text(source, encoding="utf-8")
102
+ _compile(["clang", "-O2", "-Wall", "-arch", arch, "-o", str(dest), str(src)])
103
+ dest.chmod(0o755)
104
+
105
+
106
+ def _compile(cmd: list[str]) -> None:
107
+ try:
108
+ proc = subprocess.run(cmd, capture_output=True, text=True)
109
+ except FileNotFoundError as exc:
110
+ raise AppBundleError(
111
+ "clang not found; the launcher needs the Xcode command-line tools.\n"
112
+ " Install them with: xcode-select --install"
113
+ ) from exc
114
+ if proc.returncode != 0:
115
+ raise AppBundleError(
116
+ f"failed to compile the app launcher: "
117
+ f"{(proc.stderr or proc.stdout).strip()}"
118
+ )