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,298 @@
1
+ r"""Assemble the Windows onedir bundle (windows-spec).
2
+
3
+ Orchestrates the stages: pick the assembly arch from the lock (+ ``--arch``),
4
+ stage the whole runtime prefix, install the locked wheels into it (no pip),
5
+ copy the app sources, stage any declared native binaries, generate the bootstrap
6
+ module, then place + resource-patch the launcher. Produces:
7
+
8
+ build/windows/<Name>/
9
+ ├── <Name>.exe ← prebuilt launcher, resource-patched per app
10
+ ├── _kivyforge_bootstrap.py ← generated (fixed name)
11
+ ├── app/ ← user code ([tool.kivy].app_dir)
12
+ ├── bin/ ← declared native binaries (absent when empty)
13
+ └── python/ ← WHOLE PBS prefix
14
+
15
+ The tree is assembled **in place** (see :mod:`.fsswap`): a freshly written tree
16
+ cannot be reliably renamed on Windows because the antivirus scanner holds new
17
+ files open, so any previous bundle is reserved first and restored if assembly
18
+ fails — a broken build never destroys a working one.
19
+
20
+ The onedir *folder* is a distributable format either way (portable use, zip it
21
+ up yourself) — but *this* build's contents depend on ``release``: ``build``/
22
+ ``run`` (``release=False``) keep readable ``.py`` for fast iteration, while
23
+ ``package`` (``release=True``) additionally byte-compiles per
24
+ ``[tool.kivy.windows.build_settings]`` before copying the tree into
25
+ ``dist/windows``. ``release`` is a build-policy flag, not a statement about
26
+ whether the folder *can* be distributed.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import platform
32
+ import shutil
33
+ from pathlib import Path
34
+
35
+ import click
36
+
37
+ from kivyforge.artifacts.cache import ArtifactCache
38
+ from kivyforge.bundle.pycompile import PycompileError, byte_compile, select_compiler
39
+ from kivyforge.config.model import Config
40
+
41
+ from . import WindowsBundleError
42
+ from .fsswap import discard_reserved, reserve_previous, restore_previous
43
+ from .icons import stage_icon
44
+ from .launcher import launcher_exe_name, place_launcher, write_bootstrap
45
+ from .lock import WindowsLockfile
46
+ from .naming import windows_safe_name
47
+ from .native_stage import stage_native_binaries
48
+ from .rcedit import ResourcePatch
49
+ from .runtime_stage import stage_runtime
50
+ from .signing import pep440_to_file_version
51
+ from .wheels_stage import stage_wheels
52
+
53
+ _IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc")
54
+
55
+
56
+ def resolve_assembly_arch(locked: tuple[str, ...], arch: str | None) -> str:
57
+ """The single arch to assemble. Windows ships one arch (``amd64``) per onedir."""
58
+ if not locked:
59
+ raise WindowsBundleError(
60
+ "the lock covers no architectures; re-run `kivyforge lock -p windows`."
61
+ )
62
+ if arch is None:
63
+ return locked[0]
64
+ if arch not in locked:
65
+ raise WindowsBundleError(
66
+ f"--arch {arch} is not in the lock (covers {', '.join(locked)}).\n"
67
+ f" Add {arch!r} to [tool.kivy.windows].archs and re-lock, or pick a "
68
+ "locked arch."
69
+ )
70
+ return arch
71
+
72
+
73
+ def bundle_dir_name(config: Config) -> str:
74
+ """The onedir folder name for *config* (Windows-safe display name)."""
75
+ return windows_safe_name(config.display_name)
76
+
77
+
78
+ def _setting_applies(value: bool | str, *, release: bool) -> bool:
79
+ """A ``build_settings`` tri-state resolved for the build being produced."""
80
+ if isinstance(value, bool):
81
+ return value
82
+ return release # DESKTOP_RELEASE_ONLY
83
+
84
+
85
+ def _resolve_byte_compile(
86
+ config: Config,
87
+ *,
88
+ staged_interpreter: Path,
89
+ target_arch: str,
90
+ python_version: str,
91
+ release: bool,
92
+ ) -> tuple[tuple[str, ...] | None, bool]:
93
+ """Decide whether to byte-compile, and with which interpreter.
94
+
95
+ Returns ``(compiler_argv_or_None, strip_source)``; ``None`` means do not
96
+ byte-compile. ``byte_compile = true`` is read as "I insist", so a missing
97
+ interpreter is an error; the default ``"release"`` degrades to shipping
98
+ source with a warning rather than breaking a build the user never
99
+ configured.
100
+ """
101
+ settings = config.windows_required.build_settings
102
+ if not _setting_applies(settings.byte_compile, release=release):
103
+ return None, False
104
+ native = platform.machine().lower() == target_arch
105
+ compiler = select_compiler(
106
+ staged_interpreter=staged_interpreter,
107
+ native=native,
108
+ python_version=python_version,
109
+ )
110
+ if compiler is None:
111
+ message = (
112
+ f"this project ships CPython {python_version}, and no CPython of "
113
+ "that minor could be found to byte-compile with (a .pyc is only "
114
+ "loadable by the exact CPython minor that wrote it)"
115
+ )
116
+ if settings.byte_compile is True:
117
+ raise WindowsBundleError(
118
+ "[tool.kivy.windows.build_settings].byte_compile = true but "
119
+ f"{message}.\n"
120
+ " Install a matching CPython, or set byte_compile = false."
121
+ )
122
+ click.echo(f"[stage] not byte-compiling: {message}.")
123
+ return None, False
124
+ strip = _setting_applies(settings.strip_source, release=release)
125
+ return compiler, strip
126
+
127
+
128
+ def build_onedir(
129
+ config: Config,
130
+ lock: WindowsLockfile,
131
+ project_root: Path,
132
+ *,
133
+ arch: str | None = None,
134
+ staging_dir: Path | None = None,
135
+ no_cache: bool = False,
136
+ cache: ArtifactCache | None = None,
137
+ release: bool = False,
138
+ echo=click.echo,
139
+ ) -> Path:
140
+ """Build the onedir bundle tree and return its path.
141
+
142
+ The tree is assembled **directly into its final location**. Temp-dir + rename
143
+ (the POSIX pattern) is not viable on Windows: a just-written tree cannot be
144
+ reliably renamed because the antivirus real-time scanner holds the new
145
+ executables/DLLs open — on a cold first-sight scan (cloud lookup) that lasts
146
+ well beyond any reasonable retry window. Instead we preserve any previous
147
+ bundle in a trash dir first and restore it if assembly fails, so a broken
148
+ build never destroys a working one. The trash is an already-scanned tree,
149
+ which renames instantly; only freshly written files (which we never rename)
150
+ hit the scan window.
151
+ """
152
+ target_arch = resolve_assembly_arch(lock.archs, arch)
153
+ cache = cache or ArtifactCache()
154
+ staging_dir = staging_dir or (project_root / "build" / "windows")
155
+ bundle = staging_dir / bundle_dir_name(config)
156
+
157
+ staging_dir.mkdir(parents=True, exist_ok=True)
158
+ trash = reserve_previous(bundle)
159
+ try:
160
+ echo(
161
+ f"Staging CPython {lock.python_runtime.version} runtime ({target_arch}) ..."
162
+ )
163
+ stage_runtime(
164
+ lock.python_runtime,
165
+ target_arch,
166
+ bundle / "python",
167
+ project_root=project_root,
168
+ cache=cache,
169
+ no_cache=no_cache,
170
+ )
171
+
172
+ echo(f"Installing {len(lock.packages)} locked packages ...")
173
+ stage_wheels(
174
+ lock.packages,
175
+ target_arch,
176
+ bundle / "python",
177
+ project_root=project_root,
178
+ cache=cache,
179
+ no_cache=no_cache,
180
+ )
181
+
182
+ if lock.native_binaries:
183
+ echo(f"Staging {len(lock.native_binaries)} native binaries ...")
184
+ stage_native_binaries(
185
+ lock,
186
+ bundle,
187
+ target_arch,
188
+ project_root=project_root,
189
+ cache=cache,
190
+ no_cache=no_cache,
191
+ )
192
+
193
+ _copy_app_sources(config, project_root, bundle / "app")
194
+
195
+ compiler, strip_source = _resolve_byte_compile(
196
+ config,
197
+ staged_interpreter=bundle / "python" / "python.exe",
198
+ target_arch=target_arch,
199
+ python_version=lock.python_runtime.version,
200
+ release=release,
201
+ )
202
+ if compiler is not None:
203
+ with_what = " ".join(compiler) if compiler else "this interpreter"
204
+ echo(
205
+ f"[stage] byte-compiling the Python payload with {with_what}"
206
+ + (" (.pyc only)" if strip_source else "")
207
+ )
208
+ try:
209
+ byte_compile(
210
+ [bundle / "app", bundle / "python" / "Lib" / "site-packages"],
211
+ compiler=compiler,
212
+ strip_source=strip_source,
213
+ stripdir=bundle,
214
+ )
215
+ except PycompileError as exc:
216
+ raise WindowsBundleError(
217
+ f"{exc}\n Fix it, or set "
218
+ "[tool.kivy.windows.build_settings].byte_compile = false."
219
+ ) from exc
220
+
221
+ write_bootstrap(
222
+ bundle,
223
+ entry_point=config.kivy.entry_point,
224
+ app_id=config.windows_required.app_id,
225
+ )
226
+
227
+ icon = stage_icon(config, project_root, bundle / "app.ico")
228
+ place_launcher(
229
+ bundle,
230
+ display_name=config.display_name,
231
+ patch=_resource_patch(config, icon),
232
+ )
233
+ except BaseException:
234
+ shutil.rmtree(bundle, ignore_errors=True)
235
+ restore_previous(trash, bundle)
236
+ raise
237
+
238
+ discard_reserved(trash)
239
+ return bundle
240
+
241
+
242
+ def _first_author(config: Config) -> str | None:
243
+ """First ``[project].authors`` name, or ``None`` — the publisher/copyright.
244
+
245
+ Mirrors the iOS ``NSHumanReadableCopyright`` convention (pyproject-ios): the
246
+ first author's name populates both ``LegalCopyright`` and ``CompanyName`` in
247
+ the Windows version resource. Verbatim (no synthesized year) so the patch
248
+ stays deterministic.
249
+ """
250
+ return next((a.name for a in config.project.authors if a.name), None)
251
+
252
+
253
+ def _resource_patch(config: Config, icon: Path | None) -> ResourcePatch:
254
+ """The launcher resource patch from project metadata.
255
+
256
+ The string ProductVersion carries the full PEP 440 version; the numeric
257
+ FILEVERSION/PRODUCTVERSION use the deterministic four-part mapping (signing).
258
+ LegalCopyright/CompanyName come from the first ``[project].authors`` entry.
259
+ """
260
+ version = config.project.version
261
+ numeric = pep440_to_file_version(version)
262
+ author = _first_author(config)
263
+ return ResourcePatch(
264
+ icon=icon,
265
+ product_name=config.display_name,
266
+ file_description=config.display_name,
267
+ product_version_string=version,
268
+ legal_copyright=author,
269
+ company_name=author,
270
+ file_version=numeric,
271
+ product_version=numeric,
272
+ )
273
+
274
+
275
+ def _copy_app_sources(config: Config, project_root: Path, dest: Path) -> None:
276
+ src = (project_root / config.kivy.app_dir).resolve()
277
+ if not src.is_dir():
278
+ raise WindowsBundleError(
279
+ f"[tool.kivy].app_dir points to {config.kivy.app_dir!r}, which is not "
280
+ f"a directory under {project_root}."
281
+ )
282
+ entry_rel = f"{config.kivy.entry_point.replace('.', '/')}.py"
283
+ entry = src / entry_rel
284
+ if not entry.is_file():
285
+ raise WindowsBundleError(
286
+ f"entry point {entry_rel} not found in "
287
+ f"{config.kivy.app_dir}/ (set [tool.kivy].entry_point)."
288
+ )
289
+ shutil.copytree(src, dest, ignore=_IGNORE)
290
+
291
+
292
+ # Re-exported for the verbs/status so they compute the same path.
293
+ def onedir_path(config: Config, project_root: Path) -> Path:
294
+ return project_root / "build" / "windows" / bundle_dir_name(config)
295
+
296
+
297
+ def launcher_name(config: Config) -> str:
298
+ return launcher_exe_name(config.display_name)
@@ -0,0 +1,314 @@
1
+ """Windows dispatch for the shared ``build`` / ``run`` / ``package`` verbs.
2
+
3
+ Keeps the Windows onedir flow out of the (iOS/Xcode-centric) verb modules: each
4
+ verb resolves the target platform and, when it is Windows, calls in here. Loads
5
+ the Windows config + ``pylock.windows.toml``, runs the drift check, and drives
6
+ the onedir bundler. The onedir folder is both the ``run`` target and — copied to
7
+ ``dist/windows`` — the ``package`` output (installers stay external).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ import subprocess
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import click
18
+
19
+ from kivyforge.cli._common import ToolchainError, lockfile_path_for
20
+ from kivyforge.config import ConfigError, load_config
21
+ from kivyforge.lock.reader import LockError, is_in_sync
22
+
23
+ from .. import HostCapabilityError, get_platform
24
+ from . import WindowsBundleError
25
+ from .bundle import (
26
+ build_onedir,
27
+ bundle_dir_name,
28
+ launcher_name,
29
+ onedir_path,
30
+ resolve_assembly_arch,
31
+ )
32
+ from .fsswap import discard_reserved, reserve_previous, restore_previous
33
+ from .lock import WindowsLockfile
34
+ from .lock import load as load_windows_lock
35
+ from .signing import select_signer
36
+
37
+ # Never copy VCS metadata or editor droppings into the shipped dist tree. The
38
+ # onedir bundle is assembled clean, so this is defense in depth (and covers a
39
+ # build/ dir a user pointed packaging at directly).
40
+ #
41
+ # Deliberately does NOT exclude __pycache__/*.pyc/*.pyo: those used to be junk
42
+ # patterns before byte-compilation existed, but [tool.kivy.windows
43
+ # .build_settings] now makes .pyc the actual shipped payload -- excluding it
44
+ # here would silently ship an empty app/ once strip_source has deleted the
45
+ # .py, which is exactly what byte_compile/strip_source is supposed to produce.
46
+ _PACKAGE_IGNORE = shutil.ignore_patterns(
47
+ ".git",
48
+ ".gitignore",
49
+ ".gitattributes",
50
+ ".svn",
51
+ ".hg",
52
+ ".DS_Store",
53
+ "Thumbs.db",
54
+ "desktop.ini",
55
+ ".idea",
56
+ ".vscode",
57
+ "*.swp",
58
+ "*~",
59
+ )
60
+
61
+
62
+ def windows_build(
63
+ project_root: Path,
64
+ *,
65
+ arch: str | None,
66
+ no_verify_lock: bool,
67
+ no_cache: bool,
68
+ ) -> Path:
69
+ """Assemble the Windows onedir bundle; return its path."""
70
+ _require_windows_host()
71
+ config, lock = _load_and_verify(project_root, no_verify_lock)
72
+ bundle = _assemble(
73
+ config, lock, project_root, arch=arch, no_cache=no_cache, release=False
74
+ )
75
+ click.echo(f"Built {bundle.relative_to(project_root)}")
76
+ return bundle
77
+
78
+
79
+ def windows_package(
80
+ project_root: Path,
81
+ *,
82
+ fmt: str,
83
+ arch: str | None,
84
+ no_verify_lock: bool,
85
+ no_cache: bool,
86
+ ) -> Path:
87
+ """Produce the distributable: the onedir folder copied to ``dist/windows``.
88
+
89
+ ``folder`` is the only Windows package format (installers stay external). The
90
+ copied tree is the portable, run-from-folder distributable.
91
+ """
92
+ _require_windows_host()
93
+ config, lock = _load_and_verify(project_root, no_verify_lock)
94
+ target_arch = _resolve_arch(lock, arch)
95
+ bundle = _assemble(
96
+ config, lock, project_root, arch=target_arch, no_cache=no_cache, release=True
97
+ )
98
+
99
+ # The dist folder name matches the build tree: the display_name run through
100
+ # the Windows filename sanitizer (windows-spec), not the raw project.name.
101
+ dest = (
102
+ project_root
103
+ / "dist"
104
+ / "windows"
105
+ / f"{bundle_dir_name(config)}-{config.project.version}-{target_arch}"
106
+ )
107
+ # Reserve any prior package and keep it until *signing* also succeeds, so a
108
+ # signer failure (missing cert, timestamp outage) rolls back to the previous
109
+ # known-good artifact rather than leaving a half-baked unsigned tree in place.
110
+ trash = _stage_dist_copy(bundle, dest)
111
+ try:
112
+ # Signing (when configured) targets the dist copy only, after the resource
113
+ # patch (done at build) and before any external installer. The build tree
114
+ # stays unsigned as the dev-run target.
115
+ signed = _sign_launcher(config, dest)
116
+ except BaseException:
117
+ shutil.rmtree(dest, ignore_errors=True)
118
+ restore_previous(trash, dest)
119
+ raise
120
+ discard_reserved(trash)
121
+
122
+ note = (
123
+ "signed + timestamped"
124
+ if signed
125
+ else "unsigned (configure [tool.kivy.windows.signing] to sign)"
126
+ )
127
+ click.echo(
128
+ f"Packaged {dest.relative_to(project_root)} (onedir folder, {note}).\n"
129
+ f" Run it by double-clicking {launcher_name(config)}, or zip the folder "
130
+ "to distribute. An installer is an external step."
131
+ )
132
+ return dest
133
+
134
+
135
+ def windows_run(
136
+ project_root: Path,
137
+ *,
138
+ arch: str | None,
139
+ no_build: bool,
140
+ ) -> None:
141
+ """Build (unless --no-build) and launch the app ``.exe`` in the foreground."""
142
+ if no_build:
143
+ _require_windows_host()
144
+ config = _load_config(project_root)
145
+ bundle = onedir_path(config, project_root)
146
+ if not bundle.exists():
147
+ raise ToolchainError(
148
+ f"no built bundle at {bundle.relative_to(project_root)}; run "
149
+ "without --no-build first."
150
+ )
151
+ else:
152
+ config = _load_config(project_root)
153
+ bundle = windows_build(
154
+ project_root, arch=arch, no_verify_lock=False, no_cache=False
155
+ )
156
+
157
+ exe = bundle / launcher_name(config)
158
+ click.echo(f"Launching {bundle.name} ...")
159
+ # Foreground: the launcher attaches to this console so the dev sees
160
+ # stdout/stderr + tracebacks (the Phase 3 console handoff).
161
+ proc = subprocess.run([str(exe)])
162
+ if proc.returncode != 0:
163
+ raise ToolchainError(f"{bundle.name} exited with status {proc.returncode}.")
164
+
165
+
166
+ def windows_status(project_root: Path) -> None:
167
+ """Show app identity, Python version, lock sync, and build state."""
168
+ config = _load_config(project_root)
169
+ windows = config.windows_required
170
+ click.echo(f"App: {config.display_name} ({windows.app_id})")
171
+ click.echo(f"Python: {windows.python_version or '(unset)'}")
172
+ click.echo(f"Lock: {_lock_state(project_root)}")
173
+ click.echo(f"Build: {_build_state(onedir_path(config, project_root))}")
174
+
175
+
176
+ def _assemble(config, lock, project_root, *, arch, no_cache, release) -> Path:
177
+ try:
178
+ return build_onedir(
179
+ config, lock, project_root, arch=arch, no_cache=no_cache, release=release
180
+ )
181
+ except WindowsBundleError as exc:
182
+ raise ToolchainError(str(exc)) from exc
183
+
184
+
185
+ def _resolve_arch(lock: WindowsLockfile, arch: str | None) -> str:
186
+ try:
187
+ return resolve_assembly_arch(lock.archs, arch)
188
+ except WindowsBundleError as exc:
189
+ raise ToolchainError(str(exc)) from exc
190
+
191
+
192
+ def _sign_launcher(config, dest: Path) -> bool:
193
+ """Sign the launcher in the dist copy when signing is configured.
194
+
195
+ Returns whether the artifact was signed. Translates any signing failure into
196
+ a :class:`ToolchainError` so ``package`` reports it cleanly.
197
+ """
198
+ signer = select_signer(config.windows_required.signing)
199
+ if not signer.configured:
200
+ return False
201
+ exe = dest / launcher_name(config)
202
+ try:
203
+ signer.sign([exe])
204
+ except WindowsBundleError as exc:
205
+ raise ToolchainError(str(exc)) from exc
206
+ return True
207
+
208
+
209
+ def _stage_dist_copy(bundle: Path, dest: Path) -> Path | None:
210
+ """Copy *bundle* into *dest*, returning the reserved previous tree (or None).
211
+
212
+ Copies **directly into the final location**: like the build step, a freshly
213
+ written tree cannot be reliably renamed on Windows (the antivirus scanner
214
+ holds new files open), so temp-dir + rename is not viable. Any previous dist
215
+ copy is reserved first and restored if the copy fails, so a broken package
216
+ never destroys a working one.
217
+
218
+ The reserved previous tree is **not** discarded here: the caller must call
219
+ :func:`discard_reserved` only after every later step (e.g. signing) succeeds,
220
+ or :func:`restore_previous` to roll back. This keeps the previous known-good
221
+ artifact recoverable across a post-copy failure.
222
+ """
223
+ dest.parent.mkdir(parents=True, exist_ok=True)
224
+ try:
225
+ trash = reserve_previous(dest)
226
+ except WindowsBundleError as exc:
227
+ raise ToolchainError(str(exc)) from exc
228
+ try:
229
+ # copytree needs a non-existent target; the reserve moved any prior away.
230
+ shutil.copytree(bundle, dest, ignore=_PACKAGE_IGNORE)
231
+ except BaseException:
232
+ shutil.rmtree(dest, ignore_errors=True)
233
+ restore_previous(trash, dest)
234
+ raise
235
+ return trash
236
+
237
+
238
+ def _load_and_verify(
239
+ project_root: Path, no_verify_lock: bool
240
+ ) -> tuple[object, WindowsLockfile]:
241
+ config = _load_config(project_root)
242
+ lock = _load_lock(project_root)
243
+ pyproject = project_root / "pyproject.toml"
244
+ if not no_verify_lock and not is_in_sync(lock, pyproject.read_text("utf-8")):
245
+ raise ToolchainError(
246
+ f"{lockfile_path_for('windows').name} is out of date with "
247
+ "pyproject.toml.\n"
248
+ " Run `kivyforge lock -p windows` to regenerate it (or pass "
249
+ "--no-verify-lock to build against the stale lock anyway)."
250
+ )
251
+ return config, lock
252
+
253
+
254
+ def _lock_state(project_root: Path) -> str:
255
+ path = lockfile_path_for("windows", project_root)
256
+ if not path.is_file():
257
+ return "missing (run `kivyforge lock -p windows`)"
258
+ try:
259
+ lock = load_windows_lock(path)
260
+ except LockError:
261
+ return "unreadable (run `kivyforge lock -p windows`)"
262
+ pyproject = project_root / "pyproject.toml"
263
+ if is_in_sync(lock, pyproject.read_text("utf-8")):
264
+ return "in sync"
265
+ return "out of date (run `kivyforge lock -p windows`)"
266
+
267
+
268
+ def _build_state(bundle: Path) -> str:
269
+ if not bundle.exists():
270
+ return "not built"
271
+ age = time.time() - bundle.stat().st_mtime
272
+ return f"last built {_humanize(age)}"
273
+
274
+
275
+ def _humanize(seconds: float) -> str:
276
+ seconds = int(seconds)
277
+ if seconds < 60:
278
+ return "just now"
279
+ if seconds < 3600:
280
+ m = seconds // 60
281
+ return f"{m} minute{'s' if m != 1 else ''} ago"
282
+ if seconds < 86400:
283
+ h = seconds // 3600
284
+ return f"{h} hour{'s' if h != 1 else ''} ago"
285
+ d = seconds // 86400
286
+ return f"{d} day{'s' if d != 1 else ''} ago"
287
+
288
+
289
+ def _require_windows_host() -> None:
290
+ try:
291
+ get_platform("windows").check_host_capability()
292
+ except HostCapabilityError as exc:
293
+ raise ToolchainError(str(exc)) from exc
294
+
295
+
296
+ def _load_config(project_root: Path):
297
+ try:
298
+ return load_config(
299
+ project_root / "pyproject.toml", require_ios=False, require_windows=True
300
+ )
301
+ except ConfigError as exc:
302
+ raise ToolchainError(exc.format()) from exc
303
+
304
+
305
+ def _load_lock(project_root: Path) -> WindowsLockfile:
306
+ path = lockfile_path_for("windows", project_root)
307
+ if not path.is_file():
308
+ raise ToolchainError(
309
+ f"no {path.name} found. Run `kivyforge lock -p windows` first."
310
+ )
311
+ try:
312
+ return load_windows_lock(path)
313
+ except LockError as exc:
314
+ raise ToolchainError(str(exc)) from exc