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,107 @@
1
+ """Acquire the bundled CPython runtime for the AppDir (linux-spec).
2
+
3
+ Consumes the provider-agnostic runtime pin from ``pylock.linux.toml`` (the
4
+ single ``x86_64`` artifact this phase — one arch per AppImage), fetches +
5
+ verifies its archive, extracts it, and materializes the relocatable CPython tree
6
+ at ``usr/python``. The extracted archive is normalized to its canonical root via
7
+ ``normalized_runtime_root`` keyed on the lock's recorded ``provider`` — staging
8
+ never hard-codes a provider's archive internals (spec 07). The PBS linux-gnu
9
+ ``install_only`` archive is the same unix-prefix layout as the darwin ones and
10
+ uses ``$ORIGIN``-relative rpaths, so it is relocatable as-shipped — no patchelf
11
+ pass, and (unlike macOS) no ``lipo`` merge, since Linux ships one arch per
12
+ AppImage.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import shutil
18
+ import tarfile
19
+ import tempfile
20
+ from pathlib import Path
21
+
22
+ from kivyforge.artifacts.cache import ArtifactCache
23
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
24
+ from kivyforge.lock.wheelruntime.model import PythonRuntime
25
+ from kivyforge.lock.wheelruntime.runtime import (
26
+ RuntimeProviderError,
27
+ normalized_runtime_root,
28
+ )
29
+
30
+ from . import AppDirError
31
+
32
+
33
+ def stage_runtime(
34
+ runtime: PythonRuntime,
35
+ arch: str,
36
+ home: Path,
37
+ *,
38
+ project_root: Path,
39
+ cache: ArtifactCache | None = None,
40
+ no_cache: bool = False,
41
+ ) -> Path:
42
+ """Materialize the runtime for *arch* at *home*; return *home*.
43
+
44
+ *home* is the final CPython home directory (whose ``bin/python3`` ``AppRun``
45
+ execs). It is emptied and recreated.
46
+ """
47
+ cache = cache or ArtifactCache()
48
+ artifact = runtime.artifact_for(arch)
49
+ if artifact is None:
50
+ raise AppDirError(
51
+ f"the lock has no {arch} runtime artifact; re-run "
52
+ f"`kivyforge lock -p linux` (locked archs: "
53
+ f"{', '.join(a.arch for a in runtime.artifacts)})."
54
+ )
55
+
56
+ tmp = Path(tempfile.mkdtemp(prefix="kivy-runtime-"))
57
+ try:
58
+ archive = _fetch(artifact, arch, project_root, cache, no_cache)
59
+ extracted = _extract(archive, tmp, runtime.provider)
60
+ if home.exists():
61
+ shutil.rmtree(home)
62
+ home.parent.mkdir(parents=True, exist_ok=True)
63
+ shutil.copytree(extracted, home, symlinks=True)
64
+ return home
65
+ finally:
66
+ shutil.rmtree(tmp, ignore_errors=True)
67
+
68
+
69
+ def _fetch(artifact, arch, project_root, cache, no_cache) -> Path:
70
+ filename = artifact.url.rsplit("/", 1)[-1] or f"python-{arch}.tar.gz"
71
+ try:
72
+ return fetch_artifact(
73
+ name=f"python runtime ({arch})",
74
+ sha256=artifact.sha256,
75
+ filename=filename,
76
+ url=artifact.url,
77
+ project_root=project_root,
78
+ cache=cache,
79
+ no_cache=no_cache,
80
+ )
81
+ except DownloadError as exc:
82
+ raise AppDirError(str(exc)) from exc
83
+
84
+
85
+ def _extract(archive: Path, into: Path, provider: str) -> Path:
86
+ into.mkdir(parents=True, exist_ok=True)
87
+ try:
88
+ with tarfile.open(archive, "r:*") as tf:
89
+ _safe_extractall(tf, into)
90
+ except (tarfile.TarError, OSError) as exc:
91
+ raise AppDirError(f"failed to extract {archive.name}: {exc}") from exc
92
+ try:
93
+ return normalized_runtime_root(provider, into)
94
+ except RuntimeProviderError as exc:
95
+ raise AppDirError(f"{archive.name}: {exc}") from exc
96
+
97
+
98
+ def _safe_extractall(tf: tarfile.TarFile, into: Path) -> None:
99
+ """Extract, rejecting members that would escape *into* (path traversal)."""
100
+ base = into.resolve()
101
+ for member in tf.getmembers():
102
+ target = (into / member.name).resolve()
103
+ if not (target == base or base in target.parents):
104
+ raise AppDirError(f"unsafe path in archive: {member.name!r}")
105
+ # Paths are validated above; fully_trusted preserves the runtime's
106
+ # permissions and symlinks (the restrictive filters would rewrite them).
107
+ tf.extractall(into, filter="fully_trusted") # noqa: S202 — members validated just above
@@ -0,0 +1,103 @@
1
+ """Install the locked wheels into the AppDir's ``usr/lib`` (site-packages).
2
+
3
+ Wheels are fetched + SHA-256-verified from ``pylock.linux.toml`` (URL or
4
+ vendored path) and unpacked directly — no pip, so the bundler is independent of
5
+ the host interpreter. Linux ships one arch per AppImage, so per package a single
6
+ wheel satisfies coverage: a pure-Python (``py3-none-any``) wheel or a manylinux
7
+ (or vendored ``linux_x86_64``) wheel matching the assembly arch. There is no
8
+ ``lipo``-style merge as on macOS.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import shutil
14
+ import zipfile
15
+ from pathlib import Path
16
+
17
+ from kivyforge.artifacts.cache import ArtifactCache
18
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
19
+ from kivyforge.lock.model import LockedPackage, LockedWheel
20
+
21
+ from . import AppDirError
22
+ from .lock import linux_wheel_arch
23
+
24
+
25
+ def stage_wheels(
26
+ packages: tuple[LockedPackage, ...],
27
+ arch: str,
28
+ lib_dir: Path,
29
+ *,
30
+ project_root: Path,
31
+ cache: ArtifactCache | None = None,
32
+ no_cache: bool = False,
33
+ ) -> None:
34
+ """Unpack every package's wheel for *arch* into *lib_dir*."""
35
+ cache = cache or ArtifactCache()
36
+ if lib_dir.exists():
37
+ shutil.rmtree(lib_dir)
38
+ lib_dir.mkdir(parents=True)
39
+
40
+ for pkg in packages:
41
+ wheel = _select_wheel(pkg, arch)
42
+ _unpack(_fetch(wheel, project_root, cache, no_cache), lib_dir)
43
+
44
+
45
+ def _select_wheel(pkg: LockedPackage, arch: str) -> LockedWheel:
46
+ """Pick the wheel to unpack: pure-Python, else the one covering *arch*."""
47
+ for wheel in pkg.wheels:
48
+ if wheel.is_pure_python:
49
+ return wheel
50
+ for wheel in pkg.wheels:
51
+ if any(linux_wheel_arch(sub) == arch for sub in wheel.platform_tag.split(".")):
52
+ return wheel
53
+ raise AppDirError(
54
+ f"{pkg.name} {pkg.version} has no wheel for arch {arch} in the lock.\n"
55
+ " Re-run `kivyforge lock -p linux`."
56
+ )
57
+
58
+
59
+ def _fetch(wheel: LockedWheel, project_root, cache, no_cache) -> Path:
60
+ try:
61
+ return fetch_artifact(
62
+ name=wheel.name,
63
+ sha256=wheel.sha256,
64
+ filename=wheel.name,
65
+ url=wheel.url,
66
+ path=wheel.path,
67
+ project_root=project_root,
68
+ cache=cache,
69
+ no_cache=no_cache,
70
+ )
71
+ except DownloadError as exc:
72
+ raise AppDirError(str(exc)) from exc
73
+
74
+
75
+ def _unpack(wheel: Path, target: Path) -> None:
76
+ """Extract a wheel into *target*, normalizing ``.data/{purelib,platlib}``."""
77
+ with zipfile.ZipFile(wheel) as zf:
78
+ _safe_extract(zf, target)
79
+ for data_dir in list(target.glob("*.data")):
80
+ for sub in ("purelib", "platlib"):
81
+ src = data_dir / sub
82
+ if src.is_dir():
83
+ _merge_tree(src, target)
84
+ shutil.rmtree(data_dir, ignore_errors=True)
85
+
86
+
87
+ def _merge_tree(src: Path, dest: Path) -> None:
88
+ for item in src.iterdir():
89
+ target = dest / item.name
90
+ if item.is_dir():
91
+ target.mkdir(exist_ok=True)
92
+ _merge_tree(item, target)
93
+ else:
94
+ shutil.copy2(item, target)
95
+
96
+
97
+ def _safe_extract(zf: zipfile.ZipFile, target: Path) -> None:
98
+ base = target.resolve()
99
+ for member in zf.namelist():
100
+ resolved = (target / member).resolve()
101
+ if not (resolved == base or base in resolved.parents):
102
+ raise AppDirError(f"unsafe path in wheel: {member!r}")
103
+ zf.extractall(target) # noqa: S202 — members validated just above
@@ -0,0 +1,121 @@
1
+ """macOS platform backend.
2
+
3
+ Produces a self-contained ``.app`` bundle (macos-spec). macOS is the host
4
+ default on Darwin, so a Mac with ``[tool.kivy.macos]`` configured resolves to
5
+ this backend without ``-p``. It requires a macOS host (``codesign``/``lipo``
6
+ from the Xcode command-line tools); the full Xcode IDE is not needed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import platform as _platform
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ from ..base import HostCapabilityError, Platform
16
+
17
+ if TYPE_CHECKING:
18
+ from kivyforge.doctor.result import CheckResult
19
+
20
+
21
+ class AppBundleError(Exception):
22
+ """A macOS ``.app`` bundling failure surfaced with an actionable message."""
23
+
24
+
25
+ class MacosPlatform(Platform):
26
+ name = "macos"
27
+ # macOS is a desktop target built on its own OS, so Darwin maps to it as the
28
+ # host default (resolution-chain step 3).
29
+ host_system = "Darwin"
30
+ # The distributable is the .app bundle; .dmg/installer is external.
31
+ package_formats = ("app",)
32
+
33
+ def check_host_capability(self, *, host_system: str | None = None) -> None:
34
+ host = host_system if host_system is not None else _platform.system()
35
+ if host != "Darwin":
36
+ raise HostCapabilityError(
37
+ "building for macOS requires a macOS host; "
38
+ f"this host is {host!r}.\n"
39
+ " The .app bundler signs Mach-O binaries with codesign and "
40
+ "merges per-arch runtimes with lipo — both are macOS-only.\n"
41
+ " Run `kivyforge doctor -p macos` on a Mac to check the setup."
42
+ )
43
+
44
+ def build(
45
+ self,
46
+ project_root: Path,
47
+ *,
48
+ target: str | None,
49
+ arch: str | None,
50
+ no_verify_lock: bool,
51
+ no_cache: bool,
52
+ team_id: str | None,
53
+ signing_identity: str | None,
54
+ export_method: str,
55
+ debug: bool = False,
56
+ fmt: str | None = None,
57
+ abi: str | None = None,
58
+ ) -> None:
59
+ self.reject_ios_only_target(target)
60
+ from .cli import macos_build
61
+
62
+ macos_build(
63
+ project_root,
64
+ arch=arch,
65
+ no_verify_lock=no_verify_lock,
66
+ no_cache=no_cache,
67
+ )
68
+
69
+ def run(
70
+ self,
71
+ project_root: Path,
72
+ *,
73
+ target: str,
74
+ arch: str | None,
75
+ destination: str | None,
76
+ no_build: bool,
77
+ ) -> None:
78
+ from .cli import macos_run
79
+
80
+ macos_run(project_root, arch=arch, no_build=no_build)
81
+
82
+ def package(
83
+ self,
84
+ project_root: Path,
85
+ *,
86
+ fmt: str,
87
+ arch: str | None,
88
+ team_id: str | None,
89
+ signing_identity: str | None,
90
+ export_method: str,
91
+ notarize: bool | None,
92
+ notary_profile: str | None,
93
+ no_verify_lock: bool,
94
+ no_cache: bool,
95
+ abi: str | None = None,
96
+ keystore: str | None = None,
97
+ key_alias: str | None = None,
98
+ ) -> None:
99
+ from .cli import macos_package
100
+
101
+ macos_package(
102
+ project_root,
103
+ arch=arch,
104
+ no_verify_lock=no_verify_lock,
105
+ no_cache=no_cache,
106
+ signing_identity=signing_identity,
107
+ notarize=notarize,
108
+ notary_profile=notary_profile,
109
+ )
110
+
111
+ def status(self, project_root: Path) -> None:
112
+ from .cli import macos_status
113
+
114
+ macos_status(project_root)
115
+
116
+ def doctor(
117
+ self, cwd: Path, *, kivyforge_version: str, offline: bool
118
+ ) -> list[CheckResult]:
119
+ from .doctor import macos_doctor
120
+
121
+ return macos_doctor(cwd, kivyforge_version=kivyforge_version, offline=offline)
@@ -0,0 +1,255 @@
1
+ """Assemble (and optionally sign) the macOS ``.app`` bundle (macos-spec).
2
+
3
+ Orchestrates the pieces: resolve the assembly arch from the lock + ``--arch``
4
+ (macOS is arm64-only, so this is always a single arch), stage the runtime +
5
+ wheels, copy the app sources, render the icon + Info.plist + launcher, and
6
+ ad-hoc sign. Produces the layout documented in macos-spec:
7
+
8
+ <Name>.app/Contents/{Info.plist, MacOS/<exe>,
9
+ Resources/{app,lib,python,<exe>.icns}}
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import platform
15
+ import plistlib
16
+ import shutil
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ import click
21
+
22
+ from kivyforge.artifacts.cache import ArtifactCache
23
+ from kivyforge.bundle.pycompile import PycompileError, byte_compile, select_compiler
24
+ from kivyforge.config.model import Config
25
+
26
+ from . import AppBundleError
27
+ from .icns import generate_icns
28
+ from .launcher import build_launcher
29
+ from .lock import MacosLockfile
30
+ from .native_stage import stage_native_binaries
31
+ from .plist import build_info_plist
32
+ from .runtime_stage import stage_runtime
33
+ from .signing import sign_bundle_adhoc
34
+ from .wheels_stage import stage_wheels
35
+
36
+ _IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store")
37
+
38
+
39
+ def resolve_assembly_arch(locked: tuple[str, ...], arch: str | None) -> str:
40
+ """The single arch to assemble. macOS ships arm64 only."""
41
+ if not locked:
42
+ raise AppBundleError(
43
+ "the lock covers no architectures; re-run `kivyforge lock -p macos`."
44
+ )
45
+ if arch is None:
46
+ return locked[0]
47
+ if arch not in locked:
48
+ raise AppBundleError(
49
+ f"--arch {arch} is not in the lock (covers {', '.join(locked)}).\n"
50
+ f" macOS builds are arm64-only; pick a locked arch."
51
+ )
52
+ return arch
53
+
54
+
55
+ def _setting_applies(value: bool | str, *, release: bool) -> bool:
56
+ """A ``build_settings`` tri-state resolved for the build being produced."""
57
+ if isinstance(value, bool):
58
+ return value
59
+ return release # DESKTOP_RELEASE_ONLY
60
+
61
+
62
+ def _resolve_byte_compile(
63
+ config: Config,
64
+ *,
65
+ staged_interpreter: Path,
66
+ target_arch: str,
67
+ python_version: str,
68
+ release: bool,
69
+ ) -> tuple[tuple[str, ...] | None, bool]:
70
+ """Decide whether to byte-compile, and with which interpreter.
71
+
72
+ Returns ``(compiler_argv_or_None, strip_source)``; ``None`` means do not
73
+ byte-compile. ``byte_compile = true`` is read as "I insist", so a missing
74
+ interpreter is an error; the default ``"release"`` degrades to shipping
75
+ source with a warning rather than breaking a build the user never
76
+ configured.
77
+ """
78
+ settings = config.macos_required.build_settings
79
+ if not _setting_applies(settings.byte_compile, release=release):
80
+ return None, False
81
+ native = platform.machine().lower() == target_arch
82
+ compiler = select_compiler(
83
+ staged_interpreter=staged_interpreter,
84
+ native=native,
85
+ python_version=python_version,
86
+ )
87
+ if compiler is None:
88
+ message = (
89
+ f"this project ships CPython {python_version}, and no CPython of "
90
+ "that minor could be found to byte-compile with (a .pyc is only "
91
+ "loadable by the exact CPython minor that wrote it)"
92
+ )
93
+ if settings.byte_compile is True:
94
+ raise AppBundleError(
95
+ "[tool.kivy.macos.build_settings].byte_compile = true but "
96
+ f"{message}.\n"
97
+ " Install a matching CPython, or set byte_compile = false."
98
+ )
99
+ click.echo(f"[stage] not byte-compiling: {message}.")
100
+ return None, False
101
+ strip = _setting_applies(settings.strip_source, release=release)
102
+ return compiler, strip
103
+
104
+
105
+ def build_app_bundle(
106
+ config: Config,
107
+ lock: MacosLockfile,
108
+ project_root: Path,
109
+ *,
110
+ arch: str | None = None,
111
+ staging_dir: Path | None = None,
112
+ sign: bool = True,
113
+ no_cache: bool = False,
114
+ cache: ArtifactCache | None = None,
115
+ release: bool = False,
116
+ echo=click.echo,
117
+ ) -> Path:
118
+ """Build the ``.app`` and return its path."""
119
+ target_arch = resolve_assembly_arch(lock.archs, arch)
120
+ cache = cache or ArtifactCache()
121
+ staging_dir = staging_dir or (project_root / "build" / "macos")
122
+ exe = config.app_slug
123
+ app = staging_dir / f"{config.display_name}.app"
124
+
125
+ # Assemble into a temp .app and swap it in only on success, so a failure
126
+ # mid-assembly (e.g. a transient runtime/wheel fetch error, or a signing
127
+ # failure) leaves the previous, working .app untouched instead of a
128
+ # half-written one. Mirrors the Linux AppDir builder's write-then-swap.
129
+ staging_dir.mkdir(parents=True, exist_ok=True)
130
+ work = Path(tempfile.mkdtemp(dir=staging_dir, prefix=f".{app.name}.tmp-"))
131
+ try:
132
+ contents = work / "Contents"
133
+ (contents / "MacOS").mkdir(parents=True)
134
+ resources = contents / "Resources"
135
+ resources.mkdir()
136
+
137
+ echo(
138
+ f"Staging CPython {lock.python_runtime.version} runtime ({target_arch}) ..."
139
+ )
140
+ stage_runtime(
141
+ lock.python_runtime,
142
+ target_arch,
143
+ resources / "python",
144
+ project_root=project_root,
145
+ cache=cache,
146
+ no_cache=no_cache,
147
+ )
148
+
149
+ echo(f"Installing {len(lock.packages)} locked packages ...")
150
+ stage_wheels(
151
+ lock.packages,
152
+ target_arch,
153
+ resources / "lib",
154
+ project_root=project_root,
155
+ cache=cache,
156
+ no_cache=no_cache,
157
+ )
158
+
159
+ if lock.native_binaries:
160
+ echo(f"Staging {len(lock.native_binaries)} native binaries ...")
161
+ stage_native_binaries(
162
+ lock,
163
+ resources,
164
+ project_root=project_root,
165
+ cache=cache,
166
+ no_cache=no_cache,
167
+ )
168
+
169
+ _copy_app_sources(config, project_root, resources / "app")
170
+ icon_file = _stage_icon(config, project_root, resources, exe)
171
+
172
+ build_launcher(
173
+ contents / "MacOS" / exe,
174
+ entry_point=config.kivy.entry_point,
175
+ arch=target_arch,
176
+ )
177
+
178
+ plist = build_info_plist(config, executable=exe, icon_file=icon_file)
179
+ with (contents / "Info.plist").open("wb") as fh:
180
+ plistlib.dump(plist, fh)
181
+
182
+ # Byte-compile before signing: codesign seals every file it signs, so
183
+ # mutating the payload afterward (deleting .py, writing .pyc) would
184
+ # invalidate the very signature this function is about to produce.
185
+ compiler, strip_source = _resolve_byte_compile(
186
+ config,
187
+ staged_interpreter=resources / "python" / "bin" / "python3",
188
+ target_arch=target_arch,
189
+ python_version=lock.python_runtime.version,
190
+ release=release,
191
+ )
192
+ if compiler is not None:
193
+ with_what = " ".join(compiler) if compiler else "this interpreter"
194
+ echo(
195
+ f"[stage] byte-compiling the Python payload with {with_what}"
196
+ + (" (.pyc only)" if strip_source else "")
197
+ )
198
+ try:
199
+ byte_compile(
200
+ [resources / "app", resources / "lib"],
201
+ compiler=compiler,
202
+ strip_source=strip_source,
203
+ stripdir=work,
204
+ )
205
+ except PycompileError as exc:
206
+ raise AppBundleError(
207
+ f"{exc}\n Fix it, or set "
208
+ "[tool.kivy.macos.build_settings].byte_compile = false."
209
+ ) from exc
210
+
211
+ # Sign the temp bundle before the swap: codesign embeds signatures in the
212
+ # Mach-O files + Contents/_CodeSignature, so an atomic rename preserves
213
+ # them, and a signing failure never displaces the previous good .app.
214
+ if sign:
215
+ echo("Ad-hoc signing the bundle ...")
216
+ count = sign_bundle_adhoc(work)
217
+ echo(f" signed {count} Mach-O binaries + the bundle")
218
+ except BaseException:
219
+ shutil.rmtree(work, ignore_errors=True)
220
+ raise
221
+
222
+ # Swap the freshly built tree in for the previous one. The rename is atomic
223
+ # within staging_dir; the brief rmtree→rename window only exists after a
224
+ # fully successful build.
225
+ if app.exists():
226
+ shutil.rmtree(app)
227
+ work.replace(app)
228
+ return app
229
+
230
+
231
+ def _copy_app_sources(config: Config, project_root: Path, dest: Path) -> None:
232
+ src = (project_root / config.kivy.app_dir).resolve()
233
+ if not src.is_dir():
234
+ raise AppBundleError(
235
+ f"[tool.kivy].app_dir points to {config.kivy.app_dir!r}, which is not "
236
+ f"a directory under {project_root}."
237
+ )
238
+ entry = src / f"{config.kivy.entry_point}.py"
239
+ if not entry.is_file():
240
+ raise AppBundleError(
241
+ f"entry point {config.kivy.entry_point}.py not found in "
242
+ f"{config.kivy.app_dir}/ (set [tool.kivy].entry_point)."
243
+ )
244
+ shutil.copytree(src, dest, ignore=_IGNORE)
245
+
246
+
247
+ def _stage_icon(
248
+ config: Config, project_root: Path, resources: Path, exe: str
249
+ ) -> str | None:
250
+ source = config.macos_required.icons.source
251
+ if not source:
252
+ return None
253
+ icns = f"{exe}.icns"
254
+ generate_icns((project_root / source).resolve(), resources / icns)
255
+ return icns