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,194 @@
1
+ """Wire ``Frameworks/*.xcframework`` into the generated ``.xcodeproj`` (spec 06).
2
+
3
+ ``kivyforge build`` runs ``sync_native_frameworks`` as part of the every-build,
4
+ idempotent project sync. For each ``.xcframework`` staged under ``Frameworks/``
5
+ it ensures a file reference (under the ``Frameworks`` group) and then wires the
6
+ **link** and **embed** phases independently:
7
+
8
+ * ``link`` -> a build file in the Frameworks (Link Binary) phase,
9
+ * ``embed`` -> a build file in the Embed Frameworks (Copy Files) phase, with
10
+ ``CodeSignOnCopy``.
11
+
12
+ The link/embed intent comes from the locked ``[[tool.kivyforge.xcframeworks]]``
13
+ entries (keyed by framework name). Wheel-embedded frameworks (SDL3/ANGLE, etc.)
14
+ have no lock entry and default to Embed & Sign (link + embed), preserving prior
15
+ behavior. Reconciliation is full each run, so flipping ``link``/``embed`` in the
16
+ lock removes the now-unwanted build file. Finally, references for frameworks no
17
+ longer present on disk are pruned so a removed dependency does not linger in the
18
+ project as a dangling reference.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from pbxproj.pbxextensions.ProjectFiles import FileOptions, TreeType
27
+ from pbxproj.pbxsections.PBXBuildFile import PBXBuildFile
28
+
29
+ # pbxproj ships no type stubs and builds objects dynamically (PBXGenericObject),
30
+ # so the project graph is treated as ``Any`` throughout this interop module.
31
+
32
+ # PBXCopyFilesBuildPhase destination for frameworks (Embed Frameworks).
33
+ FRAMEWORKS_DST_SUBFOLDER_SPEC = "10"
34
+
35
+
36
+ def sync_native_frameworks(
37
+ project: Any,
38
+ target_name: str,
39
+ *,
40
+ frameworks_dir: Path,
41
+ frameworks_group: Any,
42
+ link_embed: dict[str, tuple[bool, bool]],
43
+ ) -> None:
44
+ """Reconcile ``Frameworks/*.xcframework`` refs + link/embed phases with disk.
45
+
46
+ ``link_embed`` maps a framework's on-disk stem (e.g. ``"Sentry"`` for
47
+ ``Sentry.xcframework``) to ``(link, embed)``. Frameworks not present in the
48
+ map default to ``(True, True)``.
49
+ """
50
+ if frameworks_dir.is_dir():
51
+ for xc in sorted(frameworks_dir.glob("*.xcframework")):
52
+ rel = f"Frameworks/{xc.name}"
53
+ ref = _ensure_file_ref(project, rel, target_name, frameworks_group)
54
+ if ref is None:
55
+ continue
56
+ link, embed = link_embed.get(xc.stem, (True, True))
57
+ if link:
58
+ _ensure_link(project, target_name, ref)
59
+ else:
60
+ _remove_build_files(project, ref.get_id(), "PBXFrameworksBuildPhase")
61
+ if embed:
62
+ _ensure_embed(project, target_name, ref)
63
+ else:
64
+ _remove_build_files(project, ref.get_id(), "PBXCopyFilesBuildPhase")
65
+ _prune(project, frameworks_dir)
66
+
67
+
68
+ # -- file reference ------------------------------------------------------------ #
69
+
70
+
71
+ def _ensure_file_ref(
72
+ project: Any, rel: str, target_name: str, group: Any
73
+ ) -> Any | None:
74
+ existing = project.get_files_by_path(rel, tree=TreeType.SOURCE_ROOT)
75
+ if existing:
76
+ ref = existing[0]
77
+ _ensure_under_group(project, ref, group)
78
+ return ref
79
+ # First add: let pbxproj create the file reference, place it under the group,
80
+ # and register the framework search path. It also creates default link+embed
81
+ # build files; the caller's reconciliation trims them to the lock's intent.
82
+ project.add_file(
83
+ rel,
84
+ parent=group,
85
+ target_name=target_name,
86
+ tree=TreeType.SOURCE_ROOT,
87
+ file_options=FileOptions(embed_framework=True, code_sign_on_copy=True),
88
+ )
89
+ refs = project.get_files_by_path(rel, tree=TreeType.SOURCE_ROOT)
90
+ if not refs:
91
+ return None
92
+ ref = refs[0]
93
+ _ensure_under_group(project, ref, group)
94
+ return ref
95
+
96
+
97
+ def _ensure_under_group(project: Any, file_ref: Any, group: Any) -> None:
98
+ """Move an existing file reference under ``group`` (idempotent)."""
99
+ if group.has_child(file_ref):
100
+ return
101
+ for grp in project.objects.get_objects_in_section("PBXGroup"):
102
+ if grp.has_child(file_ref):
103
+ grp.remove_child(file_ref)
104
+ group.add_child(file_ref)
105
+
106
+
107
+ # -- link / embed build files -------------------------------------------------- #
108
+
109
+
110
+ def _ensure_link(project: Any, target_name: str, file_ref: Any) -> None:
111
+ phase = _link_phase(project, target_name)
112
+ if phase is None or _has_build_file(project, phase, file_ref.get_id()):
113
+ return
114
+ build_file: Any = PBXBuildFile.create(file_ref)
115
+ project.objects[build_file.get_id()] = build_file
116
+ phase.add_build_file(build_file)
117
+
118
+
119
+ def _ensure_embed(project: Any, target_name: str, file_ref: Any) -> None:
120
+ phase = _embed_phase(project, target_name)
121
+ if _has_build_file(project, phase, file_ref.get_id()):
122
+ return
123
+ build_file: Any = PBXBuildFile.create(file_ref, attributes=["CodeSignOnCopy"])
124
+ project.objects[build_file.get_id()] = build_file
125
+ phase.add_build_file(build_file)
126
+
127
+
128
+ def _link_phase(project: Any, target_name: str) -> Any:
129
+ target = project.get_target_by_name(target_name)
130
+ if target is None:
131
+ return None
132
+ phases = target.get_or_create_build_phase("PBXFrameworksBuildPhase")
133
+ return phases[0] if phases else None
134
+
135
+
136
+ def _embed_phase(project: Any, target_name: str) -> Any:
137
+ target = project.get_target_by_name(target_name)
138
+ if target is None:
139
+ raise RuntimeError(f"target {target_name!r} not found in Xcode project")
140
+ phases = target.get_or_create_build_phase(
141
+ "PBXCopyFilesBuildPhase",
142
+ search_parameters={"dstSubfolderSpec": FRAMEWORKS_DST_SUBFOLDER_SPEC},
143
+ create_parameters=(
144
+ "Embed Frameworks",
145
+ [],
146
+ "",
147
+ FRAMEWORKS_DST_SUBFOLDER_SPEC,
148
+ ),
149
+ )
150
+ return phases[0]
151
+
152
+
153
+ def _has_build_file(project: Any, phase: Any, ref_id: str) -> bool:
154
+ for build_file_id in phase["files"]:
155
+ build_file = project.objects[build_file_id]
156
+ if _file_ref(build_file) == ref_id:
157
+ return True
158
+ return False
159
+
160
+
161
+ def _remove_build_files(project: Any, ref_id: str, section: str) -> None:
162
+ for phase in project.objects.get_objects_in_section(section):
163
+ for build_file_id in list(phase["files"]):
164
+ build_file = project.objects[build_file_id]
165
+ if _file_ref(build_file) == ref_id:
166
+ phase.remove_build_file(build_file)
167
+
168
+
169
+ def _file_ref(build_file: Any) -> str | None:
170
+ return build_file["fileRef"] if "fileRef" in build_file else None
171
+
172
+
173
+ # -- pruning ------------------------------------------------------------------- #
174
+
175
+
176
+ def _prune(project: Any, frameworks_dir: Path) -> None:
177
+ """Remove references for ``Frameworks/*.xcframework`` no longer on disk."""
178
+ for ref in list(project.objects.get_objects_in_section("PBXFileReference")):
179
+ path = ref["path"] if "path" in ref else None
180
+ if (
181
+ not path
182
+ or not path.startswith("Frameworks/")
183
+ or not path.endswith(".xcframework")
184
+ ):
185
+ continue
186
+ name = path.split("/", 1)[1]
187
+ if not (frameworks_dir / name).is_dir():
188
+ ref_id = ref.get_id()
189
+ # Drop the link/embed build files first so remove_file_by_id sees no
190
+ # remaining references and actually deletes the file reference (it
191
+ # otherwise short-circuits while any build file still points at it).
192
+ _remove_build_files(project, ref_id, "PBXFrameworksBuildPhase")
193
+ _remove_build_files(project, ref_id, "PBXCopyFilesBuildPhase")
194
+ project.remove_file_by_id(ref_id)
@@ -0,0 +1,76 @@
1
+ """xcodebuild / simctl / devicectl command construction and execution (spec 05).
2
+
3
+ The command builders are pure functions returning ``argv`` lists (and the
4
+ ``ExportOptions.plist`` dict), so they unit-test hermetically. Actual process
5
+ execution funnels through a single injectable ``run`` helper.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .commands import (
11
+ SigningError,
12
+ XcodeBuild,
13
+ archive_command,
14
+ build_command,
15
+ default_simulator_arch,
16
+ export_command,
17
+ export_options_plist,
18
+ preflight_signing,
19
+ product_app_path,
20
+ resolve_team_id,
21
+ sdk_for,
22
+ )
23
+ from .runner import (
24
+ CommandError,
25
+ Device,
26
+ SimulatorDevice,
27
+ devicectl_install,
28
+ devicectl_launch,
29
+ devicectl_list,
30
+ devicectl_list_json,
31
+ open_command,
32
+ parse_devicectl_devices,
33
+ parse_simctl_devices,
34
+ pick_device,
35
+ pick_simulator,
36
+ resolve_device_destination,
37
+ resolve_simulator_destination,
38
+ run_command,
39
+ simctl_boot,
40
+ simctl_install,
41
+ simctl_launch,
42
+ simctl_list,
43
+ )
44
+
45
+ __all__ = [
46
+ "XcodeBuild",
47
+ "SigningError",
48
+ "build_command",
49
+ "default_simulator_arch",
50
+ "archive_command",
51
+ "export_command",
52
+ "export_options_plist",
53
+ "preflight_signing",
54
+ "product_app_path",
55
+ "resolve_team_id",
56
+ "sdk_for",
57
+ "run_command",
58
+ "open_command",
59
+ "CommandError",
60
+ "SimulatorDevice",
61
+ "parse_simctl_devices",
62
+ "pick_simulator",
63
+ "resolve_simulator_destination",
64
+ "simctl_boot",
65
+ "simctl_install",
66
+ "simctl_launch",
67
+ "simctl_list",
68
+ "Device",
69
+ "devicectl_install",
70
+ "devicectl_launch",
71
+ "devicectl_list",
72
+ "devicectl_list_json",
73
+ "parse_devicectl_devices",
74
+ "pick_device",
75
+ "resolve_device_destination",
76
+ ]
@@ -0,0 +1,296 @@
1
+ """xcodebuild command + ExportOptions.plist construction (spec 05 step 7).
2
+
3
+ Pure builders: no subprocess, no filesystem side effects beyond returning the
4
+ argv/plist the caller will execute or write. This keeps the signing pre-flight,
5
+ SDK selection, archive/export pipeline, and export-options mapping fully
6
+ unit-testable on any host.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import platform
13
+ from collections.abc import Mapping
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ from kivyforge.config.model import Config
18
+
19
+
20
+ def default_simulator_arch() -> str:
21
+ """The iOS-simulator CPU arch matching the host Mac.
22
+
23
+ Apple-Silicon Macs run the simulator as ``arm64``; Intel Macs as ``x86_64``.
24
+ This is the default when ``--arch`` is not given, so a bare or targeted
25
+ simulator build collects (and ``xcodebuild`` builds) the slice the developer's
26
+ own machine can actually run — collecting only ``arm64`` would break the very
27
+ first simulator build on an Intel host even though the lock pinned ``x86_64``.
28
+ ``platform.machine()`` reports ``x86_64`` under Rosetta too, which is correct:
29
+ an x86_64 Python process drives an x86_64 simulator.
30
+ """
31
+ return "x86_64" if platform.machine() == "x86_64" else "arm64"
32
+
33
+
34
+ # build-target -> xcodebuild SDK.
35
+ SDK = {
36
+ "simulator": "iphonesimulator",
37
+ "device": "iphoneos",
38
+ "release": "iphoneos",
39
+ }
40
+
41
+ TEAM_ID_ENV = "KIVYFORGE_TEAM_ID"
42
+ SIGNING_IDENTITY_ENV = "KIVYFORGE_SIGNING_IDENTITY"
43
+
44
+ # --export-method -> ExportOptions.plist `method` value.
45
+ EXPORT_METHOD = {
46
+ "app-store": "app-store",
47
+ "ad-hoc": "ad-hoc",
48
+ "development": "development",
49
+ }
50
+
51
+
52
+ class SigningError(Exception):
53
+ """Code signing is required but no team_id could be resolved."""
54
+
55
+
56
+ def sdk_for(target: str) -> str:
57
+ try:
58
+ return SDK[target]
59
+ except KeyError:
60
+ raise ValueError(f"unknown build target {target!r}") from None
61
+
62
+
63
+ def configuration_for(target: str) -> str:
64
+ return "Release" if target == "release" else "Debug"
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class XcodeBuild:
69
+ """Locates the generated project for a config under a project root."""
70
+
71
+ project_root: Path
72
+ scheme: str
73
+
74
+ @classmethod
75
+ def from_config(cls, config: Config, project_root: str | Path) -> XcodeBuild:
76
+ return cls(project_root=Path(project_root), scheme=config.app_slug)
77
+
78
+ @property
79
+ def staging(self) -> Path:
80
+ return self.project_root / f"{self.scheme}-ios"
81
+
82
+ @property
83
+ def project_path(self) -> Path:
84
+ return self.staging / f"{self.scheme}.xcodeproj"
85
+
86
+ @property
87
+ def build_dir(self) -> Path:
88
+ return self.staging / "build"
89
+
90
+ @property
91
+ def archive_path(self) -> Path:
92
+ return self.build_dir / f"{self.scheme}.xcarchive"
93
+
94
+ @property
95
+ def ipa_path(self) -> Path:
96
+ return self.build_dir / f"{self.scheme}.ipa"
97
+
98
+
99
+ def resolve_team_id(
100
+ config: Config,
101
+ *,
102
+ team_id_flag: str | None = None,
103
+ env: Mapping[str, str] | None = None,
104
+ ) -> str | None:
105
+ """Resolve the effective team_id (flag → env → pyproject), or None."""
106
+ effective_env: Mapping[str, str] = os.environ if env is None else env
107
+ candidates = (
108
+ team_id_flag,
109
+ effective_env.get(TEAM_ID_ENV),
110
+ config.ios_required.signing.team_id or None,
111
+ )
112
+ for value in candidates:
113
+ if value:
114
+ return value
115
+ return None
116
+
117
+
118
+ def resolve_signing_identity(
119
+ config: Config,
120
+ *,
121
+ identity_flag: str | None = None,
122
+ env: Mapping[str, str] | None = None,
123
+ ) -> str | None:
124
+ effective_env: Mapping[str, str] = os.environ if env is None else env
125
+ for value in (
126
+ identity_flag,
127
+ effective_env.get(SIGNING_IDENTITY_ENV),
128
+ config.ios_required.signing.identity,
129
+ ):
130
+ if value:
131
+ return value
132
+ return None
133
+
134
+
135
+ def preflight_signing(
136
+ config: Config,
137
+ target: str,
138
+ *,
139
+ team_id_flag: str | None = None,
140
+ env: dict | None = None,
141
+ ) -> str | None:
142
+ """Fail fast when --device/--release lack a team_id (spec 05 step 7).
143
+
144
+ Returns the resolved team_id (None for --simulator, which does not sign).
145
+ """
146
+ if target == "simulator":
147
+ return None
148
+ team_id = resolve_team_id(config, team_id_flag=team_id_flag, env=env)
149
+ if not team_id:
150
+ raise SigningError(
151
+ "code signing required for --device/--release, but no team_id is set.\n"
152
+ " Set it one of these ways:\n"
153
+ ' - [tool.kivy.ios.signing].team_id = "ABCDE12345" in pyproject.toml, '
154
+ "then re-lock\n"
155
+ " - kivyforge build --release --team-id ABCDE12345\n"
156
+ " - export KIVYFORGE_TEAM_ID=ABCDE12345"
157
+ )
158
+ return team_id
159
+
160
+
161
+ def build_command(
162
+ xb: XcodeBuild,
163
+ target: str,
164
+ *,
165
+ arch: str | None = None,
166
+ derived_data_path: str | Path | None = None,
167
+ signing_identity: str | None = None,
168
+ allow_provisioning_updates: bool = False,
169
+ ) -> list[str]:
170
+ """`xcodebuild build` argv for --simulator/--device (Debug).
171
+
172
+ ``derived_data_path`` pins where the ``.app`` is written so ``kivyforge run``
173
+ can locate the product deterministically. ``signing_identity`` overrides the
174
+ project's baked ``CODE_SIGN_IDENTITY`` for device builds (simulator builds are
175
+ unsigned, so it is ignored there). ``allow_provisioning_updates`` passes
176
+ ``-allowProvisioningUpdates`` — required for automatic signing to actually
177
+ register the App ID / fetch or create a matching profile when driven from
178
+ the command line (Xcode's IDE does this silently; bare ``xcodebuild`` does
179
+ not unless asked — see "Resolving common notarization issues"-adjacent
180
+ Apple guidance on ``xcodebuild`` + automatic signing).
181
+ """
182
+ cmd = [
183
+ "xcodebuild",
184
+ "-project",
185
+ str(xb.project_path),
186
+ "-scheme",
187
+ xb.scheme,
188
+ "-configuration",
189
+ configuration_for(target),
190
+ "-sdk",
191
+ sdk_for(target),
192
+ ]
193
+ if derived_data_path is not None:
194
+ cmd += ["-derivedDataPath", str(derived_data_path)]
195
+ # python.org's install_stdlib uses lib-$ARCHS; a universal simulator build
196
+ # (arm64 x86_64) produces an invalid path. Pin a single arch for CLI builds.
197
+ if target == "simulator":
198
+ cmd += [f"ARCHS={arch or default_simulator_arch()}", "ONLY_ACTIVE_ARCH=NO"]
199
+ else:
200
+ if signing_identity:
201
+ cmd.append(f"CODE_SIGN_IDENTITY={signing_identity}")
202
+ if allow_provisioning_updates:
203
+ cmd.append("-allowProvisioningUpdates")
204
+ cmd.append("build")
205
+ return cmd
206
+
207
+
208
+ def product_app_path(derived_data_path: str | Path, scheme: str, target: str) -> Path:
209
+ """Path to the built ``<scheme>.app`` under a pinned DerivedData dir."""
210
+ sdk = sdk_for(target)
211
+ config = configuration_for(target)
212
+ return (
213
+ Path(derived_data_path)
214
+ / "Build"
215
+ / "Products"
216
+ / f"{config}-{sdk}"
217
+ / f"{scheme}.app"
218
+ )
219
+
220
+
221
+ def archive_command(
222
+ xb: XcodeBuild,
223
+ *,
224
+ signing_identity: str | None = None,
225
+ allow_provisioning_updates: bool = False,
226
+ ) -> list[str]:
227
+ """`xcodebuild archive` argv for --release (step 7.1).
228
+
229
+ ``signing_identity`` overrides the project's baked ``CODE_SIGN_IDENTITY`` for
230
+ the archive's signing step — only pass this for an *explicit* user override;
231
+ forcing the pyproject default (typically "Apple Development", meant for
232
+ ``--device`` debug builds) onto a distribution archive requests the wrong
233
+ profile type and fails. ``allow_provisioning_updates``: see ``build_command``.
234
+ """
235
+ cmd = [
236
+ "xcodebuild",
237
+ "archive",
238
+ "-project",
239
+ str(xb.project_path),
240
+ "-scheme",
241
+ xb.scheme,
242
+ "-configuration",
243
+ "Release",
244
+ "-sdk",
245
+ "iphoneos",
246
+ "-archivePath",
247
+ str(xb.archive_path),
248
+ ]
249
+ if signing_identity:
250
+ cmd.append(f"CODE_SIGN_IDENTITY={signing_identity}")
251
+ if allow_provisioning_updates:
252
+ cmd.append("-allowProvisioningUpdates")
253
+ return cmd
254
+
255
+
256
+ def export_command(
257
+ xb: XcodeBuild, options_plist: Path, *, allow_provisioning_updates: bool = False
258
+ ) -> list[str]:
259
+ """`xcodebuild -exportArchive` argv for --release (step 7.2)."""
260
+ cmd = [
261
+ "xcodebuild",
262
+ "-exportArchive",
263
+ "-archivePath",
264
+ str(xb.archive_path),
265
+ "-exportPath",
266
+ str(xb.build_dir),
267
+ "-exportOptionsPlist",
268
+ str(options_plist),
269
+ ]
270
+ if allow_provisioning_updates:
271
+ cmd.append("-allowProvisioningUpdates")
272
+ return cmd
273
+
274
+
275
+ def export_options_plist(
276
+ *,
277
+ method: str,
278
+ team_id: str,
279
+ upload_symbols: bool = True,
280
+ signing_identity: str | None = None,
281
+ ) -> dict:
282
+ """Map --export-method + signing to ExportOptions.plist keys (spec 05).
283
+
284
+ ``signing_identity`` pins the certificate used to re-sign during export
285
+ (``-exportArchive``); omitted, Xcode selects one for the export method.
286
+ """
287
+ if method not in EXPORT_METHOD:
288
+ raise ValueError(f"unknown export method {method!r}")
289
+ options: dict = {
290
+ "method": EXPORT_METHOD[method],
291
+ "teamID": team_id,
292
+ "uploadSymbols": upload_symbols,
293
+ }
294
+ if signing_identity:
295
+ options["signingCertificate"] = signing_identity
296
+ return options