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,563 @@
1
+ r"""Windows-specific doctor checks (windows-spec).
2
+
3
+ Reuses the platform-neutral checks (kivyforge version, app source dir) and adds
4
+ the Windows environment (host OS, long-path support) and project checks (app_id
5
+ style, ``win_amd64`` arch coverage, icon, native-binary sources / collisions /
6
+ arch, reachable hosts, resource assets, signtool + signing certificate,
7
+ find_links) over the ``pylock.windows.toml`` model.
8
+
9
+ Every check returns an explicit PASS / WARN / FAIL / SKIP; ``doctor`` exits
10
+ non-zero only on FAIL. Signing checks SKIP unless ``[tool.kivy.windows.signing]``
11
+ is configured, and the certificate lookup targets the **same** ``store_scope``
12
+ store that ``signtool`` will use, so the two never disagree.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from pathlib import Path
19
+ from urllib.parse import urlparse
20
+
21
+ from kivyforge.config import ConfigError, load_config
22
+ from kivyforge.config.icons import APP_ICON_SIZE, icon_source_problem
23
+ from kivyforge.config.model import (
24
+ WINDOWS_APP_ID_MAX_LENGTH,
25
+ Config,
26
+ )
27
+ from kivyforge.doctor import checks_common as C
28
+ from kivyforge.doctor.probe import Probe, RealProbe
29
+ from kivyforge.doctor.result import CheckResult, Status
30
+ from kivyforge.lock.find_links import find_links_doctor_detail
31
+ from kivyforge.lock.reader import LockError
32
+
33
+ from .bundle import bundle_dir_name, onedir_path
34
+ from .lock import WindowsLockfile, load
35
+ from .naming import _RESERVED
36
+ from .petools import PeArchError, verify_pe_arch
37
+
38
+ # A well-formed AppUserModelID *style* (Microsoft's recommendation): one or more
39
+ # PascalCase segments joined by periods, e.g. ``Acme.MyApp`` or
40
+ # ``Contoso.Suite.Editor``. Only a WARN — the loader already enforces the hard
41
+ # constraints (no spaces, <=128 chars).
42
+ _APP_ID_STYLE = re.compile(r"^[A-Z][A-Za-z0-9]*(\.[A-Z][A-Za-z0-9]*)+$")
43
+
44
+ _ARCHIVE_SUFFIXES = (".zip", ".tar.gz", ".tgz")
45
+
46
+
47
+ # ---- environment checks -------------------------------------------------
48
+
49
+
50
+ def check_windows_host(probe: Probe) -> CheckResult:
51
+ host = probe.host_system()
52
+ if host != "Windows":
53
+ return CheckResult(
54
+ "Host is Windows",
55
+ Status.FAIL,
56
+ f"host is {host!r}",
57
+ hint="the onedir backend stages an MSVC amd64 "
58
+ "python-build-standalone runtime and uses Windows-only tools "
59
+ "(rcedit, signtool); build on a Windows host.",
60
+ )
61
+ return CheckResult("Host is Windows", Status.PASS, "Windows")
62
+
63
+
64
+ def check_long_path_support(probe: Probe) -> CheckResult:
65
+ enabled = probe.long_paths_enabled()
66
+ if enabled is None:
67
+ return CheckResult(
68
+ "Long-path support",
69
+ Status.SKIP,
70
+ "could not read LongPathsEnabled",
71
+ )
72
+ if not enabled:
73
+ return CheckResult(
74
+ "Long-path support",
75
+ Status.WARN,
76
+ "LongPathsEnabled is off",
77
+ hint="a full runtime prefix under build/windows can exceed the legacy "
78
+ "260-char MAX_PATH. Enable it: set HKLM\\SYSTEM\\CurrentControlSet\\"
79
+ "Control\\FileSystem\\LongPathsEnabled=1 (admin), then reboot.",
80
+ )
81
+ return CheckResult("Long-path support", Status.PASS, "LongPathsEnabled=1")
82
+
83
+
84
+ # ---- project checks -----------------------------------------------------
85
+
86
+
87
+ def check_windows_app_id(config: Config) -> CheckResult:
88
+ app_id = config.windows_required.app_id
89
+ if " " in app_id or len(app_id) > WINDOWS_APP_ID_MAX_LENGTH:
90
+ return CheckResult(
91
+ "AppUserModelID",
92
+ Status.FAIL,
93
+ f"{app_id!r} violates the AppUserModelID constraints",
94
+ hint="the AppUserModelID must contain no spaces and be at most "
95
+ f"{WINDOWS_APP_ID_MAX_LENGTH} characters.",
96
+ )
97
+ if not _APP_ID_STYLE.match(app_id):
98
+ return CheckResult(
99
+ "AppUserModelID",
100
+ Status.WARN,
101
+ f"{app_id!r} is not the recommended CompanyName.ProductName style",
102
+ hint="Microsoft recommends a PascalCase, period-delimited "
103
+ "AppUserModelID such as 'Acme.MyApp' so taskbar grouping and jump "
104
+ "lists behave; it still works as-is.",
105
+ )
106
+ return CheckResult("AppUserModelID", Status.PASS, app_id)
107
+
108
+
109
+ def check_windows_arch_coverage(
110
+ config: Config, lock: WindowsLockfile | None
111
+ ) -> CheckResult:
112
+ archs = config.windows_required.archs
113
+ if lock is None:
114
+ return CheckResult(
115
+ "Architecture coverage", Status.SKIP, "no pylock.windows.toml"
116
+ )
117
+ runtime_archs = {a.arch for a in lock.python_runtime.artifacts}
118
+ missing_runtime = [a for a in archs if a not in runtime_archs]
119
+ if missing_runtime:
120
+ return CheckResult(
121
+ "Architecture coverage",
122
+ Status.FAIL,
123
+ f"runtime missing arch(es): {', '.join(missing_runtime)}",
124
+ hint="re-run `kivyforge lock -p windows` after setting archs.",
125
+ )
126
+ # arm64: this hardcodes win_amd64; generalize to each configured arch's tag
127
+ # (windows_platform_tag(arch)) so arm64 coverage is checked too. The native
128
+ # arch check (check_windows_native_arch) already loops per arch and needs no
129
+ # change. See arm64-windows.md §8.
130
+ for pkg in lock.packages:
131
+ if any(w.is_pure_python for w in pkg.wheels):
132
+ continue
133
+ tags = {w.platform_tag for w in pkg.wheels}
134
+ if "win_amd64" not in tags:
135
+ return CheckResult(
136
+ "Architecture coverage",
137
+ Status.FAIL,
138
+ f"{pkg.name} has no win_amd64 wheel",
139
+ hint="a compiled dependency must publish a win_amd64 wheel; supply "
140
+ "it via extra_index_urls/find_links and re-lock, or drop the dep "
141
+ "on Windows.",
142
+ )
143
+ return CheckResult(
144
+ "Architecture coverage", Status.PASS, f"all deps cover {', '.join(archs)}"
145
+ )
146
+
147
+
148
+ def check_windows_app_icon(config: Config, project_root: Path) -> CheckResult:
149
+ source = config.windows_required.icons.source
150
+ if not source:
151
+ return CheckResult("App icon", Status.SKIP, "not configured")
152
+ problem = icon_source_problem((project_root / source).resolve())
153
+ if problem:
154
+ return CheckResult("App icon", Status.FAIL, f"{source} invalid", hint=problem)
155
+ return CheckResult("App icon", Status.PASS, f"{APP_ICON_SIZE}x{APP_ICON_SIZE} PNG")
156
+
157
+
158
+ def check_windows_native_sources(config: Config, project_root: Path) -> CheckResult:
159
+ declared = config.windows_required.binaries
160
+ if not declared:
161
+ return CheckResult("Native binaries: sources", Status.SKIP, "not configured")
162
+ root = project_root.resolve()
163
+ missing = [
164
+ dep.source
165
+ for dep in declared
166
+ if not dep.source.startswith(("http://", "https://"))
167
+ and not (root / dep.source).is_file()
168
+ ]
169
+ if missing:
170
+ return CheckResult(
171
+ "Native binaries: sources",
172
+ Status.FAIL,
173
+ f"vendored source(s) not found: {', '.join(missing)}",
174
+ hint="vendor the artifact under the project and re-run "
175
+ "`kivyforge lock -p windows` (URL sources are checked by the hosts "
176
+ "reachability check).",
177
+ )
178
+ return CheckResult(
179
+ "Native binaries: sources",
180
+ Status.PASS,
181
+ f"{len(declared)} declared; sources present",
182
+ )
183
+
184
+
185
+ def check_windows_native_collision(config: Config) -> CheckResult:
186
+ """Case-insensitive + reserved-name preflight for single-file sources.
187
+
188
+ Windows is case-insensitive, so ``SDK.dll`` and ``sdk.dll`` would land on
189
+ the same ``bin\\`` path; reserved device names (``NUL.dll``) are illegal.
190
+ Archive-member collisions need extraction and are caught (loudly) at build.
191
+ """
192
+ declared = config.windows_required.binaries
193
+ if not declared:
194
+ return CheckResult("Native binaries: collision", Status.SKIP, "not configured")
195
+ staged: dict[str, str] = {}
196
+ for dep in declared:
197
+ if dep.source.lower().endswith(_ARCHIVE_SUFFIXES):
198
+ continue
199
+ base = dep.source.rsplit("/", 1)[-1] or dep.name
200
+ stem = base.split(".", 1)[0].upper()
201
+ if stem in _RESERVED:
202
+ return CheckResult(
203
+ "Native binaries: collision",
204
+ Status.FAIL,
205
+ f"{dep.name!r} stages reserved device name {base!r}",
206
+ hint="rename the artifact; Windows forbids CON/PRN/AUX/NUL/"
207
+ "COM1-9/LPT1-9 even with an extension.",
208
+ )
209
+ key = base.lower()
210
+ prev = staged.get(key)
211
+ if prev is not None:
212
+ return CheckResult(
213
+ "Native binaries: collision",
214
+ Status.FAIL,
215
+ f"{prev!r} and {dep.name!r} both stage bin/{base} (case-insensitive)",
216
+ hint="two native binaries would overwrite each other in bin\\; "
217
+ "rename one so each stages to a case-insensitively unique name.",
218
+ )
219
+ staged[key] = dep.name
220
+ return CheckResult(
221
+ "Native binaries: collision", Status.PASS, "no case/reserved-name collisions"
222
+ )
223
+
224
+
225
+ def check_windows_native_arch(config: Config, project_root: Path) -> CheckResult:
226
+ declared = config.windows_required.binaries
227
+ if not declared:
228
+ return CheckResult("Native binaries: arch", Status.SKIP, "not configured")
229
+ bin_dir = (
230
+ project_root.resolve() / "build" / "windows" / bundle_dir_name(config) / "bin"
231
+ )
232
+ if not bin_dir.is_dir():
233
+ return CheckResult(
234
+ "Native binaries: arch", Status.SKIP, "not built (run `kivyforge build`)"
235
+ )
236
+ archs = config.windows_required.archs
237
+ problems: list[str] = []
238
+ pe_count = 0
239
+ for path in sorted(bin_dir.rglob("*")):
240
+ if not path.is_file():
241
+ continue
242
+ for arch in archs:
243
+ try:
244
+ verify_pe_arch(path, arch)
245
+ except PeArchError as exc:
246
+ problems.append(str(exc).splitlines()[0])
247
+ pe_count += 1
248
+ if problems:
249
+ return CheckResult(
250
+ "Native binaries: arch",
251
+ Status.FAIL,
252
+ "; ".join(problems),
253
+ hint=f"rebuild the native binaries for {', '.join(archs)} and re-lock.",
254
+ )
255
+ return CheckResult(
256
+ "Native binaries: arch", Status.PASS, f"{pe_count} staged file(s) match"
257
+ )
258
+
259
+
260
+ def check_windows_output_lock(
261
+ config: Config, project_root: Path, probe: Probe
262
+ ) -> CheckResult:
263
+ """Pre-flight the exact rename ``build``/``package`` performs on the onedir.
264
+
265
+ A running instance of the app, or an Explorer/terminal window sitting in the
266
+ output folder, holds the tree open so the assemble step can't replace it
267
+ (``WinError 5``). Catch that before a full build is wasted. SKIP when nothing
268
+ is built yet; WARN (not FAIL) since it is transient, user-fixable state.
269
+ """
270
+ onedir = onedir_path(config, project_root)
271
+ try:
272
+ locked = probe.build_output_locked(onedir)
273
+ except OSError as exc:
274
+ return CheckResult(
275
+ "Build output not locked",
276
+ Status.FAIL,
277
+ "could not restore the build output after probing its lock",
278
+ hint=str(exc),
279
+ )
280
+ if locked is None:
281
+ return CheckResult("Build output not locked", Status.SKIP, "not built")
282
+ if locked:
283
+ return CheckResult(
284
+ "Build output not locked",
285
+ Status.WARN,
286
+ f"{onedir.name!r} is held open by another process",
287
+ hint="a running instance of the app, or an Explorer/terminal window "
288
+ "in build\\windows, blocks build/package from replacing the folder "
289
+ "(WinError 5); quit the app / close the window before building.",
290
+ )
291
+ return CheckResult("Build output not locked", Status.PASS, "replaceable")
292
+
293
+
294
+ def check_windows_build_volume(
295
+ config: Config, project_root: Path, probe: Probe
296
+ ) -> CheckResult:
297
+ """Advise on the build volume's filesystem (Dev Drive vs NTFS).
298
+
299
+ Never FAILs/WARNs — a normal NTFS setup is healthy. It surfaces the sanctioned
300
+ mitigation for repeated rename-lock churn (a trusted ReFS Dev Drive, where
301
+ Defender scans asynchronously) in the always-shown detail rather than as noise.
302
+ """
303
+ onedir = onedir_path(config, project_root)
304
+ target = onedir if onedir.exists() else project_root
305
+ fs = probe.filesystem_type(target)
306
+ if fs is None:
307
+ return CheckResult("Build volume", Status.SKIP, "filesystem unknown")
308
+ if fs.upper() == "REFS":
309
+ return CheckResult(
310
+ "Build volume", Status.PASS, f"{fs} (Dev Drive — async Defender scanning)"
311
+ )
312
+ return CheckResult(
313
+ "Build volume",
314
+ Status.PASS,
315
+ f"{fs}; for repeated build-lock churn a Windows 11 Dev Drive (ReFS) "
316
+ "enables Defender performance mode (async scanning)",
317
+ )
318
+
319
+
320
+ def check_windows_resource_assets(config: Config, project_root: Path) -> CheckResult:
321
+ """The inputs the launcher resource-patch needs are present.
322
+
323
+ The patch always writes ProductName / ProductVersion (from project metadata,
324
+ always present) and, when configured, an icon (validated by the icon check).
325
+ """
326
+ version = config.project.version
327
+ source = config.windows_required.icons.source
328
+ if source and icon_source_problem((project_root / source).resolve()):
329
+ return CheckResult(
330
+ "Resource assets",
331
+ Status.WARN,
332
+ f"icon {source!r} is invalid; the launcher will ship without one",
333
+ hint="fix the icon source (see the App icon check) so the launcher "
334
+ "carries it.",
335
+ )
336
+ icon_note = "with icon" if source else "no icon (default)"
337
+ return CheckResult(
338
+ "Resource assets", Status.PASS, f"version {version}, {icon_note}"
339
+ )
340
+
341
+
342
+ def check_windows_signtool(probe: Probe, config: Config) -> CheckResult:
343
+ signing = config.windows_required.signing
344
+ if not signing.configured:
345
+ return CheckResult("signtool available", Status.SKIP, "signing not configured")
346
+ if not probe.has_signtool():
347
+ return CheckResult(
348
+ "signtool available",
349
+ Status.FAIL,
350
+ "signtool not found on PATH",
351
+ hint="install the Windows SDK (signtool ships with it) and ensure it is "
352
+ "on PATH, or run packaging from a Developer Command Prompt.",
353
+ )
354
+ return CheckResult("signtool available", Status.PASS, "found")
355
+
356
+
357
+ def check_windows_signing_cert(probe: Probe, config: Config) -> CheckResult:
358
+ signing = config.windows_required.signing
359
+ if not signing.configured:
360
+ return CheckResult("Signing certificate", Status.SKIP, "signing not configured")
361
+ want = signing.thumbprint.replace(" ", "").upper()
362
+ scope = signing.store_scope
363
+ thumbprints = probe.code_signing_thumbprints(scope)
364
+ matches = [t for t in thumbprints if t == want]
365
+ store = "LocalMachine" if scope == "machine" else "CurrentUser"
366
+ if not matches:
367
+ return CheckResult(
368
+ "Signing certificate",
369
+ Status.FAIL,
370
+ f"no code-signing cert with thumbprint {want} in Cert:\\{store}\\My",
371
+ hint="import the code-signing certificate into the configured "
372
+ f"store_scope ({scope}), or fix [tool.kivy.windows.signing].thumbprint.",
373
+ )
374
+ if len(matches) > 1:
375
+ return CheckResult(
376
+ "Signing certificate",
377
+ Status.FAIL,
378
+ f"{len(matches)} certs share thumbprint {want} in Cert:\\{store}\\My",
379
+ hint="remove the duplicate certificate so signtool selects unambiguously.",
380
+ )
381
+ return CheckResult("Signing certificate", Status.PASS, f"{want} in {scope} store")
382
+
383
+
384
+ def check_windows_find_links(config: Config, project_root: Path) -> CheckResult:
385
+ entries = config.windows_required.find_links
386
+ if not entries:
387
+ return CheckResult("find_links directories", Status.SKIP, "not configured")
388
+ root = project_root.resolve()
389
+ problems: list[tuple[str, str | None]] = []
390
+ warnings: list[tuple[str, str | None]] = []
391
+ ok: list[str] = []
392
+ for entry in entries:
393
+ path = (root / entry).resolve()
394
+ detail, hint = find_links_doctor_detail(root, entry, path, platform="windows")
395
+ if not path.is_dir():
396
+ problems.append((detail, hint))
397
+ elif not any(path.glob("*.whl")):
398
+ warnings.append((detail, hint))
399
+ else:
400
+ ok.append(detail)
401
+ if problems:
402
+ return CheckResult(
403
+ "find_links directories",
404
+ Status.FAIL,
405
+ "; ".join(d for d, _ in problems),
406
+ hint=next((h for _, h in problems if h), ""),
407
+ )
408
+ if warnings:
409
+ return CheckResult(
410
+ "find_links directories",
411
+ Status.WARN,
412
+ "; ".join(d for d, _ in warnings),
413
+ hint=next((h for _, h in warnings if h), ""),
414
+ )
415
+ return CheckResult("find_links directories", Status.PASS, "; ".join(ok))
416
+
417
+
418
+ def check_windows_hosts_reachable(
419
+ probe: Probe,
420
+ config: Config,
421
+ lock: WindowsLockfile | None,
422
+ *,
423
+ offline: bool = False,
424
+ ) -> CheckResult:
425
+ if offline:
426
+ return CheckResult("Required hosts reachable", Status.SKIP, "offline")
427
+ # (host, port) pairs: the port comes from each URL's scheme so the HTTP
428
+ # RFC-3161 timestamp server is probed on 80, not the wheels' HTTPS 443.
429
+ hosts: set[tuple[str, int]] = set()
430
+ if lock is not None:
431
+ for pkg in lock.packages:
432
+ for wheel in pkg.wheels:
433
+ if wheel.url:
434
+ _add_host(hosts, wheel.url)
435
+ for art in lock.python_runtime.artifacts:
436
+ if art.url:
437
+ _add_host(hosts, art.url)
438
+ for nb in lock.native_binaries:
439
+ if nb.url:
440
+ _add_host(hosts, nb.url)
441
+ for url in config.windows_required.extra_index_urls:
442
+ _add_host(hosts, url)
443
+ # The RFC-3161 timestamp server is fetched during signing, so its host must
444
+ # be reachable when signing is configured.
445
+ if config.windows_required.signing.configured:
446
+ _add_host(hosts, config.windows_required.signing.timestamp_url)
447
+ if not hosts:
448
+ return CheckResult(
449
+ "Required hosts reachable", Status.PASS, "all artifacts vendored"
450
+ )
451
+ unreachable = sorted(
452
+ host for host, port in hosts if not probe.tcp_reachable(host, port)
453
+ )
454
+ if unreachable:
455
+ return CheckResult(
456
+ "Required hosts reachable",
457
+ Status.FAIL,
458
+ f"unreachable: {', '.join(unreachable)}",
459
+ hint="check your network/proxy; these hosts serve pinned wheels, the "
460
+ "runtime, native binaries, and the signing timestamp server.",
461
+ )
462
+ return CheckResult(
463
+ "Required hosts reachable",
464
+ Status.PASS,
465
+ ", ".join(sorted({host for host, _ in hosts})),
466
+ )
467
+
468
+
469
+ def _add_host(hosts: set[tuple[str, int]], url: str) -> None:
470
+ parsed = urlparse(url)
471
+ host = parsed.hostname
472
+ if host:
473
+ port = parsed.port or (80 if parsed.scheme == "http" else 443)
474
+ hosts.add((host, port))
475
+
476
+
477
+ _PROJECT_CHECK_NAMES = (
478
+ "App source directory",
479
+ "AppUserModelID",
480
+ "Architecture coverage",
481
+ "App icon",
482
+ "Native binaries: sources",
483
+ "Native binaries: collision",
484
+ "Native binaries: arch",
485
+ "Build output not locked",
486
+ "Build volume",
487
+ "Resource assets",
488
+ "find_links directories",
489
+ "signtool available",
490
+ "Signing certificate",
491
+ "Required hosts reachable",
492
+ )
493
+
494
+
495
+ def run_windows_checks(
496
+ probe: Probe,
497
+ *,
498
+ kivyforge_version: str,
499
+ config: Config | None = None,
500
+ project_root: Path | None = None,
501
+ lock: WindowsLockfile | None = None,
502
+ offline: bool = False,
503
+ ) -> list[CheckResult]:
504
+ """Run Windows doctor checks (windows-spec). Project checks SKIP without config."""
505
+ project_root = project_root or Path.cwd()
506
+ results = [
507
+ check_windows_host(probe),
508
+ check_long_path_support(probe),
509
+ C.check_kivyforge_version(probe, kivyforge_version, offline=offline),
510
+ ]
511
+ if config is None:
512
+ for name in _PROJECT_CHECK_NAMES:
513
+ results.append(CheckResult(name, Status.SKIP, C.SKIP_NOTE))
514
+ return results
515
+
516
+ results += [
517
+ C.check_app_dir(config, project_root),
518
+ check_windows_app_id(config),
519
+ check_windows_arch_coverage(config, lock),
520
+ check_windows_app_icon(config, project_root),
521
+ check_windows_native_sources(config, project_root),
522
+ check_windows_native_collision(config),
523
+ check_windows_native_arch(config, project_root),
524
+ check_windows_output_lock(config, project_root, probe),
525
+ check_windows_build_volume(config, project_root, probe),
526
+ check_windows_resource_assets(config, project_root),
527
+ check_windows_find_links(config, project_root),
528
+ check_windows_signtool(probe, config),
529
+ check_windows_signing_cert(probe, config),
530
+ check_windows_hosts_reachable(probe, config, lock, offline=offline),
531
+ ]
532
+ return results
533
+
534
+
535
+ def windows_doctor(
536
+ cwd: Path, *, kivyforge_version: str, offline: bool
537
+ ) -> list[CheckResult]:
538
+ """Load the Windows project + lock (if present) and run the Windows check set."""
539
+ pyproject = cwd / "pyproject.toml"
540
+ config = None
541
+ lock = None
542
+ parse_results: list[CheckResult] = []
543
+ if pyproject.is_file():
544
+ try:
545
+ config = load_config(pyproject, require_ios=False, require_windows=True)
546
+ except ConfigError as exc:
547
+ parse_results.append(
548
+ CheckResult("pyproject.toml", Status.FAIL, exc.format())
549
+ )
550
+ lockfile = cwd / "pylock.windows.toml"
551
+ if lockfile.is_file():
552
+ try:
553
+ lock = load(lockfile)
554
+ except LockError as exc:
555
+ parse_results.append(C.lock_parse_fail("windows", exc))
556
+ return parse_results + run_windows_checks(
557
+ RealProbe(),
558
+ kivyforge_version=kivyforge_version,
559
+ config=config,
560
+ project_root=cwd,
561
+ lock=lock,
562
+ offline=offline,
563
+ )
@@ -0,0 +1,87 @@
1
+ """Windows-safe in-place tree publishing (windows-spec).
2
+
3
+ Windows cannot reliably rename a freshly-written directory: the antivirus
4
+ real-time scanner holds new executables/DLLs open (without share-delete), and a
5
+ cold *first-sight* scan (cloud lookup) can exceed any sane retry window — the
6
+ whole extracted CPython + wheels tree is thousands of new files. So we never
7
+ rename a freshly written tree. Instead callers write it **directly into its
8
+ final location** and only ever rename the *previous* (long-since-scanned) tree
9
+ aside, which renames instantly, restoring it if the write fails so a broken
10
+ operation never destroys a working artifact.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import shutil
17
+ import time
18
+ from pathlib import Path
19
+
20
+
21
+ def rename_with_retry(
22
+ src: Path, dst: Path, *, timeout: float = 30.0, initial_delay: float = 0.1
23
+ ) -> None:
24
+ """``os.replace(src, dst)`` retrying transient Windows sharing locks.
25
+
26
+ Retries until *timeout* seconds elapse, backing off from *initial_delay* up
27
+ to a 1s cap. Re-raises the last ``PermissionError`` if the lock never clears.
28
+ """
29
+ deadline = time.monotonic() + timeout
30
+ delay = initial_delay
31
+ while True:
32
+ try:
33
+ os.replace(src, dst)
34
+ return
35
+ except PermissionError:
36
+ if time.monotonic() >= deadline:
37
+ raise
38
+ time.sleep(delay)
39
+ delay = min(delay * 1.5, 1.0)
40
+
41
+
42
+ def reserve_previous(target: Path, *, timeout: float = 30.0) -> Path | None:
43
+ """Move an existing *target* aside so a fresh one can be written in place.
44
+
45
+ Returns the trash path (to restore on failure / delete on success), or
46
+ ``None`` when there was no previous tree. Renaming the old tree is fast — it
47
+ was scanned long ago — but we still retry briefly in case a just-built tree's
48
+ files are momentarily held.
49
+ """
50
+ if not target.exists():
51
+ return None
52
+ trash = target.with_name(f".{target.name}.old-{os.getpid()}")
53
+ try:
54
+ rename_with_retry(target, trash, timeout=timeout)
55
+ except PermissionError as exc:
56
+ # A persistent lock (past the retry window) is almost always a human
57
+ # holding the tree open rather than a transient scanner touch, so point
58
+ # at the usual culprits instead of surfacing a bare WinError 5.
59
+ from . import WindowsBundleError
60
+
61
+ raise WindowsBundleError(
62
+ f"could not replace the existing output at {target}: a file inside "
63
+ "it is still open in another process.\n"
64
+ " Close any Explorer or terminal window sitting in that folder and "
65
+ "quit any running copy of the app, then re-run the command.\n"
66
+ " If it persists, delete the folder manually and retry."
67
+ ) from exc
68
+ return trash
69
+
70
+
71
+ def restore_previous(trash: Path | None, target: Path) -> None:
72
+ """Move a reserved previous tree back into place after a failed write."""
73
+ if trash is None:
74
+ return
75
+ shutil.rmtree(target, ignore_errors=True)
76
+ try:
77
+ rename_with_retry(trash, target)
78
+ except OSError:
79
+ # Best-effort restore: leave the trash for manual recovery rather than
80
+ # masking the original failure with a rename error.
81
+ pass
82
+
83
+
84
+ def discard_reserved(trash: Path | None) -> None:
85
+ """Delete a reserved previous tree after a successful write (no-op if None)."""
86
+ if trash is not None:
87
+ shutil.rmtree(trash, ignore_errors=True)