kivyforge 0.1.0__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 (282) hide show
  1. kivyforge/__init__.py +11 -0
  2. kivyforge/artifacts/__init__.py +41 -0
  3. kivyforge/artifacts/cache.py +81 -0
  4. kivyforge/artifacts/collect.py +424 -0
  5. kivyforge/artifacts/download.py +150 -0
  6. kivyforge/artifacts/frameworks.py +256 -0
  7. kivyforge/artifacts/include_files.py +538 -0
  8. kivyforge/artifacts/native_stage_util.py +374 -0
  9. kivyforge/artifacts/runtime.py +65 -0
  10. kivyforge/artifacts/verify.py +45 -0
  11. kivyforge/artifacts/wheels.py +92 -0
  12. kivyforge/build_outcome.py +111 -0
  13. kivyforge/bundle/__init__.py +1 -0
  14. kivyforge/bundle/app_metadata.py +334 -0
  15. kivyforge/bundle/casefold.py +88 -0
  16. kivyforge/bundle/entry_launcher.py +128 -0
  17. kivyforge/bundle/pycompile.py +323 -0
  18. kivyforge/bundle/workdir.py +131 -0
  19. kivyforge/capabilities.py +174 -0
  20. kivyforge/cli/__init__.py +55 -0
  21. kivyforge/cli/_common.py +171 -0
  22. kivyforge/cli/_output.py +164 -0
  23. kivyforge/cli/_platform.py +137 -0
  24. kivyforge/cli/_version.py +170 -0
  25. kivyforge/cli/build.py +168 -0
  26. kivyforge/cli/capabilities.py +63 -0
  27. kivyforge/cli/clean.py +179 -0
  28. kivyforge/cli/doctor.py +145 -0
  29. kivyforge/cli/init.py +863 -0
  30. kivyforge/cli/init_writer.py +773 -0
  31. kivyforge/cli/lock.py +472 -0
  32. kivyforge/cli/open_cmd.py +27 -0
  33. kivyforge/cli/package.py +199 -0
  34. kivyforge/cli/run.py +277 -0
  35. kivyforge/cli/status.py +96 -0
  36. kivyforge/cli/upgrade.py +354 -0
  37. kivyforge/config/__init__.py +47 -0
  38. kivyforge/config/errors.py +43 -0
  39. kivyforge/config/icons.py +60 -0
  40. kivyforge/config/keys.py +313 -0
  41. kivyforge/config/loader.py +4008 -0
  42. kivyforge/config/model.py +1091 -0
  43. kivyforge/config/version.py +124 -0
  44. kivyforge/doctor/__init__.py +19 -0
  45. kivyforge/doctor/checks_common.py +229 -0
  46. kivyforge/doctor/probe.py +544 -0
  47. kivyforge/doctor/result.py +71 -0
  48. kivyforge/host.py +29 -0
  49. kivyforge/lock/__init__.py +44 -0
  50. kivyforge/lock/_pip_shim.py +261 -0
  51. kivyforge/lock/find_links.py +202 -0
  52. kivyforge/lock/inputs.py +125 -0
  53. kivyforge/lock/model.py +113 -0
  54. kivyforge/lock/pep751.py +184 -0
  55. kivyforge/lock/pip_progress.py +74 -0
  56. kivyforge/lock/prefer.py +89 -0
  57. kivyforge/lock/reader.py +12 -0
  58. kivyforge/lock/resolver.py +142 -0
  59. kivyforge/lock/sources.py +513 -0
  60. kivyforge/lock/wheelruntime/__init__.py +70 -0
  61. kivyforge/lock/wheelruntime/builder.py +432 -0
  62. kivyforge/lock/wheelruntime/model.py +102 -0
  63. kivyforge/lock/wheelruntime/native_binaries.py +127 -0
  64. kivyforge/lock/wheelruntime/pbs_github.py +288 -0
  65. kivyforge/lock/wheelruntime/profile.py +105 -0
  66. kivyforge/lock/wheelruntime/resolver.py +331 -0
  67. kivyforge/lock/wheelruntime/runtime.py +171 -0
  68. kivyforge/lock/wheelruntime/serialize.py +270 -0
  69. kivyforge/net.py +21 -0
  70. kivyforge/platforms/__init__.py +114 -0
  71. kivyforge/platforms/android/__init__.py +258 -0
  72. kivyforge/platforms/android/adb.py +340 -0
  73. kivyforge/platforms/android/bootstrap/__init__.py +7 -0
  74. kivyforge/platforms/android/bootstrap/contract.py +215 -0
  75. kivyforge/platforms/android/bootstrap/render.py +408 -0
  76. kivyforge/platforms/android/bootstrap/templates/android_pkg/__init__.py +81 -0
  77. kivyforge/platforms/android/bootstrap/templates/android_pkg/config.py +27 -0
  78. kivyforge/platforms/android/bootstrap/templates/android_pkg/loadingscreen.py +6 -0
  79. kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +127 -0
  80. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +52 -0
  81. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
  82. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +183 -0
  83. kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
  84. kivyforge/platforms/android/bootstrap/templates/cpp/main.c +157 -0
  85. kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
  86. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +319 -0
  87. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +135 -0
  88. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +271 -0
  89. kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +255 -0
  90. kivyforge/platforms/android/bootstrap/templates/p4a/LICENSE +21 -0
  91. kivyforge/platforms/android/bootstrap/templates/p4a/NOTICE +11 -0
  92. kivyforge/platforms/android/bootstrap/templates/p4a/P4A_VENDOR.toml +107 -0
  93. kivyforge/platforms/android/bootstrap/templates/p4a/android/activity.py +214 -0
  94. kivyforge/platforms/android/bootstrap/templates/p4a/android/broadcast.py +103 -0
  95. kivyforge/platforms/android/bootstrap/templates/p4a/android/darkmode.py +57 -0
  96. kivyforge/platforms/android/bootstrap/templates/p4a/android/permissions.py +621 -0
  97. kivyforge/platforms/android/bootstrap/templates/p4a/android/runnable.py +58 -0
  98. kivyforge/platforms/android/bootstrap/templates/p4a/android/storage.py +117 -0
  99. kivyforge/platforms/android/bootstrap/templates/p4a/java/PythonActivity.changeKeyboard.sdl3.java.in +9 -0
  100. kivyforge/platforms/android/bootstrap/templates/p4a/java/org/kivy/android/GenericBroadcastReceiver.java +19 -0
  101. kivyforge/platforms/android/bootstrap/templates/p4a/java/org/kivy/android/GenericBroadcastReceiverCallback.java +9 -0
  102. kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
  103. kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
  104. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
  105. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
  106. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
  107. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
  108. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
  109. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2123 -0
  110. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
  111. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
  112. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
  113. kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
  114. kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
  115. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
  116. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
  117. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
  118. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
  119. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
  120. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
  121. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
  122. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
  123. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
  124. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
  125. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
  126. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
  127. kivyforge/platforms/android/cli.py +1691 -0
  128. kivyforge/platforms/android/doctor.py +1310 -0
  129. kivyforge/platforms/android/elf.py +267 -0
  130. kivyforge/platforms/android/generate/__init__.py +1 -0
  131. kivyforge/platforms/android/generate/launcher.py +49 -0
  132. kivyforge/platforms/android/generate/manifest.py +378 -0
  133. kivyforge/platforms/android/generate/project.py +539 -0
  134. kivyforge/platforms/android/generate/services.py +234 -0
  135. kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
  136. kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
  137. kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
  138. kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
  139. kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
  140. kivyforge/platforms/android/gradlew.py +195 -0
  141. kivyforge/platforms/android/icons.py +223 -0
  142. kivyforge/platforms/android/lock/__init__.py +1 -0
  143. kivyforge/platforms/android/lock/archives.py +109 -0
  144. kivyforge/platforms/android/lock/builder.py +552 -0
  145. kivyforge/platforms/android/lock/generation.py +39 -0
  146. kivyforge/platforms/android/lock/markers.py +71 -0
  147. kivyforge/platforms/android/lock/maven.py +294 -0
  148. kivyforge/platforms/android/lock/model.py +106 -0
  149. kivyforge/platforms/android/lock/python_meta.py +226 -0
  150. kivyforge/platforms/android/lock/reader.py +195 -0
  151. kivyforge/platforms/android/lock/resolver.py +363 -0
  152. kivyforge/platforms/android/lock/writer.py +118 -0
  153. kivyforge/platforms/android/policy.py +240 -0
  154. kivyforge/platforms/android/sdk_packages.py +123 -0
  155. kivyforge/platforms/android/signing.py +155 -0
  156. kivyforge/platforms/android/smoke.py +46 -0
  157. kivyforge/platforms/android/splash.py +280 -0
  158. kivyforge/platforms/android/stage/__init__.py +1 -0
  159. kivyforge/platforms/android/stage/bundle.py +329 -0
  160. kivyforge/platforms/android/stage/conflicts.py +73 -0
  161. kivyforge/platforms/android/stage/jnilibs.py +332 -0
  162. kivyforge/platforms/android/stage/layouts.py +39 -0
  163. kivyforge/platforms/android/stage/runtime.py +61 -0
  164. kivyforge/platforms/android/stage/wheels.py +107 -0
  165. kivyforge/platforms/android/toolchain.py +41 -0
  166. kivyforge/platforms/base.py +226 -0
  167. kivyforge/platforms/ios/__init__.py +161 -0
  168. kivyforge/platforms/ios/assets.py +193 -0
  169. kivyforge/platforms/ios/buildsettings.py +315 -0
  170. kivyforge/platforms/ios/cli.py +788 -0
  171. kivyforge/platforms/ios/doctor.py +811 -0
  172. kivyforge/platforms/ios/entitlements.py +287 -0
  173. kivyforge/platforms/ios/generator.py +446 -0
  174. kivyforge/platforms/ios/includes.py +249 -0
  175. kivyforge/platforms/ios/legacy_module.py +94 -0
  176. kivyforge/platforms/ios/lock/__init__.py +79 -0
  177. kivyforge/platforms/ios/lock/builder.py +577 -0
  178. kivyforge/platforms/ios/lock/markers.py +69 -0
  179. kivyforge/platforms/ios/lock/model.py +74 -0
  180. kivyforge/platforms/ios/lock/python_meta.py +200 -0
  181. kivyforge/platforms/ios/lock/reader.py +173 -0
  182. kivyforge/platforms/ios/lock/resolver.py +388 -0
  183. kivyforge/platforms/ios/lock/spm.py +317 -0
  184. kivyforge/platforms/ios/lock/writer.py +86 -0
  185. kivyforge/platforms/ios/lock/xcframework.py +188 -0
  186. kivyforge/platforms/ios/materialize.py +67 -0
  187. kivyforge/platforms/ios/plist.py +91 -0
  188. kivyforge/platforms/ios/privacy.py +39 -0
  189. kivyforge/platforms/ios/skeleton.py +163 -0
  190. kivyforge/platforms/ios/sources.py +128 -0
  191. kivyforge/platforms/ios/staging.py +281 -0
  192. kivyforge/platforms/ios/swift_packages.py +324 -0
  193. kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +42 -0
  194. kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +225 -0
  195. kivyforge/platforms/ios/templates/kivyforge_native_modules.h.in +35 -0
  196. kivyforge/platforms/ios/templates/main.m +16 -0
  197. kivyforge/platforms/ios/templates/main_config.h.in +15 -0
  198. kivyforge/platforms/ios/xcframeworks.py +194 -0
  199. kivyforge/platforms/ios/xcode/__init__.py +80 -0
  200. kivyforge/platforms/ios/xcode/commands.py +329 -0
  201. kivyforge/platforms/ios/xcode/runner.py +424 -0
  202. kivyforge/platforms/linux/__init__.py +156 -0
  203. kivyforge/platforms/linux/appimage.py +229 -0
  204. kivyforge/platforms/linux/bundle.py +263 -0
  205. kivyforge/platforms/linux/cli.py +282 -0
  206. kivyforge/platforms/linux/desktop.py +73 -0
  207. kivyforge/platforms/linux/doctor.py +478 -0
  208. kivyforge/platforms/linux/elftools.py +95 -0
  209. kivyforge/platforms/linux/icons.py +117 -0
  210. kivyforge/platforms/linux/launcher.py +136 -0
  211. kivyforge/platforms/linux/lock/__init__.py +136 -0
  212. kivyforge/platforms/linux/lock/profile.py +254 -0
  213. kivyforge/platforms/linux/lock/runtime.py +81 -0
  214. kivyforge/platforms/linux/native_stage.py +47 -0
  215. kivyforge/platforms/linux/runtime_stage.py +128 -0
  216. kivyforge/platforms/linux/wheels_stage.py +103 -0
  217. kivyforge/platforms/macos/__init__.py +142 -0
  218. kivyforge/platforms/macos/bundle.py +337 -0
  219. kivyforge/platforms/macos/cli.py +324 -0
  220. kivyforge/platforms/macos/doctor.py +604 -0
  221. kivyforge/platforms/macos/icns.py +84 -0
  222. kivyforge/platforms/macos/includes.py +97 -0
  223. kivyforge/platforms/macos/launcher.py +231 -0
  224. kivyforge/platforms/macos/lock/__init__.py +152 -0
  225. kivyforge/platforms/macos/lock/profile.py +119 -0
  226. kivyforge/platforms/macos/lock/runtime.py +63 -0
  227. kivyforge/platforms/macos/machotools.py +402 -0
  228. kivyforge/platforms/macos/native_links.py +289 -0
  229. kivyforge/platforms/macos/native_stage.py +330 -0
  230. kivyforge/platforms/macos/notarize.py +151 -0
  231. kivyforge/platforms/macos/plist.py +43 -0
  232. kivyforge/platforms/macos/runtime_stage.py +130 -0
  233. kivyforge/platforms/macos/signing.py +84 -0
  234. kivyforge/platforms/macos/swift_packages.py +287 -0
  235. kivyforge/platforms/macos/wheels_stage.py +108 -0
  236. kivyforge/platforms/windows/__init__.py +154 -0
  237. kivyforge/platforms/windows/assets.py +76 -0
  238. kivyforge/platforms/windows/bundle.py +330 -0
  239. kivyforge/platforms/windows/cli.py +427 -0
  240. kivyforge/platforms/windows/doctor.py +575 -0
  241. kivyforge/platforms/windows/fsswap.py +68 -0
  242. kivyforge/platforms/windows/icons.py +52 -0
  243. kivyforge/platforms/windows/launcher/__init__.py +198 -0
  244. kivyforge/platforms/windows/launcher/build_launcher.py +583 -0
  245. kivyforge/platforms/windows/launcher/launcher.c +379 -0
  246. kivyforge/platforms/windows/lock/__init__.py +122 -0
  247. kivyforge/platforms/windows/lock/profile.py +101 -0
  248. kivyforge/platforms/windows/lock/runtime.py +69 -0
  249. kivyforge/platforms/windows/naming.py +52 -0
  250. kivyforge/platforms/windows/native_stage.py +66 -0
  251. kivyforge/platforms/windows/pathdepth.py +60 -0
  252. kivyforge/platforms/windows/petools.py +98 -0
  253. kivyforge/platforms/windows/rcedit.py +121 -0
  254. kivyforge/platforms/windows/runtime_stage.py +242 -0
  255. kivyforge/platforms/windows/signing.py +173 -0
  256. kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
  257. kivyforge/platforms/windows/vendor/NOTICE +17 -0
  258. kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
  259. kivyforge/platforms/windows/vendor/TOOLSET.txt +5 -0
  260. kivyforge/platforms/windows/vendor/__init__.py +12 -0
  261. kivyforge/platforms/windows/vendor/fetch_rcedit.py +99 -0
  262. kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
  263. kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
  264. kivyforge/platforms/windows/wheels_stage.py +139 -0
  265. kivyforge/report/__init__.py +31 -0
  266. kivyforge/report/console.py +231 -0
  267. kivyforge/report/diagnostics.py +300 -0
  268. kivyforge/report/envelope.py +79 -0
  269. kivyforge/report/exit_codes.py +56 -0
  270. kivyforge/report/failures.py +164 -0
  271. kivyforge/report/streams.py +71 -0
  272. kivyforge/report/transfers.py +209 -0
  273. kivyforge/report/verbosity.py +74 -0
  274. kivyforge/status.py +179 -0
  275. kivyforge/streaming.py +92 -0
  276. kivyforge/versioning.py +285 -0
  277. kivyforge-0.1.0.dist-info/METADATA +578 -0
  278. kivyforge-0.1.0.dist-info/RECORD +282 -0
  279. kivyforge-0.1.0.dist-info/WHEEL +5 -0
  280. kivyforge-0.1.0.dist-info/entry_points.txt +3 -0
  281. kivyforge-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. kivyforge-0.1.0.dist-info/top_level.txt +1 -0
kivyforge/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """kivyforge: build, run, and package Kivy apps for Android, iOS, macOS,
2
+ Windows, and Linux.
3
+
4
+ kivyforge reads a user-authored ``pyproject.toml`` (PEP 621 ``[project]``
5
+ plus ``[tool.kivy]`` and one ``[tool.kivy.<platform>]`` overlay per target),
6
+ resolves a PEP 751 ``pylock.<platform>.toml`` build manifest, and materializes
7
+ it into that platform's project or bundle. See ``docs/design`` for the full
8
+ specification.
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,41 @@
1
+ """kivyforge artifact layer: fetch/cache/verify, wheels, frameworks, runtimes (spec 03)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cache import ArtifactCache
6
+ from .download import Downloader, DownloadError, UrllibDownloader, fetch_artifact
7
+ from .frameworks import (
8
+ FrameworkConflict,
9
+ copy_wheel_frameworks,
10
+ extract_xcframework_archive,
11
+ )
12
+ from .runtime import (
13
+ PythonOrgRuntime,
14
+ PythonRuntime,
15
+ RuntimeArtifact,
16
+ get_runtime,
17
+ )
18
+ from .verify import HashMismatch, sha256_bytes, sha256_file, verify_file
19
+ from .wheels import BuildSlice, WheelSelectionError, select_wheel
20
+
21
+ __all__ = [
22
+ "ArtifactCache",
23
+ "DownloadError",
24
+ "Downloader",
25
+ "UrllibDownloader",
26
+ "fetch_artifact",
27
+ "FrameworkConflict",
28
+ "copy_wheel_frameworks",
29
+ "extract_xcframework_archive",
30
+ "PythonOrgRuntime",
31
+ "PythonRuntime",
32
+ "RuntimeArtifact",
33
+ "get_runtime",
34
+ "HashMismatch",
35
+ "sha256_bytes",
36
+ "sha256_file",
37
+ "verify_file",
38
+ "BuildSlice",
39
+ "WheelSelectionError",
40
+ "select_wheel",
41
+ ]
@@ -0,0 +1,81 @@
1
+ """Content-addressed artifact cache (spec 03).
2
+
3
+ Artifacts (wheels, xcframework archives, Python.xcframework) are cached by
4
+ SHA-256 in a per-user, OS-appropriate cache directory so repeated builds and
5
+ ``--no-cache`` opt-outs are cheap and deterministic. The cache key is the
6
+ content hash, so a tampered/replaced upstream artifact can never silently
7
+ satisfy a cache hit for a different hash.
8
+
9
+ The cache root follows each host's convention: ``~/Library/Caches/kivyforge``
10
+ on macOS, ``$XDG_CACHE_HOME/kivyforge`` (default ``~/.cache/kivyforge``) on
11
+ Linux and other Unixes, and ``%LOCALAPPDATA%\\kivyforge\\Cache`` on Windows.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import platform as _platform
18
+ import shutil
19
+ from pathlib import Path
20
+
21
+ APP_NAME = "kivyforge"
22
+
23
+
24
+ def _default_cache_root() -> Path:
25
+ """The per-user artifact cache directory for the current host OS."""
26
+ system = _platform.system()
27
+ if system == "Darwin":
28
+ return Path.home() / "Library" / "Caches" / APP_NAME / "artifacts"
29
+ if system == "Windows":
30
+ base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
31
+ return Path(base) / APP_NAME / "Cache" / "artifacts"
32
+ # Linux and other Unixes: the XDG Base Directory spec.
33
+ xdg = os.environ.get("XDG_CACHE_HOME")
34
+ base = Path(xdg) if xdg else (Path.home() / ".cache")
35
+ return base / APP_NAME / "artifacts"
36
+
37
+
38
+ DEFAULT_CACHE_ROOT = _default_cache_root()
39
+
40
+
41
+ class ArtifactCache:
42
+ def __init__(self, root: str | Path | None = None) -> None:
43
+ self.root = DEFAULT_CACHE_ROOT if root is None else Path(root)
44
+
45
+ def path_for(self, sha256: str, filename: str) -> Path:
46
+ # Shard by the first two hex chars to keep directories small.
47
+ return self.root / sha256[:2] / f"{sha256}-{filename}"
48
+
49
+ def get(self, sha256: str, filename: str) -> Path | None:
50
+ candidate = self.path_for(sha256, filename)
51
+ return candidate if candidate.is_file() else None
52
+
53
+ def find_by_filename(self, filename: str) -> Path | None:
54
+ """Return a cached artifact path matching ``{sha256}-{filename}``, if any."""
55
+ if not self.root.is_dir():
56
+ return None
57
+ suffix = f"-{filename}"
58
+ for shard in self.root.iterdir():
59
+ if not shard.is_dir():
60
+ continue
61
+ for path in shard.iterdir():
62
+ if path.is_file() and path.name.endswith(suffix):
63
+ return path
64
+ return None
65
+
66
+ def put_file(self, src: str | Path, sha256: str, filename: str) -> Path:
67
+ dest = self.path_for(sha256, filename)
68
+ dest.parent.mkdir(parents=True, exist_ok=True)
69
+ if not dest.is_file():
70
+ shutil.copyfile(src, dest)
71
+ return dest
72
+
73
+ def put_bytes(self, data: bytes, sha256: str, filename: str) -> Path:
74
+ dest = self.path_for(sha256, filename)
75
+ dest.parent.mkdir(parents=True, exist_ok=True)
76
+ dest.write_bytes(data)
77
+ return dest
78
+
79
+ def clear(self) -> None:
80
+ if self.root.is_dir():
81
+ shutil.rmtree(self.root)
@@ -0,0 +1,424 @@
1
+ """Collect lockfile artifacts into the staging tree (build steps 2-5, spec 06).
2
+
3
+ Downloads/verifies the Python.xcframework, installs pinned wheels into
4
+ ``pip-deps/``, copies wheel-embedded ``.frameworks/`` into ``Frameworks/``, and
5
+ extracts user-declared native xcframeworks; ``Frameworks/`` is rebuilt from the
6
+ lock every run. The subprocess runner is injectable so the orchestration can be
7
+ unit-tested without invoking pip.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tarfile
16
+ import tempfile
17
+ import zipfile
18
+ from collections.abc import Callable
19
+ from pathlib import Path
20
+
21
+ from ..build_outcome import discard_note
22
+ from ..bundle.pycompile import PycompileError, byte_compile, select_compiler
23
+ from ..config.model import DesktopBuildSettings
24
+ from ..lock.model import LockedWheel
25
+ from ..platforms.ios.lock.model import Lockfile
26
+ from ..report.diagnostics import BYTECOMPILE_NO_INTERP
27
+ from ..report.failures import ClassifiedError, reclassify
28
+ from .cache import ArtifactCache
29
+ from .download import Downloader, fetch_artifact
30
+ from .frameworks import (
31
+ PlacedFramework,
32
+ clear_staged_frameworks,
33
+ copy_wheel_frameworks,
34
+ extract_xcframework_archive,
35
+ )
36
+ from .wheels import BuildSlice, select_wheel
37
+
38
+
39
+ class CollectError(ClassifiedError):
40
+ pass
41
+
42
+
43
+ def pip_install_command(
44
+ wheel_files: list[str],
45
+ *,
46
+ target_dir: str,
47
+ platform_tag: str,
48
+ python_version: str,
49
+ abi: str,
50
+ python_executable: str | None = None,
51
+ ) -> list[str]:
52
+ """Build the pip cross-install command (spec 05 step 4); no re-resolution."""
53
+ py = python_executable or sys.executable
54
+ cmd = [
55
+ py,
56
+ "-m",
57
+ "pip",
58
+ "install",
59
+ # pip is the one tool here that can genuinely prompt: an index needing
60
+ # auth asks for credentials, which in CI hangs until the job times out.
61
+ "--no-input",
62
+ # pip would byte-compile what it installs with the *host* interpreter,
63
+ # whose .pyc the target's CPython cannot load. That includes a wheel's
64
+ # data files, whose __pycache__ then outlives them. A release build
65
+ # compiles pip-deps itself, with a matching interpreter.
66
+ "--no-compile",
67
+ "--no-deps",
68
+ "--only-binary=:all:",
69
+ "--platform",
70
+ platform_tag,
71
+ "--python-version",
72
+ python_version,
73
+ "--abi",
74
+ abi,
75
+ "--implementation",
76
+ "cp",
77
+ "--target",
78
+ target_dir,
79
+ ]
80
+ cmd += wheel_files
81
+ return cmd
82
+
83
+
84
+ def collect_artifacts(
85
+ lock: Lockfile,
86
+ layout,
87
+ *,
88
+ build_slices: list[BuildSlice],
89
+ project_root: Path,
90
+ cache: ArtifactCache | None = None,
91
+ downloader: Downloader | None = None,
92
+ no_cache: bool = False,
93
+ runner=subprocess.run,
94
+ python_executable: str | None = None,
95
+ release: bool = False,
96
+ build_settings: DesktopBuildSettings | None = None,
97
+ echo: Callable[[str], None] = lambda msg: None,
98
+ note=discard_note,
99
+ ) -> None:
100
+ cache = cache or ArtifactCache()
101
+
102
+ # Python.xcframework is multi-slice and slice-independent, so it is fetched
103
+ # once regardless of how many pip-deps slices we populate. pip-deps itself is
104
+ # installed per slice (device/simulator never share .so files): a bare
105
+ # `kivyforge build` passes both slices so either Xcode destination builds
106
+ # without a re-run, while a targeted build passes only its slice.
107
+ _install_python_xcframework(
108
+ lock, layout, cache=cache, downloader=downloader, no_cache=no_cache
109
+ )
110
+ # Wheel-embedded frameworks are multi-slice xcframeworks; union them across
111
+ # slices (dedup by name) so user-declared natives below don't clobber them.
112
+ # Shared registry of staged frameworks (name -> provenance + content hash).
113
+ # Threaded across every pip-deps slice and into native xcframework
114
+ # extraction so identical duplicates dedupe and conflicting ones fail loudly.
115
+ staged: dict[str, PlacedFramework] = {}
116
+ # Frameworks/ is rebuilt from this lock alone, so a removed wheel or
117
+ # native entry takes its xcframework with it.
118
+ clear_staged_frameworks(layout.frameworks)
119
+ for build_slice in build_slices:
120
+ slice_pip_deps = layout.pip_deps_for_slice(build_slice.target)
121
+ _install_wheels(
122
+ lock,
123
+ layout,
124
+ slice_pip_deps=slice_pip_deps,
125
+ build_slice=build_slice,
126
+ project_root=project_root,
127
+ cache=cache,
128
+ downloader=downloader,
129
+ no_cache=no_cache,
130
+ runner=runner,
131
+ python_executable=python_executable,
132
+ )
133
+ _compile_pip_deps(
134
+ slice_pip_deps,
135
+ python_version=lock.python_xcframework.version,
136
+ release=release,
137
+ build_settings=build_settings,
138
+ echo=echo,
139
+ note=note,
140
+ )
141
+ _stamp_collected(slice_pip_deps)
142
+ copy_wheel_frameworks(slice_pip_deps, layout.frameworks, existing=staged)
143
+ _install_native_xcframeworks(
144
+ lock,
145
+ layout,
146
+ staged=staged,
147
+ project_root=project_root,
148
+ cache=cache,
149
+ downloader=downloader,
150
+ no_cache=no_cache,
151
+ )
152
+
153
+
154
+ def _stamp_collected(slice_pip_deps: Path) -> None:
155
+ """Mark a pip-deps slice as collected.
156
+
157
+ The Build Python run script uses this marker — not directory emptiness — to
158
+ tell "collected, no third-party deps" (valid: an app with an empty pip-deps
159
+ slice) from "never collected" (error: wrong/uncollected Xcode destination).
160
+ It lives beside the slice dir so it is never rsync'd into the app bundle.
161
+ """
162
+ marker = slice_pip_deps.parent / f"{slice_pip_deps.name}.collected"
163
+ marker.write_text("", encoding="utf-8")
164
+
165
+
166
+ def _setting_applies(value: bool | str, *, release: bool) -> bool:
167
+ """A ``build_settings`` tri-state resolved for the build being produced."""
168
+ if isinstance(value, bool):
169
+ return value
170
+ return release # DESKTOP_RELEASE_ONLY
171
+
172
+
173
+ def _compile_pip_deps(
174
+ slice_pip_deps: Path,
175
+ *,
176
+ python_version: str,
177
+ release: bool,
178
+ build_settings: DesktopBuildSettings | None,
179
+ echo: Callable[[str], None],
180
+ note=discard_note,
181
+ ) -> None:
182
+ """Byte-compile/strip a collected pip-deps slice, per ``build_settings``.
183
+
184
+ Mirrors each desktop backend's own ``_resolve_byte_compile`` (macOS/Linux/
185
+ Windows ``bundle.py``), with one iOS-specific simplification: the
186
+ Python.xcframework ships no standalone interpreter binary to shell out to
187
+ (it's a linkable library, not an executable), so the "staged interpreter"
188
+ rung of the compiler ladder never applies here — it always falls straight
189
+ to "this process's interpreter, if its CPython minor matches the target",
190
+ or degrades.
191
+ """
192
+ settings = build_settings or DesktopBuildSettings()
193
+ if not _setting_applies(settings.byte_compile, release=release):
194
+ return
195
+ compiler = select_compiler(
196
+ staged_interpreter=Path(), native=False, python_version=python_version
197
+ )
198
+ if compiler is None:
199
+ minor = ".".join(python_version.split(".")[:2])
200
+ headline = (
201
+ f"no final CPython {minor} found (this project ships {python_version})"
202
+ )
203
+ why_and_fix = (
204
+ " Pre-releases do not count: CPython only freezes the .pyc "
205
+ f"magic number at the first release candidate, so a {minor} alpha "
206
+ f"writes bytecode {python_version} refuses to import.\n"
207
+ f" Fix: install a final CPython {minor} — kivyforge finds it "
208
+ "automatically — or set byte_compile = false in "
209
+ "[tool.kivy.ios.python.build_settings]."
210
+ )
211
+ if settings.byte_compile is True:
212
+ raise CollectError(
213
+ "[tool.kivy.ios.python.build_settings].byte_compile = true, but "
214
+ f"{headline}.\n{why_and_fix}"
215
+ )
216
+ echo(f"[stage] not byte-compiling pip-deps: {headline}.\n{why_and_fix}")
217
+ note(
218
+ BYTECOMPILE_NO_INTERP,
219
+ f"not byte-compiling pip-deps: {headline}.",
220
+ {"payload": "pip-deps"},
221
+ )
222
+ return
223
+ strip_source = _setting_applies(settings.strip_source, release=release)
224
+ with_what = " ".join(compiler) if compiler else "this interpreter"
225
+ echo(
226
+ f"[stage] byte-compiling pip-deps with {with_what}"
227
+ + (" (.pyc only)" if strip_source else "")
228
+ )
229
+ try:
230
+ byte_compile(
231
+ [slice_pip_deps],
232
+ compiler=compiler,
233
+ strip_source=strip_source,
234
+ stripdir=slice_pip_deps,
235
+ )
236
+ except PycompileError as exc:
237
+ raise CollectError(
238
+ f"{exc}\n Fix it, or set "
239
+ "[tool.kivy.ios.python.build_settings].byte_compile = false.",
240
+ **reclassify(exc),
241
+ ) from exc
242
+
243
+
244
+ def _install_python_xcframework(lock, layout, *, cache, downloader, no_cache) -> None:
245
+ px = lock.python_xcframework
246
+ filename = px.url.rsplit("/", 1)[-1]
247
+ archive = fetch_artifact(
248
+ name=f"Python.xcframework {px.version}",
249
+ sha256=px.sha256,
250
+ filename=filename,
251
+ url=px.url,
252
+ cache=cache,
253
+ downloader=downloader,
254
+ no_cache=no_cache,
255
+ )
256
+ dest = layout.python_xcframework
257
+ if dest.exists():
258
+ shutil.rmtree(dest)
259
+ with tempfile.TemporaryDirectory(prefix="kivy-pyxc-") as tmp:
260
+ tmp_dir = Path(tmp)
261
+ with tarfile.open(archive, "r:gz") as tf:
262
+ # filter="data" rejects path traversal, absolute paths, and
263
+ # unsafe special files (Python 3.12+ / PEP 706).
264
+ tf.extractall(tmp_dir, filter="data")
265
+ found = next(
266
+ (p for p in tmp_dir.rglob("Python.xcframework") if p.is_dir()), None
267
+ )
268
+ if found is None:
269
+ raise CollectError(
270
+ "Python.xcframework not found inside downloaded archive."
271
+ )
272
+ shutil.copytree(found, dest)
273
+
274
+
275
+ def _install_wheels(
276
+ lock,
277
+ layout,
278
+ *,
279
+ slice_pip_deps,
280
+ build_slice,
281
+ project_root,
282
+ cache,
283
+ downloader,
284
+ no_cache,
285
+ runner,
286
+ python_executable,
287
+ ) -> None:
288
+ with tempfile.TemporaryDirectory(prefix="kivy-wheels-") as tmp:
289
+ stage = Path(tmp)
290
+ wheel_files: list[str] = []
291
+ selected_wheels: list[LockedWheel] = []
292
+ for pkg in lock.packages:
293
+ wheel = select_wheel(pkg, build_slice)
294
+ selected_wheels.append(wheel)
295
+ local = fetch_artifact(
296
+ name=f"{pkg.name} {pkg.version}",
297
+ sha256=wheel.sha256,
298
+ filename=wheel.name,
299
+ url=wheel.url,
300
+ path=wheel.path,
301
+ project_root=project_root,
302
+ cache=cache,
303
+ downloader=downloader,
304
+ no_cache=no_cache,
305
+ )
306
+ # Cache entries are content-addressed ({sha256}-{name}); pip rejects
307
+ # that filename, so stage each wheel under its canonical PEP 427 name.
308
+ staged = stage / wheel.name
309
+ if local.resolve() != staged.resolve():
310
+ shutil.copy2(local, staged)
311
+ wheel_files.append(str(staged))
312
+ # Each collect always starts from a clean slate so no stale .so
313
+ # files from a previous slice accumulate in the slice-specific dir --
314
+ # also when the lock now pins nothing, or removing the last dependency
315
+ # would leave the previous collect's packages in the app.
316
+ if slice_pip_deps.is_dir():
317
+ shutil.rmtree(slice_pip_deps)
318
+ slice_pip_deps.mkdir(parents=True, exist_ok=True)
319
+ if not wheel_files:
320
+ return
321
+ abi = "cp" + "".join(lock.python_xcframework.version.split(".")[:2])
322
+ # Use the selected wheels' actual platform tag for pip's --platform.
323
+ # The wheel tag reflects the Python binary's compile-time minimum OS
324
+ # (e.g. ios_13_0 from python.org's cp315 binary), which may be lower
325
+ # than the project's deployment_target. pip requires --platform to
326
+ # match the wheel being installed, so use the selected native wheel's
327
+ # tag rather than the computed build_slice tag.
328
+ pip_platform = _native_platform_tag(selected_wheels, build_slice)
329
+ cmd = pip_install_command(
330
+ wheel_files,
331
+ target_dir=str(slice_pip_deps),
332
+ platform_tag=pip_platform,
333
+ python_version=".".join(lock.python_xcframework.version.split(".")[:2]),
334
+ abi=abi,
335
+ python_executable=python_executable,
336
+ )
337
+ proc = runner(cmd, capture_output=True, text=True)
338
+ if proc.returncode != 0:
339
+ raise CollectError(
340
+ f"pip install of pinned wheels failed:\n{proc.stderr or proc.stdout}"
341
+ )
342
+ # pip --target creates a bin/ of host-platform console scripts.
343
+ # Strip it immediately — these macOS executables cannot run on iOS
344
+ # and should not appear in the Xcode project tree or app bundle.
345
+ bin_dir = slice_pip_deps / "bin"
346
+ if bin_dir.is_dir():
347
+ shutil.rmtree(bin_dir)
348
+ for wheel_file in wheel_files:
349
+ _drop_wheel_data_files(Path(wheel_file), slice_pip_deps)
350
+
351
+
352
+ def _drop_wheel_data_files(wheel: Path, slice_pip_deps: Path) -> None:
353
+ """Remove what *wheel* installed from its ``.data/data/`` tree.
354
+
355
+ That tree is a wheel's ``data_files``: paths relative to an install prefix
356
+ (``share/...``, examples, headers, test sources), which ``pip --target``
357
+ lays down at the root of the slice. Nothing on iOS can find them: the app's
358
+ ``sys.prefix`` is not ``pip-deps``. They are dead weight, and a release
359
+ build byte-compiles them, so one Python 2 example fails the whole package
360
+ (pyobjus ships 18). macOS never installs this tree (``wheels_stage._unpack``
361
+ keeps only ``purelib``/``platlib``); this brings iOS in line.
362
+
363
+ Only the files the wheel itself names are removed, never a whole
364
+ directory, so a data file beside a real package cannot take the package
365
+ with it. Directories left empty are pruned.
366
+ """
367
+ with zipfile.ZipFile(wheel) as zf:
368
+ names = zf.namelist()
369
+ root = slice_pip_deps.resolve()
370
+ emptied: set[Path] = set()
371
+ for name in names:
372
+ parts = name.split("/")
373
+ if len(parts) < 3 or not parts[0].endswith(".data") or parts[1] != "data":
374
+ continue
375
+ installed = (slice_pip_deps / Path(*parts[2:])).resolve()
376
+ if not installed.is_relative_to(root) or not installed.is_file():
377
+ continue
378
+ installed.unlink()
379
+ emptied.update(p for p in installed.parents if p.is_relative_to(root))
380
+ # Deepest first, so a parent is only tried once its children are gone.
381
+ for directory in sorted(emptied, key=lambda p: len(p.parts), reverse=True):
382
+ if directory != root and directory.is_dir() and not any(directory.iterdir()):
383
+ directory.rmdir()
384
+
385
+
386
+ def _install_native_xcframeworks(
387
+ lock, layout, *, staged, project_root, cache, downloader, no_cache
388
+ ) -> None:
389
+ for xc in lock.xcframeworks:
390
+ filename = (xc.url or xc.path or xc.name).rsplit("/", 1)[-1]
391
+ archive = fetch_artifact(
392
+ name=xc.name,
393
+ sha256=xc.sha256,
394
+ filename=filename,
395
+ url=xc.url,
396
+ path=xc.path,
397
+ project_root=project_root,
398
+ cache=cache,
399
+ downloader=downloader,
400
+ no_cache=no_cache,
401
+ )
402
+ extract_xcframework_archive(
403
+ archive,
404
+ layout.frameworks,
405
+ name=xc.name,
406
+ archive_format=xc.archive_format,
407
+ archive_member=xc.archive_member,
408
+ existing=staged,
409
+ )
410
+
411
+
412
+ def _native_platform_tag(selected: list[LockedWheel], slice_: BuildSlice) -> str:
413
+ """Return the pip --platform tag to use for the selected wheels.
414
+
415
+ All native wheels in a single build are produced by the same Python binary
416
+ and therefore share the same platform tag (e.g. ios_13_0_arm64_iphoneos).
417
+ Using the wheel's actual tag (rather than the project deployment_target tag)
418
+ ensures pip accepts the wheel even when the wheel's min-OS is lower than the
419
+ project's deployment_target.
420
+ """
421
+ for wheel in selected:
422
+ if not wheel.is_pure_python:
423
+ return wheel.platform_tag
424
+ return slice_.platform_tag