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
kivyforge/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """kivyforge: a declarative iOS bundler for Kivy apps.
2
+
3
+ kivyforge 3.0 reads a user-authored ``pyproject.toml`` (PEP 621 ``[project]``
4
+ plus ``[tool.kivy]`` / ``[tool.kivy.ios]``), resolves a PEP 751
5
+ ``pylock.ios.toml`` build manifest, and materializes it into a generated
6
+ Xcode project. See ``docs/proposals`` for the full specification.
7
+ """
8
+
9
+ __version__ = "3.0.0.dev0"
@@ -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,354 @@
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. The subprocess runner is injectable
6
+ so the orchestration can be unit-tested without invoking pip.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import tarfile
15
+ import tempfile
16
+ from collections.abc import Callable
17
+ from pathlib import Path
18
+
19
+ from ..bundle.pycompile import PycompileError, byte_compile, select_compiler
20
+ from ..config.model import DesktopBuildSettings
21
+ from ..lock.model import LockedWheel
22
+ from ..platforms.ios.lock.model import Lockfile
23
+ from .cache import ArtifactCache
24
+ from .download import Downloader, fetch_artifact
25
+ from .frameworks import (
26
+ PlacedFramework,
27
+ copy_wheel_frameworks,
28
+ extract_xcframework_archive,
29
+ )
30
+ from .wheels import BuildSlice, select_wheel
31
+
32
+
33
+ class CollectError(Exception):
34
+ pass
35
+
36
+
37
+ def pip_install_command(
38
+ wheel_files: list[str],
39
+ *,
40
+ target_dir: str,
41
+ platform_tag: str,
42
+ python_version: str,
43
+ abi: str,
44
+ python_executable: str | None = None,
45
+ ) -> list[str]:
46
+ """Build the pip cross-install command (spec 05 step 4); no re-resolution."""
47
+ py = python_executable or sys.executable
48
+ cmd = [
49
+ py,
50
+ "-m",
51
+ "pip",
52
+ "install",
53
+ "--no-deps",
54
+ "--only-binary=:all:",
55
+ "--platform",
56
+ platform_tag,
57
+ "--python-version",
58
+ python_version,
59
+ "--abi",
60
+ abi,
61
+ "--implementation",
62
+ "cp",
63
+ "--target",
64
+ target_dir,
65
+ ]
66
+ cmd += wheel_files
67
+ return cmd
68
+
69
+
70
+ def collect_artifacts(
71
+ lock: Lockfile,
72
+ layout,
73
+ *,
74
+ build_slices: list[BuildSlice],
75
+ project_root: Path,
76
+ cache: ArtifactCache | None = None,
77
+ downloader: Downloader | None = None,
78
+ no_cache: bool = False,
79
+ runner=subprocess.run,
80
+ python_executable: str | None = None,
81
+ release: bool = False,
82
+ build_settings: DesktopBuildSettings | None = None,
83
+ echo: Callable[[str], None] = lambda msg: None,
84
+ ) -> None:
85
+ cache = cache or ArtifactCache()
86
+
87
+ # Python.xcframework is multi-slice and slice-independent, so it is fetched
88
+ # once regardless of how many pip-deps slices we populate. pip-deps itself is
89
+ # installed per slice (device/simulator never share .so files): a bare
90
+ # `kivyforge build` passes both slices so either Xcode destination builds
91
+ # without a re-run, while a targeted build passes only its slice.
92
+ _install_python_xcframework(
93
+ lock, layout, cache=cache, downloader=downloader, no_cache=no_cache
94
+ )
95
+ # Wheel-embedded frameworks are multi-slice xcframeworks; union them across
96
+ # slices (dedup by name) so user-declared natives below don't clobber them.
97
+ # Shared registry of staged frameworks (name -> provenance + content hash).
98
+ # Threaded across every pip-deps slice and into native xcframework
99
+ # extraction so identical duplicates dedupe and conflicting ones fail loudly.
100
+ staged: dict[str, PlacedFramework] = {}
101
+ for build_slice in build_slices:
102
+ slice_pip_deps = layout.pip_deps_for_slice(build_slice.target)
103
+ _install_wheels(
104
+ lock,
105
+ layout,
106
+ slice_pip_deps=slice_pip_deps,
107
+ build_slice=build_slice,
108
+ project_root=project_root,
109
+ cache=cache,
110
+ downloader=downloader,
111
+ no_cache=no_cache,
112
+ runner=runner,
113
+ python_executable=python_executable,
114
+ )
115
+ _compile_pip_deps(
116
+ slice_pip_deps,
117
+ python_version=lock.python_xcframework.version,
118
+ release=release,
119
+ build_settings=build_settings,
120
+ echo=echo,
121
+ )
122
+ _stamp_collected(slice_pip_deps)
123
+ copy_wheel_frameworks(slice_pip_deps, layout.frameworks, existing=staged)
124
+ _install_native_xcframeworks(
125
+ lock,
126
+ layout,
127
+ staged=staged,
128
+ project_root=project_root,
129
+ cache=cache,
130
+ downloader=downloader,
131
+ no_cache=no_cache,
132
+ )
133
+
134
+
135
+ def _stamp_collected(slice_pip_deps: Path) -> None:
136
+ """Mark a pip-deps slice as collected.
137
+
138
+ The Build Python run script uses this marker — not directory emptiness — to
139
+ tell "collected, no third-party deps" (valid: an app with an empty pip-deps
140
+ slice) from "never collected" (error: wrong/uncollected Xcode destination).
141
+ It lives beside the slice dir so it is never rsync'd into the app bundle.
142
+ """
143
+ marker = slice_pip_deps.parent / f"{slice_pip_deps.name}.collected"
144
+ marker.write_text("", encoding="utf-8")
145
+
146
+
147
+ def _setting_applies(value: bool | str, *, release: bool) -> bool:
148
+ """A ``build_settings`` tri-state resolved for the build being produced."""
149
+ if isinstance(value, bool):
150
+ return value
151
+ return release # DESKTOP_RELEASE_ONLY
152
+
153
+
154
+ def _compile_pip_deps(
155
+ slice_pip_deps: Path,
156
+ *,
157
+ python_version: str,
158
+ release: bool,
159
+ build_settings: DesktopBuildSettings | None,
160
+ echo: Callable[[str], None],
161
+ ) -> None:
162
+ """Byte-compile/strip a collected pip-deps slice, per ``build_settings``.
163
+
164
+ Mirrors each desktop backend's own ``_resolve_byte_compile`` (macOS/Linux/
165
+ Windows ``bundle.py``), with one iOS-specific simplification: the
166
+ Python.xcframework ships no standalone interpreter binary to shell out to
167
+ (it's a linkable library, not an executable), so the "staged interpreter"
168
+ rung of the compiler ladder never applies here — it always falls straight
169
+ to "this process's interpreter, if its CPython minor matches the target",
170
+ or degrades.
171
+ """
172
+ settings = build_settings or DesktopBuildSettings()
173
+ if not _setting_applies(settings.byte_compile, release=release):
174
+ return
175
+ compiler = select_compiler(
176
+ staged_interpreter=Path(), native=False, python_version=python_version
177
+ )
178
+ if compiler is None:
179
+ message = (
180
+ f"this project ships CPython {python_version}, and no CPython of "
181
+ "that minor could be found to byte-compile with (a .pyc is only "
182
+ "loadable by the exact CPython minor that wrote it)"
183
+ )
184
+ if settings.byte_compile is True:
185
+ raise CollectError(
186
+ "[tool.kivy.ios.python.build_settings].byte_compile = true but "
187
+ f"{message}.\n"
188
+ " Install a matching CPython, or set byte_compile = false."
189
+ )
190
+ echo(f"[stage] not byte-compiling pip-deps: {message}.")
191
+ return
192
+ strip_source = _setting_applies(settings.strip_source, release=release)
193
+ with_what = " ".join(compiler) if compiler else "this interpreter"
194
+ echo(
195
+ f"[stage] byte-compiling pip-deps with {with_what}"
196
+ + (" (.pyc only)" if strip_source else "")
197
+ )
198
+ try:
199
+ byte_compile(
200
+ [slice_pip_deps],
201
+ compiler=compiler,
202
+ strip_source=strip_source,
203
+ stripdir=slice_pip_deps,
204
+ )
205
+ except PycompileError as exc:
206
+ raise CollectError(
207
+ f"{exc}\n Fix it, or set "
208
+ "[tool.kivy.ios.python.build_settings].byte_compile = false."
209
+ ) from exc
210
+
211
+
212
+ def _install_python_xcframework(lock, layout, *, cache, downloader, no_cache) -> None:
213
+ px = lock.python_xcframework
214
+ filename = px.url.rsplit("/", 1)[-1]
215
+ archive = fetch_artifact(
216
+ name=f"Python.xcframework {px.version}",
217
+ sha256=px.sha256,
218
+ filename=filename,
219
+ url=px.url,
220
+ cache=cache,
221
+ downloader=downloader,
222
+ no_cache=no_cache,
223
+ )
224
+ dest = layout.python_xcframework
225
+ if dest.exists():
226
+ shutil.rmtree(dest)
227
+ with tempfile.TemporaryDirectory(prefix="kivy-pyxc-") as tmp:
228
+ tmp_dir = Path(tmp)
229
+ with tarfile.open(archive, "r:gz") as tf:
230
+ # filter="data" rejects path traversal, absolute paths, and
231
+ # unsafe special files (Python 3.12+ / PEP 706).
232
+ tf.extractall(tmp_dir, filter="data")
233
+ found = next(
234
+ (p for p in tmp_dir.rglob("Python.xcframework") if p.is_dir()), None
235
+ )
236
+ if found is None:
237
+ raise CollectError(
238
+ "Python.xcframework not found inside downloaded archive."
239
+ )
240
+ shutil.copytree(found, dest)
241
+
242
+
243
+ def _install_wheels(
244
+ lock,
245
+ layout,
246
+ *,
247
+ slice_pip_deps,
248
+ build_slice,
249
+ project_root,
250
+ cache,
251
+ downloader,
252
+ no_cache,
253
+ runner,
254
+ python_executable,
255
+ ) -> None:
256
+ with tempfile.TemporaryDirectory(prefix="kivy-wheels-") as tmp:
257
+ stage = Path(tmp)
258
+ wheel_files: list[str] = []
259
+ selected_wheels: list[LockedWheel] = []
260
+ for pkg in lock.packages:
261
+ wheel = select_wheel(pkg, build_slice)
262
+ selected_wheels.append(wheel)
263
+ local = fetch_artifact(
264
+ name=f"{pkg.name} {pkg.version}",
265
+ sha256=wheel.sha256,
266
+ filename=wheel.name,
267
+ url=wheel.url,
268
+ path=wheel.path,
269
+ project_root=project_root,
270
+ cache=cache,
271
+ downloader=downloader,
272
+ no_cache=no_cache,
273
+ )
274
+ # Cache entries are content-addressed ({sha256}-{name}); pip rejects
275
+ # that filename, so stage each wheel under its canonical PEP 427 name.
276
+ staged = stage / wheel.name
277
+ if local.resolve() != staged.resolve():
278
+ shutil.copy2(local, staged)
279
+ wheel_files.append(str(staged))
280
+ if not wheel_files:
281
+ return
282
+ # Each collect always starts from a clean slate so no stale .so
283
+ # files from a previous slice accumulate in the slice-specific dir.
284
+ if slice_pip_deps.is_dir():
285
+ shutil.rmtree(slice_pip_deps)
286
+ slice_pip_deps.mkdir(parents=True, exist_ok=True)
287
+ abi = "cp" + "".join(lock.python_xcframework.version.split(".")[:2])
288
+ # Use the selected wheels' actual platform tag for pip's --platform.
289
+ # The wheel tag reflects the Python binary's compile-time minimum OS
290
+ # (e.g. ios_13_0 from python.org's cp315 binary), which may be lower
291
+ # than the project's deployment_target. pip requires --platform to
292
+ # match the wheel being installed, so use the selected native wheel's
293
+ # tag rather than the computed build_slice tag.
294
+ pip_platform = _native_platform_tag(selected_wheels, build_slice)
295
+ cmd = pip_install_command(
296
+ wheel_files,
297
+ target_dir=str(slice_pip_deps),
298
+ platform_tag=pip_platform,
299
+ python_version=".".join(lock.python_xcframework.version.split(".")[:2]),
300
+ abi=abi,
301
+ python_executable=python_executable,
302
+ )
303
+ proc = runner(cmd, capture_output=True, text=True)
304
+ if proc.returncode != 0:
305
+ raise CollectError(
306
+ f"pip install of pinned wheels failed:\n{proc.stderr or proc.stdout}"
307
+ )
308
+ # pip --target creates a bin/ of host-platform console scripts.
309
+ # Strip it immediately — these macOS executables cannot run on iOS
310
+ # and should not appear in the Xcode project tree or app bundle.
311
+ bin_dir = slice_pip_deps / "bin"
312
+ if bin_dir.is_dir():
313
+ shutil.rmtree(bin_dir)
314
+
315
+
316
+ def _install_native_xcframeworks(
317
+ lock, layout, *, staged, project_root, cache, downloader, no_cache
318
+ ) -> None:
319
+ for xc in lock.xcframeworks:
320
+ filename = (xc.url or xc.path or xc.name).rsplit("/", 1)[-1]
321
+ archive = fetch_artifact(
322
+ name=xc.name,
323
+ sha256=xc.sha256,
324
+ filename=filename,
325
+ url=xc.url,
326
+ path=xc.path,
327
+ project_root=project_root,
328
+ cache=cache,
329
+ downloader=downloader,
330
+ no_cache=no_cache,
331
+ )
332
+ extract_xcframework_archive(
333
+ archive,
334
+ layout.frameworks,
335
+ name=xc.name,
336
+ archive_format=xc.archive_format,
337
+ archive_member=xc.archive_member,
338
+ existing=staged,
339
+ )
340
+
341
+
342
+ def _native_platform_tag(selected: list[LockedWheel], slice_: BuildSlice) -> str:
343
+ """Return the pip --platform tag to use for the selected wheels.
344
+
345
+ All native wheels in a single build are produced by the same Python binary
346
+ and therefore share the same platform tag (e.g. ios_13_0_arm64_iphoneos).
347
+ Using the wheel's actual tag (rather than the project deployment_target tag)
348
+ ensures pip accepts the wheel even when the wheel's min-OS is lower than the
349
+ project's deployment_target.
350
+ """
351
+ for wheel in selected:
352
+ if not wheel.is_pure_python:
353
+ return wheel.platform_tag
354
+ return slice_.platform_tag
@@ -0,0 +1,125 @@
1
+ """Fetch + verify + cache artifacts by URL or repo-relative path (spec 03).
2
+
3
+ A single ``fetch_artifact`` entry point handles the three lockfile sources:
4
+ ``url`` (download), local ``path`` (vendored), and cache hits. Every byte that
5
+ enters the build is SHA-256-verified against the lockfile pin before use.
6
+
7
+ The network fetch is injectable (``Downloader``) so unit tests stay hermetic.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import tempfile
13
+ import urllib.request
14
+ from pathlib import Path, PurePosixPath
15
+ from typing import Protocol
16
+
17
+ from ..lock.find_links import FindLinksError, wheel_path_from_project_root
18
+ from .cache import ArtifactCache
19
+ from .verify import HashMismatch, sha256_file, verify_file
20
+
21
+
22
+ class DownloadError(Exception):
23
+ pass
24
+
25
+
26
+ class Downloader(Protocol):
27
+ def fetch_to(self, url: str, dest: Path) -> None: ...
28
+
29
+
30
+ class UrllibDownloader:
31
+ def fetch_to(self, url: str, dest: Path) -> None:
32
+ try:
33
+ with urllib.request.urlopen(url) as resp, open(dest, "wb") as out: # noqa: S310
34
+ for chunk in iter(lambda: resp.read(1 << 20), b""):
35
+ out.write(chunk)
36
+ except (OSError, ValueError) as exc:
37
+ raise DownloadError(f"failed to download {url}: {exc}") from exc
38
+
39
+
40
+ def fetch_artifact(
41
+ *,
42
+ name: str,
43
+ sha256: str,
44
+ filename: str,
45
+ url: str | None = None,
46
+ path: str | None = None,
47
+ project_root: Path | None = None,
48
+ cache: ArtifactCache | None = None,
49
+ downloader: Downloader | None = None,
50
+ no_cache: bool = False,
51
+ ) -> Path:
52
+ """Return a verified local path for an artifact (url xor path).
53
+
54
+ * ``url`` — cache-first download, then SHA-256 verify.
55
+ * ``path`` — repo-relative (resolved against ``project_root``); verified in
56
+ place. Absolute paths or paths escaping the project are rejected.
57
+ """
58
+ if bool(url) == bool(path):
59
+ raise DownloadError(f"{name}: exactly one of url/path is required")
60
+
61
+ if path is not None:
62
+ return _resolve_local(
63
+ name=name, path=path, sha256=sha256, project_root=project_root
64
+ )
65
+
66
+ if url is None:
67
+ # xor check above guarantees one of url/path; path branch already returned.
68
+ raise DownloadError(f"{name}: exactly one of url/path is required")
69
+
70
+ cache = cache or ArtifactCache()
71
+ downloader = downloader or UrllibDownloader()
72
+
73
+ if not no_cache:
74
+ cached = cache.get(sha256, filename)
75
+ if cached is not None and _cache_hit_intact(cached, sha256, name=name):
76
+ return cached
77
+
78
+ with tempfile.TemporaryDirectory(prefix="kivy-dl-") as tmp:
79
+ staged = Path(tmp) / filename
80
+ downloader.fetch_to(url, staged)
81
+ verify_file(staged, sha256, name=name, source=url)
82
+ return cache.put_file(staged, sha256, filename)
83
+
84
+
85
+ def _cache_hit_intact(cached: Path, sha256: str, *, name: str) -> bool:
86
+ """Re-verify a cache hit before trusting it (spec 03 "every byte verified").
87
+
88
+ The cache key is only the *filename* (``{sha256}-{name}``); it is not proof
89
+ that the bytes on disk still match. A non-atomic ``copyfile`` killed mid-write
90
+ leaves a truncated file at the hash-named path, and the cache dir is
91
+ user-writable (bit-rot/tampering). On a mismatch we drop the corrupt entry
92
+ and return False so the caller re-downloads rather than feeding bad bytes
93
+ into the build.
94
+ """
95
+ try:
96
+ verify_file(cached, sha256, name=name, source=str(cached))
97
+ except HashMismatch:
98
+ cached.unlink(missing_ok=True)
99
+ return False
100
+ return True
101
+
102
+
103
+ def _resolve_local(
104
+ *, name: str, path: str, sha256: str, project_root: Path | None
105
+ ) -> Path:
106
+ if PurePosixPath(path).is_absolute() or Path(path).is_absolute():
107
+ raise DownloadError(
108
+ f"{name}: lockfile path {path!r} is absolute; only repo-relative "
109
+ f"paths are allowed (they resolve identically on every clone)."
110
+ )
111
+ root = (project_root or Path.cwd()).resolve()
112
+ resolved = (root / path).resolve()
113
+ try:
114
+ wheel_path_from_project_root(root, resolved)
115
+ except FindLinksError as exc:
116
+ raise DownloadError(
117
+ f"{name}: lockfile path {path!r} escapes the allowed vendored "
118
+ f"wheel scope for this project."
119
+ ) from exc
120
+ if not resolved.is_file():
121
+ raise DownloadError(f"{name}: vendored artifact not found at {path!r}.")
122
+ actual = sha256_file(resolved)
123
+ if actual != sha256:
124
+ raise HashMismatch(name=name, source=path, expected=sha256, actual=actual)
125
+ return resolved