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,89 @@
1
+ """Select pinned iOS wheels for a build slice (spec 03 / spec 05 step 4).
2
+
3
+ Slice selection is driven by the build target + ``--arch``. The pip
4
+ cross-install command lives in ``collect.py`` next to where it is invoked.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from ..lock.model import LockedPackage, LockedWheel
12
+
13
+ # Build target -> the simulator/device platform-tag suffix.
14
+ TARGET_SUFFIX = {
15
+ ("device", "arm64"): "arm64_iphoneos",
16
+ ("simulator", "arm64"): "arm64_iphonesimulator",
17
+ ("simulator", "x86_64"): "x86_64_iphonesimulator",
18
+ }
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class BuildSlice:
23
+ target: str # "device" | "simulator"
24
+ arch: str # "arm64" | "x86_64"
25
+ deployment_target: str
26
+
27
+ @property
28
+ def platform_tag(self) -> str:
29
+ suffix = TARGET_SUFFIX.get((self.target, self.arch))
30
+ if suffix is None:
31
+ raise ValueError(f"unsupported target/arch: {self.target}/{self.arch}")
32
+ dt = self.deployment_target.replace(".", "_")
33
+ return f"ios_{dt}_{suffix}"
34
+
35
+
36
+ def select_wheel(package: LockedPackage, slice_: BuildSlice) -> LockedWheel:
37
+ """Pick the wheel matching the build slice (pure-Python wheels match any).
38
+
39
+ For native wheels, the arch/sdk suffix must match exactly, but the iOS
40
+ version component uses compatibility matching: a wheel tagged ios_13_0_*
41
+ is valid for a project targeting ios_16_0 because the wheel can run on
42
+ any iOS >= 13.0. The best (highest compatible) version is preferred.
43
+
44
+ Background: wheel tags are determined by the Python binary's compile-time
45
+ sysconfig.get_platform(), which reflects the Python.xcframework minimum OS
46
+ (e.g. python.org cp315 targets 13.0 regardless of IPHONEOS_DEPLOYMENT_TARGET
47
+ used when compiling the extension code itself).
48
+ """
49
+ pure = [w for w in package.wheels if w.is_pure_python]
50
+ if pure:
51
+ return pure[0]
52
+
53
+ arch_sdk = TARGET_SUFFIX.get((slice_.target, slice_.arch))
54
+ if arch_sdk is None:
55
+ raise ValueError(f"unsupported target/arch: {slice_.target}/{slice_.arch}")
56
+
57
+ dt_parts = tuple(int(x) for x in slice_.deployment_target.split("."))
58
+
59
+ candidates: list[tuple[tuple[int, ...], LockedWheel]] = []
60
+ for wheel in package.wheels:
61
+ tag = wheel.platform_tag # e.g. "ios_13_0_arm64_iphonesimulator"
62
+ if not tag.endswith(f"_{arch_sdk}"):
63
+ continue
64
+ # Extract the ios_<major>_<minor> prefix.
65
+ prefix = tag[: -(len(arch_sdk) + 1)] # "ios_13_0"
66
+ parts = prefix.split("_") # ["ios", "13", "0"]
67
+ if parts[0] != "ios" or len(parts) < 3:
68
+ continue
69
+ try:
70
+ wheel_ver = tuple(int(x) for x in parts[1:])
71
+ except ValueError:
72
+ continue
73
+ if wheel_ver <= dt_parts:
74
+ candidates.append((wheel_ver, wheel))
75
+
76
+ if candidates:
77
+ # Prefer the highest compatible version.
78
+ candidates.sort(key=lambda t: t[0], reverse=True)
79
+ return candidates[0][1]
80
+
81
+ available = ", ".join(sorted(w.platform_tag for w in package.wheels))
82
+ raise WheelSelectionError(
83
+ f"{package.name} has no compatible wheel for slice "
84
+ f"{slice_.platform_tag} (have: {available})."
85
+ )
86
+
87
+
88
+ class WheelSelectionError(Exception):
89
+ pass
@@ -0,0 +1 @@
1
+ """Bundling helpers shared by more than one platform backend."""
@@ -0,0 +1,161 @@
1
+ """Byte-compile a staged Python payload, optionally stripping the sources.
2
+
3
+ Shared by every backend that ships a Python tree: Android's asset bundle and
4
+ the three desktop bundles. The mechanics below are identical everywhere; what
5
+ differs per platform is only *which* trees get compiled and *which* interpreter
6
+ does the compiling, both of which the caller supplies.
7
+
8
+ Two layouts, and the difference matters:
9
+
10
+ - Keeping the source, the ``.pyc`` goes in ``__pycache__/`` as usual, so imports
11
+ find it next to the ``.py`` it was built from.
12
+ - Shipping ``.pyc`` only requires the *legacy* layout — PEP 3147 sourceless
13
+ imports look for ``foo.pyc`` at the source's own path, never inside
14
+ ``__pycache__/``. Compiling to ``__pycache__/`` and then deleting the ``.py``
15
+ would produce a tree that imports nothing at all.
16
+
17
+ Hash-based, unchecked invalidation (PEP 552) is used rather than the default
18
+ mtime+size: it saves a stat per import for a payload that cannot have a newer
19
+ source than the one shipped, and it keeps an mtime out of every ``.pyc``.
20
+
21
+ Paths are stripped to be payload-relative because a ``.pyc`` records the path it
22
+ was compiled from: the default would leak local host paths into the shipped
23
+ artifact and make otherwise-identical builds differ per machine.
24
+
25
+ ``compiler`` is an interpreter argv prefix whose CPython *minor* matches the
26
+ target runtime — empty means "this interpreter". Establishing that match is the
27
+ caller's job, because a ``.pyc`` is only loadable by the exact CPython minor
28
+ that wrote it.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+ from collections.abc import Sequence
37
+ from pathlib import Path
38
+
39
+
40
+ class PycompileError(Exception):
41
+ """Byte-compilation failed, or the compiler could not be run."""
42
+
43
+
44
+ def select_compiler(
45
+ *, staged_interpreter: Path, native: bool, python_version: str
46
+ ) -> tuple[str, ...] | None:
47
+ """Pick the interpreter argv to byte-compile the staged payload with.
48
+
49
+ The staged interpreter is the one actually shipped, so it is the first
50
+ choice — but only when *native*, i.e. the caller has confirmed it can run
51
+ on this host without emulation, which kivyforge never depends on. When it
52
+ can't run here, fall back to this process's own interpreter: a ``.pyc``'s
53
+ magic number is keyed to CPython's *minor* version only, never
54
+ architecture, so any interpreter of the right minor will do regardless of
55
+ which arch it runs on. Returns ``None`` when neither works, telling the
56
+ caller to degrade (skip compiling, ship source) rather than write a
57
+ ``.pyc`` nothing can load.
58
+ """
59
+ if native and staged_interpreter.is_file():
60
+ return (str(staged_interpreter),)
61
+ if sys.version_info[:2] == _target_minor(python_version):
62
+ return ()
63
+ return None
64
+
65
+
66
+ def _target_minor(python_version: str) -> tuple[int, int]:
67
+ parts = python_version.split(".")[:2]
68
+ return (int(parts[0].split("rc")[0]), int(parts[1].split("rc")[0]))
69
+
70
+
71
+ def byte_compile(
72
+ trees: Sequence[Path],
73
+ *,
74
+ compiler: Sequence[str] = (),
75
+ strip_source: bool = False,
76
+ stripdir: Path,
77
+ ) -> None:
78
+ """Compile every tree in *trees* in place; strip ``.py`` when asked.
79
+
80
+ Missing trees are skipped (a backend may stage some payload areas only
81
+ conditionally). Raises :class:`PycompileError` naming the tree that failed.
82
+ """
83
+ for tree in trees:
84
+ if not tree.is_dir():
85
+ continue
86
+ if not compile_tree(
87
+ tree, compiler=compiler, legacy=strip_source, stripdir=stripdir
88
+ ):
89
+ raise PycompileError(
90
+ f"byte-compiling {tree.name}/ failed; the output above names the "
91
+ "file.\n"
92
+ " A syntax error in app code fails here rather than at runtime."
93
+ )
94
+ if strip_source:
95
+ strip_sources(tree)
96
+
97
+
98
+ def strip_sources(tree: Path) -> None:
99
+ """Delete every ``.py`` that has a sibling ``.pyc``, and all ``__pycache__``.
100
+
101
+ Only sources with a compiled counterpart are removed, so a file the
102
+ compiler skipped is never silently dropped from the payload. The
103
+ ``__pycache__`` sweep matters because nothing can import from it once the
104
+ sources are gone — leaving it behind would ship dead weight.
105
+ """
106
+ for source in tree.rglob("*.py"):
107
+ if source.with_suffix(".pyc").is_file():
108
+ source.unlink()
109
+ for cache in tree.rglob("__pycache__"):
110
+ shutil.rmtree(cache, ignore_errors=True)
111
+
112
+
113
+ def compile_tree(
114
+ target: Path,
115
+ *,
116
+ compiler: Sequence[str] = (),
117
+ legacy: bool = False,
118
+ stripdir: Path,
119
+ ) -> bool:
120
+ """Byte-compile *target*; return whether it succeeded.
121
+
122
+ Uses ``compileall`` in-process when *compiler* is empty, otherwise shells
123
+ out to the named interpreter so the ``.pyc`` carries the target runtime's
124
+ magic number.
125
+ """
126
+ if not compiler:
127
+ import compileall
128
+ import py_compile
129
+
130
+ return bool(
131
+ compileall.compile_dir(
132
+ target,
133
+ quiet=1,
134
+ legacy=legacy,
135
+ optimize=0,
136
+ invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH,
137
+ force=True,
138
+ stripdir=str(stripdir),
139
+ )
140
+ )
141
+
142
+ argv = [
143
+ *compiler,
144
+ "-m",
145
+ "compileall",
146
+ "-q",
147
+ "-f",
148
+ "--invalidation-mode",
149
+ "unchecked-hash",
150
+ "-s",
151
+ str(stripdir),
152
+ ]
153
+ if legacy:
154
+ argv.append("-b")
155
+ argv.append(str(target))
156
+ try:
157
+ return subprocess.run(argv, check=False).returncode == 0
158
+ except OSError as exc:
159
+ raise PycompileError(
160
+ f"could not run {' '.join(compiler)} to byte-compile {target.name}: {exc}"
161
+ ) from exc
@@ -0,0 +1,52 @@
1
+ """``kivyforge`` — the kivyforge 3.0 command-line interface.
2
+
3
+ A slim click-based dispatcher. Each verb lives in its own module under
4
+ ``kivyforge.cli`` and is registered onto the top-level group here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import click
10
+
11
+ from .. import __version__
12
+ from . import (
13
+ build,
14
+ clean,
15
+ doctor,
16
+ init,
17
+ lock,
18
+ open_cmd,
19
+ package,
20
+ run,
21
+ status,
22
+ upgrade,
23
+ )
24
+
25
+
26
+ @click.group(
27
+ context_settings={"help_option_names": ["-h", "--help"]},
28
+ help=(
29
+ "kivyforge 3.0 — a declarative iOS bundler for Kivy apps.\n\n"
30
+ "Run commands from the directory that contains your pyproject.toml."
31
+ ),
32
+ )
33
+ @click.version_option(__version__, "-V", "--version", prog_name="kivyforge")
34
+ def main() -> None:
35
+ pass
36
+
37
+
38
+ # Real 3.0 verbs.
39
+ main.add_command(init.init)
40
+ main.add_command(lock.lock)
41
+ main.add_command(build.build)
42
+ main.add_command(run.run)
43
+ main.add_command(open_cmd.open_)
44
+ main.add_command(package.package)
45
+ main.add_command(upgrade.upgrade)
46
+ main.add_command(clean.clean)
47
+ main.add_command(status.status)
48
+ main.add_command(doctor.doctor)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,62 @@
1
+ """Shared helpers for the ``kivyforge`` CLI.
2
+
3
+ All verbs look for ``pyproject.toml`` in the **current working directory
4
+ only** — no parent-directory traversal (per spec 05).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+ PYPROJECT_NAME = "pyproject.toml"
14
+ # iOS lockfile name, kept for the iOS verbs' backward-compatible call sites.
15
+ # New/platform-aware code uses ``lockfile_name``/``lockfile_path_for``.
16
+ LOCKFILE_NAME = "pylock.ios.toml"
17
+
18
+
19
+ def lockfile_name(platform: str) -> str:
20
+ """The per-platform lockfile filename, e.g. ``pylock.macos.toml``."""
21
+ return f"pylock.{platform}.toml"
22
+
23
+
24
+ class ToolchainError(click.ClickException):
25
+ """A user-facing error that prints cleanly and exits non-zero.
26
+
27
+ Unlike a bare exception, ``click`` renders this as ``Error: <message>``
28
+ without a traceback, which is what we want for expected failure modes
29
+ (missing pyproject, validation errors, drift, etc.).
30
+ """
31
+
32
+ exit_code = 1
33
+
34
+
35
+ def find_pyproject(start: Path | None = None) -> Path:
36
+ """Return the path to ``pyproject.toml`` in the current directory.
37
+
38
+ Raises ``ToolchainError`` if it is not present. No parent traversal is
39
+ performed — the tool is intentionally CWD-scoped so it never picks up an
40
+ unrelated ``pyproject.toml`` from an ancestor directory.
41
+ """
42
+ base = Path.cwd() if start is None else start
43
+ candidate = base / PYPROJECT_NAME
44
+ if not candidate.is_file():
45
+ raise ToolchainError(
46
+ f"no {PYPROJECT_NAME} found in the current directory ({base}).\n"
47
+ f" Run kivyforge commands from the directory that contains your "
48
+ f"{PYPROJECT_NAME}, or run `kivyforge init` to create one."
49
+ )
50
+ return candidate
51
+
52
+
53
+ def lockfile_path(start: Path | None = None) -> Path:
54
+ """Return the path where ``pylock.ios.toml`` lives (sibling to pyproject)."""
55
+ base = Path.cwd() if start is None else start
56
+ return base / LOCKFILE_NAME
57
+
58
+
59
+ def lockfile_path_for(platform: str, start: Path | None = None) -> Path:
60
+ """Return the ``pylock.<platform>.toml`` path (sibling to pyproject)."""
61
+ base = Path.cwd() if start is None else start
62
+ return base / lockfile_name(platform)
@@ -0,0 +1,84 @@
1
+ """CLI glue for target-platform selection (common design doc 02).
2
+
3
+ Provides the shared ``-p/--platform`` option and ``resolve_target``, which reads
4
+ which ``[tool.kivy.<platform>]`` overlays a project configures and runs the
5
+ resolution chain, surfacing failures as clean ``ToolchainError`` messages.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import tomllib
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from ..platforms import (
17
+ Platform,
18
+ PlatformResolutionError,
19
+ available_platform_names,
20
+ )
21
+ from ..platforms import (
22
+ resolve_target as _resolve_target,
23
+ )
24
+ from ._common import ToolchainError, find_pyproject
25
+
26
+ platform_option = click.option(
27
+ "--platform",
28
+ "-p",
29
+ "cli_platform",
30
+ type=click.Choice(available_platform_names()),
31
+ default=None,
32
+ help="Target platform (overrides KIVYFORGE_PLATFORM and the host default).",
33
+ )
34
+
35
+
36
+ def configured_platforms(pyproject: Path) -> set[str]:
37
+ """Platform names whose ``[tool.kivy.<name>]`` overlay this project declares."""
38
+ try:
39
+ raw = tomllib.loads(pyproject.read_text(encoding="utf-8"))
40
+ except (OSError, tomllib.TOMLDecodeError):
41
+ return set()
42
+ kivy = raw.get("tool", {}).get("kivy", {})
43
+ if not isinstance(kivy, dict):
44
+ return set()
45
+ return {name for name in available_platform_names() if name in kivy}
46
+
47
+
48
+ def reject_android_only(backend: Platform, options: dict[str, object]) -> None:
49
+ """Fail when Android-only options are used against another platform.
50
+
51
+ The shared verbs carry the full option superset, so an option that only
52
+ Android acts on would otherwise be silently ignored elsewhere — the worst
53
+ outcome, since the user believes it took effect.
54
+ """
55
+ used = [name for name, value in options.items() if value]
56
+ if not used or backend.name == "android":
57
+ return
58
+ raise ToolchainError(
59
+ f"{', '.join(used)} {'is' if len(used) == 1 else 'are'} "
60
+ f"Android-only; not valid for {backend.name}."
61
+ )
62
+
63
+
64
+ def resolve_target(
65
+ cli_platform: str | None, *, verb: str = "build"
66
+ ) -> tuple[Platform, Path]:
67
+ """Resolve the target platform and return it with the project root.
68
+
69
+ Looks for ``pyproject.toml`` in the CWD (like every verb), determines the
70
+ configured overlays, and runs the resolution chain. ``verb`` names the
71
+ calling CLI command so an unresolved-platform error's example command is
72
+ accurate (e.g. ``kivyforge lock --platform ios``, not always ``build``).
73
+ """
74
+ pyproject = find_pyproject()
75
+ try:
76
+ backend = _resolve_target(
77
+ cli_platform,
78
+ configured=configured_platforms(pyproject),
79
+ env=os.environ,
80
+ verb=verb,
81
+ )
82
+ except PlatformResolutionError as exc:
83
+ raise ToolchainError(str(exc)) from exc
84
+ return backend, pyproject.parent
kivyforge/cli/build.py ADDED
@@ -0,0 +1,111 @@
1
+ """``kivyforge build`` — materialize the lock into an Xcode project (specs 05/06).
2
+
3
+ Build performs, in order:
4
+
5
+ 1. **Drift check** — the lock must be in sync with ``pyproject.toml``.
6
+ 2-5. **Artifact collection** — Python.xcframework, pinned wheels into
7
+ ``pip-deps/``, wheel-embedded + native ``.xcframework`` into ``Frameworks/``.
8
+ 6. **Project materialization** — generate ``<app>-ios/`` and the ``.xcodeproj``.
9
+ 7. **xcodebuild** — only when a target flag is given (Phase 6); a bare
10
+ ``kivyforge build`` stops after step 6 with a ready-to-open project.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import click
16
+
17
+ from ._platform import platform_option, reject_android_only, resolve_target
18
+
19
+
20
+ @click.command()
21
+ @platform_option
22
+ @click.option(
23
+ "--simulator",
24
+ "target",
25
+ flag_value="simulator",
26
+ help="Build for the iOS simulator (Debug).",
27
+ )
28
+ @click.option(
29
+ "--device", "target", flag_value="device", help="Build for a device (Debug)."
30
+ )
31
+ @click.option(
32
+ "--release", "target", flag_value="release", help="Archive + export a .ipa."
33
+ )
34
+ @click.option(
35
+ "--arch",
36
+ # x86_64 stays: it is the Linux arch and an Android ABI. It is not a macOS
37
+ # or iOS-simulator arch any more -- those backends reject it themselves.
38
+ type=click.Choice(["arm64", "x86_64"]),
39
+ default=None,
40
+ help="iOS: simulator arch. macOS: assemble a subset of the locked archs.",
41
+ )
42
+ @click.option(
43
+ "--no-verify-lock", is_flag=True, help="Skip the pyproject drift check (CI only)."
44
+ )
45
+ @click.option("--no-cache", is_flag=True, help="Force re-download of every artifact.")
46
+ @click.option(
47
+ "--team-id", default=None, help="Override [tool.kivy.ios.signing].team_id."
48
+ )
49
+ @click.option(
50
+ "--signing-identity",
51
+ default=None,
52
+ help="Override [tool.kivy.ios.signing].identity.",
53
+ )
54
+ @click.option(
55
+ "--export-method",
56
+ type=click.Choice(["app-store", "ad-hoc", "development"]),
57
+ default="app-store",
58
+ help="Export method (only meaningful with --release).",
59
+ )
60
+ # --- Android-only ---
61
+ @click.option(
62
+ "--debug",
63
+ is_flag=True,
64
+ help="Android: assemble the debug-signed artifact after generating.",
65
+ )
66
+ @click.option(
67
+ "-f",
68
+ "--format",
69
+ "fmt",
70
+ type=click.Choice(["apk", "aab"]),
71
+ default=None,
72
+ help="Android (with --debug): debug artifact format (default apk).",
73
+ )
74
+ @click.option(
75
+ "--abi",
76
+ type=click.Choice(["arm64_v8a", "x86_64"]),
77
+ default=None,
78
+ help="Android: restrict this build to one ABI.",
79
+ )
80
+ def build(
81
+ cli_platform: str | None,
82
+ target: str | None,
83
+ arch: str | None,
84
+ no_verify_lock: bool,
85
+ no_cache: bool,
86
+ team_id: str | None,
87
+ signing_identity: str | None,
88
+ export_method: str,
89
+ debug: bool,
90
+ fmt: str | None,
91
+ abi: str | None,
92
+ ) -> None:
93
+ """Download artifacts, generate the platform project, and optionally build it."""
94
+ # Resolve the target platform before any work so an unresolved target fails
95
+ # fast with an actionable message (common design doc 02).
96
+ backend, project_root = resolve_target(cli_platform, verb="build")
97
+ reject_android_only(backend, {"--debug": debug, "-f/--format": fmt, "--abi": abi})
98
+
99
+ backend.build(
100
+ project_root,
101
+ target=target,
102
+ arch=arch,
103
+ no_verify_lock=no_verify_lock,
104
+ no_cache=no_cache,
105
+ team_id=team_id,
106
+ signing_identity=signing_identity,
107
+ export_method=export_method,
108
+ debug=debug,
109
+ fmt=fmt,
110
+ abi=abi,
111
+ )