kivyforge 3.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. kivyforge/__init__.py +9 -0
  2. kivyforge/artifacts/__init__.py +41 -0
  3. kivyforge/artifacts/cache.py +81 -0
  4. kivyforge/artifacts/collect.py +354 -0
  5. kivyforge/artifacts/download.py +125 -0
  6. kivyforge/artifacts/frameworks.py +240 -0
  7. kivyforge/artifacts/native_stage_util.py +346 -0
  8. kivyforge/artifacts/runtime.py +65 -0
  9. kivyforge/artifacts/verify.py +45 -0
  10. kivyforge/artifacts/wheels.py +89 -0
  11. kivyforge/bundle/__init__.py +1 -0
  12. kivyforge/bundle/pycompile.py +161 -0
  13. kivyforge/cli/__init__.py +52 -0
  14. kivyforge/cli/_common.py +62 -0
  15. kivyforge/cli/_platform.py +84 -0
  16. kivyforge/cli/build.py +111 -0
  17. kivyforge/cli/clean.py +135 -0
  18. kivyforge/cli/doctor.py +52 -0
  19. kivyforge/cli/init.py +612 -0
  20. kivyforge/cli/init_writer.py +716 -0
  21. kivyforge/cli/lock.py +261 -0
  22. kivyforge/cli/open_cmd.py +15 -0
  23. kivyforge/cli/package.py +134 -0
  24. kivyforge/cli/run.py +162 -0
  25. kivyforge/cli/status.py +20 -0
  26. kivyforge/cli/upgrade.py +314 -0
  27. kivyforge/config/__init__.py +46 -0
  28. kivyforge/config/errors.py +43 -0
  29. kivyforge/config/icons.py +60 -0
  30. kivyforge/config/loader.py +3079 -0
  31. kivyforge/config/model.py +817 -0
  32. kivyforge/doctor/__init__.py +19 -0
  33. kivyforge/doctor/checks_common.py +95 -0
  34. kivyforge/doctor/probe.py +357 -0
  35. kivyforge/doctor/result.py +41 -0
  36. kivyforge/lock/__init__.py +42 -0
  37. kivyforge/lock/_pip_shim.py +63 -0
  38. kivyforge/lock/find_links.py +182 -0
  39. kivyforge/lock/model.py +69 -0
  40. kivyforge/lock/pep751.py +111 -0
  41. kivyforge/lock/reader.py +30 -0
  42. kivyforge/lock/resolver.py +124 -0
  43. kivyforge/lock/wheelruntime/__init__.py +70 -0
  44. kivyforge/lock/wheelruntime/builder.py +280 -0
  45. kivyforge/lock/wheelruntime/model.py +94 -0
  46. kivyforge/lock/wheelruntime/native_binaries.py +127 -0
  47. kivyforge/lock/wheelruntime/pbs_github.py +133 -0
  48. kivyforge/lock/wheelruntime/profile.py +104 -0
  49. kivyforge/lock/wheelruntime/resolver.py +248 -0
  50. kivyforge/lock/wheelruntime/runtime.py +161 -0
  51. kivyforge/lock/wheelruntime/serialize.py +245 -0
  52. kivyforge/platforms/__init__.py +114 -0
  53. kivyforge/platforms/android/__init__.py +140 -0
  54. kivyforge/platforms/android/adb.py +215 -0
  55. kivyforge/platforms/android/bootstrap/__init__.py +7 -0
  56. kivyforge/platforms/android/bootstrap/contract.py +166 -0
  57. kivyforge/platforms/android/bootstrap/render.py +244 -0
  58. kivyforge/platforms/android/bootstrap/templates/androidtest/KivyforgeContractTest.java +76 -0
  59. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivy_bootstrap.py +50 -0
  60. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_bootstrap.py +54 -0
  61. kivyforge/platforms/android/bootstrap/templates/bootstrap_py/_kivyforge_selftest.py +97 -0
  62. kivyforge/platforms/android/bootstrap/templates/cpp/CMakeLists.txt +20 -0
  63. kivyforge/platforms/android/bootstrap/templates/cpp/main.c +150 -0
  64. kivyforge/platforms/android/bootstrap/templates/java/org/jnius/NativeInvocationHandler.java +48 -0
  65. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonActivity.java +75 -0
  66. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonBundle.java +132 -0
  67. kivyforge/platforms/android/bootstrap/templates/java/org/kivy/android/PythonService.java +189 -0
  68. kivyforge/platforms/android/bootstrap/templates/java/org/renpy/android/Hardware.java +276 -0
  69. kivyforge/platforms/android/bootstrap/templates/sdl2/LICENSE-SDL.txt +18 -0
  70. kivyforge/platforms/android/bootstrap/templates/sdl2/SDL_REVISION.txt +3 -0
  71. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDevice.java +22 -0
  72. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceBLESteamController.java +650 -0
  73. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceManager.java +698 -0
  74. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/HIDDeviceUSB.java +309 -0
  75. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDL.java +90 -0
  76. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLActivity.java +2120 -0
  77. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLAudioManager.java +514 -0
  78. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLControllerManager.java +856 -0
  79. kivyforge/platforms/android/bootstrap/templates/sdl2/org/libsdl/app/SDLSurface.java +405 -0
  80. kivyforge/platforms/android/bootstrap/templates/sdl3/LICENSE-SDL.txt +18 -0
  81. kivyforge/platforms/android/bootstrap/templates/sdl3/SDL_REVISION.txt +3 -0
  82. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDevice.java +21 -0
  83. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceBLESteamController.java +829 -0
  84. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceManager.java +698 -0
  85. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/HIDDeviceUSB.java +354 -0
  86. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDL.java +90 -0
  87. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLActivity.java +2240 -0
  88. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLAudioManager.java +126 -0
  89. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLControllerManager.java +1010 -0
  90. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLDummyEdit.java +66 -0
  91. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLInputConnection.java +136 -0
  92. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSensorManager.java +73 -0
  93. kivyforge/platforms/android/bootstrap/templates/sdl3/org/libsdl/app/SDLSurface.java +464 -0
  94. kivyforge/platforms/android/cli.py +1023 -0
  95. kivyforge/platforms/android/doctor.py +899 -0
  96. kivyforge/platforms/android/elf.py +148 -0
  97. kivyforge/platforms/android/generate/__init__.py +1 -0
  98. kivyforge/platforms/android/generate/manifest.py +261 -0
  99. kivyforge/platforms/android/generate/project.py +462 -0
  100. kivyforge/platforms/android/generate/services.py +187 -0
  101. kivyforge/platforms/android/gradle_wrapper/NOTICE +3 -0
  102. kivyforge/platforms/android/gradle_wrapper/SHA256SUMS +1 -0
  103. kivyforge/platforms/android/gradle_wrapper/gradle-wrapper.jar +0 -0
  104. kivyforge/platforms/android/gradle_wrapper/gradlew +164 -0
  105. kivyforge/platforms/android/gradle_wrapper/gradlew.bat +90 -0
  106. kivyforge/platforms/android/gradlew.py +60 -0
  107. kivyforge/platforms/android/icons.py +237 -0
  108. kivyforge/platforms/android/lock/__init__.py +1 -0
  109. kivyforge/platforms/android/lock/archives.py +109 -0
  110. kivyforge/platforms/android/lock/builder.py +387 -0
  111. kivyforge/platforms/android/lock/markers.py +71 -0
  112. kivyforge/platforms/android/lock/maven.py +252 -0
  113. kivyforge/platforms/android/lock/model.py +113 -0
  114. kivyforge/platforms/android/lock/python_meta.py +199 -0
  115. kivyforge/platforms/android/lock/reader.py +206 -0
  116. kivyforge/platforms/android/lock/resolver.py +276 -0
  117. kivyforge/platforms/android/lock/writer.py +121 -0
  118. kivyforge/platforms/android/policy.py +214 -0
  119. kivyforge/platforms/android/signing.py +145 -0
  120. kivyforge/platforms/android/smoke.py +43 -0
  121. kivyforge/platforms/android/splash.py +293 -0
  122. kivyforge/platforms/android/stage/__init__.py +1 -0
  123. kivyforge/platforms/android/stage/bundle.py +295 -0
  124. kivyforge/platforms/android/stage/jnilibs.py +186 -0
  125. kivyforge/platforms/android/stage/runtime.py +61 -0
  126. kivyforge/platforms/android/stage/wheels.py +106 -0
  127. kivyforge/platforms/android/toolchain.py +29 -0
  128. kivyforge/platforms/base.py +157 -0
  129. kivyforge/platforms/ios/__init__.py +129 -0
  130. kivyforge/platforms/ios/assets.py +193 -0
  131. kivyforge/platforms/ios/buildsettings.py +128 -0
  132. kivyforge/platforms/ios/cli.py +543 -0
  133. kivyforge/platforms/ios/doctor.py +410 -0
  134. kivyforge/platforms/ios/entitlements.py +34 -0
  135. kivyforge/platforms/ios/generator.py +318 -0
  136. kivyforge/platforms/ios/lock/__init__.py +71 -0
  137. kivyforge/platforms/ios/lock/builder.py +393 -0
  138. kivyforge/platforms/ios/lock/markers.py +69 -0
  139. kivyforge/platforms/ios/lock/model.py +95 -0
  140. kivyforge/platforms/ios/lock/python_meta.py +187 -0
  141. kivyforge/platforms/ios/lock/reader.py +191 -0
  142. kivyforge/platforms/ios/lock/resolver.py +304 -0
  143. kivyforge/platforms/ios/lock/spm.py +217 -0
  144. kivyforge/platforms/ios/lock/writer.py +114 -0
  145. kivyforge/platforms/ios/lock/xcframework.py +188 -0
  146. kivyforge/platforms/ios/materialize.py +59 -0
  147. kivyforge/platforms/ios/plist.py +91 -0
  148. kivyforge/platforms/ios/privacy.py +39 -0
  149. kivyforge/platforms/ios/skeleton.py +165 -0
  150. kivyforge/platforms/ios/sources.py +63 -0
  151. kivyforge/platforms/ios/staging.py +225 -0
  152. kivyforge/platforms/ios/swift_packages.py +324 -0
  153. kivyforge/platforms/ios/templates/kivyforge_bootstrap.h +34 -0
  154. kivyforge/platforms/ios/templates/kivyforge_bootstrap.m +146 -0
  155. kivyforge/platforms/ios/templates/main.m +15 -0
  156. kivyforge/platforms/ios/templates/main_config.h.in +12 -0
  157. kivyforge/platforms/ios/xcframeworks.py +194 -0
  158. kivyforge/platforms/ios/xcode/__init__.py +76 -0
  159. kivyforge/platforms/ios/xcode/commands.py +296 -0
  160. kivyforge/platforms/ios/xcode/runner.py +316 -0
  161. kivyforge/platforms/linux/__init__.py +120 -0
  162. kivyforge/platforms/linux/appimage.py +169 -0
  163. kivyforge/platforms/linux/bundle.py +235 -0
  164. kivyforge/platforms/linux/cli.py +232 -0
  165. kivyforge/platforms/linux/desktop.py +71 -0
  166. kivyforge/platforms/linux/doctor.py +438 -0
  167. kivyforge/platforms/linux/elftools.py +95 -0
  168. kivyforge/platforms/linux/icons.py +129 -0
  169. kivyforge/platforms/linux/launcher.py +107 -0
  170. kivyforge/platforms/linux/lock/__init__.py +126 -0
  171. kivyforge/platforms/linux/lock/profile.py +254 -0
  172. kivyforge/platforms/linux/lock/runtime.py +75 -0
  173. kivyforge/platforms/linux/native_stage.py +47 -0
  174. kivyforge/platforms/linux/runtime_stage.py +107 -0
  175. kivyforge/platforms/linux/wheels_stage.py +103 -0
  176. kivyforge/platforms/macos/__init__.py +121 -0
  177. kivyforge/platforms/macos/bundle.py +255 -0
  178. kivyforge/platforms/macos/cli.py +269 -0
  179. kivyforge/platforms/macos/doctor.py +480 -0
  180. kivyforge/platforms/macos/icns.py +75 -0
  181. kivyforge/platforms/macos/launcher.py +118 -0
  182. kivyforge/platforms/macos/lock/__init__.py +112 -0
  183. kivyforge/platforms/macos/lock/profile.py +119 -0
  184. kivyforge/platforms/macos/lock/runtime.py +58 -0
  185. kivyforge/platforms/macos/machotools.py +138 -0
  186. kivyforge/platforms/macos/native_stage.py +47 -0
  187. kivyforge/platforms/macos/notarize.py +143 -0
  188. kivyforge/platforms/macos/plist.py +37 -0
  189. kivyforge/platforms/macos/runtime_stage.py +110 -0
  190. kivyforge/platforms/macos/signing.py +78 -0
  191. kivyforge/platforms/macos/wheels_stage.py +108 -0
  192. kivyforge/platforms/windows/__init__.py +133 -0
  193. kivyforge/platforms/windows/assets.py +76 -0
  194. kivyforge/platforms/windows/bundle.py +298 -0
  195. kivyforge/platforms/windows/cli.py +314 -0
  196. kivyforge/platforms/windows/doctor.py +563 -0
  197. kivyforge/platforms/windows/fsswap.py +87 -0
  198. kivyforge/platforms/windows/icons.py +64 -0
  199. kivyforge/platforms/windows/launcher/__init__.py +193 -0
  200. kivyforge/platforms/windows/launcher/build_launcher.py +507 -0
  201. kivyforge/platforms/windows/launcher/launcher.c +379 -0
  202. kivyforge/platforms/windows/lock/__init__.py +112 -0
  203. kivyforge/platforms/windows/lock/profile.py +101 -0
  204. kivyforge/platforms/windows/lock/runtime.py +64 -0
  205. kivyforge/platforms/windows/naming.py +52 -0
  206. kivyforge/platforms/windows/native_stage.py +66 -0
  207. kivyforge/platforms/windows/petools.py +98 -0
  208. kivyforge/platforms/windows/rcedit.py +116 -0
  209. kivyforge/platforms/windows/runtime_stage.py +221 -0
  210. kivyforge/platforms/windows/signing.py +168 -0
  211. kivyforge/platforms/windows/vendor/LICENSE.rcedit +29 -0
  212. kivyforge/platforms/windows/vendor/NOTICE +17 -0
  213. kivyforge/platforms/windows/vendor/SHA256SUMS +4 -0
  214. kivyforge/platforms/windows/vendor/TOOLSET.txt +4 -0
  215. kivyforge/platforms/windows/vendor/__init__.py +12 -0
  216. kivyforge/platforms/windows/vendor/fetch_rcedit.py +97 -0
  217. kivyforge/platforms/windows/vendor/launcher-amd64.exe +0 -0
  218. kivyforge/platforms/windows/vendor/rcedit-x64.exe +0 -0
  219. kivyforge/platforms/windows/wheels_stage.py +139 -0
  220. kivyforge-3.0.0.dev0.dist-info/METADATA +609 -0
  221. kivyforge-3.0.0.dev0.dist-info/RECORD +225 -0
  222. kivyforge-3.0.0.dev0.dist-info/WHEEL +5 -0
  223. kivyforge-3.0.0.dev0.dist-info/entry_points.txt +3 -0
  224. kivyforge-3.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  225. kivyforge-3.0.0.dev0.dist-info/top_level.txt +1 -0
kivyforge/cli/init.py ADDED
@@ -0,0 +1,612 @@
1
+ """``kivyforge init`` — smart project initialization (spec 05).
2
+
3
+ One write path:
4
+
5
+ * **update** — a ``pyproject.toml`` exists. Only add/replace the target
6
+ platform's ``[tool.kivy]`` + ``[tool.kivy.<platform>]``; ``[project]`` and
7
+ every other namespace are left untouched. No venv required.
8
+
9
+ Platform-aware: resolves the target the same way as every other verb
10
+ (``-p`` / ``KIVYFORGE_PLATFORM``), plus two init-only fallbacks that make sense
11
+ only when *configuring* a platform for the first time — see
12
+ ``_resolve_init_platform``.
13
+
14
+ If ``requirements.txt`` is found but no ``pyproject.toml``, init exits non-zero
15
+ with a migration pointer rather than auto-migrating.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import platform as _platform_mod
22
+ import sys
23
+ import tomllib
24
+ from importlib import metadata
25
+ from pathlib import Path
26
+
27
+ import click
28
+
29
+ from ..config.model import (
30
+ DEFAULT_ANDROID_KEY_PASSWORD_ENV,
31
+ DEFAULT_ANDROID_STORE_PASSWORD_ENV,
32
+ DEFAULT_WINDOWS_STORE_SCOPE,
33
+ DEFAULT_WINDOWS_TIMESTAMP_URL,
34
+ AndroidSigningConfig,
35
+ MacosSigningConfig,
36
+ SigningConfig,
37
+ WindowsSigningConfig,
38
+ )
39
+ from ..platforms import PLATFORM_ENV_VAR, available_platform_names, get_platform
40
+ from ._common import PYPROJECT_NAME, ToolchainError
41
+ from ._platform import configured_platforms, platform_option
42
+ from .init_writer import (
43
+ append_block,
44
+ has_kivy_dep,
45
+ has_platform_overlay,
46
+ has_shared_table,
47
+ normalize_package_name,
48
+ render_android_tables,
49
+ render_kivy_tables,
50
+ render_linux_tables,
51
+ render_macos_tables,
52
+ render_windows_tables,
53
+ strip_platform_tables,
54
+ )
55
+
56
+ REQUIREMENTS_NAME = "requirements.txt"
57
+ BUILDOZER_SPEC_NAME = "buildozer.spec"
58
+
59
+ # buildozer.spec `[app]` key -> the kivyforge key that replaces it. The value is
60
+ # the destination table and key; `None` means the setting has no counterpart
61
+ # because kivyforge does the thing differently (see _BUILDOZER_DROPPED).
62
+ _BUILDOZER_MAP: tuple[tuple[str, str], ...] = (
63
+ ("title", "[tool.kivy].display_name"),
64
+ ("package.name", "[tool.kivy.android].package (with package.domain)"),
65
+ ("package.domain", "[tool.kivy.android].package (with package.name)"),
66
+ ("version", "[project].version"),
67
+ ("source.dir", "[tool.kivy].app_dir"),
68
+ ("requirements", "[project].dependencies (as PEP 508 requirements)"),
69
+ ("orientation", "[tool.kivy].orientation"),
70
+ ("icon.filename", "[tool.kivy.android.icons].source"),
71
+ ("presplash.filename", "[tool.kivy.android.splash].source"),
72
+ ("android.api", "[tool.kivy.android].target_sdk / compile_sdk"),
73
+ ("android.minapi", "[tool.kivy.android].min_sdk (kivyforge's floor is 24)"),
74
+ (
75
+ "android.archs",
76
+ "[tool.kivy.android].abis (arm64-v8a -> arm64_v8a; the 32-bit ABIs "
77
+ "armeabi-v7a and x86 are not supported)",
78
+ ),
79
+ ("android.permissions", "[tool.kivy.android.permissions].uses"),
80
+ ("android.features", "[[tool.kivy.android.permissions.features]]"),
81
+ ("services", "[[tool.kivy.android.services]]"),
82
+ ("android.meta_data", "[tool.kivy.android.manifest].extra_application_xml"),
83
+ ("android.add_activities", "[tool.kivy.android.manifest].extra_manifest_xml"),
84
+ ("android.gradle_dependencies", "[tool.kivy.android].gradle_dependencies"),
85
+ ("android.add_jars", "[[tool.kivy.android.android_libs]]"),
86
+ ("android.release_artifact", "`kivyforge package -f apk|aab`"),
87
+ ("android.debug_artifact", "`kivyforge build --debug -f apk|aab`"),
88
+ )
89
+
90
+ # Settings with no kivyforge counterpart, and why — these are the migration's
91
+ # real work, so name them rather than letting the user discover them one by one.
92
+ _BUILDOZER_DROPPED: tuple[tuple[str, str], ...] = (
93
+ (
94
+ "p4a.*, android.p4a_whitelist, android.blacklist_src",
95
+ "there is no python-for-android layer: dependencies are Android wheels "
96
+ "resolved by `kivyforge lock`.",
97
+ ),
98
+ (
99
+ "requirements with a p4a recipe (e.g. 'kivy,pyjnius,numpy')",
100
+ "each becomes a normal requirement; anything compiled needs an Android "
101
+ "wheel (see docs/design/platforms/android/03).",
102
+ ),
103
+ (
104
+ "android.ndk, android.sdk, android.ndk_api, android.gradle_version",
105
+ "kivyforge pins the toolchain itself; `kivyforge doctor -p android` "
106
+ "checks the host against those pins.",
107
+ ),
108
+ (
109
+ "android.add_src",
110
+ "app-side Java is not a kivyforge concept; the bootstrap's Java is "
111
+ "generated, and `include_files` covers extra assets.",
112
+ ),
113
+ (
114
+ "android.entrypoint, android.activity_class_name",
115
+ "the launcher is generated; `[tool.kivy].entry_point` names the Python "
116
+ "module to import instead.",
117
+ ),
118
+ )
119
+
120
+ _BUILDOZER_MSG_HEAD = (
121
+ "buildozer.spec found but no pyproject.toml.\n"
122
+ " kivyforge is configured through pyproject.toml and will not migrate a\n"
123
+ " buildozer.spec automatically. Create one with your app's metadata, then\n"
124
+ " re-run `kivyforge init -p android`.\n"
125
+ )
126
+
127
+ _REQUIREMENTS_MSG = (
128
+ "requirements.txt found but no pyproject.toml.\n"
129
+ " kivyforge requires pyproject.toml — it will not migrate requirements.txt\n"
130
+ " automatically. Transfer your dependencies to a new pyproject.toml:\n\n"
131
+ " [project]\n"
132
+ ' name = "myapp" # your app name\n'
133
+ ' version = "0.1.0"\n'
134
+ " dependencies = [\n"
135
+ ' "kivy>=3.0",\n'
136
+ " # ... paste your other requirements here\n"
137
+ " ]\n\n"
138
+ " Then re-run kivyforge init."
139
+ )
140
+
141
+
142
+ @click.command()
143
+ @platform_option
144
+ @click.option(
145
+ "--force", is_flag=True, help="Regenerate [tool.kivy*] (preserves signing)."
146
+ )
147
+ def init(cli_platform: str | None, force: bool) -> None:
148
+ """Seed [tool.kivy] + the target platform's overlay into pyproject.toml."""
149
+ cwd = Path.cwd()
150
+ pyproject = cwd / PYPROJECT_NAME
151
+ requirements = cwd / REQUIREMENTS_NAME
152
+ buildozer_spec = cwd / BUILDOZER_SPEC_NAME
153
+
154
+ if pyproject.is_file():
155
+ platform_name = _resolve_init_platform(cli_platform, pyproject)
156
+ _run_update_path(pyproject, force=force, platform_name=platform_name)
157
+ elif buildozer_spec.is_file():
158
+ raise ToolchainError(_buildozer_migration_message(buildozer_spec))
159
+ elif requirements.is_file():
160
+ raise ToolchainError(_REQUIREMENTS_MSG)
161
+ else:
162
+ raise ToolchainError(
163
+ f"no {PYPROJECT_NAME} found in {cwd}.\n"
164
+ f" Create a minimal {PYPROJECT_NAME} with your app's metadata, then re-run:\n\n"
165
+ f" [project]\n"
166
+ f' name = "myapp"\n'
167
+ f' version = "0.1.0"\n'
168
+ f" dependencies = [\n"
169
+ f' "kivy>=3.0",\n'
170
+ f" ]\n\n"
171
+ f" See https://packaging.python.org/tutorials/packaging-projects/ for details."
172
+ )
173
+
174
+
175
+ def _read_buildozer_spec(path: Path) -> dict[str, str]:
176
+ """The ``[app]`` section as a flat dict, or ``{}`` if it will not parse.
177
+
178
+ buildozer.spec is INI, but a hand-edited one need not be valid, and this is
179
+ an error path — a spec we cannot read still gets the generic key mapping.
180
+ """
181
+ import configparser
182
+
183
+ # Raw: buildozer.spec uses %(source.dir)s-style interpolation that means
184
+ # something to buildozer and nothing here, and utf-8-sig because an editor
185
+ # that BOMs the file would otherwise hide the section header.
186
+ parser = configparser.RawConfigParser(strict=False)
187
+ try:
188
+ parser.read(path, encoding="utf-8-sig")
189
+ if not parser.has_section("app"):
190
+ return {}
191
+ return {key: value.strip() for key, value in parser.items("app")}
192
+ except (configparser.Error, OSError, UnicodeDecodeError):
193
+ return {}
194
+
195
+
196
+ def _buildozer_migration_message(path: Path) -> str:
197
+ """The non-zero exit's migration pointer (android/06 §init).
198
+
199
+ Echoes the spec's own values next to their kivyforge keys where it can:
200
+ a mapping table the user has to re-read against their file is busywork, and
201
+ the values are right there.
202
+ """
203
+ spec = _read_buildozer_spec(path)
204
+ lines = [_BUILDOZER_MSG_HEAD, " Your buildozer.spec maps onto these keys:\n"]
205
+ width = max(len(key) for key, _ in _BUILDOZER_MAP)
206
+ for key, target in _BUILDOZER_MAP:
207
+ value = spec.get(key)
208
+ shown = f" {key.ljust(width)} -> {target}"
209
+ if value:
210
+ shown += f"\n {' ' * width} currently: {_ellipsize(value)}"
211
+ lines.append(shown)
212
+ lines.append("\n No kivyforge counterpart:\n")
213
+ for keys, why in _BUILDOZER_DROPPED:
214
+ lines.append(f" {keys}\n {why}")
215
+ lines.append(
216
+ "\n Full reference: docs/design/platforms/android/01-pyproject-android.md"
217
+ )
218
+ return "\n".join(lines)
219
+
220
+
221
+ def _ellipsize(value: str, limit: int = 70) -> str:
222
+ flat = " ".join(value.split())
223
+ return flat if len(flat) <= limit else flat[: limit - 3] + "..."
224
+
225
+
226
+ def _resolve_init_platform(
227
+ cli_platform: str | None,
228
+ pyproject: Path,
229
+ *,
230
+ host_system: str | None = None,
231
+ ) -> str:
232
+ """Resolve init's target platform.
233
+
234
+ Same top two steps as every other verb (``-p`` then ``KIVYFORGE_PLATFORM``),
235
+ but init additionally needs to handle the case it alone faces: *configuring*
236
+ a platform for the first time, when nothing is "configured" yet. So instead
237
+ of the shared ``resolve_target`` (whose host-default step requires the
238
+ platform to already be configured — the right call for build/run/etc., but
239
+ backwards for init), this:
240
+
241
+ 1. ``--platform`` / ``-p``.
242
+ 2. ``KIVYFORGE_PLATFORM``.
243
+ 3. The project's one already-configured overlay, if exactly one exists
244
+ (the ``--force``-regenerate case — no flag needed to update what's
245
+ already there).
246
+ 4. The host OS's own platform (unconditionally — this *is* how it gets
247
+ configured the first time). iOS never matches here (no host maps to
248
+ it); it always needs an explicit choice, same as everywhere else.
249
+
250
+ ``host_system`` is injectable (like the shared ``resolve_target``'s) so
251
+ tests aren't at the mercy of the machine they happen to run on; it
252
+ defaults to the real host (``platform.system()``).
253
+ """
254
+ if cli_platform:
255
+ return cli_platform
256
+
257
+ env_platform = os.environ.get(PLATFORM_ENV_VAR)
258
+ if env_platform:
259
+ if env_platform not in available_platform_names():
260
+ raise ToolchainError(
261
+ f"unknown platform {env_platform!r} in ${PLATFORM_ENV_VAR}; "
262
+ f"registered: {', '.join(available_platform_names())}"
263
+ )
264
+ return env_platform
265
+
266
+ configured = configured_platforms(pyproject)
267
+ if len(configured) == 1:
268
+ return next(iter(configured))
269
+ if len(configured) > 1:
270
+ example = sorted(configured)[0]
271
+ raise ToolchainError(
272
+ "multiple platforms configured in this pyproject.toml "
273
+ f"({', '.join(sorted(configured))}); pass one explicitly:\n"
274
+ f" kivyforge init --platform {example}"
275
+ )
276
+
277
+ host = host_system if host_system is not None else _platform_mod.system()
278
+ for name in available_platform_names():
279
+ if get_platform(name).host_system == host:
280
+ return name
281
+
282
+ raise ToolchainError(
283
+ "cannot infer a target platform on this host.\n"
284
+ " Pass one explicitly: kivyforge init --platform macos\n"
285
+ f" Or set a session default: export {PLATFORM_ENV_VAR}=macos"
286
+ )
287
+
288
+
289
+ def _run_update_path(pyproject: Path, *, force: bool, platform_name: str) -> None:
290
+ text = pyproject.read_text(encoding="utf-8")
291
+ raw = _safe_parse(text, pyproject)
292
+
293
+ if "project" not in raw:
294
+ raise ToolchainError(
295
+ f"{PYPROJECT_NAME} has no [project] table.\n"
296
+ " init updates only [tool.kivy*]; it will not author [project] for "
297
+ "an existing file. Add a minimal [project] (name + version) first."
298
+ )
299
+
300
+ table_key = f"tool.kivy.{platform_name}"
301
+ existing_overlay = has_platform_overlay(text, platform_name)
302
+ if existing_overlay and not force:
303
+ raise ToolchainError(
304
+ f"[{table_key}] already exists. Re-run with --force to regenerate "
305
+ f"it (your [{table_key}.signing] is preserved)."
306
+ )
307
+
308
+ app_slug = _project_slug(raw)
309
+ deps = raw.get("project", {}).get("dependencies", [])
310
+ kivy = has_kivy_dep(deps) if isinstance(deps, list) else False
311
+ include_shared = not has_shared_table(text)
312
+ table = _platform_table(raw, platform_name)
313
+
314
+ if existing_overlay and force:
315
+ stripped = strip_platform_tables(text, platform_name)
316
+ block = _render_overlay(
317
+ platform_name,
318
+ app_slug,
319
+ table,
320
+ has_kivy=kivy,
321
+ include_shared=include_shared,
322
+ preserve=True,
323
+ )
324
+ new_text = append_block(stripped, block)
325
+ pyproject.write_text(new_text, encoding="utf-8")
326
+ click.echo(
327
+ f"Regenerated [{table_key}] (signing/python/icon settings preserved)."
328
+ )
329
+ else:
330
+ block = _render_overlay(
331
+ platform_name,
332
+ app_slug,
333
+ table,
334
+ has_kivy=kivy,
335
+ include_shared=include_shared,
336
+ preserve=False,
337
+ )
338
+ new_text = append_block(text, block)
339
+ pyproject.write_text(new_text, encoding="utf-8")
340
+ added = (
341
+ f"[{table_key}]" if not include_shared else f"[tool.kivy] + [{table_key}]"
342
+ )
343
+ click.echo(f"Added {added} to pyproject.toml.")
344
+
345
+ _maybe_warn_drift(raw)
346
+ click.echo(
347
+ f"Next: fill in the TODOs in [{table_key}], then `kivyforge lock -p {platform_name}`."
348
+ )
349
+
350
+
351
+ def _render_overlay(
352
+ platform_name: str,
353
+ app_slug: str,
354
+ table: dict,
355
+ *,
356
+ has_kivy: bool,
357
+ include_shared: bool,
358
+ preserve: bool,
359
+ ) -> str:
360
+ """Render the (optional) shared ``[tool.kivy]`` + the target platform's overlay."""
361
+ if platform_name == "ios":
362
+ splash_source, splash_background = (
363
+ _read_splash(table) if preserve else (None, None)
364
+ )
365
+ return render_kivy_tables(
366
+ app_slug,
367
+ signing=_read_signing(table) if preserve else None,
368
+ python_version=_read_python_version(table) if preserve else None,
369
+ has_kivy=has_kivy,
370
+ simulator_archs=_read_str_list(table, "simulator_archs")
371
+ if preserve
372
+ else None,
373
+ icon_source=_read_icon_source(table) if preserve else None,
374
+ splash_source=splash_source,
375
+ splash_background=splash_background,
376
+ include_shared=include_shared,
377
+ )
378
+ if platform_name == "macos":
379
+ return render_macos_tables(
380
+ app_slug,
381
+ signing=_read_macos_signing(table) if preserve else None,
382
+ python_version=_read_python_version(table) if preserve else None,
383
+ has_kivy=has_kivy,
384
+ archs=_read_str_list(table, "archs") if preserve else None,
385
+ icon_source=_read_icon_source(table) if preserve else None,
386
+ include_shared=include_shared,
387
+ )
388
+ if platform_name == "linux":
389
+ return render_linux_tables(
390
+ app_slug,
391
+ python_version=_read_python_version(table) if preserve else None,
392
+ has_kivy=has_kivy,
393
+ archs=_read_str_list(table, "archs") if preserve else None,
394
+ icon_source=_read_icon_source(table) if preserve else None,
395
+ categories=_read_categories(table) if preserve else None,
396
+ include_shared=include_shared,
397
+ )
398
+ if platform_name == "windows":
399
+ return render_windows_tables(
400
+ app_slug,
401
+ signing=_read_windows_signing(table) if preserve else None,
402
+ python_version=_read_python_version(table) if preserve else None,
403
+ has_kivy=has_kivy,
404
+ archs=_read_str_list(table, "archs") if preserve else None,
405
+ icon_source=_read_icon_source(table) if preserve else None,
406
+ include_shared=include_shared,
407
+ )
408
+ if platform_name == "android":
409
+ splash_source, splash_background = (
410
+ _read_splash(table) if preserve else (None, None)
411
+ )
412
+ package = table.get("package") if preserve else None
413
+ kivy_generation = table.get("kivy_generation") if preserve else None
414
+ return render_android_tables(
415
+ app_slug,
416
+ signing=_read_android_signing(table) if preserve else None,
417
+ python_version=_read_python_version(table) if preserve else None,
418
+ has_kivy=has_kivy,
419
+ package=package if isinstance(package, str) and package else None,
420
+ abis=_read_str_list(table, "abis") if preserve else None,
421
+ kivy_generation=kivy_generation
422
+ if isinstance(kivy_generation, int)
423
+ and not isinstance(kivy_generation, bool)
424
+ else None,
425
+ icon_source=_read_icon_source(table) if preserve else None,
426
+ splash_source=splash_source,
427
+ splash_background=splash_background,
428
+ include_shared=include_shared,
429
+ )
430
+ raise ToolchainError(
431
+ f"`kivyforge init` does not support platform {platform_name!r} yet."
432
+ )
433
+
434
+
435
+ # --------------------------------------------------------------------------- #
436
+ # environment probing
437
+ # --------------------------------------------------------------------------- #
438
+ def _venv_active() -> bool:
439
+ return sys.prefix != sys.base_prefix
440
+
441
+
442
+ def _installed_versions() -> dict[str, str]:
443
+ out: dict[str, str] = {}
444
+ for dist in metadata.distributions():
445
+ name = dist.metadata["Name"]
446
+ if name:
447
+ out[_canon(name)] = dist.version
448
+ return out
449
+
450
+
451
+ def _canon(name: str) -> str:
452
+ import re
453
+
454
+ return re.sub(r"[-_.]+", "-", name).lower()
455
+
456
+
457
+ def _safe_parse(text: str, path: Path) -> dict:
458
+ try:
459
+ return tomllib.loads(text)
460
+ except tomllib.TOMLDecodeError as exc:
461
+ raise ToolchainError(f"{path} is not valid TOML: {exc}") from exc
462
+
463
+
464
+ def _project_slug(raw: dict) -> str:
465
+ name = raw.get("project", {}).get("name")
466
+ if isinstance(name, str) and name:
467
+ return normalize_package_name(name)
468
+ return "app"
469
+
470
+
471
+ def _platform_table(raw: dict, platform_name: str) -> dict:
472
+ try:
473
+ table = raw["tool"]["kivy"][platform_name]
474
+ except (KeyError, TypeError):
475
+ return {}
476
+ return table if isinstance(table, dict) else {}
477
+
478
+
479
+ def _read_python_version(table: dict) -> str | None:
480
+ try:
481
+ version = table["python"]["version"]
482
+ except (KeyError, TypeError):
483
+ return None
484
+ return version if isinstance(version, str) and version else None
485
+
486
+
487
+ def _read_str_list(table: dict, key: str) -> list[str] | None:
488
+ """Return a user-set list-of-strings field, or None when absent/invalid.
489
+
490
+ None preserves the commented stub on ``--force`` so the default still holds.
491
+ """
492
+ values = table.get(key)
493
+ if isinstance(values, list) and values and all(isinstance(v, str) for v in values):
494
+ return values
495
+ return None
496
+
497
+
498
+ def _read_icon_source(table: dict) -> str | None:
499
+ icons = table.get("icons")
500
+ if isinstance(icons, dict):
501
+ source = icons.get("source")
502
+ if isinstance(source, str) and source:
503
+ return source
504
+ return None
505
+
506
+
507
+ def _read_splash(table: dict) -> tuple[str | None, str | None]:
508
+ splash = table.get("splash")
509
+ if not isinstance(splash, dict):
510
+ return None, None
511
+ source = splash.get("source")
512
+ background = splash.get("background")
513
+ return (
514
+ source if isinstance(source, str) and source else None,
515
+ background if isinstance(background, str) and background else None,
516
+ )
517
+
518
+
519
+ def _read_signing(table: dict) -> SigningConfig | None:
520
+ signing = table.get("signing")
521
+ if not isinstance(signing, dict):
522
+ return None
523
+ return SigningConfig(
524
+ team_id=signing.get("team_id", ""),
525
+ identity=signing.get("identity", "Apple Development"),
526
+ provisioning_profile=signing.get("provisioning_profile", ""),
527
+ auto_signing=bool(signing.get("auto_signing", True)),
528
+ upload_symbols=bool(signing.get("upload_symbols", True)),
529
+ )
530
+
531
+
532
+ def _read_macos_signing(table: dict) -> MacosSigningConfig | None:
533
+ signing = table.get("signing")
534
+ if not isinstance(signing, dict):
535
+ return None
536
+ identity = signing.get("identity", "")
537
+ if not identity:
538
+ return None
539
+ return MacosSigningConfig(
540
+ identity=identity,
541
+ team_id=signing.get("team_id", ""),
542
+ notary_profile=signing.get("notary_profile", ""),
543
+ )
544
+
545
+
546
+ def _read_windows_signing(table: dict) -> WindowsSigningConfig | None:
547
+ signing = table.get("signing")
548
+ if not isinstance(signing, dict):
549
+ return None
550
+ thumbprint = signing.get("thumbprint", "")
551
+ if not thumbprint:
552
+ return None
553
+ return WindowsSigningConfig(
554
+ thumbprint=thumbprint,
555
+ timestamp_url=signing.get("timestamp_url", DEFAULT_WINDOWS_TIMESTAMP_URL),
556
+ store_scope=signing.get("store_scope", DEFAULT_WINDOWS_STORE_SCOPE),
557
+ )
558
+
559
+
560
+ def _read_android_signing(table: dict) -> AndroidSigningConfig | None:
561
+ signing = table.get("signing")
562
+ if not isinstance(signing, dict):
563
+ return None
564
+ keystore = signing.get("keystore", "")
565
+ if not keystore:
566
+ return None
567
+ return AndroidSigningConfig(
568
+ keystore=keystore,
569
+ key_alias=signing.get("key_alias", ""),
570
+ store_password_env=signing.get(
571
+ "store_password_env", DEFAULT_ANDROID_STORE_PASSWORD_ENV
572
+ ),
573
+ key_password_env=signing.get(
574
+ "key_password_env", DEFAULT_ANDROID_KEY_PASSWORD_ENV
575
+ ),
576
+ )
577
+
578
+
579
+ def _read_categories(table: dict) -> list[str] | None:
580
+ desktop = table.get("desktop")
581
+ if not isinstance(desktop, dict):
582
+ return None
583
+ return _read_str_list(desktop, "categories")
584
+
585
+
586
+ def _maybe_warn_drift(raw: dict) -> None:
587
+ """If a venv is active, warn when an installed dep drifts from its specifier."""
588
+ if not _venv_active():
589
+ return
590
+ deps = raw.get("project", {}).get("dependencies", [])
591
+ if not isinstance(deps, list):
592
+ return
593
+ installed = _installed_versions()
594
+ from packaging.requirements import InvalidRequirement, Requirement
595
+ from packaging.version import Version
596
+
597
+ for raw_req in deps:
598
+ try:
599
+ req = Requirement(raw_req)
600
+ except InvalidRequirement:
601
+ continue
602
+ version = installed.get(_canon(req.name))
603
+ if (
604
+ version
605
+ and str(req.specifier)
606
+ and not req.specifier.contains(Version(version), prereleases=True)
607
+ ):
608
+ click.echo(
609
+ f"warning: installed {req.name} {version} is outside declared "
610
+ f"'{req.specifier}'. (Not modified.)",
611
+ err=True,
612
+ )