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,110 @@
1
+ """Acquire + normalize the bundled CPython runtime for the ``.app``.
2
+
3
+ Consumes the provider-agnostic runtime pin from ``pylock.macos.toml`` (per-arch
4
+ URL + SHA-256), fetches + verifies the arm64 archive, extracts it, and produces
5
+ the **canonical relocatable CPython tree**. Nothing downstream (launcher,
6
+ bundler, signing) knows or cares which provider produced it — the whole point of
7
+ the runtime-provider seam.
8
+
9
+ macOS is arm64-only, so there is no second arch and no ``lipo`` merge: the
10
+ extracted tree ships as-is.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import shutil
16
+ import tarfile
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ from kivyforge.artifacts.cache import ArtifactCache
21
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
22
+ from kivyforge.lock.wheelruntime.model import PythonRuntime
23
+ from kivyforge.lock.wheelruntime.runtime import (
24
+ RuntimeProviderError,
25
+ normalized_runtime_root,
26
+ )
27
+
28
+ from . import AppBundleError
29
+
30
+
31
+ def stage_runtime(
32
+ runtime: PythonRuntime,
33
+ arch: str,
34
+ home: Path,
35
+ *,
36
+ project_root: Path,
37
+ cache: ArtifactCache | None = None,
38
+ no_cache: bool = False,
39
+ ) -> Path:
40
+ """Materialize the runtime for *arch* at *home*; return *home*.
41
+
42
+ *home* is the final CPython home directory (whose ``bin/python3`` the
43
+ launcher execs). It is emptied and recreated. Deliberately placed under the
44
+ bundle's ``Resources/`` (not ``Frameworks/``): a unix-prefix Python tree
45
+ under ``Frameworks/`` trips ``codesign``'s framework auto-discovery on
46
+ non-framework subdirs (e.g. ``lib/tk*``), whereas ``Resources`` content is
47
+ sealed as data and the Mach-O files are signed individually.
48
+ """
49
+ cache = cache or ArtifactCache()
50
+
51
+ tmp = Path(tempfile.mkdtemp(prefix="kivy-runtime-"))
52
+ try:
53
+ artifact = runtime.artifact_for(arch)
54
+ if artifact is None:
55
+ raise AppBundleError(
56
+ f"the lock has no {arch} runtime artifact; re-run "
57
+ f"`kivyforge lock -p macos` (locked archs: "
58
+ f"{', '.join(a.arch for a in runtime.artifacts)})."
59
+ )
60
+ archive = _fetch(artifact, arch, project_root, cache, no_cache)
61
+ extracted = _extract(archive, tmp / arch, runtime.provider)
62
+
63
+ if home.exists():
64
+ shutil.rmtree(home)
65
+ home.parent.mkdir(parents=True, exist_ok=True)
66
+ shutil.copytree(extracted, home, symlinks=True)
67
+ return home
68
+ finally:
69
+ shutil.rmtree(tmp, ignore_errors=True)
70
+
71
+
72
+ def _fetch(artifact, arch, project_root, cache, no_cache) -> Path:
73
+ filename = artifact.url.rsplit("/", 1)[-1] or f"python-{arch}.tar.gz"
74
+ try:
75
+ return fetch_artifact(
76
+ name=f"python runtime ({arch})",
77
+ sha256=artifact.sha256,
78
+ filename=filename,
79
+ url=artifact.url,
80
+ project_root=project_root,
81
+ cache=cache,
82
+ no_cache=no_cache,
83
+ )
84
+ except DownloadError as exc:
85
+ raise AppBundleError(str(exc)) from exc
86
+
87
+
88
+ def _extract(archive: Path, into: Path, provider: str) -> Path:
89
+ into.mkdir(parents=True, exist_ok=True)
90
+ try:
91
+ with tarfile.open(archive, "r:*") as tf:
92
+ _safe_extractall(tf, into)
93
+ except (tarfile.TarError, OSError) as exc:
94
+ raise AppBundleError(f"failed to extract {archive.name}: {exc}") from exc
95
+ try:
96
+ return normalized_runtime_root(provider, into)
97
+ except RuntimeProviderError as exc:
98
+ raise AppBundleError(f"{archive.name}: {exc}") from exc
99
+
100
+
101
+ def _safe_extractall(tf: tarfile.TarFile, into: Path) -> None:
102
+ """Extract, rejecting members that would escape *into* (path traversal)."""
103
+ base = into.resolve()
104
+ for member in tf.getmembers():
105
+ target = (into / member.name).resolve()
106
+ if not (target == base or base in target.parents):
107
+ raise AppBundleError(f"unsafe path in archive: {member.name!r}")
108
+ # Paths are validated above; fully_trusted preserves the runtime's
109
+ # permissions and symlinks (the restrictive filters would rewrite them).
110
+ tf.extractall(into, filter="fully_trusted") # noqa: S202 — members validated just above
@@ -0,0 +1,78 @@
1
+ """Code signing of the assembled ``.app`` (macos-spec).
2
+
3
+ Two tiers, both **inside-out** — every nested Mach-O (the runtime's
4
+ ``python3``/``libpython``/``.dylib`` and the wheels' ``.so`` extensions) is
5
+ signed first, deepest first, then the ``.app`` bundle last — because signing
6
+ the bundle seals a hash of its already-signed contents:
7
+
8
+ - **Ad-hoc** (``codesign --sign -``): the mandatory Apple-Silicon floor (the
9
+ kernel refuses to run unsigned arm64 Mach-O). Integrity only, no trust.
10
+ - **Developer ID** (``--sign <identity> --options runtime --timestamp``): the
11
+ distribution tier. Hardened Runtime + a secure timestamp are notarization
12
+ requirements; the main executable additionally carries entitlements that a
13
+ hardened Python bundle needs (see ``DEFAULT_HARDENED_ENTITLEMENTS``).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import plistlib
19
+ import tempfile
20
+ from pathlib import Path
21
+
22
+ from .machotools import codesign_adhoc, codesign_identity, is_macho
23
+
24
+ # Hardened-Runtime entitlements a bundled CPython app needs by default:
25
+ # - allow-unsigned-executable-memory: ctypes/cffi trampolines allocate W+X
26
+ # pages, which the Hardened Runtime otherwise forbids.
27
+ # - disable-library-validation: the app loads wheel `.so`/`.dylib` binaries
28
+ # signed by other teams (or re-signed here); library validation would
29
+ # require every image to share the app's team ID.
30
+ # Users may override either (or add more) via [tool.kivy.macos.entitlements].
31
+ DEFAULT_HARDENED_ENTITLEMENTS: dict[str, object] = {
32
+ "com.apple.security.cs.allow-unsigned-executable-memory": True,
33
+ "com.apple.security.cs.disable-library-validation": True,
34
+ }
35
+
36
+
37
+ def sign_bundle_adhoc(app: Path) -> int:
38
+ """Ad-hoc sign every Mach-O in *app*, then *app* itself; return the count.
39
+
40
+ Returns the number of ``codesign`` invocations (nested binaries + the bundle),
41
+ which callers use for a one-line progress message.
42
+ """
43
+ machos = _machos_inside_out(app)
44
+ for macho in machos:
45
+ codesign_adhoc(macho)
46
+ codesign_adhoc(app)
47
+ return len(machos) + 1
48
+
49
+
50
+ def sign_bundle_developer_id(
51
+ app: Path,
52
+ identity: str,
53
+ *,
54
+ extra_entitlements: dict[str, object] | None = None,
55
+ ) -> int:
56
+ """Developer ID deep-sign *app* for distribution; return the codesign count.
57
+
58
+ Nested Mach-O binaries get Hardened Runtime + timestamp; the ``.app`` seal
59
+ (which signs the main executable) additionally carries the merged
60
+ entitlements (defaults + ``[tool.kivy.macos.entitlements]``, user wins).
61
+ """
62
+ entitlements = {**DEFAULT_HARDENED_ENTITLEMENTS, **(extra_entitlements or {})}
63
+ machos = _machos_inside_out(app)
64
+ with tempfile.TemporaryDirectory(prefix="kivyforge-ent-") as tmp:
65
+ ent_file = Path(tmp) / "entitlements.plist"
66
+ with ent_file.open("wb") as fh:
67
+ plistlib.dump(entitlements, fh)
68
+ for macho in machos:
69
+ codesign_identity(macho, identity)
70
+ codesign_identity(app, identity, entitlements=ent_file)
71
+ return len(machos) + 1
72
+
73
+
74
+ def _machos_inside_out(app: Path) -> list[Path]:
75
+ """Every Mach-O under *app*, deepest paths first (containers sign last)."""
76
+ machos = [p for p in app.rglob("*") if is_macho(p)]
77
+ machos.sort(key=lambda p: len(p.parts), reverse=True)
78
+ return machos
@@ -0,0 +1,108 @@
1
+ """Install the locked wheels into the ``.app``'s ``lib/`` (site-packages).
2
+
3
+ Wheels are fetched + SHA-256-verified from ``pylock.macos.toml`` (URL or vendored
4
+ path) and unpacked directly — no pip, so the bundler is independent of the host
5
+ interpreter's version/arch.
6
+
7
+ macOS builds are arm64-only, so exactly one wheel is unpacked per package and
8
+ there is no ``lipo`` merge. A ``universal2`` wheel is still perfectly valid — it
9
+ *contains* arm64 — which is why the arch match accepts it alongside a thin
10
+ ``arm64`` wheel and a pure-Python one.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import shutil
16
+ import zipfile
17
+ from pathlib import Path
18
+
19
+ from kivyforge.artifacts.cache import ArtifactCache
20
+ from kivyforge.artifacts.download import DownloadError, fetch_artifact
21
+ from kivyforge.lock.model import LockedPackage, LockedWheel
22
+
23
+ from . import AppBundleError
24
+ from .lock import wheel_arch
25
+
26
+
27
+ def stage_wheels(
28
+ packages: tuple[LockedPackage, ...],
29
+ arch: str,
30
+ lib_dir: Path,
31
+ *,
32
+ project_root: Path,
33
+ cache: ArtifactCache | None = None,
34
+ no_cache: bool = False,
35
+ ) -> None:
36
+ """Unpack every package's wheel for *arch* into *lib_dir*."""
37
+ cache = cache or ArtifactCache()
38
+ if lib_dir.exists():
39
+ shutil.rmtree(lib_dir)
40
+ lib_dir.mkdir(parents=True)
41
+
42
+ for pkg in packages:
43
+ wheel = _select_wheel(pkg, arch)
44
+ _unpack(_fetch(wheel, project_root, cache, no_cache), lib_dir)
45
+
46
+
47
+ def _select_wheel(pkg: LockedPackage, arch: str) -> LockedWheel:
48
+ """The one wheel to unpack for *arch*.
49
+
50
+ Pure-Python and ``universal2`` wheels both satisfy an arm64 build — the
51
+ latter *contains* arm64 — as does a thin ``arm64`` wheel.
52
+ """
53
+ for wheel in pkg.wheels:
54
+ if wheel.is_pure_python:
55
+ return wheel
56
+ if wheel_arch(wheel.platform_tag) in ("universal2", arch):
57
+ return wheel
58
+ raise AppBundleError(
59
+ f"{pkg.name} {pkg.version} has no {arch} wheel in the lock.\n"
60
+ f" Re-run `kivyforge lock -p macos`."
61
+ )
62
+
63
+
64
+ def _fetch(wheel: LockedWheel, project_root, cache, no_cache) -> Path:
65
+ try:
66
+ return fetch_artifact(
67
+ name=wheel.name,
68
+ sha256=wheel.sha256,
69
+ filename=wheel.name,
70
+ url=wheel.url,
71
+ path=wheel.path,
72
+ project_root=project_root,
73
+ cache=cache,
74
+ no_cache=no_cache,
75
+ )
76
+ except DownloadError as exc:
77
+ raise AppBundleError(str(exc)) from exc
78
+
79
+
80
+ def _unpack(wheel: Path, target: Path) -> None:
81
+ """Extract a wheel into *target*, normalizing ``.data/{purelib,platlib}``."""
82
+ with zipfile.ZipFile(wheel) as zf:
83
+ _safe_extract(zf, target)
84
+ for data_dir in list(target.glob("*.data")):
85
+ for sub in ("purelib", "platlib"):
86
+ src = data_dir / sub
87
+ if src.is_dir():
88
+ _merge_tree(src, target)
89
+ shutil.rmtree(data_dir, ignore_errors=True)
90
+
91
+
92
+ def _merge_tree(src: Path, dest: Path) -> None:
93
+ for item in src.iterdir():
94
+ target = dest / item.name
95
+ if item.is_dir():
96
+ target.mkdir(exist_ok=True)
97
+ _merge_tree(item, target)
98
+ else:
99
+ shutil.copy2(item, target)
100
+
101
+
102
+ def _safe_extract(zf: zipfile.ZipFile, target: Path) -> None:
103
+ base = target.resolve()
104
+ for member in zf.namelist():
105
+ resolved = (target / member).resolve()
106
+ if not (resolved == base or base in resolved.parents):
107
+ raise AppBundleError(f"unsafe path in wheel: {member!r}")
108
+ zf.extractall(target) # noqa: S202 — members validated just above
@@ -0,0 +1,133 @@
1
+ """Windows platform backend (windows-spec).
2
+
3
+ Produces a run-from-folder ``onedir`` bundle: a prebuilt windowed-subsystem
4
+ launcher ``.exe`` beside a whole python-build-standalone prefix, the app source,
5
+ a generated bootstrap, and the declared native-binaries channel. Windows is the
6
+ host default on Windows, so a Windows box with ``[tool.kivy.windows]`` configured
7
+ resolves to this backend without ``-p``. It requires a Windows host: the bundled
8
+ runtime is an MSVC amd64 build and the resource/signing tools are Windows-only.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import platform as _platform
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ from ..base import HostCapabilityError, Platform
18
+
19
+ if TYPE_CHECKING:
20
+ from kivyforge.doctor.result import CheckResult
21
+
22
+
23
+ class WindowsBundleError(Exception):
24
+ """A Windows onedir bundling failure surfaced with an actionable message."""
25
+
26
+
27
+ class WindowsPlatform(Platform):
28
+ name = "windows"
29
+ aliases = ("win",)
30
+ # Windows is a desktop target built on its own OS, so Windows maps to it as
31
+ # the host default (resolution-chain step 3).
32
+ host_system = "Windows"
33
+ # The onedir folder is both the dev-run target and the shipped distributable;
34
+ # installers stay permanently external (windows-spec). Folder is the only
35
+ # package format.
36
+ package_formats = ("folder",)
37
+
38
+ def check_host_capability(self, *, host_system: str | None = None) -> None:
39
+ # arm64: keep gating on the OS only, never the host CPU. Building an
40
+ # arm64 onedir needs no build-time compilation (runtime + wheels are
41
+ # fetched per target arch, the launcher is prebuilt/vendored, rcedit is
42
+ # emulated), so cross-arch builds are the intended model: an x64 Windows
43
+ # host should cross-build arm64 bundles (and vice versa) with no arm64
44
+ # hardware. The only host-CPU coupling is the msvcp140.dll System32
45
+ # fallback, which arm64 removes by pinning msvcp140.dll as an artifact
46
+ # (see runtime_stage.py). Do NOT add a host==target check. arm64-windows.md §7.
47
+ host = host_system if host_system is not None else _platform.system()
48
+ if host != "Windows":
49
+ raise HostCapabilityError(
50
+ "building for Windows requires a Windows host; "
51
+ f"this host is {host!r}.\n"
52
+ " The onedir bundle stages an MSVC amd64 python-build-standalone "
53
+ "runtime and uses Windows-only tools (rcedit, signtool).\n"
54
+ " Run `kivyforge doctor -p windows` on a Windows box to check "
55
+ "the setup."
56
+ )
57
+
58
+ def build(
59
+ self,
60
+ project_root: Path,
61
+ *,
62
+ target: str | None,
63
+ arch: str | None,
64
+ no_verify_lock: bool,
65
+ no_cache: bool,
66
+ team_id: str | None,
67
+ signing_identity: str | None,
68
+ export_method: str,
69
+ debug: bool = False,
70
+ fmt: str | None = None,
71
+ abi: str | None = None,
72
+ ) -> None:
73
+ self.reject_ios_only_target(target)
74
+ from .cli import windows_build
75
+
76
+ windows_build(
77
+ project_root,
78
+ arch=arch,
79
+ no_verify_lock=no_verify_lock,
80
+ no_cache=no_cache,
81
+ )
82
+
83
+ def run(
84
+ self,
85
+ project_root: Path,
86
+ *,
87
+ target: str,
88
+ arch: str | None,
89
+ destination: str | None,
90
+ no_build: bool,
91
+ ) -> None:
92
+ from .cli import windows_run
93
+
94
+ windows_run(project_root, arch=arch, no_build=no_build)
95
+
96
+ def package(
97
+ self,
98
+ project_root: Path,
99
+ *,
100
+ fmt: str,
101
+ arch: str | None,
102
+ team_id: str | None,
103
+ signing_identity: str | None,
104
+ export_method: str,
105
+ notarize: bool | None,
106
+ notary_profile: str | None,
107
+ no_verify_lock: bool,
108
+ no_cache: bool,
109
+ abi: str | None = None,
110
+ keystore: str | None = None,
111
+ key_alias: str | None = None,
112
+ ) -> None:
113
+ from .cli import windows_package
114
+
115
+ windows_package(
116
+ project_root,
117
+ fmt=fmt,
118
+ arch=arch,
119
+ no_verify_lock=no_verify_lock,
120
+ no_cache=no_cache,
121
+ )
122
+
123
+ def status(self, project_root: Path) -> None:
124
+ from .cli import windows_status
125
+
126
+ windows_status(project_root)
127
+
128
+ def doctor(
129
+ self, cwd: Path, *, kivyforge_version: str, offline: bool
130
+ ) -> list[CheckResult]:
131
+ from .doctor import windows_doctor
132
+
133
+ return windows_doctor(cwd, kivyforge_version=kivyforge_version, offline=offline)
@@ -0,0 +1,76 @@
1
+ """Access + integrity-check the vendored Windows binaries (windows-spec).
2
+
3
+ The onedir bundler consumes two prebuilt binaries shipped inside the package:
4
+ the launcher (``launcher-amd64.exe``) and the resource-patch tool
5
+ (``rcedit-x64.exe``). This module locates them and verifies each against the
6
+ SHA-256 pinned in ``vendor/SHA256SUMS`` before use, so a corrupted or
7
+ tampered-with vendored asset fails loudly instead of ending up in a shipped app.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from pathlib import Path
14
+
15
+ from . import WindowsBundleError
16
+
17
+ VENDOR_DIR = Path(__file__).resolve().parent / "vendor"
18
+ MANIFEST = VENDOR_DIR / "SHA256SUMS"
19
+ LAUNCHER_NAME = "launcher-amd64.exe"
20
+ RCEDIT_NAME = "rcedit-x64.exe"
21
+
22
+
23
+ def read_manifest() -> dict[str, str]:
24
+ """Parse ``vendor/SHA256SUMS`` into ``{filename: sha256}``."""
25
+ if not MANIFEST.is_file():
26
+ raise WindowsBundleError(
27
+ f"vendored-binary manifest missing: {MANIFEST}. The kivyforge "
28
+ "install is incomplete."
29
+ )
30
+ entries: dict[str, str] = {}
31
+ for line in MANIFEST.read_text(encoding="utf-8").splitlines():
32
+ line = line.strip()
33
+ if not line or line.startswith("#"):
34
+ continue
35
+ digest, _, name = line.partition(" ")
36
+ if digest and name:
37
+ entries[name.strip()] = digest.strip()
38
+ return entries
39
+
40
+
41
+ def _verified_asset(name: str, *, human: str) -> Path:
42
+ path = VENDOR_DIR / name
43
+ if not path.is_file():
44
+ raise WindowsBundleError(
45
+ f"the vendored {human} ({name}) is missing from the kivyforge "
46
+ f"install at {path}.\n"
47
+ " It is built/fetched in CI; a source checkout may need "
48
+ "`python -m kivyforge.platforms.windows.launcher.build_launcher build` "
49
+ "(launcher) or "
50
+ "`python -m kivyforge.platforms.windows.vendor.fetch_rcedit` (rcedit)."
51
+ )
52
+ expected = read_manifest().get(name)
53
+ if not expected:
54
+ raise WindowsBundleError(
55
+ f"the vendored {human} ({name}) has no pinned SHA-256 in {MANIFEST}."
56
+ )
57
+ actual = hashlib.sha256(path.read_bytes()).hexdigest()
58
+ if actual != expected:
59
+ raise WindowsBundleError(
60
+ f"the vendored {human} ({name}) failed its integrity check:\n"
61
+ f" expected sha256 {expected}\n actual sha256 {actual}\n"
62
+ " Refuse to ship a tampered/corrupted binary; re-vendor it."
63
+ )
64
+ return path
65
+
66
+
67
+ def vendored_launcher() -> Path:
68
+ """The verified prebuilt launcher (``launcher-amd64.exe``)."""
69
+ # arm64: take a target-arch argument and select launcher-<arch>.exe (rcedit
70
+ # stays x64 — it is emulated and only edits resources). See arm64-windows.md §5.
71
+ return _verified_asset(LAUNCHER_NAME, human="launcher")
72
+
73
+
74
+ def vendored_rcedit() -> Path:
75
+ """The verified prebuilt resource editor (``rcedit-x64.exe``)."""
76
+ return _verified_asset(RCEDIT_NAME, human="rcedit resource editor")