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,543 @@
1
+ """iOS build/run/package/open/status logic (specs 05/06).
2
+
3
+ Extracted from the shared ``cli`` verbs so all iOS-specific behavior lives behind
4
+ the iOS backend, symmetric with ``platforms/macos/cli.py`` and
5
+ ``platforms/linux/cli.py``. The verb modules stay thin dispatchers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import plistlib
12
+ import re
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import click
17
+
18
+ from kivyforge.artifacts.collect import CollectError, collect_artifacts
19
+ from kivyforge.artifacts.wheels import BuildSlice
20
+ from kivyforge.cli._common import (
21
+ LOCKFILE_NAME,
22
+ ToolchainError,
23
+ lockfile_path,
24
+ )
25
+ from kivyforge.config import ConfigError, load_config
26
+ from kivyforge.config.icons import IconSourceError
27
+ from kivyforge.lock import LockError, is_in_sync
28
+ from kivyforge.platforms.base import HostCapabilityError
29
+
30
+ from .lock import load
31
+ from .materialize import materialize_project
32
+ from .staging import StagingError, create_staging
33
+ from .xcode import (
34
+ CommandError,
35
+ SigningError,
36
+ XcodeBuild,
37
+ archive_command,
38
+ build_command,
39
+ default_simulator_arch,
40
+ devicectl_install,
41
+ devicectl_launch,
42
+ devicectl_list,
43
+ export_command,
44
+ export_options_plist,
45
+ open_command,
46
+ preflight_signing,
47
+ product_app_path,
48
+ resolve_device_destination,
49
+ resolve_simulator_destination,
50
+ run_command,
51
+ simctl_install,
52
+ simctl_launch,
53
+ simctl_list,
54
+ )
55
+ from .xcode.commands import SIGNING_IDENTITY_ENV, resolve_signing_identity
56
+
57
+ # ---- build --------------------------------------------------------------- #
58
+
59
+
60
+ def ios_build(
61
+ project_root: Path,
62
+ *,
63
+ target: str | None,
64
+ arch: str | None,
65
+ no_verify_lock: bool,
66
+ no_cache: bool,
67
+ team_id: str | None,
68
+ signing_identity: str | None,
69
+ export_method: str,
70
+ ) -> None:
71
+ """Download artifacts, generate the Xcode project, and optionally build it."""
72
+ _require_macos_host()
73
+ # Signing pre-flight runs before any artifact work so --device/--release
74
+ # fail fast on a missing team_id (spec 05 step 7).
75
+ config = _load_config(project_root / "pyproject.toml")
76
+ if target is not None:
77
+ try:
78
+ preflight_signing(config, target, team_id_flag=team_id)
79
+ except SigningError as exc:
80
+ raise ToolchainError(str(exc)) from exc
81
+
82
+ prepare_build(
83
+ config,
84
+ project_root,
85
+ target=target,
86
+ arch=arch,
87
+ no_verify_lock=no_verify_lock,
88
+ no_cache=no_cache,
89
+ )
90
+
91
+ if target is None:
92
+ click.echo(
93
+ "Project ready. Open it with `kivyforge open` or build with "
94
+ "`kivyforge build --simulator`."
95
+ )
96
+ return
97
+
98
+ xb = XcodeBuild.from_config(config, project_root)
99
+ try:
100
+ _xcodebuild_step7(
101
+ xb,
102
+ config,
103
+ target=target,
104
+ arch=arch,
105
+ team_id_flag=team_id,
106
+ signing_identity_flag=signing_identity,
107
+ export_method=export_method,
108
+ )
109
+ except (CommandError, SigningError) as exc:
110
+ raise ToolchainError(str(exc)) from exc
111
+
112
+
113
+ def prepare_build(
114
+ config,
115
+ project_root: Path,
116
+ *,
117
+ target: str | None,
118
+ arch: str | None,
119
+ no_verify_lock: bool,
120
+ no_cache: bool,
121
+ ) -> BuildSlice:
122
+ """Run build steps 1-6 (drift, artifact collection, project generation).
123
+
124
+ Shared by ``kivyforge build`` and ``kivyforge run``. A bare ``toolchain
125
+ build`` (no target) collects both the device and simulator pip-deps slices
126
+ so the generated project builds either Xcode destination without re-running
127
+ the toolchain; a targeted build collects only its slice. Returns the primary
128
+ slice (the targeted one, or the device slice for a bare build).
129
+ """
130
+ pyproject = project_root / "pyproject.toml"
131
+ lock = _load_lock(project_root)
132
+ if not no_verify_lock and not is_in_sync(lock, pyproject.read_text("utf-8")):
133
+ raise ToolchainError(
134
+ f"{LOCKFILE_NAME} is out of date with pyproject.toml.\n"
135
+ " Run `kivyforge lock` to regenerate it (or pass --no-verify-lock "
136
+ "to build against the stale lock anyway)."
137
+ )
138
+
139
+ build_slices = _resolve_slices(target, arch, config.ios.deployment_target)
140
+ tags = ", ".join(s.platform_tag for s in build_slices)
141
+ # Only `kivyforge package` (target="release") gets byte-compiled/stripped
142
+ # app sources + pip-deps; `build`/`run` always keep readable .py, matching
143
+ # every other platform's release-only default.
144
+ release = target == "release"
145
+ try:
146
+ layout = create_staging(
147
+ config,
148
+ project_root,
149
+ release=release,
150
+ python_version=lock.python_xcframework.version if release else None,
151
+ echo=click.echo,
152
+ )
153
+ click.echo(f"Collecting artifacts for {tags} ...")
154
+ collect_artifacts(
155
+ lock,
156
+ layout,
157
+ build_slices=build_slices,
158
+ project_root=project_root,
159
+ no_cache=no_cache,
160
+ release=release,
161
+ build_settings=config.ios.python_build_settings,
162
+ echo=click.echo,
163
+ )
164
+ materialize_project(
165
+ config,
166
+ project_root,
167
+ layout=layout,
168
+ python_version=lock.python_xcframework.version,
169
+ last_upgrade_check=_xcode_last_upgrade_check(),
170
+ swift_packages=lock.swift_packages,
171
+ xcframeworks=lock.xcframeworks,
172
+ )
173
+ except CollectError as exc:
174
+ raise ToolchainError(str(exc)) from exc
175
+ except IconSourceError as exc:
176
+ raise ToolchainError(str(exc)) from exc
177
+ except StagingError as exc:
178
+ raise ToolchainError(str(exc)) from exc
179
+
180
+ staging = project_root / f"{config.app_slug}-ios"
181
+ click.echo(f"Generated {staging.relative_to(project_root)}")
182
+ return build_slices[0]
183
+
184
+
185
+ def _xcodebuild_step7(
186
+ xb: XcodeBuild,
187
+ config,
188
+ *,
189
+ target: str,
190
+ arch: str | None,
191
+ team_id_flag: str | None,
192
+ signing_identity_flag: str | None,
193
+ export_method: str,
194
+ ) -> None:
195
+ auto_signing = config.ios.signing.auto_signing
196
+ if target in ("simulator", "device"):
197
+ # Effective signing identity: --signing-identity → env → pyproject.
198
+ # The pyproject default ("Apple Development") is meant for exactly this
199
+ # case — a device debug build.
200
+ identity = resolve_signing_identity(config, identity_flag=signing_identity_flag)
201
+ sim_arch = arch if target == "simulator" else None
202
+ click.echo(f"xcodebuild build ({target}) ...")
203
+ run_command(
204
+ build_command(
205
+ xb,
206
+ target,
207
+ arch=sim_arch,
208
+ signing_identity=identity,
209
+ allow_provisioning_updates=auto_signing and target == "device",
210
+ )
211
+ )
212
+ return
213
+
214
+ # --release: archive, then export a signed .ipa (spec 05 step 7).
215
+ # Only an *explicit* --signing-identity flag or KIVYFORGE_SIGNING_IDENTITY
216
+ # env var applies here — the pyproject default is for --device debug builds
217
+ # and would force the wrong certificate type (Development) onto what needs
218
+ # to be a Distribution-signed archive/export (see archive_command).
219
+ release_identity = signing_identity_flag or os.environ.get(SIGNING_IDENTITY_ENV)
220
+ resolved_team_id = preflight_signing(config, "release", team_id_flag=team_id_flag)
221
+ if resolved_team_id is None:
222
+ # preflight_signing only returns None for --simulator; unreachable here.
223
+ raise SigningError(
224
+ "code signing required for --release, but no team_id is set."
225
+ )
226
+ xb.build_dir.mkdir(parents=True, exist_ok=True)
227
+ click.echo("xcodebuild archive ...")
228
+ run_command(
229
+ archive_command(
230
+ xb,
231
+ signing_identity=release_identity,
232
+ allow_provisioning_updates=auto_signing,
233
+ )
234
+ )
235
+
236
+ options = export_options_plist(
237
+ method=export_method,
238
+ team_id=resolved_team_id,
239
+ upload_symbols=config.ios.signing.upload_symbols,
240
+ signing_identity=release_identity,
241
+ )
242
+ options_path = xb.build_dir / "ExportOptions.plist"
243
+ with open(options_path, "wb") as fh:
244
+ plistlib.dump(options, fh)
245
+
246
+ click.echo("xcodebuild -exportArchive ...")
247
+ run_command(
248
+ export_command(xb, options_path, allow_provisioning_updates=auto_signing)
249
+ )
250
+ click.echo(f"Exported {xb.ipa_path.relative_to(xb.project_root)}")
251
+
252
+
253
+ def _resolve_slices(
254
+ target: str | None, arch: str | None, deployment_target: str
255
+ ) -> list[BuildSlice]:
256
+ """Slices to collect for *target*.
257
+
258
+ A bare ``kivyforge build`` (``target is None``) collects both device and
259
+ simulator so the "ready to open" project builds either Xcode destination —
260
+ Xcode's default destination is usually a simulator, so collecting only the
261
+ device slice would make the very first in-Xcode build fail. A targeted build
262
+ collects only the slice it is about to build.
263
+
264
+ The simulator slice defaults to the host's native arch (``--arch`` overrides):
265
+ a single ``pip-deps-simulator`` directory holds one arch's compiled ``.so``
266
+ files, so the right one to stage is the arch the developer's machine actually
267
+ runs — ``x86_64`` on Intel, ``arm64`` on Apple Silicon. The lock pins both
268
+ simulator arches; the build picks the host's.
269
+ """
270
+ sim_arch = arch or default_simulator_arch()
271
+ if target is None:
272
+ return [
273
+ BuildSlice("device", "arm64", deployment_target),
274
+ BuildSlice("simulator", sim_arch, deployment_target),
275
+ ]
276
+ return [_resolve_slice(target, arch, deployment_target)]
277
+
278
+
279
+ def _resolve_slice(
280
+ target: str | None, arch: str | None, deployment_target: str
281
+ ) -> BuildSlice:
282
+ if target == "simulator":
283
+ return BuildSlice(
284
+ "simulator", arch or default_simulator_arch(), deployment_target
285
+ )
286
+ # device or release builds target device/arm64.
287
+ return BuildSlice("device", "arm64", deployment_target)
288
+
289
+
290
+ def _xcode_last_upgrade_check() -> str | None:
291
+ """LastUpgradeCheck for the installed Xcode (e.g. Xcode 26.5 -> "2650").
292
+
293
+ Setting the generated project's ``LastUpgradeCheck`` to the running Xcode's
294
+ version suppresses the "Update to recommended settings" banner. Returns
295
+ ``None`` when Xcode can't be queried, in which case the generator falls back
296
+ to its built-in constant.
297
+ """
298
+ from kivyforge.doctor.probe import RealProbe
299
+
300
+ version = RealProbe().xcode_version()
301
+ return _encode_last_upgrade_check(version) if version else None
302
+
303
+
304
+ def _encode_last_upgrade_check(version: str) -> str | None:
305
+ """Encode an Xcode version string ("26.5", "16.2.1") as Xcode's integer
306
+ LastUpgradeCheck code (major*100 + minor*10 + patch), or ``None``.
307
+ """
308
+ match = re.search(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
309
+ if match is None:
310
+ return None
311
+ major, minor, patch = (int(match.group(i) or 0) for i in (1, 2, 3))
312
+ return str(major * 100 + minor * 10 + patch)
313
+
314
+
315
+ def _require_macos_host() -> None:
316
+ """Gate the Xcode-bound verbs (mirrors the macOS/Linux/Windows backends).
317
+
318
+ ``IosPlatform.check_host_capability`` has always described this rule; until
319
+ now nothing called it, so an off-macOS invocation failed later with
320
+ whatever error xcodebuild or SPM happened to raise first.
321
+ """
322
+ from kivyforge.platforms import get_platform
323
+
324
+ try:
325
+ get_platform("ios").check_host_capability()
326
+ except HostCapabilityError as exc:
327
+ raise ToolchainError(str(exc)) from exc
328
+
329
+
330
+ def _load_config(pyproject: Path):
331
+ try:
332
+ return load_config(pyproject)
333
+ except ConfigError as exc:
334
+ raise ToolchainError(exc.format()) from exc
335
+
336
+
337
+ def _load_lock(project_root: Path):
338
+ path = lockfile_path(project_root)
339
+ if not path.is_file():
340
+ raise ToolchainError(f"no {LOCKFILE_NAME} found. Run `kivyforge lock` first.")
341
+ try:
342
+ return load(path)
343
+ except LockError as exc:
344
+ raise ToolchainError(str(exc)) from exc
345
+
346
+
347
+ # ---- run ----------------------------------------------------------------- #
348
+
349
+
350
+ def ios_run(
351
+ project_root: Path,
352
+ *,
353
+ target: str,
354
+ destination: str | None,
355
+ no_build: bool,
356
+ ) -> None:
357
+ """Build (unless --no-build), install, and launch the app."""
358
+ _require_macos_host()
359
+ try:
360
+ config = load_config(project_root / "pyproject.toml")
361
+ except ConfigError as exc:
362
+ raise ToolchainError(exc.format()) from exc
363
+
364
+ xb = XcodeBuild.from_config(config, project_root)
365
+ derived_data = xb.build_dir / "DerivedData"
366
+
367
+ try:
368
+ if not no_build:
369
+ if target == "device":
370
+ preflight_signing(config, "device")
371
+ prepare_build(
372
+ config,
373
+ project_root,
374
+ target=target,
375
+ arch=None,
376
+ no_verify_lock=False,
377
+ no_cache=False,
378
+ )
379
+ click.echo(f"xcodebuild build ({target}) ...")
380
+ run_command(build_command(xb, target, derived_data_path=derived_data))
381
+
382
+ app = product_app_path(derived_data, xb.scheme, target)
383
+ if not app.exists():
384
+ raise ToolchainError(
385
+ f"built app not found at {app}.\n"
386
+ " Run without --no-build, or open the project in Xcode and build "
387
+ "once to populate DerivedData."
388
+ )
389
+
390
+ bundle_id = config.ios_required.bundle_id
391
+ if target == "simulator":
392
+ _run_simulator(destination, app, bundle_id)
393
+ else:
394
+ _run_device(destination, app, bundle_id)
395
+ except SigningError as exc:
396
+ raise ToolchainError(str(exc)) from exc
397
+ except CommandError as exc:
398
+ raise ToolchainError(str(exc)) from exc
399
+
400
+
401
+ def ios_list_devices() -> None:
402
+ for builder in (simctl_list, devicectl_list):
403
+ proc = run_command(builder(), check=False)
404
+ if proc.stdout:
405
+ click.echo(proc.stdout)
406
+
407
+
408
+ def _run_simulator(destination: str | None, app: Path, bundle_id: str) -> None:
409
+ device = resolve_simulator_destination(destination)
410
+ label = f"{device.name} ({device.udid})"
411
+ click.echo(f"Installing on simulator {label} ...")
412
+ run_command(simctl_install(device.udid, app))
413
+ click.echo(f"Launching {bundle_id} ...")
414
+ run_command(simctl_launch(device.udid, bundle_id))
415
+
416
+
417
+ def _run_device(destination: str | None, app: Path, bundle_id: str) -> None:
418
+ device = resolve_device_destination(destination)
419
+ label = f"{device.name} ({device.identifier})"
420
+ click.echo(f"Installing on device {label} ...")
421
+ run_command(devicectl_install(device.identifier, app))
422
+ click.echo(f"Launching {bundle_id} ...")
423
+ run_command(devicectl_launch(device.identifier, bundle_id))
424
+
425
+
426
+ # ---- package ------------------------------------------------------------- #
427
+
428
+
429
+ def ios_package(
430
+ project_root: Path,
431
+ *,
432
+ team_id: str | None,
433
+ signing_identity: str | None,
434
+ export_method: str,
435
+ no_verify_lock: bool,
436
+ no_cache: bool,
437
+ ) -> None:
438
+ """Build the release archive and export a signed ``.ipa``."""
439
+ _require_macos_host()
440
+ config = _load_config(project_root / "pyproject.toml")
441
+ try:
442
+ prepare_build(
443
+ config,
444
+ project_root,
445
+ target="release",
446
+ arch=None,
447
+ no_verify_lock=no_verify_lock,
448
+ no_cache=no_cache,
449
+ )
450
+ xb = XcodeBuild.from_config(config, project_root)
451
+ _xcodebuild_step7(
452
+ xb,
453
+ config,
454
+ target="release",
455
+ arch=None,
456
+ team_id_flag=team_id,
457
+ signing_identity_flag=signing_identity,
458
+ export_method=export_method,
459
+ )
460
+ except (CommandError, SigningError) as exc:
461
+ raise ToolchainError(str(exc)) from exc
462
+
463
+
464
+ # ---- open ---------------------------------------------------------------- #
465
+
466
+
467
+ def ios_open(project_root: Path) -> None:
468
+ """Open ``<app>-ios/<app>.xcodeproj`` in Xcode."""
469
+ _require_macos_host()
470
+ try:
471
+ config = load_config(project_root / "pyproject.toml")
472
+ except ConfigError as exc:
473
+ raise ToolchainError(exc.format()) from exc
474
+
475
+ slug = config.app_slug
476
+ xcodeproj = project_root / f"{slug}-ios" / f"{slug}.xcodeproj"
477
+ if not xcodeproj.exists():
478
+ raise ToolchainError(
479
+ f"{xcodeproj.name} does not exist yet.\n"
480
+ " Run `kivyforge build` first to generate the Xcode project."
481
+ )
482
+ try:
483
+ run_command(open_command(xcodeproj))
484
+ except CommandError as exc:
485
+ raise ToolchainError(str(exc)) from exc
486
+
487
+
488
+ # ---- status -------------------------------------------------------------- #
489
+
490
+
491
+ def ios_status(project_root: Path) -> None:
492
+ """Show app identity, Python version, lock sync, and build state."""
493
+ pyproject = project_root / "pyproject.toml"
494
+ try:
495
+ config = load_config(pyproject)
496
+ except ConfigError as exc:
497
+ raise ToolchainError(exc.format()) from exc
498
+
499
+ ios = config.ios_required
500
+ click.echo(f"App: {config.display_name} ({ios.bundle_id})")
501
+ click.echo(f"Python: {ios.python_version or '(unset)'}")
502
+ click.echo(f"Lock: {_lock_state(project_root, pyproject)}")
503
+
504
+ click.echo("Build:")
505
+ build_dir = project_root / f"{config.app_slug}-ios" / "build" / "DerivedData"
506
+ sim_label = f"simulator ({default_simulator_arch()})"
507
+ for label, target in ((sim_label, "simulator"), ("device", "device")):
508
+ app = product_app_path(build_dir, config.app_slug, target)
509
+ click.echo(f" {label:<20}{_build_state(app)}")
510
+
511
+
512
+ def _lock_state(project_root: Path, pyproject: Path) -> str:
513
+ lockfile = project_root / LOCKFILE_NAME
514
+ if not lockfile.is_file():
515
+ return "missing (run `kivyforge lock`)"
516
+ try:
517
+ lock = load(lockfile)
518
+ except LockError:
519
+ return "unreadable (run `kivyforge lock`)"
520
+ if is_in_sync(lock, pyproject.read_text("utf-8")):
521
+ return "in sync"
522
+ return "out of date (run `kivyforge lock`)"
523
+
524
+
525
+ def _build_state(app: Path) -> str:
526
+ if not app.exists():
527
+ return "not built"
528
+ age = time.time() - app.stat().st_mtime
529
+ return f"last built {_humanize(age)}"
530
+
531
+
532
+ def _humanize(seconds: float) -> str:
533
+ seconds = int(seconds)
534
+ if seconds < 60:
535
+ return "just now"
536
+ if seconds < 3600:
537
+ m = seconds // 60
538
+ return f"{m} minute{'s' if m != 1 else ''} ago"
539
+ if seconds < 86400:
540
+ h = seconds // 3600
541
+ return f"{h} hour{'s' if h != 1 else ''} ago"
542
+ d = seconds // 86400
543
+ return f"{d} day{'s' if d != 1 else ''} ago"